Files
kefu/wechat_rpa/.venv/Lib/site-packages/mcp/cli/claude.py
T
2026-09-21 10:34:06 +08:00

171 lines
6.0 KiB
Python

"""Claude app integration utilities."""
import importlib.metadata
import json
import os
import shutil
import sys
from pathlib import Path
from typing import Any
from mcp.server.mcpserver.utilities.logging import get_logger
logger = get_logger(__name__)
def mcp_requirement(package: str = "mcp") -> str:
"""Requirement string pinning spawned environments to the running SDK version.
`uv run --with mcp` resolves the requirement in a fresh environment, where
an unpinned `mcp` means the latest stable release — not necessarily the
version the user installed (pre-releases in particular are never selected
without an explicit pin). Source builds carry dev/local version segments
that are not published to PyPI, so they fall back to the unpinned form,
as does a missing distribution (no metadata to pin from).
"""
try:
version = importlib.metadata.version("mcp")
except importlib.metadata.PackageNotFoundError:
return package
if ".dev" in version or "+" in version:
return package
return f"{package}=={version}"
def get_claude_config_path() -> Path | None: # pragma: no cover
"""Get the Claude config directory based on platform."""
if sys.platform == "win32":
path = Path(Path.home(), "AppData", "Roaming", "Claude")
elif sys.platform == "darwin":
path = Path(Path.home(), "Library", "Application Support", "Claude")
elif sys.platform.startswith("linux"):
path = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude")
else:
return None
if path.exists():
return path
return None
def get_uv_path() -> str:
"""Get the full path to the uv executable."""
uv_path = shutil.which("uv")
if not uv_path:
logger.error(
"uv executable not found in PATH, falling back to 'uv'. Please ensure uv is installed and in your PATH"
)
return "uv" # Fall back to just "uv" if not found
return uv_path
def update_claude_config(
file_spec: str,
server_name: str,
*,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
env_vars: dict[str, str] | None = None,
) -> bool:
"""Add or update an MCP server in Claude's configuration.
Args:
file_spec: Path to the server file, optionally with :object suffix
server_name: Name for the server in Claude's config
with_editable: Optional directory to install in editable mode
with_packages: Optional list of additional packages to install
env_vars: Optional dictionary of environment variables. These are merged with
any existing variables, with new values taking precedence.
Raises:
RuntimeError: If Claude Desktop's config directory is not found, indicating
Claude Desktop may not be installed or properly set up.
"""
config_dir = get_claude_config_path()
uv_path = get_uv_path()
if not config_dir:
raise RuntimeError(
"Claude Desktop config directory not found. Please ensure Claude Desktop"
" is installed and has been run at least once to initialize its config."
)
config_file = config_dir / "claude_desktop_config.json"
if not config_file.exists(): # pragma: lax no cover
try:
config_file.write_text("{}", encoding="utf-8")
except Exception:
logger.exception(
"Failed to create Claude config file",
extra={
"config_file": str(config_file),
},
)
return False
try:
config = json.loads(config_file.read_bytes())
if "mcpServers" not in config:
config["mcpServers"] = {}
# Always preserve existing env vars and merge with new ones
if server_name in config["mcpServers"] and "env" in config["mcpServers"][server_name]:
existing_env = config["mcpServers"][server_name]["env"]
if env_vars:
# New vars take precedence over existing ones
env_vars = {**existing_env, **env_vars}
else:
env_vars = existing_env
# Build uv run command
args = ["run", "--frozen"]
# Collect all packages in a set to deduplicate
packages = {mcp_requirement("mcp[cli]")}
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
# Convert file path to absolute before adding to command
# Split off any :object suffix first
# First check if we have a Windows path (e.g., C:\...)
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
# Split on the last colon, but only if it's not part of the Windows drive letter
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
file_path, server_object = file_spec.rsplit(":", 1)
file_spec = f"{Path(file_path).resolve()}:{server_object}"
else:
file_spec = str(Path(file_spec).resolve())
# Add mcp run command
args.extend(["mcp", "run", file_spec])
server_config: dict[str, Any] = {"command": uv_path, "args": args}
# Add environment variables if specified
if env_vars:
server_config["env"] = env_vars
config["mcpServers"][server_name] = server_config
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
logger.info(
f"Added server '{server_name}' to Claude config",
extra={"config_file": str(config_file)},
)
return True
except Exception: # pragma: no cover
logger.exception(
"Failed to update Claude config",
extra={
"config_file": str(config_file),
},
)
return False