Skip to content

Commit 64ff939

Browse files
committed
CM-61587 added mcp file path for scan commands and documentation for mcp permissions
1 parent 49ec713 commit 64ff939

File tree

3 files changed

+321
-55
lines changed

3 files changed

+321
-55
lines changed

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,22 @@ The MCP server provides the following tools that AI systems can use:
384384

385385
| Tool Name | Description |
386386
|----------------------|---------------------------------------------------------------------------------------------|
387-
| `cycode_secret_scan` | Scan files for hardcoded secrets |
388-
| `cycode_sca_scan` | Scan files for Software Composition Analysis (SCA) - vulnerabilities and license issues |
389-
| `cycode_iac_scan` | Scan files for Infrastructure as Code (IaC) misconfigurations |
390-
| `cycode_sast_scan` | Scan files for Static Application Security Testing (SAST) - code quality and security flaws |
387+
| `cycode_secret_scan` | Scan for hardcoded secrets |
388+
| `cycode_sca_scan` | Scan for Software Composition Analysis (SCA) - vulnerabilities and license issues |
389+
| `cycode_iac_scan` | Scan for Infrastructure as Code (IaC) misconfigurations |
390+
| `cycode_sast_scan` | Scan for Static Application Security Testing (SAST) - code quality and security flaws |
391391
| `cycode_status` | Get Cycode CLI version, authentication status, and configuration information |
392392

393+
Each scan tool accepts two mutually exclusive input modes:
394+
395+
- **`paths`** *(preferred)* — one or more file or directory paths that exist on disk. Directories are scanned recursively. The Cycode engine handles file discovery and filtering, just as `cycode scan -t <type> path ./src` does from the CLI.
396+
- **`files`** *(fallback)* — a dictionary mapping file paths to their full content as strings. Use this only when the files are not available on disk (e.g. in-memory edits not yet saved).
397+
398+
> [!TIP]
399+
> Use `paths` whenever possible. Passing large files (like `package-lock.json`) as inline content can exceed token limits and slow down the AI client. With `paths`, the Cycode engine reads files directly from disk.
400+
401+
All scan tools return a JSON object that includes a `"summary"` field with a human-readable violation count (e.g. `"Cycode found 3 violations: 1 CRITICAL, 2 HIGH."`) in addition to the full `"detections"` array.
402+
393403
### Usage Examples
394404

395405
#### Basic Command Examples
@@ -547,6 +557,26 @@ cycode mcp -t streamable-http -H 127.0.0.2 -p 9000 &
547557
> [!NOTE]
548558
> 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.
549559

560+
### Pre-authorizing Tools for Subagents (Claude Code)
561+
562+
When Claude Code delegates work to background subagents (e.g. to run scans in parallel), those subagents cannot display interactive permission prompts. If the Cycode tools have not been pre-approved, scans will fail silently in subagent contexts.
563+
564+
To pre-authorize the Cycode MCP tools so they work in all contexts including subagents, add them to the `allowedTools` list in your Claude Code settings (`~/.claude/settings.json`):
565+
566+
```json
567+
{
568+
"allowedTools": [
569+
"mcp__cycode__cycode_secret_scan",
570+
"mcp__cycode__cycode_sca_scan",
571+
"mcp__cycode__cycode_iac_scan",
572+
"mcp__cycode__cycode_sast_scan",
573+
"mcp__cycode__cycode_status"
574+
]
575+
}
576+
```
577+
578+
Once added, Claude Code will not prompt for approval when these tools are called, and they will work correctly inside subagents.
579+
550580
### Troubleshooting MCP
551581

552582
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:

cycode/cli/apps/mcp/mcp_command.py

Lines changed: 134 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
import tempfile
88
import uuid
9-
from typing import Annotated, Any
9+
from typing import Annotated, Any, Optional
1010

1111
import typer
1212
from pathvalidate import sanitize_filepath
@@ -28,7 +28,25 @@
2828

2929
_DEFAULT_RUN_COMMAND_TIMEOUT = 10 * 60
3030

31-
_FILES_TOOL_FIELD = Field(description='Files to scan, mapping file paths to their content')
31+
_FILES_TOOL_FIELD = Field(
32+
default=None,
33+
description=(
34+
'Files to scan, mapping file paths to their content. '
35+
'Provide either this or "paths". '
36+
'Note: for large codebases, prefer "paths" to avoid token overhead.'
37+
),
38+
)
39+
_PATHS_TOOL_FIELD = Field(
40+
default=None,
41+
description=(
42+
'Paths to scan — file paths or directory paths that exist on disk. '
43+
'Directories are scanned recursively. '
44+
'Provide either this or "files". '
45+
'Preferred over "files" when the files already exist on disk.'
46+
),
47+
)
48+
49+
_SEVERITY_ORDER = ('CRITICAL', 'HIGH', 'MEDIUM', 'LOW')
3250

3351

