-
Notifications
You must be signed in to change notification settings - Fork 37
feat(ENG-9528): MCP Server #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds Model Context Protocol (MCP) server support to the Cloudsmith CLI, enabling MCP clients to call Cloudsmith API operations via dynamically generated tools from the OpenAPI spec.
Changes:
- Add MCP/TOON dependencies and update pinned requirements for Python 3.10.
- Introduce a dynamic MCP server that discovers OpenAPI specs, generates/registers tools, and optionally TOON-encodes responses.
- Add
cloudsmith mcpCLI commands (start/list_tools/list_groups/configure) plus config options and tests.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| setup.py | Adds MCP + TOON runtime dependencies. |
| requirements.in | Adds MCP + TOON inputs for lockfile generation. |
| requirements.txt | Regenerates lockfile under Python 3.10 with new deps and updated pins. |
| cloudsmith_cli/core/mcp/server.py | Implements dynamic MCP server, OpenAPI discovery, tool generation/registration, and response formatting. |
| cloudsmith_cli/core/mcp/data.py | Adds OpenAPITool dataclass used by the MCP server/tooling. |
| cloudsmith_cli/core/mcp/init.py | Initializes the new core.mcp package. |
| cloudsmith_cli/cli/commands/mcp.py | Adds cloudsmith mcp command group and subcommands including client auto-config. |
| cloudsmith_cli/cli/decorators.py | Adds MCP initialization decorator and updates config option inheritance via ctx.meta. |
| cloudsmith_cli/cli/config.py | Adds config keys and option accessors for allowed MCP tools/tool-groups. |
| cloudsmith_cli/cli/commands/init.py | Ensures the new mcp command module is imported/registered. |
| cloudsmith_cli/cli/tests/commands/test_mcp.py | Adds tests for list commands and tool generation/filtering behavior. |
| CHANGELOG.md | Documents MCP server feature release notes. |
| .pylintrc | Relaxes class attribute/public method limits to accommodate new code. |
| .flake8 | Relaxes max-complexity threshold to accommodate new code. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| help="Show all tools", | ||
| ) | ||
| @click.option( | ||
| "-d", |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialise_mcp defines -d/--allow-destructive-tools, which conflicts with the existing -d/--debug option added by common_cli_output_options on list_tools/list_groups. Click will raise a duplicate option error and these commands won’t be usable. Use a different short flag (or drop the short flag) for --allow-destructive-tools.
| "-d", | |
| "-D", |
| response = await http_client.get(spec_url) | ||
| response.raise_for_status() | ||
| self.spec = response.json() | ||
| await self._generate_tools_from_spec() | ||
|
|
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load_openapi_spec() will abort on the first OpenAPI URL that returns a non-2xx status (raise_for_status()), so it won’t actually “discover” alternative versions (e.g., if v1 is missing but v2 exists). Consider catching 404/HTTPStatusError and continuing to the next version, and stop after the first successful spec load to avoid generating/registering duplicate tools across versions.
| response = await http_client.get(spec_url) | |
| response.raise_for_status() | |
| self.spec = response.json() | |
| await self._generate_tools_from_spec() | |
| try: | |
| response = await http_client.get(spec_url) | |
| response.raise_for_status() | |
| except httpx.HTTPStatusError as exc: | |
| # if this version of the API is not available, try the next one | |
| if exc.response is not None and exc.response.status_code == 404: | |
| continue | |
| # for non-404 errors, preserve existing failure behavior | |
| raise | |
| self.spec = response.json() | |
| await self._generate_tools_from_spec() | |
| # stop after the first successful spec load to avoid duplicate tools | |
| return | |
| # if we reach here, no OpenAPI spec could be loaded for any discovered version | |
| raise Exception("Failed to load OpenAPI spec for any discovered API version") |
| if "enum" in param_schema: | ||
| if value not in param_schema["enum"]: | ||
| allowed_values = ", ".join(param_schema["enum"]) | ||
| return f"Invalid value '{value}' for parameter '{key}'. Allowed values: {allowed_values}" |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_get_request_params() returns a string error message when enum validation fails, but callers expect a (url, query_params, body_params) tuple (see _execute_api_call destructuring). This will raise at runtime instead of returning a clean error. Return a consistent type (e.g., raise a click.UsageError/ValueError or return a structured error and handle it in _execute_api_call).
| return f"Invalid value '{value}' for parameter '{key}'. Allowed values: {allowed_values}" | |
| # raise a clear error instead of returning a string to keep return type consistent | |
| raise ValueError( | |
| f"Invalid value '{value}' for parameter '{key}'. " | |
| f"Allowed values: {allowed_values}" | |
| ) |
| except OSError as e: | ||
| if not use_stderr: | ||
| click.echo( | ||
| click.style( | ||
| f"✗ Error configuring {client_name.title()}: {str(e)}", fg="red" |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
configure_client() can raise ValueError (e.g., invalid JSON), but configure() only catches OSError. This will crash the command instead of reporting a per-client failure. Catch ValueError (and include it in the results list) similarly to OSError.
| with open(config_path) as f: | ||
| try: | ||
| config = json.load(f) | ||
| except json.JSONDecodeError: | ||
| raise ValueError(f"Invalid JSON in config file: {config_path}") |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VS Code’s settings.json is commonly JSONC (comments/trailing commas). Parsing it with json.load() will fail even though VS Code accepts the file, preventing configuration for many users. Consider using a JSONC-capable parser for the VS Code path (or a safer merge strategy) and preserve the existing file format.
| import toon_python as toon | ||
| from mcp import types | ||
| from mcp.server.fastmcp import FastMCP | ||
| from mcp.shared._httpx_utils import create_mcp_http_client |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing create_mcp_http_client from mcp.shared._httpx_utils relies on a private/underscored module that can change without notice. Prefer a public MCP API (if available) or instantiate/configure an httpx.AsyncClient directly so this integration is less brittle across MCP upgrades.
| from mcp.shared._httpx_utils import create_mcp_http_client |
Re-release of #244
Add MCP Server Support to Cloudsmith CLI
This PR introduces Model Context Protocol (MCP) server support to the Cloudsmith CLI, enabling AI assistants and other MCP clients to interact with Cloudsmith's API programmatically.
What is MCP?
The Model Context Protocol is an open standard that enables AI assistants to securely connect to external data sources and tools. By adding MCP server support to the Cloudsmith CLI, users can now leverage AI assistants (like Claude Desktop, and others) to manage their Cloudsmith repositories, packages, and artifacts through natural language.
Key Features
x-simplifiedparameter in the OpenAPI spec) to minimize token usage and improve performanceDefault Available Tools
The MCP server dynamically generates tools from Cloudsmith's OpenAPI specification, which results in a very large number of available tools. While this provides complete API coverage, exposing all tools simultaneously would immediately fill the LLM's context windows.
To address this, the server exposes only a curated subset of commonly-used tools by default. Users can customize which tools and tool groups are available based on their specific workflows, ensuring MCP clients remain efficient and responsive while still providing access to the full API when needed.
The list of tools is filtered by disabling certain categories found here https://github.com/cloudsmith-io/cloudsmith-cli/blob/eng-9528/mcp-integration/cloudsmith_cli/core/mcp/server.py#L38
Commands Added
Configuration
Control which tools are exposed by adding configuration to
~/.cloudsmith/config.ini:This exposes the specified individual tools and all tools within the listed tool groups.
Breaking Changes
This release requires Python 3.10 or later due to MCP SDK dependencies.
Additional Notes
cloudsmith authbefore starting the MCP server as MCP clients will not trigger the SSO authentication flow automatically