Skip to content

Commit c725d60

Browse files
MaorDavidzonclaude
andcommitted
Remove MCP server changes, keep CLI-only scope
Revert MCP command to original, remove fastmcp/httpx deps, remove async_auth_client. MCP server enhancement will be a separate PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6f24356 commit c725d60

File tree

6 files changed

+55
-1247
lines changed

6 files changed

+55
-1247
lines changed

cycode/cli/apps/api/async_auth_client.py

Lines changed: 0 additions & 124 deletions
This file was deleted.

cycode/cli/apps/api/openapi_spec.py

Lines changed: 1 addition & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
import json
44
import os
5-
import re
65
import time
76
from pathlib import Path
8-
from typing import Any, Optional
7+
from typing import Optional
98

109
import requests
1110

@@ -22,13 +21,6 @@
2221

2322
_OPENAPI_SPEC_PATH = '/v4/api-docs/cycode-api-swagger.json'
2423

25-
# MCP has a 60-character limit for combined server name + tool name
26-
_MCP_SERVER_NAME = 'cycode'
27-
_MAX_TOOL_NAME_LENGTH = 60 - len(_MCP_SERVER_NAME) - 1 # -1 for the colon separator
28-
29-
# Pattern to match parenthetical deprecation notices
30-
_DEPRECATION_PATTERN = re.compile(r'\s*\([^)]*(?:please use|deprecated|use the new)[^)]*\)', re.IGNORECASE)
31-
3224