3452
def _is_debug_mode() -> bool:
@@ -163,48 +181,99 @@ def __exit__(self, *_) -> None:
163181
shutil.rmtree(self.temp_base_dir, ignore_errors=True)
164182

165183

166-
async def _run_cycode_scan(scan_type: ScanTypeOption, temp_files: list[str]) -> dict[str, Any]:
184+
async def _run_cycode_scan(scan_type: ScanTypeOption, paths: list[str]) -> dict[str, Any]:
167185
"""Run cycode scan command and return the result."""
168-
return await _run_cycode_command(*['scan', '-t', str(scan_type), 'path', *temp_files])
186+
return await _run_cycode_command(*['scan', '-t', str(scan_type), 'path', *paths])
169187

170188

171189
async def _run_cycode_status() -> dict[str, Any]:
172190
"""Run cycode status command and return the result."""
173191
return await _run_cycode_command('status')
174192

175193

176-
async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
194+
def _build_scan_summary(result: dict[str, Any]) -> str:
195+
"""Build a human-readable summary line from a scan result dict.
196+
197+
Args:
198+
result: Parsed JSON scan result from the CLI.
199+
200+
Returns:
201+
A one-line summary string describing what was found.
202+
"""
203+
detections = result.get('detections', [])
204+
errors = result.get('errors', [])
205+
206+
if not detections:
207+
if errors:
208+
return f'Scan completed with {len(errors)} error(s) and no violations found.'
209+
return 'No violations found.'
210+
211+
total = len(detections)
212+
severity_counts: dict[str, int] = {}
213+
for d in detections:
214+
sev = (d.get('severity') or 'UNKNOWN').upper()
215+
severity_counts[sev] = severity_counts.get(sev, 0) + 1
216+
217+
parts = [f'{severity_counts[s]} {s}' for s in _SEVERITY_ORDER if s in severity_counts]
218+
other_keys = [k for k in severity_counts if k not in _SEVERITY_ORDER]
219+
parts += [f'{severity_counts[k]} {k}' for k in other_keys]
220+
221+
label = 'violation' if total == 1 else 'violations'
222+
return f'Cycode found {total} {label}: {", ".join(parts)}.'
223+
224+
225+
async def _cycode_scan_tool(
226+
scan_type: ScanTypeOption,
227+
files: Optional[dict[str, str]] = None,
228+
paths: Optional[list[str]] = None,
229+
) -> str:
177230
_tool_call_id = _gen_random_id()
178231
_logger.info('Scan tool called, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
179232

180-
if not files:
181-
_logger.error('No files provided for scan')
182-
return json.dumps({'error': 'No files provided'})
233+
if not files and not paths:
234+
_logger.error('No files or paths provided for scan')
235+
return json.dumps(
236+
{'error': 'No files or paths provided. Pass file contents via "files" or disk paths via "paths".'}
237+
)
183238

184239
try:
185-
with _TempFilesManager(files, _tool_call_id) as temp_files:
186-
original_count = len(files)
187-
processed_count = len(temp_files)
188-
189-
if processed_count < original_count:
190-
_logger.warning(
191-
'Some files were rejected during sanitization, %s',
192-
{
193-
'scan_type': scan_type,
194-
'original_count': original_count,
195-
'processed_count': processed_count,
196-
'call_id': _tool_call_id,
197-
},
198-
)
240+
if paths:
241+
missing = [p for p in paths if not os.path.exists(p)]
242+
if missing:
243+
return json.dumps({'error': f'Paths not found on disk: {missing}'}, indent=2)
199244

200245
_logger.info(
201-
'Running Cycode scan, %s',
202-
{'scan_type': scan_type, 'files_count': processed_count, 'call_id': _tool_call_id},
246+
'Running Cycode scan (path-based), %s',
247+
{'scan_type': scan_type, 'paths': paths, 'call_id': _tool_call_id},
203248
)
204-
result = await _run_cycode_scan(scan_type, temp_files)
249+
result = await _run_cycode_scan(scan_type, paths)
250+
else:
251+
with _TempFilesManager(files, _tool_call_id) as temp_files:
252+
original_count = len(files)
253+
processed_count = len(temp_files)
254+
255+
if processed_count < original_count:
256+
_logger.warning(
257+
'Some files were rejected during sanitization, %s',
258+
{
259+
'scan_type': scan_type,
260+
'original_count': original_count,
261+
'processed_count': processed_count,
262+
'call_id': _tool_call_id,
263+
},
264+
)
265+
266+
_logger.info(
267+
'Running Cycode scan (files-based), %s',
268+
{'scan_type': scan_type, 'files_count': processed_count, 'call_id': _tool_call_id},
269+
)
270+
result = await _run_cycode_scan(scan_type, temp_files)
205271

206-
_logger.info('Scan completed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
207-
return json.dumps(result, indent=2)
272+
if 'error' not in result:
273+
result['summary'] = _build_scan_summary(result)
274+
275+
_logger.info('Scan completed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
276+
return json.dumps(result, indent=2)
208277
except ValueError as e:
209278
_logger.error('Invalid input files, %s', {'scan_type': scan_type, 'call_id': _tool_call_id, 'error': str(e)})
210279
return json.dumps({'error': f'Invalid input files: {e!s}'}, indent=2)
@@ -213,25 +282,32 @@ async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _
213282
return json.dumps({'error': f'Scan failed: {e!s}'}, indent=2)
214283

215284

216-
async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
217-
"""Scan files for hardcoded secrets.
285+
async def cycode_secret_scan(
286+
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
287+
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
288+
) -> str:
289+
"""Scan for hardcoded secrets.
218290
219291
Use this tool when you need to:
220292
- scan code for hardcoded secrets, API keys, passwords, tokens
221293
- verify that code doesn't contain exposed credentials
222294
- detect potential security vulnerabilities from secret exposure
223295
224296
Args:
225-
files: Dictionary mapping file paths to their content
297+
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
298+
files: Dictionary mapping file paths to their content (fallback when files are not on disk).
226299
227300
Returns:
228-
JSON string containing scan results and any secrets found
301+
JSON string with a "summary" field (human-readable violation count) plus full scan results.
229302
"""
230-
return await _cycode_scan_tool(ScanTypeOption.SECRET, files)
303+
return await _cycode_scan_tool(ScanTypeOption.SECRET, files=files, paths=paths)
231304

232305

233-
async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
234-
"""Scan files for Software Composition Analysis (SCA) - vulnerabilities and license issues.
306+
async def cycode_sca_scan(
307+
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
308+
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
309+
) -> str:
310+
"""Scan for Software Composition Analysis (SCA) - vulnerabilities and license issues.
235311
236312
Use this tool when you need to:
237313
- scan dependencies for known security vulnerabilities
@@ -242,19 +318,24 @@ async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
242318
243319
Important:
244320
You must also include lock files (like package-lock.json, Pipfile.lock, etc.) to get accurate results.
245-
You must provide manifest and lock files together.
321+
When using "paths", pass the directory containing both manifest and lock files.
322+
When using "files", provide both manifest and lock files together.
246323
247324
Args:
248-
files: Dictionary mapping file paths to their content
325+
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
326+
files: Dictionary mapping file paths to their content (fallback when files are not on disk).
249327
250328
Returns:
251-
JSON string containing scan results, vulnerabilities, and license issues found
329+
JSON string with a "summary" field (human-readable violation count) plus full scan results.
252330
"""
253-
return await _cycode_scan_tool(ScanTypeOption.SCA, files)
331+
return await _cycode_scan_tool(ScanTypeOption.SCA, files=files, paths=paths)
254332

255333

256-
async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
257-
"""Scan files for Infrastructure as Code (IaC) misconfigurations.
334+
async def cycode_iac_scan(
335+
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
336+
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
337+
) -> str:
338+
"""Scan for Infrastructure as Code (IaC) misconfigurations.
258339
259340
Use this tool when you need to:
260341
- scan Terraform, CloudFormation, Kubernetes YAML files
@@ -264,16 +345,20 @@ async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
264345
- review Docker files for security issues
265346
266347
Args:
267-
files: Dictionary mapping file paths to their content
348+
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
349+
files: Dictionary mapping file paths to their content (fallback when files are not on disk).
268350
269351
Returns:
270-
JSON string containing scan results and any misconfigurations found
352+
JSON string with a "summary" field (human-readable violation count) plus full scan results.
271353
"""
272-
return await _cycode_scan_tool(ScanTypeOption.IAC, files)
354+
return await _cycode_scan_tool(ScanTypeOption.IAC, files=files, paths=paths)
273355

274356

275-
async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
276-
"""Scan files for Static Application Security Testing (SAST) - code quality and security flaws.
357+
async def cycode_sast_scan(
358+
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
359+
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
360+
) -> str:
361+
"""Scan for Static Application Security Testing (SAST) - code quality and security flaws.
277362
278363
Use this tool when you need to:
279364
- scan source code for security vulnerabilities
@@ -283,12 +368,13 @@ async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
283368
- find SQL injection, XSS, and other application security issues
284369
285370
Args:
286-
files: Dictionary mapping file paths to their content
371+
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
372+
files: Dictionary mapping file paths to their content (fallback when files are not on disk).
287373
288374
Returns:
289-
JSON string containing scan results and any security flaws found
375+
JSON string with a "summary" field (human-readable violation count) plus full scan results.
290376
"""
291-
return await _cycode_scan_tool(ScanTypeOption.SAST, files)
377+
return await _cycode_scan_tool(ScanTypeOption.SAST, files=files, paths=paths)
292378

293379

294380
async def cycode_status() -> str:

0 commit comments

Comments
 (0)