11import asyncio
22import json
3+ import logging
34import os
45import sys
56import tempfile
1213
1314from cycode .cli .cli_types import McpTransportOption , ScanTypeOption
1415from cycode .cli .utils .sentry import add_breadcrumb
16+ from cycode .logger import LoggersManager , get_logger
1517
1618try :
1719 from mcp .server .fastmcp import FastMCP
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+
3442def _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
126138async 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
132143async 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+
137173async 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
165190async 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
195209async 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
225228async 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
255247async 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
287294def _run_mcp_server (transport : McpTransportOption , host : str , port : int ) -> None :
0 commit comments