Skip to content

Conversation

@BartoszBlizniak
Copy link
Member

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

  • Dynamic Tool Generation: MCP tools are automatically generated from Cloudsmith's OpenAPI specification, ensuring the server stays in sync with the API
  • Optimized API Responses: JSON responses are encoded to TOON format and use sparse fieldsets (taken from the x-simplified parameter in the OpenAPI spec) to minimize token usage and improve performance
  • Configurable Tool Exposure: Control which tools and tool groups are exposed to prevent context window overflow in MCP clients
  • Seamless Authentication: Authentication is handled by the CLI using existing Cloudsmith API keys or SSO credentials
  • Multi-Profile Support: Configure separate MCP server instances for different Cloudsmith profiles

Default 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

# Start the MCP server
$ cloudsmith mcp start

# List available tools (use -a to show all tools)
$ cloudsmith mcp list_tools

# List available tool groups (use -a to show all groups)
$ cloudsmith mcp list_groups

# Auto-configure MCP clients (use -P to configure for a specific profile)
$ cloudsmith mcp configure

Configuration

Control which tools are exposed by adding configuration to ~/.cloudsmith/config.ini:

mcp_allowed_tools=workspaces_policies_simulate_list
mcp_allowed_tool_groups=metrics

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

  • Authentication uses the existing CLI credentials (API keys or SSO)
  • For SSO authentication, the auth flow must be completed via cloudsmith auth before starting the MCP server as MCP clients will not trigger the SSO authentication flow automatically

@BartoszBlizniak BartoszBlizniak marked this pull request as ready for review January 23, 2026 18:25
@BartoszBlizniak BartoszBlizniak requested a review from a team as a code owner January 23, 2026 18:25
Copilot AI review requested due to automatic review settings January 23, 2026 18:25
Copy link
Contributor

Copilot AI left a 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 mcp CLI 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",
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
"-d",
"-D",

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +216
response = await http_client.get(spec_url)
response.raise_for_status()
self.spec = response.json()
await self._generate_tools_from_spec()

Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
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")

Copilot uses AI. Check for mistakes.
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}"
Copy link

Copilot AI Jan 23, 2026

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

Suggested change
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}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +264
except OSError as e:
if not use_stderr:
click.echo(
click.style(
f"✗ Error configuring {client_name.title()}: {str(e)}", fg="red"
Copy link

Copilot AI Jan 23, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +399 to +403
with open(config_path) as f:
try:
config = json.load(f)
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON in config file: {config_path}")
Copy link

Copilot AI Jan 23, 2026

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.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
from mcp.shared._httpx_utils import create_mcp_http_client

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants