From b114f8b4a073aa7c11b6c6250f8792d12219034c Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Mon, 9 Jun 2025 17:32:00 +0200 Subject: [PATCH 01/10] CM-49113 - Add Cycode MCP (Model Context Protocol) --- README.md | 209 +++++++++++++- cycode/cli/app.py | 7 + cycode/cli/apps/mcp/__init__.py | 14 + cycode/cli/apps/mcp/mcp_command.py | 335 ++++++++++++++++++++++ cycode/cli/cli_types.py | 6 + poetry.lock | 430 ++++++++++++++++++++++++++++- pyproject.toml | 2 + 7 files changed, 992 insertions(+), 11 deletions(-) create mode 100644 cycode/cli/apps/mcp/__init__.py create mode 100644 cycode/cli/apps/mcp/mcp_command.py diff --git a/README.md b/README.md index 13e23a6f..081f3bf5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,12 @@ This guide walks you through both installation and usage. 2. [On Windows](#on-windows) 2. [Install Pre-Commit Hook](#install-pre-commit-hook) 3. [Cycode CLI Commands](#cycode-cli-commands) -4. [Scan Command](#scan-command) +4. [MCP Command](#mcp-command) + 1. [Starting the MCP Server](#starting-the-mcp-server) + 2. [Available Options](#available-options) + 3. [MCP Tools](#mcp-tools) + 4. [Usage Examples](#usage-examples) +5. [Scan Command](#scan-command) 1. [Running a Scan](#running-a-scan) 1. [Options](#options) 1. [Severity Threshold](#severity-option) @@ -48,9 +53,9 @@ This guide walks you through both installation and usage. 4. [Ignoring a Secret, IaC, or SCA Rule](#ignoring-a-secret-iac-sca-or-sast-rule) 5. [Ignoring a Package](#ignoring-a-package) 6. [Ignoring via a config file](#ignoring-via-a-config-file) -5. [Report command](#report-command) +6. [Report command](#report-command) 1. [Generating SBOM Report](#generating-sbom-report) -6. [Syntax Help](#syntax-help) +7. [Syntax Help](#syntax-help) # Prerequisites @@ -221,7 +226,7 @@ Perform the following steps to install the pre-commit hook: ```yaml repos: - repo: https://github.com/cycodehq/cycode-cli - rev: v3.0.0 + rev: v3.1.0 hooks: - id: cycode stages: @@ -233,7 +238,7 @@ Perform the following steps to install the pre-commit hook: ```yaml repos: - repo: https://github.com/cycodehq/cycode-cli - rev: v3.0.0 + rev: v3.1.0 hooks: - id: cycode stages: @@ -281,10 +286,204 @@ The following are the options and commands available with the Cycode CLI applica | [auth](#using-the-auth-command) | Authenticate your machine to associate the CLI with your Cycode account. | | [configure](#using-the-configure-command) | Initial command to configure your CLI client authentication. | | [ignore](#ignoring-scan-results) | Ignores a specific value, path or rule ID. | +| [mcp](#mcp-command) | Start the Model Context Protocol (MCP) server to enable AI integration with Cycode scanning capabilities. | | [scan](#running-a-scan) | Scan the content for Secrets/IaC/SCA/SAST violations. You`ll need to specify which scan type to perform: commit-history/path/repository/etc. | | [report](#report-command) | Generate report. You`ll need to specify which report type to perform as SBOM. | | status | Show the CLI status and exit. | +# MCP Command + +The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools through a standardized protocol. + +> [!TIP] +> For the best experience, install Cycode CLI globally on your system using `pip install cycode` or `brew install cycode`, then authenticate once with `cycode auth`. After global installation and authentication, you won't need to configure `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` environment variables in your MCP configuration files. + +## Starting the MCP Server + +To start the MCP server, use the following command: + +```bash +cycode mcp +``` + +By default, this starts the server using the `stdio` transport, which is suitable for local integrations and AI applications that can spawn subprocess. + +### Available Options + +| Option | Description | +|-------------------|--------------------------------------------------------------------------------------------| +| `-t, --transport` | Transport type for the MCP server: `stdio`, `sse`, or `streamable-http` (default: `stdio`) | +| `-H, --host` | Host address to bind the server (used only for non stdio transport) (default: `127.0.0.1`) | +| `-p, --port` | Port number to bind the server (used only for non stdio transport) (default: `8000`) | +| `--help` | Show help message and available options | + +### MCP Tools + +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_status` | Get Cycode CLI version, authentication status, and configuration information | + +### Usage Examples + +#### Basic Command Examples + +Start the MCP server with default settings (stdio transport): +```bash +cycode mcp +``` + +Start the MCP server with explicit stdio transport: +```bash +cycode mcp -t stdio +``` + +Start the MCP server with Server-Sent Events (SSE) transport: +```bash +cycode mcp -t sse -p 8080 +``` + +Start the MCP server with streamable HTTP transport on custom host and port: +```bash +cycode mcp -t streamable-http -H 0.0.0.0 -p 9000 +``` + +Learn more about MCP Transport types in the [MCP Protocol Specification – Transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports). + +#### Configuration Examples + +##### Using MCP with Cursor/Claude Desktop/etc (mcp.json) + +> [!NOTE] +> 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`). + +For **stdio transport** (direct execution): +```json +{ + "mcpServers": { + "cycode": { + "command": "cycode", + "args": ["mcp"], + "env": { + "CYCODE_CLIENT_ID": "your-cycode-id", + "CYCODE_CLIENT_SECRET": "your-cycode-secret-key", + "CYCODE_API_URL": "https://api.cycode.com", + "CYCODE_APP_URL": "https://app.cycode.com" + } + } + } +} +``` + +For **stdio transport** with `pipx` installation: +```json +{ + "mcpServers": { + "cycode": { + "command": "pipx", + "args": ["run", "cycode", "mcp"], + "env": { + "CYCODE_CLIENT_ID": "your-cycode-id", + "CYCODE_CLIENT_SECRET": "your-cycode-secret-key", + "CYCODE_API_URL": "https://api.cycode.com", + "CYCODE_APP_URL": "https://app.cycode.com" + } + } + } +} +``` + +For **stdio transport** with `uvx` installation: +```json +{ + "mcpServers": { + "cycode": { + "command": "uvx", + "args": ["cycode", "mcp"], + "env": { + "CYCODE_CLIENT_ID": "your-cycode-id", + "CYCODE_CLIENT_SECRET": "your-cycode-secret-key", + "CYCODE_API_URL": "https://api.cycode.com", + "CYCODE_APP_URL": "https://app.cycode.com" + } + } + } +} +``` + +For **SSE transport** (Server-Sent Events): +```json +{ + "mcpServers": { + "cycode": { + "url": "http://127.0.0.1:8000/sse" + } + } +} +``` + +For **SSE transport** on custom port: +```json +{ + "mcpServers": { + "cycode": { + "url": "http://127.0.0.1:8080/sse" + } + } +} +``` + +For **streamable HTTP transport**: +```json +{ + "mcpServers": { + "cycode": { + "url": "http://127.0.0.1:8000/mcp" + } + } +} +``` + +##### Running MCP Server in Background + +For **SSE transport** (start server first, then configure client): +```bash +# Start the MCP server in background +cycode mcp -t sse -p 8000 & + +# Configure in mcp.json +{ + "mcpServers": { + "cycode": { + "url": "http://127.0.0.1:8000/sse" + } + } +} +``` + +For **streamable HTTP transport**: +```bash +# Start the MCP server in background +cycode mcp -t streamable-http -H 0.0.0.0 -p 9000 & + +# Configure in mcp.json +{ + "mcpServers": { + "cycode": { + "url": "http://0.0.0.0:9000/mcp" + } + } +} +``` + +> [!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. + # Scan Command ## Running a Scan diff --git a/cycode/cli/app.py b/cycode/cli/app.py index 2ae004a6..1b13ebf2 100644 --- a/cycode/cli/app.py +++ b/cycode/cli/app.py @@ -1,4 +1,5 @@ import logging +import sys from typing import Annotated, Optional import typer @@ -9,6 +10,10 @@ from cycode import __version__ from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, scan, status + +if sys.version_info >= (3, 10): + from cycode.cli.apps import mcp + from cycode.cli.cli_types import OutputTypeOption from cycode.cli.consts import CLI_CONTEXT_SETTINGS from cycode.cli.printers import ConsolePrinter @@ -47,6 +52,8 @@ app.add_typer(report.app) app.add_typer(scan.app) app.add_typer(status.app) +if sys.version_info >= (3, 10): + app.add_typer(mcp.app) def check_latest_version_on_close(ctx: typer.Context) -> None: diff --git a/cycode/cli/apps/mcp/__init__.py b/cycode/cli/apps/mcp/__init__.py new file mode 100644 index 00000000..8ec71d93 --- /dev/null +++ b/cycode/cli/apps/mcp/__init__.py @@ -0,0 +1,14 @@ +import typer + +from cycode.cli.apps.mcp.mcp_command import mcp_command + +app = typer.Typer() + +_mcp_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#mcp-command' +_mcp_command_epilog = f'[bold]Documentation:[/] [link={_mcp_command_docs}]{_mcp_command_docs}[/link]' + +app.command( + name='mcp', + short_help='Start the Cycode MCP (Model Context Protocol) server.', + epilog=_mcp_command_epilog, +)(mcp_command) diff --git a/cycode/cli/apps/mcp/mcp_command.py b/cycode/cli/apps/mcp/mcp_command.py new file mode 100644 index 00000000..26b571cd --- /dev/null +++ b/cycode/cli/apps/mcp/mcp_command.py @@ -0,0 +1,335 @@ +import asyncio +import json +import os +import sys +import tempfile +import uuid +from pathlib import Path +from typing import Annotated, Any + +import typer +from pydantic import Field + +from cycode.cli.cli_types import McpTransportOption, ScanTypeOption +from cycode.cli.utils.sentry import add_breadcrumb + +try: + from mcp.server.fastmcp import FastMCP + from mcp.server.fastmcp.tools import Tool +except ImportError: + raise ImportError( + 'Cycode MCP is not supported for your Python version. MCP support requires Python 3.10 or higher.' + ) from None + + +from cycode.logger import get_logger + +_logger = get_logger('Cycode MCP') + +_DEFAULT_RUN_COMMAND_TIMEOUT = 5 * 60 + +_FILES_TOOL_FIELD = Field(description='Files to scan, mapping file paths to their content') + + +def _get_current_executable() -> str: + """Get the current executable path for spawning subprocess.""" + if getattr(sys, 'frozen', False): # pyinstaller bundle + return sys.executable + + return 'cycode' + + +async def _run_cycode_command(*args: str, timeout: int = _DEFAULT_RUN_COMMAND_TIMEOUT) -> dict[str, Any]: + """Run a cycode command asynchronously and return the parsed result. + + Args: + *args: Command arguments to append after 'cycode -o json' + timeout: Timeout in seconds (default 5 minutes) + + Returns: + Dictionary containing the parsed JSON result or error information + """ + cmd_args = [_get_current_executable(), '-o', 'json', *list(args)] + _logger.debug('Running Cycode CLI command: %s', ' '.join(cmd_args)) + + try: + process = await asyncio.create_subprocess_exec( + *cmd_args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) + stdout_str = stdout.decode('UTF-8', errors='replace') if stdout else '' + stderr_str = stderr.decode('UTF-8', errors='replace') if stderr else '' + + if not stdout_str: + return {'error': 'No output from command', 'stderr': stderr_str, 'returncode': process.returncode} + + try: + return json.loads(stdout_str) + except json.JSONDecodeError: + return { + 'error': 'Failed to parse JSON output', + 'stdout': stdout_str, + 'stderr': stderr_str, + 'returncode': process.returncode, + } + except asyncio.TimeoutError: + return {'error': f'Command timeout after {timeout} seconds'} + except Exception as e: + return {'error': f'Failed to run command: {e!s}'} + + +def _create_temp_files(files_content: dict[str, str]) -> list[str]: + """Create temporary files from content and return their paths.""" + temp_dir = tempfile.mkdtemp(prefix='cycode_mcp_') + temp_files = [] + + _logger.debug('Creating temporary files in directory: %s', temp_dir) + + for file_path, content in files_content.items(): + safe_filename = f'{uuid.uuid4().hex}_{Path(file_path).name}' + temp_file_path = os.path.join(temp_dir, safe_filename) + + os.makedirs(os.path.dirname(temp_file_path), exist_ok=True) + + _logger.debug('Creating temp file: %s', temp_file_path) + with open(temp_file_path, 'w', encoding='UTF-8') as f: + f.write(content) + + temp_files.append(temp_file_path) + + return temp_files + + +def _cleanup_temp_files(temp_files: list[str]) -> None: + """Clean up temporary files and directories.""" + + temp_dirs = set() + for temp_file in temp_files: + try: + if os.path.exists(temp_file): + _logger.debug('Removing temp file: %s', temp_file) + os.remove(temp_file) + temp_dirs.add(os.path.dirname(temp_file)) + except OSError as e: + _logger.warning('Failed to remove temp file %s: %s', temp_file, e) + + for temp_dir in temp_dirs: + try: + if os.path.exists(temp_dir) and not os.listdir(temp_dir): + _logger.debug('Removing temp directory: %s', temp_dir) + os.rmdir(temp_dir) + except OSError as e: + _logger.warning('Failed to remove temp directory %s: %s', temp_dir, e) + + +async def _run_cycode_scan(scan_type: ScanTypeOption, temp_files: list[str]) -> dict[str, Any]: + """Run cycode scan command and return the result.""" + args = ['scan', '-t', str(scan_type), 'path', *temp_files] + return await _run_cycode_command(*args) + + +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_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str: + """Scan files 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 + + Returns: + JSON string containing scan results and any secrets found + """ + _logger.info('Secret scan tool called') + + if not files: + return json.dumps({'error': 'No files provided'}) + + temp_files = _create_temp_files(files) + + try: + result = await _run_cycode_scan(ScanTypeOption.SECRET, temp_files) + return json.dumps(result, indent=2) + finally: + _cleanup_temp_files(temp_files) + + +async def cycode_sca_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str: + """Scan files for Software Composition Analysis (SCA) - vulnerabilities and license issues. + + Use this tool when you need to: + - scan dependencies for known security vulnerabilities + - check for license compliance issues + - analyze third-party component risks + - verify software supply chain security + - review package.json, requirements.txt, pom.xml and other dependency files + + Args: + files: Dictionary mapping file paths to their content + + Returns: + JSON string containing scan results, vulnerabilities, and license issues found + """ + _logger.info('SCA scan tool called') + + if not files: + return json.dumps({'error': 'No files provided'}) + + temp_files = _create_temp_files(files) + + try: + result = await _run_cycode_scan(ScanTypeOption.SCA, temp_files) + return json.dumps(result, indent=2) + finally: + _cleanup_temp_files(temp_files) + + +async def cycode_iac_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str: + """Scan files for Infrastructure as Code (IaC) misconfigurations. + + Use this tool when you need to: + - scan Terraform, CloudFormation, Kubernetes YAML files + - check for cloud security misconfigurations + - verify infrastructure compliance and best practices + - detect potential security issues in infrastructure definitions + - review Docker files for security issues + + Args: + files: Dictionary mapping file paths to their content + + Returns: + JSON string containing scan results and any misconfigurations found + """ + _logger.info('IaC scan tool called') + + if not files: + return json.dumps({'error': 'No files provided'}) + + temp_files = _create_temp_files(files) + + try: + result = await _run_cycode_scan(ScanTypeOption.IAC, temp_files) + return json.dumps(result, indent=2) + finally: + _cleanup_temp_files(temp_files) + + +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. + + Use this tool when you need to: + - scan source code for security vulnerabilities + - detect code quality issues and potential bugs + - check for insecure coding practices + - verify code follows security best practices + - find SQL injection, XSS, and other application security issues + + Args: + files: Dictionary mapping file paths to their content + + Returns: + JSON string containing scan results and any security flaws found + """ + _logger.info('SAST scan tool called') + + if not files: + return json.dumps({'error': 'No files provided'}) + + temp_files = _create_temp_files(files) + + try: + result = await _run_cycode_scan(ScanTypeOption.SAST, temp_files) + return json.dumps(result, indent=2) + finally: + _cleanup_temp_files(temp_files) + + +async def cycode_status() -> str: + """Get Cycode CLI version, authentication status, and configuration information. + + Use this tool when you need to: + - verify Cycode CLI is properly configured + - check authentication status + - get CLI version information + - troubleshoot setup issues + - confirm service connectivity + + Returns: + JSON string containing CLI status, version, and configuration details + """ + _logger.info('Status tool called') + + result = await _run_cycode_status() + return json.dumps(result, indent=2) + + +def _create_mcp_server(host: str = '127.0.0.1', port: int = 8000) -> FastMCP: + """Create and configure the MCP server.""" + tools = [ + Tool.from_function(cycode_status), + Tool.from_function(cycode_secret_scan), + Tool.from_function(cycode_sca_scan), + Tool.from_function(cycode_iac_scan), + Tool.from_function(cycode_sast_scan), + ] + _logger.info('Creating MCP server with tools: %s', [tool.name for tool in tools]) + return FastMCP('cycode', tools=tools, host=host, port=port) + + +def _run_mcp_server(transport: McpTransportOption, host: str, port: int) -> None: + """Run the MCP server using transport.""" + mcp = _create_mcp_server(host, port) + mcp.run(transport=str(transport)) # type: ignore[arg-type] + + +def mcp_command( + transport: Annotated[ + McpTransportOption, + typer.Option( + '--transport', + '-t', + case_sensitive=False, + help='Transport type for the MCP server.', + ), + ] = McpTransportOption.STDIO, + host: str = typer.Option( + '127.0.0.1', + '--host', + '-H', + help='Host address to bind the server (used only for non stdio transport).', + ), + port: int = typer.Option( + 8000, + '--port', + '-p', + help='Port number to bind the server (used only for non stdio transport).', + ), +) -> None: + """:robot: Start the Cycode MCP (Model Context Protocol) server. + + The MCP server provides tools for scanning code with Cycode CLI: + - cycode_secret_scan: Scan for hardcoded secrets + - cycode_sca_scan: Software Composition Analysis scanning + - cycode_iac_scan: Infrastructure as Code scanning + - cycode_sast_scan: Static Application Security Testing scanning + - cycode_status: Get Cycode CLI status (version, auth status) and configuration + + Examples: + cycode mcp # Start with default transport (stdio) + cycode mcp -t sse -p 8080 # Start with Server-Sent Events (SSE) transport on port 8080 + """ + add_breadcrumb('mcp') + + try: + _run_mcp_server(transport, host, port) + except Exception as e: + _logger.error('MCP server error', exc_info=e) + raise typer.Exit(1) from e diff --git a/cycode/cli/cli_types.py b/cycode/cli/cli_types.py index c2fa12a2..a5d7f9d9 100644 --- a/cycode/cli/cli_types.py +++ b/cycode/cli/cli_types.py @@ -8,6 +8,12 @@ def __str__(self) -> str: return self.value +class McpTransportOption(StrEnum): + STDIO = 'stdio' + SSE = 'sse' + STREAMABLE_HTTP = 'streamable-http' + + class OutputTypeOption(StrEnum): RICH = 'rich' TEXT = 'text' diff --git a/poetry.lock b/poetry.lock index 65e6a971..57958c82 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "altgraph" @@ -13,6 +13,42 @@ files = [ {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +trio = ["trio (>=0.26.1)"] + [[package]] name = "arrow" version = "1.3.0" @@ -295,12 +331,12 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" +groups = ["main", "test"] files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] +markers = {main = "python_version == \"3.10\"", test = "python_version < \"3.11\""} [package.extras] test = ["pytest (>=6)"] @@ -339,6 +375,81 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + [[package]] name = "idna" version = "3.10" @@ -361,7 +472,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["executable"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, @@ -452,6 +563,35 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] tests = ["pytest", "pytz", "simplejson"] +[[package]] +name = "mcp" +version = "1.9.3" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9"}, + {file = "mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27" +httpx-sse = ">=0.4" +pydantic = ">=2.7.2,<3.0.0" +pydantic-settings = ">=2.5.2" +python-multipart = ">=0.0.9" +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""} + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] +rich = ["rich (>=13.9.4)"] +ws = ["websockets (>=15.0.1)"] + [[package]] name = "mdurl" version = "0.1.2" @@ -533,6 +673,165 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pydantic" +version = "2.11.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, + {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, + {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyfakefs" version = "5.7.4" @@ -687,6 +986,35 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + [[package]] name = "pywin32-ctypes" version = "0.2.3" @@ -969,6 +1297,60 @@ files = [ {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sse-starlette" +version = "2.3.6" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760"}, + {file = "sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3"}, +] + +[package.dependencies] +anyio = ">=4.7.0" + +[package.extras] +daphne = ["daphne (>=4.2.0)"] +examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio,examples] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"] +granian = ["granian (>=2.3.1)"] +uvicorn = ["uvicorn (>=0.34.0)"] + +[[package]] +name = "starlette" +version = "0.47.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37"}, + {file = "starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tenacity" version = "9.0.0" @@ -1082,6 +1464,21 @@ files = [ {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "1.26.19" @@ -1099,6 +1496,27 @@ brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and p secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "uvicorn" +version = "0.34.3" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and sys_platform != \"emscripten\"" +files = [ + {file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, + {file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "zipp" version = "3.21.0" @@ -1106,7 +1524,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["executable"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, @@ -1123,4 +1541,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "14f258101aa534aadfc871aa5082ad773aa99873587c21c0598567435bfa5d9a" +content-hash = "c381f7fa685c2eb33c68f23610cfa6edd7d51661003d533d73ec5f66457ceb96" diff --git a/pyproject.toml b/pyproject.toml index 755d8207..6537304c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ rich = ">=13.9.4, <14" patch-ng = "1.18.1" typer = "^0.15.3" tenacity = ">=9.0.0,<9.1.0" +mcp = { version = ">=1.9.3,<2.0.0", markers = "python_version >= '3.10'" } +pydantic = ">=2.11.5,<3.0.0" [tool.poetry.group.test.dependencies] mock = ">=4.0.3,<4.1.0" From e44f7f54f27fe19ae54d81f26d398ab19f8222ba Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 10 Jun 2025 15:39:40 +0200 Subject: [PATCH 02/10] improve troubleshooting --- README.md | 33 +++++++- cycode/cli/apps/mcp/mcp_command.py | 123 +++++++++++++++-------------- cycode/logger.py | 15 +++- 3 files changed, 106 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 081f3bf5..d2645725 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ Learn more about MCP Transport types in the [MCP Protocol Specification – Tran #### Configuration Examples -##### Using MCP with Cursor/Claude Desktop/etc (mcp.json) +##### Using MCP with Cursor/VS Code/Claude Desktop/etc (mcp.json) > [!NOTE] > 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 & For **streamable HTTP transport**: ```bash # Start the MCP server in background -cycode mcp -t streamable-http -H 0.0.0.0 -p 9000 & +cycode mcp -t streamable-http -H 127.0.0.2 -p 9000 & # Configure in mcp.json { "mcpServers": { "cycode": { - "url": "http://0.0.0.0:9000/mcp" + "url": "http://127.0.0.2:9000/mcp" } } } @@ -484,6 +484,33 @@ cycode mcp -t streamable-http -H 0.0.0.0 -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. +### 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: + +1. Using the `-v` or `--verbose` flag: +```bash +cycode -v mcp +``` + +2. Using the `CYCODE_CLI_VERBOSE` environment variable: +```bash +CYCODE_CLI_VERBOSE=1 cycode mcp +``` + +The debug logs will show detailed information about: +- Server startup and configuration +- Connection attempts and status +- Tool execution and results +- Any errors or warnings that occur + +This information can be helpful when: +- Diagnosing connection issues +- Understanding why certain tools aren't working +- Identifying authentication problems +- Debugging transport-specific issues + + # Scan Command ## Running a Scan diff --git a/cycode/cli/apps/mcp/mcp_command.py b/cycode/cli/apps/mcp/mcp_command.py index 26b571cd..0dcef968 100644 --- a/cycode/cli/apps/mcp/mcp_command.py +++ b/cycode/cli/apps/mcp/mcp_command.py @@ -1,5 +1,6 @@ import asyncio import json +import logging import os import sys import tempfile @@ -12,6 +13,7 @@ from cycode.cli.cli_types import McpTransportOption, ScanTypeOption from cycode.cli.utils.sentry import add_breadcrumb +from cycode.logger import LoggersManager, get_logger try: from mcp.server.fastmcp import FastMCP @@ -22,8 +24,6 @@ ) from None -from cycode.logger import get_logger - _logger = get_logger('Cycode MCP') _DEFAULT_RUN_COMMAND_TIMEOUT = 5 * 60 @@ -31,6 +31,14 @@ _FILES_TOOL_FIELD = Field(description='Files to scan, mapping file paths to their content') +def _is_debug_mode() -> bool: + return LoggersManager.global_logging_level == logging.DEBUG + + +def _gen_random_id() -> str: + return uuid.uuid4().hex + + def _get_current_executable() -> str: """Get the current executable path for spawning subprocess.""" if getattr(sys, 'frozen', False): # pyinstaller bundle @@ -49,7 +57,8 @@ async def _run_cycode_command(*args: str, timeout: int = _DEFAULT_RUN_COMMAND_TI Returns: Dictionary containing the parsed JSON result or error information """ - cmd_args = [_get_current_executable(), '-o', 'json', *list(args)] + verbose = ['-v'] if _is_debug_mode() else [] + cmd_args = [_get_current_executable(), *verbose, '-o', 'json', *list(args)] _logger.debug('Running Cycode CLI command: %s', ' '.join(cmd_args)) try: @@ -61,6 +70,9 @@ async def _run_cycode_command(*args: str, timeout: int = _DEFAULT_RUN_COMMAND_TI stdout_str = stdout.decode('UTF-8', errors='replace') if stdout else '' stderr_str = stderr.decode('UTF-8', errors='replace') if stderr else '' + if _is_debug_mode(): # redirect debug output + sys.stderr.write(stderr_str) + if not stdout_str: return {'error': 'No output from command', 'stderr': stderr_str, 'returncode': process.returncode} @@ -87,7 +99,7 @@ def _create_temp_files(files_content: dict[str, str]) -> list[str]: _logger.debug('Creating temporary files in directory: %s', temp_dir) for file_path, content in files_content.items(): - safe_filename = f'{uuid.uuid4().hex}_{Path(file_path).name}' + safe_filename = f'{_gen_random_id()}_{Path(file_path).name}' temp_file_path = os.path.join(temp_dir, safe_filename) os.makedirs(os.path.dirname(temp_file_path), exist_ok=True) @@ -125,8 +137,7 @@ def _cleanup_temp_files(temp_files: list[str]) -> None: async def _run_cycode_scan(scan_type: ScanTypeOption, temp_files: list[str]) -> dict[str, Any]: """Run cycode scan command and return the result.""" - args = ['scan', '-t', str(scan_type), 'path', *temp_files] - return await _run_cycode_command(*args) + return await _run_cycode_command(*['scan', '-t', str(scan_type), 'path', *temp_files]) async def _run_cycode_status() -> dict[str, Any]: @@ -134,6 +145,31 @@ async def _run_cycode_status() -> dict[str, Any]: return await _run_cycode_command('status') +async def _cycode_scan_tool(scan_type: ScanTypeOption, files: dict[str, str] = _FILES_TOOL_FIELD) -> 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'}) + + temp_files = _create_temp_files(files) + + try: + _logger.info( + 'Running Cycode scan, %s', + {'scan_type': scan_type, 'files_count': len(temp_files), '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) + except Exception as e: + _logger.error('Scan failed, %s', {'scan_type': scan_type, 'call_id': _tool_call_id, 'error': str(e)}) + return json.dumps({'error': f'Scan failed: {e!s}'}, indent=2) + finally: + _cleanup_temp_files(temp_files) + + async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str: """Scan files for hardcoded secrets. @@ -148,18 +184,7 @@ async def cycode_secret_scan(files: dict[str, str] = _FILES_TOOL_FIELD) -> str: Returns: JSON string containing scan results and any secrets found """ - _logger.info('Secret scan tool called') - - if not files: - return json.dumps({'error': 'No files provided'}) - - temp_files = _create_temp_files(files) - - try: - result = await _run_cycode_scan(ScanTypeOption.SECRET, temp_files) - return json.dumps(result, indent=2) - finally: - _cleanup_temp_files(temp_files) + return await _cycode_scan_tool(ScanTypeOption.SECRET, files) 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: Returns: JSON string containing scan results, vulnerabilities, and license issues found """ - _logger.info('SCA scan tool called') - - if not files: - return json.dumps({'error': 'No files provided'}) - - temp_files = _create_temp_files(files) - - try: - result = await _run_cycode_scan(ScanTypeOption.SCA, temp_files) - return json.dumps(result, indent=2) - finally: - _cleanup_temp_files(temp_files) + return await _cycode_scan_tool(ScanTypeOption.SCA, files) 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: Returns: JSON string containing scan results and any misconfigurations found """ - _logger.info('IaC scan tool called') - - if not files: - return json.dumps({'error': 'No files provided'}) - - temp_files = _create_temp_files(files) - - try: - result = await _run_cycode_scan(ScanTypeOption.IAC, temp_files) - return json.dumps(result, indent=2) - finally: - _cleanup_temp_files(temp_files) + return await _cycode_scan_tool(ScanTypeOption.IAC, files) 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: Returns: JSON string containing scan results and any security flaws found """ - _logger.info('SAST scan tool called') - - if not files: - return json.dumps({'error': 'No files provided'}) - - temp_files = _create_temp_files(files) - - try: - result = await _run_cycode_scan(ScanTypeOption.SAST, temp_files) - return json.dumps(result, indent=2) - finally: - _cleanup_temp_files(temp_files) + return await _cycode_scan_tool(ScanTypeOption.SAST, files) async def cycode_status() -> str: @@ -265,13 +257,21 @@ async def cycode_status() -> str: Returns: JSON string containing CLI status, version, and configuration details """ + _tool_call_id = _gen_random_id() _logger.info('Status tool called') - result = await _run_cycode_status() - return json.dumps(result, indent=2) + try: + _logger.info('Running Cycode status check, %s', {'call_id': _tool_call_id}) + result = await _run_cycode_status() + _logger.info('Status check completed, %s', {'call_id': _tool_call_id}) + + return json.dumps(result, indent=2) + except Exception as e: + _logger.error('Status check failed, %s', {'call_id': _tool_call_id, 'error': str(e)}) + return json.dumps({'error': f'Status check failed: {e!s}'}, indent=2) -def _create_mcp_server(host: str = '127.0.0.1', port: int = 8000) -> FastMCP: +def _create_mcp_server(host: str, port: int) -> FastMCP: """Create and configure the MCP server.""" tools = [ Tool.from_function(cycode_status), @@ -281,7 +281,14 @@ def _create_mcp_server(host: str = '127.0.0.1', port: int = 8000) -> FastMCP: Tool.from_function(cycode_sast_scan), ] _logger.info('Creating MCP server with tools: %s', [tool.name for tool in tools]) - return FastMCP('cycode', tools=tools, host=host, port=port) + return FastMCP( + 'cycode', + tools=tools, + host=host, + port=port, + debug=_is_debug_mode(), + log_level='DEBUG' if _is_debug_mode() else 'INFO', + ) def _run_mcp_server(transport: McpTransportOption, host: str, port: int) -> None: diff --git a/cycode/logger.py b/cycode/logger.py index 0ec6023f..2fd44e4f 100644 --- a/cycode/logger.py +++ b/cycode/logger.py @@ -1,6 +1,6 @@ import logging import sys -from typing import NamedTuple, Optional, Union +from typing import ClassVar, NamedTuple, Optional, Union import click import typer @@ -42,10 +42,15 @@ class CreatedLogger(NamedTuple): control_level_in_runtime: bool -_CREATED_LOGGERS: set[CreatedLogger] = set() +class LoggersManager: + loggers: ClassVar[set[CreatedLogger]] = set() + global_logging_level: Optional[int] = None def get_logger_level() -> Optional[Union[int, str]]: + if LoggersManager.global_logging_level is not None: + return LoggersManager.global_logging_level + config_level = get_val_as_string(consts.LOGGING_LEVEL_ENV_VAR_NAME) return logging.getLevelName(config_level) @@ -54,12 +59,14 @@ def get_logger(logger_name: Optional[str] = None, control_level_in_runtime: bool new_logger = logging.getLogger(logger_name) new_logger.setLevel(get_logger_level()) - _CREATED_LOGGERS.add(CreatedLogger(logger=new_logger, control_level_in_runtime=control_level_in_runtime)) + LoggersManager.loggers.add(CreatedLogger(logger=new_logger, control_level_in_runtime=control_level_in_runtime)) return new_logger def set_logging_level(level: int) -> None: - for created_logger in _CREATED_LOGGERS: + LoggersManager.global_logging_level = level + + for created_logger in LoggersManager.loggers: if created_logger.control_level_in_runtime: created_logger.logger.setLevel(level) From e23734fa90844d868406500c4bc292f93628610c Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 10 Jun 2025 15:45:19 +0200 Subject: [PATCH 03/10] add python 3.10 warn --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2645725..796072c6 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,9 @@ The following are the options and commands available with the Cycode CLI applica # MCP Command +> [!WARNING] +> The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available. + The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools through a standardized protocol. > [!TIP] @@ -306,7 +309,7 @@ To start the MCP server, use the following command: cycode mcp ``` -By default, this starts the server using the `stdio` transport, which is suitable for local integrations and AI applications that can spawn subprocess. +By default, this starts the server using the `stdio` transport, which is suitable for local integrations and AI applications that can spawn subprocesses. ### Available Options From 3a6d55c02445a56ec6231205ff473dea69931d51 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 10 Jun 2025 17:02:14 +0200 Subject: [PATCH 04/10] mark MCP as EXPERIMENT --- README.md | 6 +++--- cycode/cli/apps/mcp/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 796072c6..e466aa70 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This guide walks you through both installation and usage. 2. [On Windows](#on-windows) 2. [Install Pre-Commit Hook](#install-pre-commit-hook) 3. [Cycode CLI Commands](#cycode-cli-commands) -4. [MCP Command](#mcp-command) +4. [MCP Command](#mcp-command-experiment) 1. [Starting the MCP Server](#starting-the-mcp-server) 2. [Available Options](#available-options) 3. [MCP Tools](#mcp-tools) @@ -286,12 +286,12 @@ The following are the options and commands available with the Cycode CLI applica | [auth](#using-the-auth-command) | Authenticate your machine to associate the CLI with your Cycode account. | | [configure](#using-the-configure-command) | Initial command to configure your CLI client authentication. | | [ignore](#ignoring-scan-results) | Ignores a specific value, path or rule ID. | -| [mcp](#mcp-command) | Start the Model Context Protocol (MCP) server to enable AI integration with Cycode scanning capabilities. | +| [mcp](#mcp-command-experiment) | Start the Model Context Protocol (MCP) server to enable AI integration with Cycode scanning capabilities. | | [scan](#running-a-scan) | Scan the content for Secrets/IaC/SCA/SAST violations. You`ll need to specify which scan type to perform: commit-history/path/repository/etc. | | [report](#report-command) | Generate report. You`ll need to specify which report type to perform as SBOM. | | status | Show the CLI status and exit. | -# MCP Command +# MCP Command \[EXPERIMENT\] > [!WARNING] > The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available. diff --git a/cycode/cli/apps/mcp/__init__.py b/cycode/cli/apps/mcp/__init__.py index 8ec71d93..fd328845 100644 --- a/cycode/cli/apps/mcp/__init__.py +++ b/cycode/cli/apps/mcp/__init__.py @@ -4,11 +4,11 @@ app = typer.Typer() -_mcp_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#mcp-command' +_mcp_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#mcp-command-experiment' _mcp_command_epilog = f'[bold]Documentation:[/] [link={_mcp_command_docs}]{_mcp_command_docs}[/link]' app.command( name='mcp', - short_help='Start the Cycode MCP (Model Context Protocol) server.', + short_help='[EXPERIMENT] Start the Cycode MCP (Model Context Protocol) server.', epilog=_mcp_command_epilog, )(mcp_command) From 08a0aa7c4ba7b6a4576f6106c0d51ac2ed08da17 Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 10 Jun 2025 17:30:22 +0200 Subject: [PATCH 05/10] fix CI by bumping windows image --- .github/workflows/build_executable.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_executable.yml b/.github/workflows/build_executable.yml index c9154d7d..87979685 100644 --- a/.github/workflows/build_executable.yml +++ b/.github/workflows/build_executable.yml @@ -15,12 +15,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-22.04, macos-13, macos-14, windows-2019 ] + os: [ ubuntu-22.04, macos-13, macos-14, windows-2022 ] mode: [ 'onefile', 'onedir' ] exclude: - os: ubuntu-22.04 mode: onedir - - os: windows-2019 + - os: windows-2022 mode: onedir runs-on: ${{ matrix.os }} From 44b93eb0c625a16cbae96f875c69c86ac82a0a6b Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Tue, 10 Jun 2025 17:31:42 +0200 Subject: [PATCH 06/10] bump requests --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 57958c82..cdaff5cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1093,19 +1093,19 @@ files = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["main", "test"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -1541,4 +1541,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "c381f7fa685c2eb33c68f23610cfa6edd7d51661003d533d73ec5f66457ceb96" +content-hash = "2a401929c8b999931e32f020bd794e9dc3716647bacc79afa70b7791ab86ce00" diff --git a/pyproject.toml b/pyproject.toml index 6537304c..022d8206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ marshmallow = ">=3.15.0,<3.23.0" # 3.23 dropped support for Python 3.8 gitpython = ">=3.1.30,<3.2.0" arrow = ">=1.0.0,<1.4.0" binaryornot = ">=0.4.4,<0.5.0" -requests = ">=2.32.2,<3.0" +requests = ">=2.32.4,<3.0" urllib3 = "1.26.19" # lock v1 to avoid issues with openssl and old Python versions (<3.9.11) on macOS sentry-sdk = ">=2.8.0,<3.0" pyjwt = ">=2.8.0,<3.0" From 6086d02ddc33f26dc4d1fe3394c9a382e10aa3ea Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 11 Jun 2025 16:07:10 +0200 Subject: [PATCH 07/10] Update README.md Co-authored-by: Philip Hayton --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0904232f..a7c73bb3 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ The following are the options and commands available with the Cycode CLI applica > [!WARNING] > The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available. -The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools through a standardized protocol. +The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools via a standardized protocol. > [!TIP] > For the best experience, install Cycode CLI globally on your system using `pip install cycode` or `brew install cycode`, then authenticate once with `cycode auth`. After global installation and authentication, you won't need to configure `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` environment variables in your MCP configuration files. From 23cb8870b65c96a5879f1ab6c1c30c9eba583c8c Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 11 Jun 2025 16:36:58 +0200 Subject: [PATCH 08/10] add cursor deeplink --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ee4c2ce0..767532a8 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,9 @@ The Model Context Protocol (MCP) command allows you to start an MCP server that > [!TIP] > For the best experience, install Cycode CLI globally on your system using `pip install cycode` or `brew install cycode`, then authenticate once with `cycode auth`. After global installation and authentication, you won't need to configure `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` environment variables in your MCP configuration files. +[![Add MCP Server to Cursor using UV](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=cycode&config=eyJjb21tYW5kIjoidXZ4IGN5Y29kZSBtY3AiLCJlbnYiOnsiQ1lDT0RFX0NMSUVOVF9JRCI6InlvdXItY3ljb2RlLWlkIiwiQ1lDT0RFX0NMSUVOVF9TRUNSRVQiOiJ5b3VyLWN5Y29kZS1zZWNyZXQta2V5IiwiQ1lDT0RFX0FQSV9VUkwiOiJodHRwczovL2FwaS5jeWNvZGUuY29tIiwiQ1lDT0RFX0FQUF9VUkwiOiJodHRwczovL2FwcC5jeWNvZGUuY29tIn19) + + ## Starting the MCP Server To start the MCP server, use the following command: @@ -378,6 +381,8 @@ Learn more about MCP Transport types in the [MCP Protocol Specification – Tran > [!NOTE] > 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`). +Follow [this guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) to configure the MCP server in your **VS Code/GitHub Copilot**. Keep in mind that in `settings.json`, there is an `mcp` object containing a nested `servers` sub-object, rather than a standalone `mcpServers` object. + For **stdio transport** (direct execution): ```json { From 9b5b4f03b87087b2c7c4516c1d2e854a0fb5f15d Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 11 Jun 2025 16:47:19 +0200 Subject: [PATCH 09/10] Update README.md Co-authored-by: elsapet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 767532a8..8daea55f 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,7 @@ For **streamable HTTP transport**: For **SSE transport** (start server first, then configure client): ```bash -# Start the MCP server in background +# Start the MCP server in the background cycode mcp -t sse -p 8000 & # Configure in mcp.json From 9db5786981cfe4c11a4623d1b6fe65afbe1d77ff Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 11 Jun 2025 16:47:27 +0200 Subject: [PATCH 10/10] Update README.md Co-authored-by: elsapet --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8daea55f..9604bd59 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ cycode mcp -t sse -p 8000 & For **streamable HTTP transport**: ```bash -# Start the MCP server in background +# Start the MCP server in the background cycode mcp -t streamable-http -H 127.0.0.2 -p 9000 & # Configure in mcp.json