Skip to content

Commit e44f7f5

Browse files
committed
improve troubleshooting
1 parent b114f8b commit e44f7f5

File tree

3 files changed

+106
-65
lines changed

3 files changed

+106
-65
lines changed

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ Learn more about MCP Transport types in the [MCP Protocol Specification – Tran
357357

358358
#### Configuration Examples
359359

360-
##### Using MCP with Cursor/Claude Desktop/etc (mcp.json)
360+
##### Using MCP with Cursor/VS Code/Claude Desktop/etc (mcp.json)
361361

362362
> [!NOTE]
363363
> For EU Cycode environments, make sure to set the appropriate `CYCODE_API_URL` and `CYCODE_APP_URL` values in the environment variables (e.g., `https://api.eu.cycode.com` and `https://app.eu.cycode.com`).
@@ -469,13 +469,13 @@ cycode mcp -t sse -p 8000 &
469469
For **streamable HTTP transport**:
470470
```bash
471471
# Start the MCP server in background
472-
cycode mcp -t streamable-http -H 0.0.0.0 -p 9000 &
472+
cycode mcp -t streamable-http -H 127.0.0.2 -p 9000 &
473473
474474
# Configure in mcp.json
475475
{
476476
"mcpServers": {
477477
"cycode": {
478-
"url": "http://0.0.0.0:9000/mcp"
478+
"url": "http://127.0.0.2:9000/mcp"
479479
}
480480
}
481481
}
@@ -484,6 +484,33 @@ cycode mcp -t streamable-http -H 0.0.0.0 -p 9000 &
484484
> [!NOTE]
485485
> The MCP server requires proper Cycode CLI authentication to function. Make sure you have authenticated using `cycode auth` or configured your credentials before starting the MCP server.
486486

487+
### Troubleshooting MCP
488+
489+
If you encounter issues with the MCP server, you can enable debug logging to get more detailed information about what's happening. There are two ways to enable debug logging:
490+
491+
1. Using the `-v` or `--verbose` flag:
492+
```bash
493+
cycode -v mcp
494+
```
495+
496+
2. Using the `CYCODE_CLI_VERBOSE` environment variable:
497+
```bash
498+
CYCODE_CLI_VERBOSE=1 cycode mcp
499+
```
500+
501+
The debug logs will show detailed information about:
502+
- Server startup and configuration
503+
- Connection attempts and status
504+
- Tool execution and results
505+
- Any errors or warnings that occur
506+
507+
This information can be helpful when:
508+
- Diagnosing connection issues
509+
- Understanding why certain tools aren't working
510+
- Identifying authentication problems
511+
- Debugging transport-specific issues
512+
513+
487514
# Scan Command
488515

489516
## Running a Scan

cycode/cli/apps/mcp/mcp_command.py

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import json
3+
import logging
34
import os
45
import sys
56
import tempfile
@@ -12,6 +13,7 @@
1213

1314
from cycode.cli.cli_types import McpTransportOption, ScanTypeOption
1415
from cycode.cli.utils.sentry import add_breadcrumb
16+
from cycode.logger import LoggersManager, get_logger
1517

1618
try:
1719
from mcp.server.fastmcp import FastMCP
@@ -22,15 +24,21 @@
2224
) from None
2325

2426

25-
from cycode.logger import get_logger
26-
2727
_logger = get_logger('Cycode MCP')
2828

2929
_DEFAULT_RUN_COMMAND_TIMEOUT = 5 * 60
3030

3131
_FILES_TOOL_FIELD = Field(description='Files to scan, mapping file paths to their content')
3232

3333

34+
def _is_debug_mode() -> bool:
35+
return LoggersManager.global_logging_level == logging.DEBUG
36+
37+
38+
def _gen_random_id() -> str:
39+
return uuid.uuid4().hex
40+
41+
3442
def _get_current_executable() -> str:
3543
"""Get the current executable path for spawning subprocess."""
3644
if getattr(sys, 'frozen', False): # pyinstaller bundle
@@ -49,7 +57,8 @@ async def _run_cycode_command(*args: str, timeout: int = _DEFAULT_RUN_COMMAND_TI
4957
Returns:
5058
Dictionary containing the parsed JSON result or error information
5159
"""
52-
cmd_args = [_get_current_executable(), '-o', 'json', *list(args)]
60+
verbose = ['-v'] if _is_debug_mode() else []
61+
cmd_args = [_get_current_executable(), *verbose, '-o', 'json', *list(args)]
5362
_logger.debug('Running Cycode CLI command: %s', ' '.join(cmd_args))
5463

5564
try:
@@ -61,6 +70,9 @@ async def _run_cycode_command(*args: str, timeout: int = _DEFAULT_RUN_COMMAND_TI
6170
stdout_str = stdout.decode('UTF-8', errors='replace') if stdout else ''
6271
stderr_str = stderr.decode('UTF-8', errors='replace') if stderr else ''
6372

73+
if _is_debug_mode(): # redirect debug output
74+
sys.stderr.write(stderr_str)
75+
6476
if not stdout_str:
6577
return {'error': 'No output from command', 'stderr': stderr_str, 'returncode': process.returncode}
6678

@@ -87,7 +99,7 @@ def _create_temp_files(files_content: dict[str, str]) -> list[str]:
8799
_logger.debug('Creating temporary files in directory: %s', temp_dir)
88100

89101
for file_path, content in files_content.items():
90-
safe_filename = f'{uuid.uuid4().hex}_{Path(file_path).name}'
102+
safe_filename = f'{_gen_random_id()}_{Path(file_path).name}'
91103
temp_file_path = os.path.join(temp_dir, safe_filename)
92104

93105
os.makedirs(os.path.dirname(temp_file_path), exist_ok=True)
@@ -125,15 +137,39 @@ def _cleanup_temp_files(temp_files: list[str]) -> None:
125137

126138
async def _run_cycode_scan(scan_type: ScanTypeOption, temp_files: list[str]) -> dict[str, Any]:
127139
"""Run cycode scan command and return the result."""
128-
args = ['scan', '-t', str(scan_type), 'path', *temp_files]
129-
return await _run_cycode_command(*args)
140+
return await _run_cycode_command(*['scan', '-t', str(scan_type), 'path', *temp_files])
130141

131142

132143
async def _run_cycode_status() -> dict[str, Any]:
133144
"""Run cycode status command and return the result."""
134145
return await _run_cycode_command('status')
135146

136147

148+
async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
149+
_tool_call_id = _gen_random_id()
150+
_logger.info('Scan tool called, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
151+
152+
if not files:
153+
_logger.error('No files provided for scan')
154+
return json.dumps({'error': 'No files provided'})
155+
156+
temp_files = _create_temp_files(files)
157+
158+
try:
159+
_logger.info(
160+
'Running Cycode scan, %s',
161+
{'scan_type': scan_type, 'files_count': len(temp_files), 'call_id': _tool_call_id},
162+
)
163+
result = await _run_cycode_scan(scan_type, temp_files)
164+
_logger.info('Scan completed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
165+
return json.dumps(result, indent=2)
166+
except Exception as e:
167+
_logger.error('Scan failed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id, 'error': str(e)})
168+
return json.dumps({'error': f'Scan failed: {e!s}'}, indent=2)
169+
finally:
170+
_cleanup_temp_files(temp_files)
171+
172+
137173
async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
138174
"""Scan files for hardcoded secrets.
139175
@@ -148,18 +184,7 @@ async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
148184
Returns:
149185
JSON string containing scan results and any secrets found
150186
"""
151-
_logger.info('Secret scan tool called')
152-
153-
if not files:
154-
return json.dumps({'error': 'No files provided'})
155-
156-
temp_files = _create_temp_files(files)
157-
158-
try:
159-
result = await _run_cycode_scan(ScanTypeOption.SECRET, temp_files)
160-
return json.dumps(result, indent=2)
161-
finally:
162-
_cleanup_temp_files(temp_files)
187+
return await _cycode_scan_tool(ScanTypeOption.SECRET, files)
163188

164189

165190
async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
@@ -178,18 +203,7 @@ async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
178203
Returns:
179204
JSON string containing scan results, vulnerabilities, and license issues found
180205
"""
181-
_logger.info('SCA scan tool called')
182-
183-
if not files:
184-
return json.dumps({'error': 'No files provided'})
185-
186-
temp_files = _create_temp_files(files)
187-
188-
try:
189-
result = await _run_cycode_scan(ScanTypeOption.SCA, temp_files)
190-
return json.dumps(result, indent=2)
191-
finally:
192-
_cleanup_temp_files(temp_files)
206+
return await _cycode_scan_tool(ScanTypeOption.SCA, files)
193207

194208

195209
async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
@@ -208,18 +222,7 @@ async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
208222
Returns:
209223
JSON string containing scan results and any misconfigurations found
210224
"""
211-
_logger.info('IaC scan tool called')
212-
213-
if not files:
214-
return json.dumps({'error': 'No files provided'})
215-
216-
temp_files = _create_temp_files(files)
217-
218-
try:
219-
result = await _run_cycode_scan(ScanTypeOption.IAC, temp_files)
220-
return json.dumps(result, indent=2)
221-
finally:
222-
_cleanup_temp_files(temp_files)
225+
return await _cycode_scan_tool(ScanTypeOption.IAC, files)
223226

224227

225228
async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
@@ -238,18 +241,7 @@ async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
238241
Returns:
239242
JSON string containing scan results and any security flaws found
240243
"""
241-
_logger.info('SAST scan tool called')
242-
243-
if not files:
244-
return json.dumps({'error': 'No files provided'})
245-
246-
temp_files = _create_temp_files(files)
247-
248-
try:
249-
result = await _run_cycode_scan(ScanTypeOption.SAST, temp_files)
250-
return json.dumps(result, indent=2)
251-
finally:
252-
_cleanup_temp_files(temp_files)
244+
return await _cycode_scan_tool(ScanTypeOption.SAST, files)
253245

254246

255247
async def cycode_status() -> str:
@@ -265,13 +257,21 @@ async def cycode_status() -> str:
265257
Returns:
266258
JSON string containing CLI status, version, and configuration details
267259
"""
260+
_tool_call_id = _gen_random_id()
268261
_logger.info('Status tool called')
269262

270-
result = await _run_cycode_status()
271-
return json.dumps(result, indent=2)
263+
try:
264+
_logger.info('Running Cycode status check, %s', {'call_id': _tool_call_id})
265+
result = await _run_cycode_status()
266+
_logger.info('Status check completed, %s', {'call_id': _tool_call_id})
267+
268+
return json.dumps(result, indent=2)
269+
except Exception as e:
270+
_logger.error('Status check failed, %s', {'call_id': _tool_call_id, 'error': str(e)})
271+
return json.dumps({'error': f'Status check failed: {e!s}'}, indent=2)
272272

273273

274-
def _create_mcp_server(host: str = '127.0.0.1', port: int = 8000) -> FastMCP:
274+
def _create_mcp_server(host: str, port: int) -> FastMCP:
275275
"""Create and configure the MCP server."""
276276
tools = [
277277
Tool.from_function(cycode_status),
@@ -281,7 +281,14 @@ def _create_mcp_server(host: str = '127.0.0.1', port: int = 8000) -> FastMCP:
281281
Tool.from_function(cycode_sast_scan),
282282
]
283283
_logger.info('Creating MCP server with tools: %s', [tool.name for tool in tools])
284-
return FastMCP('cycode', tools=tools, host=host, port=port)
284+
return FastMCP(
285+
'cycode',
286+
tools=tools,
287+
host=host,
288+
port=port,
289+
debug=_is_debug_mode(),
290+
log_level='DEBUG' if _is_debug_mode() else 'INFO',
291+
)
285292

286293

287294
def _run_mcp_server(transport: McpTransportOption, host: str, port: int) -> None:

cycode/logger.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import sys
3-
from typing import NamedTuple, Optional, Union
3+
from typing import ClassVar, NamedTuple, Optional, Union
44

55
import click
66
import typer
@@ -42,10 +42,15 @@ class CreatedLogger(NamedTuple):
4242
control_level_in_runtime: bool
4343

4444

45-
_CREATED_LOGGERS: set[CreatedLogger] = set()
45+
class LoggersManager:
46+
loggers: ClassVar[set[CreatedLogger]] = set()
47+
global_logging_level: Optional[int] = None
4648

4749

4850
def get_logger_level() -> Optional[Union[int, str]]:
51+
if LoggersManager.global_logging_level is not None:
52+
return LoggersManager.global_logging_level
53+
4954
config_level = get_val_as_string(consts.LOGGING_LEVEL_ENV_VAR_NAME)
5055
return logging.getLevelName(config_level)
5156

@@ -54,12 +59,14 @@ def get_logger(logger_name: Optional[str] = None, control_level_in_runtime: bool
5459
new_logger = logging.getLogger(logger_name)
5560
new_logger.setLevel(get_logger_level())
5661

57-
_CREATED_LOGGERS.add(CreatedLogger(logger=new_logger, control_level_in_runtime=control_level_in_runtime))
62+
LoggersManager.loggers.add(CreatedLogger(logger=new_logger, control_level_in_runtime=control_level_in_runtime))
5863

5964
return new_logger
6065

6166

6267
def set_logging_level(level: int) -> None:
63-
for created_logger in _CREATED_LOGGERS:
68+
LoggersManager.global_logging_level = level
69+
70+
for created_logger in LoggersManager.loggers:
6471
if created_logger.control_level_in_runtime:
6572
created_logger.logger.setLevel(level)

0 commit comments

Comments
 (0)