3325
def get_openapi_spec(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> dict:
3426
"""Get the OpenAPI spec, using cache if fresh, otherwise fetching from API.
@@ -146,95 +138,6 @@ def _cache_spec(spec: dict) -> None:
146138
logger.warning('Failed to cache OpenAPI spec: %s', e)
147139

148140

149-
def filter_openapi_get_only(spec: dict) -> dict:
150-
"""Filter OpenAPI spec to include only GET operations (read-only mode)."""
151-
filtered = spec.copy()
152-
if 'paths' in filtered:
153-
filtered['paths'] = {
154-
path: {'get': methods['get']} for path, methods in filtered['paths'].items() if 'get' in methods
155-
}
156-
return filtered
157-
158-
159-
def _path_to_snake_case(path: str, method: str) -> str:
160-
"""Convert an API path to a snake_case operation ID.
161-
162-
GET /v4/projects -> list_projects
163-
GET /v4/projects/{projectId} -> get_project
164-
GET /v4/scans/statistics/cli/scan-count -> get_cli_scan_count
165-
"""
166-
# Strip /v4/ prefix
167-
clean = re.sub(r'^/v\d+/', '', path)
168-
169-
# Remove path parameter placeholders
170-
has_path_param = '{' in clean
171-
clean = re.sub(r'/\{[^}]+\}', '', clean)
172-
173-
# Convert to snake_case: hyphens, slashes, dots -> underscores
174-
clean = re.sub(r'[^a-zA-Z0-9]+', '_', clean).strip('_')
175-
176-
# Convert camelCase segments to snake_case
177-
clean = re.sub(r'([a-z])([A-Z])', r'\1_\2', clean).lower()
178-
179-
# Collapse multiple underscores
180-
clean = re.sub(r'_+', '_', clean)
181-
182-
# Choose prefix based on whether this is a collection or single resource
183-
prefix = ('get' if has_path_param else 'list') if method == 'get' else method
184-
185-
name = f'cycode_{prefix}_{clean}'
186-
187-
# Truncate to fit MCP limit
188-
if len(name) > _MAX_TOOL_NAME_LENGTH:
189-
name = name[:_MAX_TOOL_NAME_LENGTH].rstrip('_')
190-
191-
return name
192-
193-
194-
def normalize_tool_names(spec: dict) -> dict:
195-
"""Generate proper snake_case operationId values for MCP tool names.
196-
197-
Derives names from the URL path and HTTP method, following MCP conventions:
198-
- list_* for collection endpoints (GET without path params)
199-
- get_* for single resource endpoints (GET with path params)
200-
"""
201-
if 'paths' not in spec:
202-
return spec
203-
204-
original_paths = spec['paths']
205-
spec = spec.copy()
206-
spec['paths'] = {}
207-
used_names: dict[str, int] = {}
208-
209-
for path, methods in original_paths.items():
210-
spec['paths'][path] = {}
211-
for method, details in methods.items():
212-
details = details.copy()
213-
name = _path_to_snake_case(path, method)
214-
215-
# Handle duplicates with _v2, _v3, etc.
216-
if name in used_names:
217-
used_names[name] += 1
218-
name = f'{name}_v{used_names[name]}'
219-
else:
220-
used_names[name] = 1
221-
222-
details['operationId'] = name
223-
spec['paths'][path][method] = details
224-
225-
return spec
226-
227-
228-
def disable_output_validation(route: Any, component: Any) -> None:
229-
"""Disable output schema validation for MCP tools.
230-
231-
The Cycode API returns additional fields not defined in the OpenAPI spec,
232-
causing MCP output validation to fail.
233-
"""
234-
if hasattr(component, 'output_schema'):
235-
component.output_schema = None
236-
237-
238141
def parse_spec_commands(spec: dict) -> dict[str, list[dict]]:
239142
"""Parse OpenAPI spec into resource groups with their endpoints.
240143

cycode/cli/apps/mcp/mcp_command.py

Lines changed: 19 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from cycode.logger import LoggersManager, get_logger
1717

1818
try:
19-
from fastmcp import FastMCP
20-
from fastmcp.tools import Tool
19+
from mcp.server.fastmcp import FastMCP
20+
from mcp.server.fastmcp.tools import Tool
2121
except ImportError:
2222
raise ImportError(
2323
'Cycode MCP is not supported for your Python version. MCP support requires Python 3.10 or higher.'
@@ -404,74 +404,30 @@ async def cycode_status() -> str:
404404
return json.dumps({'error': f'Status check failed: {e!s}'}, indent=2)
405405

406406

407-
_SCAN_TOOLS = [cycode_status, cycode_secret_scan, cycode_sca_scan, cycode_iac_scan, cycode_sast_scan]
408-
409-
410-
def _create_scan_only_mcp_server(host: str, port: int) -> FastMCP:
411-
"""Create MCP server with scan tools only (fallback when API spec unavailable)."""
412-
tools = [Tool.from_function(fn) for fn in _SCAN_TOOLS]
413-
_logger.info('Creating MCP server with scan tools only: %s', [t.name for t in tools])
407+
def _create_mcp_server(host: str, port: int) -> FastMCP:
408+
"""Create and configure the MCP server."""
409+
tools = [
410+
Tool.from_function(cycode_status),
411+
Tool.from_function(cycode_secret_scan),
412+
Tool.from_function(cycode_sca_scan),
413+
Tool.from_function(cycode_iac_scan),
414+
Tool.from_function(cycode_sast_scan),
415+
]
416+
_logger.info('Creating MCP server with tools: %s', [tool.name for tool in tools])
414417
return FastMCP(
415418
'cycode',
416419
tools=tools,
420+
host=host,
421+
port=port,
422+
debug=_is_debug_mode(),
423+
log_level='DEBUG' if _is_debug_mode() else 'INFO',
417424
)
418425

419426

420-
async def _create_mcp_server_with_api(host: str, port: int) -> FastMCP:
421-
"""Create MCP server with both API v4 tools and scan tools."""
422-
from cycode.cli.apps.api.async_auth_client import AsyncTokenManager, CycodeAsyncAuthClient
423-
from cycode.cli.apps.api.openapi_spec import (
424-
disable_output_validation,
425-
filter_openapi_get_only,
426-
get_openapi_spec,
427-
normalize_tool_names,
428-
)
429-
430-
# Load and filter OpenAPI spec
431-
spec = get_openapi_spec()
432-
spec = filter_openapi_get_only(spec)
433-
spec = normalize_tool_names(spec)
434-
435-
# Create async auth client
436-
token_manager = AsyncTokenManager()
437-
client = CycodeAsyncAuthClient(token_manager)
438-
439-
# Count endpoints for logging
440-
endpoint_count = sum(len(methods) for methods in spec.get('paths', {}).values())
441-
_logger.info('Creating MCP server with %d API endpoints + scan tools', endpoint_count)
442-
443-
# Create server from OpenAPI (API v4 tools - experimental)
444-
server = FastMCP.from_openapi(
445-
openapi_spec=spec,
446-
client=client,
447-
name='cycode',
448-
mcp_component_fn=disable_output_validation,
449-
instructions='Cycode API v4 tools are experimental and may change.',
450-
)
451-
452-
# Add scan tools on top
453-
for fn in _SCAN_TOOLS:
454-
server.add_tool(Tool.from_function(fn))
455-
456-
return server
457-
458-
459427
def _run_mcp_server(transport: McpTransportOption, host: str, port: int) -> None:
460428
"""Run the MCP server using transport."""
461-
try:
462-
# Try to create server with API v4 tools
463-
server = asyncio.run(_create_mcp_server_with_api(host, port))
464-
_logger.info('MCP server created with API v4 tools and scan tools')
465-
except Exception as e:
466-
# Fall back to scan-only server if API spec is unavailable
467-
_logger.warning('Could not load API v4 tools, falling back to scan-only: %s', e)
468-
server = _create_scan_only_mcp_server(host, port)
469-
470-
run_kwargs = {'transport': str(transport)}
471-
if str(transport) != 'stdio':
472-
run_kwargs['host'] = host
473-
run_kwargs['port'] = port
474-
server.run(**run_kwargs) # type: ignore[arg-type]
429+
mcp = _create_mcp_server(host, port)
430+
mcp.run(transport=str(transport)) # type: ignore[arg-type]
475431

476432

477433
def mcp_command(
@@ -499,19 +455,13 @@ def mcp_command(
499455
) -> None:
500456
""":robot: Start the Cycode MCP (Model Context Protocol) server.
501457
502-
The MCP server provides tools for:
503-
504-
**Scanning:**
458+
The MCP server provides tools for scanning code with Cycode CLI:
505459
- cycode_secret_scan: Scan for hardcoded secrets
506460
- cycode_sca_scan: Software Composition Analysis scanning
507461
- cycode_iac_scan: Infrastructure as Code scanning
508462
- cycode_sast_scan: Static Application Security Testing scanning
509463
- cycode_status: Get Cycode CLI status (version, auth status) and configuration
510464
511-
**Cycode API v4** (when authenticated):
512-
- 100+ read-only tools auto-generated from the Cycode API
513-
- Projects, compliance, members, scans, workflows, and more
514-
515465
Examples:
516466
cycode mcp # Start with default transport (stdio)
517467
cycode mcp -t sse -p 8080 # Start with Server-Sent Events (SSE) transport on port 8080

0 commit comments

Comments
 (0)