Skip to content

Commit 503312b

Browse files
v0.2.0: Add auto-discovery, discover/serve CLI, and scan entry points
- New discover module scans console_scripts entry points for Click/typer CLIs - 'click-to-mcp discover' lists all installed Click/typer CLIs - 'click-to-mcp serve <name>' wraps any discovered CLI as MCP server - 'click-to-mcp serve --all' serves first discoverable CLI - Added importlib.metadata-based entry point scanning (no setuptools dep) - Bumped version to 0.2.0 - Updated README with new CLI commands and library integration docs
1 parent 8156a5a commit 503312b

8 files changed

Lines changed: 514 additions & 42 deletions

File tree

README.md

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,43 @@ Auto-wrap any [Click](https://click.palletsprojects.com/)/[typer](https://typer.
77
AI coding agents (Claude Code, Codex, Cursor) use MCP to interact with tools.
88
Instead of rewriting your CLI tools as MCP servers, use click-to-mcp to wrap them automatically.
99

10+
## Quick Start
11+
12+
```bash
13+
pip install click-to-mcp
14+
15+
# Discover all Click/typer CLIs installed in your environment
16+
click-to-mcp discover
17+
18+
# Serve a specific CLI as an MCP server
19+
click-to-mcp serve api-contract-guardian
20+
21+
# Or serve the built-in demo
22+
click-to-mcp demo
23+
```
24+
25+
Then configure your MCP client to run `click-to-mcp serve <name>` as a stdio transport.
26+
1027
## Usage
1128

29+
### CLI — Discover and Serve
30+
31+
```bash
32+
# List all installed Click/typer CLIs
33+
click-to-mcp discover
34+
35+
# Serve a specific CLI as an MCP server over stdio
36+
click-to-mcp serve <name>
37+
38+
# Serve the built-in demo
39+
click-to-mcp demo
40+
41+
# Version info
42+
click-to-mcp --version
43+
```
44+
45+
### Library — Integrate directly
46+
1247
```python
1348
# my_cli.py
1449
import click
@@ -26,36 +61,67 @@ def validate(file: str, verbose: bool) -> None:
2661
"""Validate a file."""
2762
click.echo(f"Validating {file}...")
2863

29-
if __name__ == "__main__":
30-
# Run as MCP server over stdio
31-
serve_stdio(cli, name="my-cli", description="My CLI as MCP server")
64+
# Run as MCP server
65+
serve_stdio(cli, name="my-cli", description="My CLI as MCP server")
3266
```
3367

34-
Then configure MCP clients to run `python my_cli.py` as a stdio transport.
68+
### Library — High-level `run()` API
3569

36-
## Install
70+
```python
71+
from click_to_mcp import run
72+
from my_cli import app
3773

38-
```bash
39-
pip install click-to-mcp
74+
# Automatically detects Click/typer instances
75+
run(app, prefix="my-cli")
4076
```
4177

78+
## Features
79+
80+
- **Auto-discovery**: `click-to-mcp discover` scans `console_scripts` entry points for Click/typer CLIs
81+
- **Serve any CLI**: `click-to-mcp serve <name>` wraps any discovered CLI as an MCP server
82+
- **Supports both Click and Typer**: Full compatibility with both frameworks
83+
- **Nested command groups**: Handles subcommand groups recursively with prefixed tool names
84+
- **Parameter introspection**: Correctly maps Click options, arguments, types, enums, defaults, and help text to JSON Schema
85+
- **Full MCP protocol**: Implements `initialize`, `tools/list`, `tools/call` over stdio
86+
87+
## MCP Protocol
88+
89+
Click-to-MCP implements the standard MCP protocol with:
90+
91+
- `initialize` — protocol handshake (returns server capabilities)
92+
- `tools/list` — discover all CLI commands as MCP tools with JSON Schema inputs
93+
- `tools/call` — invoke a CLI command with typed arguments
94+
95+
## Integration with Existing CLIs
96+
97+
Add an MCP server entry point to any Click/typer CLI:
98+
99+
```python
100+
# cli.py — add a subcommand to run as MCP server
101+
import typer
102+
from click_to_mcp import run
103+
104+
app = typer.Typer(...)
105+
106+
@app.command()
107+
def mcp():
108+
"""Run as an MCP server over stdio."""
109+
from click_to_mcp import run
110+
run(app)
111+
```
112+
113+
Then agents can use it as: `your-cli mcp`
114+
42115
## Development
43116

44117
```bash
45118
git clone https://github.com/Coding-Dev-Tools/click-to-mcp
46119
cd click-to-mcp
47120
pip install -e .
48-
python -m click_to_mcp # starts MCP server for demo CLI
121+
python -m click_to_mcp discover
122+
click-to-mcp demo # starts MCP server for demo CLI
49123
```
50124

51-
## MCP Protocol
52-
53-
Click-to-MCP implements the standard MCP protocol with:
54-
55-
- `initialize` — protocol handshake
56-
- `tools/list` — discover all CLI commands as MCP tools
57-
- `tools/call` — invoke a CLI command with arguments
58-
59125
## License
60126

61127
Apache 2.0

click_to_mcp/__init__.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
"""
2-
click-to-mcp — Auto-wrap any Click/typer CLI as an MCP server.
2+
click-to-mcp — Auto-wrap any Click/typer CLI as an MCP (Model Context Protocol) server.
33
44
Usage:
5-
from click_to_mcp import run
6-
from my_cli import cli_group
7-
run(cli_group, prefix="my-cli")
5+
pip install click-to-mcp
6+
click-to-mcp discover # List all Click/typer CLIs
7+
click-to-mcp serve <name> # Serve a specific CLI as MCP
8+
click-to-mcp serve --all # Serve first discoverable CLI
9+
click-to-mcp demo # Run built-in demo
10+
11+
Or use as a library:
12+
from click_to_mcp import run, serve_stdio, cli_to_mcp_tools
13+
from my_cli import app
14+
run(app, prefix="my-cli")
815
"""
916

1017
from __future__ import annotations
@@ -13,8 +20,9 @@
1320

1421
from .adapter import cli_to_mcp_tools, CliToolDef
1522
from .server import serve_stdio
23+
from .discover import scan_entry_points, load_cli, find_our_clis, DiscoveredCLI
1624

17-
__version__ = "0.1.0"
25+
__version__ = "0.2.0"
1826

1927

2028
def run(app: Any, prefix: str = "", name: str = "") -> None:
@@ -32,7 +40,6 @@ def run(app: Any, prefix: str = "", name: str = "") -> None:
3240

3341
if not name:
3442
name = getattr(app, "name", None) or "cli"
35-
# Get description, handling TyperInfo objects
3643
desc = ""
3744
if hasattr(app, "info"):
3845
info_desc = getattr(app.info, "help", None)
@@ -51,4 +58,7 @@ def run(app: Any, prefix: str = "", name: str = "") -> None:
5158
)
5259

5360

54-
__all__ = ["cli_to_mcp_tools", "CliToolDef", "serve_stdio", "run"]
61+
__all__ = [
62+
"cli_to_mcp_tools", "CliToolDef", "serve_stdio", "run",
63+
"scan_entry_points", "load_cli", "find_our_clis", "DiscoveredCLI",
64+
]

click_to_mcp/__main__.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,4 @@
1-
"""Entry points for wrapping CLIs as MCP servers."""
1+
"""Entry point for running click-to-mcp as 'python -m click_to_mcp'."""
2+
from .cli import main
23

3-
from click_to_mcp import serve_stdio
4-
from click_to_mcp.demo import cli
5-
6-
7-
def main():
8-
"""Run click-to-mcp demo as an MCP server."""
9-
serve_stdio(
10-
cli,
11-
name="click-to-mcp-demo",
12-
description="Demo CLI exposing Click commands as MCP tools",
13-
)
14-
15-
16-
if __name__ == "__main__":
17-
main()
4+
main()

click_to_mcp/cli.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""click-to-mcp meta-CLI — discover and serve Click/typer CLIs as MCP servers.
2+
3+
Usage:
4+
click-to-mcp discover # List all discoverable CLIs
5+
click-to-mcp serve <name> # Serve a specific CLI
6+
click-to-mcp serve --all # Serve all discoverable CLIs
7+
click-to-mcp demo # Run the demo CLI as MCP server
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import sys
13+
from typing import Optional
14+
15+
import click
16+
17+
from . import serve_stdio, __version__
18+
from .discover import scan_entry_points, load_cli
19+
20+
21+
@click.group()
22+
@click.version_option(version=__version__, prog_name="click-to-mcp")
23+
def cli():
24+
"""Auto-wrap any Click/typer CLI as an MCP (Model Context Protocol) server."""
25+
pass
26+
27+
28+
@cli.command()
29+
def discover():
30+
"""List all installed Click/typer CLIs that can be served as MCP tools."""
31+
clis = scan_entry_points()
32+
33+
if not clis:
34+
click.echo("No Click/typer CLIs found in installed packages.")
35+
click.echo("Install a Click/typer-based CLI and run this command again.")
36+
return
37+
38+
click.echo(f"Found {len(clis)} Click/typer CLI(s):")
39+
click.echo("")
40+
41+
for i, cli_info in enumerate(clis, 1):
42+
type_tag = "[Typer]" if cli_info.is_typer else "[Click]"
43+
summary = cli_info.summary[:80] if cli_info.summary else "(no description)"
44+
click.echo(f" {i}. {type_tag} {cli_info.name}")
45+
click.echo(f" Package: {cli_info.package_name} {cli_info.package_version}")
46+
click.echo(f" Module: {cli_info.module_path}:{cli_info.attr_name}")
47+
click.echo(f" Summary: {summary}")
48+
click.echo("")
49+
50+
click.echo("Usage: click-to-mcp serve <name>")
51+
click.echo(" click-to-mcp serve --all")
52+
53+
54+
@cli.command()
55+
@click.argument("name", required=False, default=None)
56+
@click.option("--all", "serve_all", is_flag=True, help="Serve all discoverable CLIs")
57+
def serve(name: Optional[str], serve_all: bool):
58+
"""Serve a CLI as an MCP server over stdio.
59+
60+
Specify a CLI NAME from 'click-to-mcp discover', or use --all.
61+
"""
62+
if not name and not serve_all:
63+
click.echo("Error: Specify a CLI name or --all", err=True)
64+
sys.exit(1)
65+
66+
if serve_all:
67+
clis = scan_entry_points()
68+
if not clis:
69+
click.echo("No CLIs found to serve.", err=True)
70+
sys.exit(1)
71+
# Serve the first one as primary — MCP stdio is single-server.
72+
# For --all we could eventually multiplex, but for now serve the first.
73+
click.echo(f"Multiple CLIs found. Serving the first one: {clis[0].name}", err=True)
74+
target_cli = load_cli(clis[0].name)
75+
if target_cli is None:
76+
click.echo(f"Error: Could not load CLI '{clis[0].name}'", err=True)
77+
sys.exit(1)
78+
serve_stdio(target_cli, name=clis[0].name)
79+
return
80+
81+
target_cli = load_cli(name)
82+
if target_cli is None:
83+
click.echo(f"Error: CLI '{name}' not found.", err=True)
84+
click.echo("Run 'click-to-mcp discover' to see available CLIs.", err=True)
85+
sys.exit(1)
86+
87+
serve_stdio(target_cli, name=name)
88+
89+
90+
@cli.command()
91+
def demo():
92+
"""Run the built-in demo CLI as an MCP server."""
93+
from .demo import cli as demo_cli
94+
95+
serve_stdio(demo_cli, name="click-to-mcp-demo")
96+
97+
98+
def main():
99+
"""Entry point for the click-to-mcp CLI."""
100+
cli()
101+
102+
103+
if __name__ == "__main__":
104+
main()

0 commit comments

Comments
 (0)