257 lines
7.5 KiB
Python
257 lines
7.5 KiB
Python
"""Claude Code integration for FastMCP install using Cyclopts."""
|
|
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
import cyclopts
|
|
from rich import print
|
|
|
|
from fastmcp.utilities.logging import get_logger
|
|
|
|
from .shared import process_common_args
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def find_claude_command() -> str | None:
|
|
"""Find the Claude Code CLI command.
|
|
|
|
Checks common installation locations since 'claude' is often a shell alias
|
|
that doesn't work with subprocess calls.
|
|
"""
|
|
# First try shutil.which() in case it's a real executable in PATH
|
|
claude_in_path = shutil.which("claude")
|
|
if claude_in_path:
|
|
try:
|
|
result = subprocess.run(
|
|
[claude_in_path, "--version"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if "Claude Code" in result.stdout:
|
|
return claude_in_path
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
pass
|
|
|
|
# Check common installation locations (aliases don't work with subprocess)
|
|
potential_paths = [
|
|
# Default Claude Code installation location (after migration)
|
|
Path.home() / ".claude" / "local" / "claude",
|
|
# npm global installation on macOS/Linux (default)
|
|
Path("/usr/local/bin/claude"),
|
|
# npm global installation with custom prefix
|
|
Path.home() / ".npm-global" / "bin" / "claude",
|
|
]
|
|
|
|
for path in potential_paths:
|
|
if path.exists():
|
|
try:
|
|
result = subprocess.run(
|
|
[str(path), "--version"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if "Claude Code" in result.stdout:
|
|
return str(path)
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
continue
|
|
|
|
return None
|
|
|
|
|
|
def check_claude_code_available() -> bool:
|
|
"""Check if Claude Code CLI is available."""
|
|
return find_claude_command() is not None
|
|
|
|
|
|
def install_claude_code(
|
|
file: Path,
|
|
server_object: str | None,
|
|
name: str,
|
|
*,
|
|
with_editable: Path | None = None,
|
|
with_packages: list[str] | None = None,
|
|
env_vars: dict[str, str] | None = None,
|
|
python_version: str | None = None,
|
|
with_requirements: Path | None = None,
|
|
project: Path | None = None,
|
|
) -> bool:
|
|
"""Install FastMCP server in Claude Code.
|
|
|
|
Args:
|
|
file: Path to the server file
|
|
server_object: Optional server object name (for :object suffix)
|
|
name: Name for the server in Claude Code
|
|
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
|
|
python_version: Optional Python version to use
|
|
with_requirements: Optional requirements file to install from
|
|
project: Optional project directory to run within
|
|
|
|
Returns:
|
|
True if installation was successful, False otherwise
|
|
"""
|
|
# Check if Claude Code CLI is available
|
|
claude_cmd = find_claude_command()
|
|
if not claude_cmd:
|
|
print(
|
|
"[red]Claude Code CLI not found.[/red]\n"
|
|
"[blue]Please ensure Claude Code is installed. Try running 'claude --version' to verify.[/blue]"
|
|
)
|
|
return False
|
|
|
|
# Build uv run command
|
|
args = ["run"]
|
|
|
|
# Add Python version if specified
|
|
if python_version:
|
|
args.extend(["--python", python_version])
|
|
|
|
# Add project if specified
|
|
if project:
|
|
args.extend(["--project", str(project)])
|
|
|
|
# Collect all packages in a set to deduplicate
|
|
packages = {"fastmcp"}
|
|
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)])
|
|
|
|
if with_requirements:
|
|
args.extend(["--with-requirements", str(with_requirements)])
|
|
|
|
# Build server spec from parsed components
|
|
if server_object:
|
|
server_spec = f"{file.resolve()}:{server_object}"
|
|
else:
|
|
server_spec = str(file.resolve())
|
|
|
|
# Add fastmcp run command
|
|
args.extend(["fastmcp", "run", server_spec])
|
|
|
|
# Build claude mcp add command
|
|
cmd_parts = [claude_cmd, "mcp", "add"]
|
|
|
|
# Add environment variables if specified (before the name and command)
|
|
if env_vars:
|
|
for key, value in env_vars.items():
|
|
cmd_parts.extend(["-e", f"{key}={value}"])
|
|
|
|
# Add server name and command
|
|
cmd_parts.extend([name, "--"])
|
|
cmd_parts.extend(["uv"] + args)
|
|
|
|
try:
|
|
# Run the claude mcp add command
|
|
subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
print(
|
|
f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e.stderr.strip() if e.stderr else str(e)}[/red]"
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
print(f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e}[/red]")
|
|
return False
|
|
|
|
|
|
async def claude_code_command(
|
|
server_spec: str,
|
|
*,
|
|
server_name: Annotated[
|
|
str | None,
|
|
cyclopts.Parameter(
|
|
name=["--name", "-n"],
|
|
help="Custom name for the server in Claude Code",
|
|
),
|
|
] = None,
|
|
with_editable: Annotated[
|
|
Path | None,
|
|
cyclopts.Parameter(
|
|
name=["--with-editable", "-e"],
|
|
help="Directory with pyproject.toml to install in editable mode",
|
|
),
|
|
] = None,
|
|
with_packages: Annotated[
|
|
list[str],
|
|
cyclopts.Parameter(
|
|
"--with",
|
|
help="Additional packages to install",
|
|
negative=False,
|
|
),
|
|
] = [],
|
|
env_vars: Annotated[
|
|
list[str],
|
|
cyclopts.Parameter(
|
|
"--env",
|
|
help="Environment variables in KEY=VALUE format",
|
|
negative=False,
|
|
),
|
|
] = [],
|
|
env_file: Annotated[
|
|
Path | None,
|
|
cyclopts.Parameter(
|
|
"--env-file",
|
|
help="Load environment variables from .env file",
|
|
),
|
|
] = None,
|
|
python: Annotated[
|
|
str | None,
|
|
cyclopts.Parameter(
|
|
"--python",
|
|
help="Python version to use (e.g., 3.10, 3.11)",
|
|
),
|
|
] = None,
|
|
with_requirements: Annotated[
|
|
Path | None,
|
|
cyclopts.Parameter(
|
|
"--with-requirements",
|
|
help="Requirements file to install dependencies from",
|
|
),
|
|
] = None,
|
|
project: Annotated[
|
|
Path | None,
|
|
cyclopts.Parameter(
|
|
"--project",
|
|
help="Run the command within the given project directory",
|
|
),
|
|
] = None,
|
|
) -> None:
|
|
"""Install an MCP server in Claude Code.
|
|
|
|
Args:
|
|
server_spec: Python file to install, optionally with :object suffix
|
|
"""
|
|
file, server_object, name, packages, env_dict = await process_common_args(
|
|
server_spec, server_name, with_packages, env_vars, env_file
|
|
)
|
|
|
|
success = install_claude_code(
|
|
file=file,
|
|
server_object=server_object,
|
|
name=name,
|
|
with_editable=with_editable,
|
|
with_packages=packages,
|
|
env_vars=env_dict,
|
|
python_version=python,
|
|
with_requirements=with_requirements,
|
|
project=project,
|
|
)
|
|
|
|
if success:
|
|
print(f"[green]Successfully installed '{name}' in Claude Code[/green]")
|
|
else:
|
|
sys.exit(1)
|