Skip to content

Commit 87cee0f

Browse files
committed
feat: Add CLI package
1 parent 15cd7df commit 87cee0f

File tree

5 files changed

+646
-1
lines changed

5 files changed

+646
-1
lines changed

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp"
3-
version = "1.1.2.dev0"
3+
version = "1.2.0.dev0"
44
description = "Model Context Protocol SDK"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -33,6 +33,10 @@ dependencies = [
3333

3434
[project.optional-dependencies]
3535
rich = ["rich>=13.9.4"]
36+
cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"]
37+
38+
[project.scripts]
39+
mcp = "mcp.cli:app [cli]"
3640

3741
[tool.uv]
3842
resolution = "lowest-direct"

src/mcp/cli/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""FastMCP CLI package."""
2+
3+
from .cli import app
4+
5+
6+
if __name__ == "__main__":
7+
app()

src/mcp/cli/claude.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Claude app integration utilities."""
2+
3+
import json
4+
import sys
5+
from pathlib import Path
6+
7+
from mcp.server.fastmcp.utilities.logging import get_logger
8+
9+
logger = get_logger(__name__)
10+
11+
12+
def get_claude_config_path() -> Path | None:
13+
"""Get the Claude config directory based on platform."""
14+
if sys.platform == "win32":
15+
path = Path(Path.home(), "AppData", "Roaming", "Claude")
16+
elif sys.platform == "darwin":
17+
path = Path(Path.home(), "Library", "Application Support", "Claude")
18+
else:
19+
return None
20+
21+
if path.exists():
22+
return path
23+
return None
24+
25+
26+
def update_claude_config(
27+
file_spec: str,
28+
server_name: str,
29+
*,
30+
with_editable: Path | None = None,
31+
with_packages: list[str] | None = None,
32+
env_vars: dict[str, str] | None = None,
33+
) -> bool:
34+
"""Add or update a FastMCP server in Claude's configuration.
35+
36+
Args:
37+
file_spec: Path to the server file, optionally with :object suffix
38+
server_name: Name for the server in Claude's config
39+
with_editable: Optional directory to install in editable mode
40+
with_packages: Optional list of additional packages to install
41+
env_vars: Optional dictionary of environment variables. These are merged with
42+
any existing variables, with new values taking precedence.
43+
44+
Raises:
45+
RuntimeError: If Claude Desktop's config directory is not found, indicating
46+
Claude Desktop may not be installed or properly set up.
47+
"""
48+
config_dir = get_claude_config_path()
49+
if not config_dir:
50+
raise RuntimeError(
51+
"Claude Desktop config directory not found. Please ensure Claude Desktop "
52+
"is installed and has been run at least once to initialize its configuration."
53+
)
54+
55+
config_file = config_dir / "claude_desktop_config.json"
56+
if not config_file.exists():
57+
try:
58+
config_file.write_text("{}")
59+
except Exception as e:
60+
logger.error(
61+
"Failed to create Claude config file",
62+
extra={
63+
"error": str(e),
64+
"config_file": str(config_file),
65+
},
66+
)
67+
return False
68+
69+
try:
70+
config = json.loads(config_file.read_text())
71+
if "mcpServers" not in config:
72+
config["mcpServers"] = {}
73+
74+
# Always preserve existing env vars and merge with new ones
75+
if (
76+
server_name in config["mcpServers"]
77+
and "env" in config["mcpServers"][server_name]
78+
):
79+
existing_env = config["mcpServers"][server_name]["env"]
80+
if env_vars:
81+
# New vars take precedence over existing ones
82+
env_vars = {**existing_env, **env_vars}
83+
else:
84+
env_vars = existing_env
85+
86+
# Build uv run command
87+
args = ["run"]
88+
89+
# Collect all packages in a set to deduplicate
90+
packages = {"fastmcp"}
91+
if with_packages:
92+
packages.update(pkg for pkg in with_packages if pkg)
93+
94+
# Add all packages with --with
95+
for pkg in sorted(packages):
96+
args.extend(["--with", pkg])
97+
98+
if with_editable:
99+
args.extend(["--with-editable", str(with_editable)])
100+
101+
# Convert file path to absolute before adding to command
102+
# Split off any :object suffix first
103+
if ":" in file_spec:
104+
file_path, server_object = file_spec.rsplit(":", 1)
105+
file_spec = f"{Path(file_path).resolve()}:{server_object}"
106+
else:
107+
file_spec = str(Path(file_spec).resolve())
108+
109+
# Add fastmcp run command
110+
args.extend(["fastmcp", "run", file_spec])
111+
112+
server_config = {
113+
"command": "uv",
114+
"args": args,
115+
}
116+
117+
# Add environment variables if specified
118+
if env_vars:
119+
server_config["env"] = env_vars
120+
121+
config["mcpServers"][server_name] = server_config
122+
123+
config_file.write_text(json.dumps(config, indent=2))
124+
logger.info(
125+
f"Added server '{server_name}' to Claude config",
126+
extra={"config_file": str(config_file)},
127+
)
128+
return True
129+
except Exception as e:
130+
logger.error(
131+
"Failed to update Claude config",
132+
extra={
133+
"error": str(e),
134+
"config_file": str(config_file),
135+
},
136+
)
137+
return False

0 commit comments

Comments
 (0)