Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,22 @@ The MCP server provides the following tools that AI systems can use:

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

Each scan tool accepts two mutually exclusive input modes:

- **`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.
- **`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).

> [!TIP]
> 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.

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.

### Usage Examples

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

### Pre-authorizing Tools for Subagents (Claude Code)

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.

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`):

```json
{
"allowedTools": [
"mcp__cycode__cycode_secret_scan",
"mcp__cycode__cycode_sca_scan",
"mcp__cycode__cycode_iac_scan",
"mcp__cycode__cycode_sast_scan",
"mcp__cycode__cycode_status"
]
}
```

Once added, Claude Code will not prompt for approval when these tools are called, and they will work correctly inside subagents.

### Troubleshooting MCP

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:
Expand Down
182 changes: 134 additions & 48 deletions cycode/cli/apps/mcp/mcp_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import tempfile
import uuid
from typing import Annotated, Any
from typing import Annotated, Any, Optional

import typer
from pathvalidate import sanitize_filepath
Expand All @@ -28,7 +28,25 @@

_DEFAULT_RUN_COMMAND_TIMEOUT = 10 * 60

_FILES_TOOL_FIELD = Field(description='Files to scan, mapping file paths to their content')
_FILES_TOOL_FIELD = Field(
default=None,
description=(
'Files to scan, mapping file paths to their content. '
'Provide either this or "paths". '
'Note: for large codebases, prefer "paths" to avoid token overhead.'
),
)
_PATHS_TOOL_FIELD = Field(
default=None,
description=(
'Paths to scan — file paths or directory paths that exist on disk. '
'Directories are scanned recursively. '
'Provide either this or "files". '
'Preferred over "files" when the files already exist on disk.'
),
)

_SEVERITY_ORDER = ('CRITICAL', 'HIGH', 'MEDIUM', 'LOW')


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


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


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


async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
def _build_scan_summary(result: dict[str, Any]) -> str:
"""Build a human-readable summary line from a scan result dict.

Args:
result: Parsed JSON scan result from the CLI.

Returns:
A one-line summary string describing what was found.
"""
detections = result.get('detections', [])
errors = result.get('errors', [])

if not detections:
if errors:
return f'Scan completed with {len(errors)} error(s) and no violations found.'
return 'No violations found.'

total = len(detections)
severity_counts: dict[str, int] = {}
for d in detections:
sev = (d.get('severity') or 'UNKNOWN').upper()
severity_counts[sev] = severity_counts.get(sev, 0) + 1

parts = [f'{severity_counts[s]} {s}' for s in _SEVERITY_ORDER if s in severity_counts]
other_keys = [k for k in severity_counts if k not in _SEVERITY_ORDER]
parts += [f'{severity_counts[k]} {k}' for k in other_keys]

label = 'violation' if total == 1 else 'violations'
return f'Cycode found {total} {label}: {", ".join(parts)}.'


async def _cycode_scan_tool(
scan_type: ScanTypeOption,
files: Optional[dict[str, str]] = None,
paths: Optional[list[str]] = None,
) -> str:
_tool_call_id = _gen_random_id()
_logger.info('Scan tool called, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})

if not files:
_logger.error('No files provided for scan')
return json.dumps({'error': 'No files provided'})
if not files and not paths:
_logger.error('No files or paths provided for scan')
return json.dumps(
{'error': 'No files or paths provided. Pass file contents via "files" or disk paths via "paths".'}
)

try:
with _TempFilesManager(files, _tool_call_id) as temp_files:
original_count = len(files)
processed_count = len(temp_files)

if processed_count < original_count:
_logger.warning(
'Some files were rejected during sanitization, %s',
{
'scan_type': scan_type,
'original_count': original_count,
'processed_count': processed_count,
'call_id': _tool_call_id,
},
)
if paths:
missing = [p for p in paths if not os.path.exists(p)]
if missing:
return json.dumps({'error': f'Paths not found on disk: {missing}'}, indent=2)

_logger.info(
'Running Cycode scan, %s',
{'scan_type': scan_type, 'files_count': processed_count, 'call_id': _tool_call_id},
'Running Cycode scan (path-based), %s',
{'scan_type': scan_type, 'paths': paths, 'call_id': _tool_call_id},
)
result = await _run_cycode_scan(scan_type, temp_files)
result = await _run_cycode_scan(scan_type, paths)
else:
with _TempFilesManager(files, _tool_call_id) as temp_files:
original_count = len(files)
processed_count = len(temp_files)

if processed_count < original_count:
_logger.warning(
'Some files were rejected during sanitization, %s',
{
'scan_type': scan_type,
'original_count': original_count,
'processed_count': processed_count,
'call_id': _tool_call_id,
},
)

_logger.info(
'Running Cycode scan (files-based), %s',
{'scan_type': scan_type, 'files_count': processed_count, 'call_id': _tool_call_id},
)
result = await _run_cycode_scan(scan_type, temp_files)

_logger.info('Scan completed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
return json.dumps(result, indent=2)
if 'error' not in result:
result['summary'] = _build_scan_summary(result)

_logger.info('Scan completed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id})
return json.dumps(result, indent=2)
except ValueError as e:
_logger.error('Invalid input files, %s', {'scan_type': scan_type, 'call_id': _tool_call_id, 'error': str(e)})
return json.dumps({'error': f'Invalid input files: {e!s}'}, indent=2)
Expand All @@ -213,25 +282,32 @@ async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _
return json.dumps({'error': f'Scan failed: {e!s}'}, indent=2)


async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
"""Scan files for hardcoded secrets.
async def cycode_secret_scan(
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
) -> str:
"""Scan for hardcoded secrets.

Use this tool when you need to:
- scan code for hardcoded secrets, API keys, passwords, tokens
- verify that code doesn't contain exposed credentials
- detect potential security vulnerabilities from secret exposure

Args:
files: Dictionary mapping file paths to their content
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
files: Dictionary mapping file paths to their content (fallback when files are not on disk).

Returns:
JSON string containing scan results and any secrets found
JSON string with a "summary" field (human-readable violation count) plus full scan results.
"""
return await _cycode_scan_tool(ScanTypeOption.SECRET, files)
return await _cycode_scan_tool(ScanTypeOption.SECRET, files=files, paths=paths)


async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
"""Scan files for Software Composition Analysis (SCA) - vulnerabilities and license issues.
async def cycode_sca_scan(
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
) -> str:
"""Scan for Software Composition Analysis (SCA) - vulnerabilities and license issues.

Use this tool when you need to:
- scan dependencies for known security vulnerabilities
Expand All @@ -242,19 +318,24 @@ async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:

Important:
You must also include lock files (like package-lock.json, Pipfile.lock, etc.) to get accurate results.
You must provide manifest and lock files together.
When using "paths", pass the directory containing both manifest and lock files.
When using "files", provide both manifest and lock files together.

Args:
files: Dictionary mapping file paths to their content
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
files: Dictionary mapping file paths to their content (fallback when files are not on disk).

Returns:
JSON string containing scan results, vulnerabilities, and license issues found
JSON string with a "summary" field (human-readable violation count) plus full scan results.
"""
return await _cycode_scan_tool(ScanTypeOption.SCA, files)
return await _cycode_scan_tool(ScanTypeOption.SCA, files=files, paths=paths)


async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
"""Scan files for Infrastructure as Code (IaC) misconfigurations.
async def cycode_iac_scan(
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
) -> str:
"""Scan for Infrastructure as Code (IaC) misconfigurations.

Use this tool when you need to:
- scan Terraform, CloudFormation, Kubernetes YAML files
Expand All @@ -264,16 +345,20 @@ async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
- review Docker files for security issues

Args:
files: Dictionary mapping file paths to their content
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
files: Dictionary mapping file paths to their content (fallback when files are not on disk).

Returns:
JSON string containing scan results and any misconfigurations found
JSON string with a "summary" field (human-readable violation count) plus full scan results.
"""
return await _cycode_scan_tool(ScanTypeOption.IAC, files)
return await _cycode_scan_tool(ScanTypeOption.IAC, files=files, paths=paths)


async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
"""Scan files for Static Application Security Testing (SAST) - code quality and security flaws.
async def cycode_sast_scan(
paths: Optional[list[str]] = _PATHS_TOOL_FIELD,
files: Optional[dict[str, str]] = _FILES_TOOL_FIELD,
) -> str:
"""Scan for Static Application Security Testing (SAST) - code quality and security flaws.

Use this tool when you need to:
- scan source code for security vulnerabilities
Expand All @@ -283,12 +368,13 @@ async def cycode_sast_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str:
- find SQL injection, XSS, and other application security issues

Args:
files: Dictionary mapping file paths to their content
paths: File or directory paths on disk to scan (preferred). Directories are scanned recursively.
files: Dictionary mapping file paths to their content (fallback when files are not on disk).

Returns:
JSON string containing scan results and any security flaws found
JSON string with a "summary" field (human-readable violation count) plus full scan results.
"""
return await _cycode_scan_tool(ScanTypeOption.SAST, files)
return await _cycode_scan_tool(ScanTypeOption.SAST, files=files, paths=paths)


async def cycode_status() -> str:
Expand Down
Loading
Loading