mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 07:24:18 +02:00
Merge main into concurrency-fix-1054
Resolved conflicts in client.py by keeping our improved RuntimeError checks instead of the assert statements from main.
This commit is contained in:
commit
a975db6e6c
27 changed files with 1176 additions and 233 deletions
|
|
@ -18,7 +18,7 @@ from typer import Context, Exit
|
|||
|
||||
import fastmcp
|
||||
from fastmcp.cli import run as run_module
|
||||
from fastmcp.cli.install.install import install
|
||||
from fastmcp.cli.install import install_app
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -323,8 +323,8 @@ def run(
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
# Add install command directly
|
||||
app.command()(install)
|
||||
# Add install subcommands
|
||||
app.add_typer(install_app)
|
||||
|
||||
|
||||
@app.command()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,27 @@
|
|||
"""Install module for FastMCP CLI."""
|
||||
"""Install subcommands for FastMCP CLI."""
|
||||
|
||||
from .install import install
|
||||
import typer
|
||||
|
||||
__all__ = ["install"]
|
||||
from .claude_code import claude_code_command
|
||||
from .claude_desktop import claude_desktop_command
|
||||
from .cursor import cursor_command
|
||||
from .mcp_config import mcp_config_command
|
||||
|
||||
# Create a typer app for install subcommands
|
||||
install_app = typer.Typer(
|
||||
name="install",
|
||||
help="Install MCP servers in various clients and formats",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
# Register each command from its respective module
|
||||
install_app.command("claude-code", help="Install a MCP server in Claude Code")(
|
||||
claude_code_command
|
||||
)
|
||||
install_app.command("claude-desktop", help="Install a MCP server in Claude Desktop")(
|
||||
claude_desktop_command
|
||||
)
|
||||
install_app.command("cursor", help="Install a MCP server in Cursor")(cursor_command)
|
||||
install_app.command(
|
||||
"mcp-json", help="Generate MCP JSON configuration for manual installation"
|
||||
)(mcp_config_command)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -116,3 +121,73 @@ def install_claude_code(
|
|||
except Exception as e:
|
||||
print(f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e}[/red]")
|
||||
return False
|
||||
|
||||
|
||||
def claude_code_command(
|
||||
server_spec: Annotated[
|
||||
str, typer.Argument(help="Python file to run, optionally with :object suffix")
|
||||
],
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with", help="Additional packages to install, in PEP 508 format"
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var", "-v", help="Environment variables in KEY=VALUE format"
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a MCP server in Claude Code."""
|
||||
file, server_object, name, packages, env_dict = 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,
|
||||
)
|
||||
|
||||
if success:
|
||||
print(
|
||||
f"[green bold]Successfully installed '[bold]{name}[/bold]' in Claude Code[/green bold]"
|
||||
)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ from __future__ import annotations
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from fastmcp.mcp_config import StdioMCPServer, update_config_file
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -119,3 +123,73 @@ def install_claude_desktop(
|
|||
f"[red]Failed to install '[bold]{name}[/bold]' in Claude Desktop: {e}[/red]"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def claude_desktop_command(
|
||||
server_spec: Annotated[
|
||||
str, typer.Argument(help="Python file to run, optionally with :object suffix")
|
||||
],
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with", help="Additional packages to install, in PEP 508 format"
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var", "-v", help="Environment variables in KEY=VALUE format"
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a MCP server in Claude Desktop."""
|
||||
file, server_object, name, packages, env_dict = process_common_args(
|
||||
server_spec, server_name, with_packages, env_vars, env_file
|
||||
)
|
||||
|
||||
success = install_claude_desktop(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=packages,
|
||||
env_vars=env_dict,
|
||||
)
|
||||
|
||||
if success:
|
||||
print(
|
||||
f"[green bold]Successfully installed '[bold]{name}[/bold]' in Claude Desktop[/green bold]"
|
||||
)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@ import base64
|
|||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from fastmcp.mcp_config import StdioMCPServer
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -133,3 +137,70 @@ def install_cursor(
|
|||
except Exception as e:
|
||||
print(f"[red]Failed to generate Cursor deeplink: {e}[/red]")
|
||||
return False
|
||||
|
||||
|
||||
def cursor_command(
|
||||
server_spec: Annotated[
|
||||
str, typer.Argument(help="Python file to run, optionally with :object suffix")
|
||||
],
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with", help="Additional packages to install, in PEP 508 format"
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var", "-v", help="Environment variables in KEY=VALUE format"
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a MCP server in Cursor."""
|
||||
file, server_object, name, packages, env_dict = process_common_args(
|
||||
server_spec, server_name, with_packages, env_vars, env_file
|
||||
)
|
||||
|
||||
success = install_cursor(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=packages,
|
||||
env_vars=env_dict,
|
||||
)
|
||||
|
||||
# Cursor handles its own messaging, no generic success message needed
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
"""Main install logic for FastMCP CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from dotenv import dotenv_values
|
||||
from rich import print
|
||||
|
||||
from fastmcp.cli.run import import_server, parse_file_path
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
from .claude_code import install_claude_code
|
||||
from .claude_desktop import install_claude_desktop
|
||||
from .cursor import install_cursor
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Client(str, Enum):
|
||||
"""Supported MCP clients."""
|
||||
|
||||
CLAUDE_CODE = "claude-code"
|
||||
CLAUDE_DESKTOP = "claude-desktop"
|
||||
CURSOR = "cursor"
|
||||
|
||||
|
||||
def install(
|
||||
client: Annotated[
|
||||
Client,
|
||||
typer.Argument(help="MCP client to install the server into"),
|
||||
],
|
||||
server_spec: Annotated[
|
||||
str, typer.Argument(help="Python file to run, optionally with :object suffix")
|
||||
],
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode. Use this to include local packages that are not available on PyPI.",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with",
|
||||
help="Additional packages to install, in PEP 508 format (e.g. 'httpx>=0.25.2')",
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var",
|
||||
"-v",
|
||||
help="Environment variables in KEY=VALUE format",
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Install a MCP server in the specified target application.
|
||||
|
||||
Environment variables are preserved once added and only updated if new values
|
||||
are explicitly provided.
|
||||
"""
|
||||
|
||||
# Parse server spec
|
||||
file, server_object = parse_file_path(server_spec)
|
||||
|
||||
logger.debug(
|
||||
"Installing server",
|
||||
extra={
|
||||
"client": client,
|
||||
"file": str(file),
|
||||
"server_name": server_name,
|
||||
"server_object": server_object,
|
||||
"with_editable": str(with_editable) if with_editable else None,
|
||||
"with_packages": with_packages,
|
||||
},
|
||||
)
|
||||
|
||||
# Try to import server to get its name and dependencies
|
||||
name = server_name
|
||||
server = None
|
||||
if not name:
|
||||
try:
|
||||
server = import_server(file, server_object)
|
||||
name = server.name
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.debug(
|
||||
"Could not import server (likely missing dependencies), using file name",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
name = file.stem
|
||||
|
||||
# Get server dependencies if available
|
||||
server_dependencies = getattr(server, "dependencies", []) if server else []
|
||||
if server_dependencies:
|
||||
with_packages = list(set(with_packages + server_dependencies))
|
||||
|
||||
# Process environment variables if provided
|
||||
env_dict: dict[str, str] | None = None
|
||||
if env_file or env_vars:
|
||||
env_dict = {}
|
||||
# Load from .env file if specified
|
||||
if env_file:
|
||||
try:
|
||||
env_dict |= {
|
||||
k: v for k, v in dotenv_values(env_file).items() if v is not None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[red]❌ Failed to load .env file: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
# Add command line environment variables
|
||||
for env_var in env_vars:
|
||||
key, value = _parse_env_var(env_var)
|
||||
env_dict[key] = value
|
||||
|
||||
# Route to appropriate installer
|
||||
if client == Client.CLAUDE_CODE:
|
||||
success = install_claude_code(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=with_packages,
|
||||
env_vars=env_dict,
|
||||
)
|
||||
elif client == Client.CLAUDE_DESKTOP:
|
||||
success = install_claude_desktop(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=with_packages,
|
||||
env_vars=env_dict,
|
||||
)
|
||||
elif client == Client.CURSOR:
|
||||
success = install_cursor(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=with_packages,
|
||||
env_vars=env_dict,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[red bold]Unknown client: {client!r}[/red bold]. Supported clients: [bold]{Client.CLAUDE_CODE}[/bold], [bold]{Client.CLAUDE_DESKTOP}[/bold], [bold]{Client.CURSOR}[/bold]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if success:
|
||||
# Only show generic success message for clients that don't have their own messaging
|
||||
if client != Client.CURSOR:
|
||||
print(
|
||||
f"[green bold]Successfully installed '[bold]{name}[/bold]' in {client.value}[/green bold]"
|
||||
)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _parse_env_var(env_var: str) -> tuple[str, str]:
|
||||
"""Parse environment variable string in format KEY=VALUE."""
|
||||
if "=" not in env_var:
|
||||
print(
|
||||
f"[red]❌ Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]"
|
||||
)
|
||||
sys.exit(1)
|
||||
key, value = env_var.split("=", 1)
|
||||
return key.strip(), value.strip()
|
||||
179
src/fastmcp/cli/install/mcp_config.py
Normal file
179
src/fastmcp/cli/install/mcp_config.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
"""MCP configuration JSON generation for FastMCP install."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def install_mcp_config(
|
||||
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,
|
||||
copy: bool = False,
|
||||
) -> bool:
|
||||
"""Generate MCP configuration JSON for manual installation.
|
||||
|
||||
Args:
|
||||
file: Path to the server file
|
||||
server_object: Optional server object name (for :object suffix)
|
||||
name: Name for the server in MCP 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
|
||||
copy: If True, copy to clipboard instead of printing to stdout
|
||||
|
||||
Returns:
|
||||
True if generation was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Build uv run command
|
||||
args = ["run"]
|
||||
|
||||
# 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)])
|
||||
|
||||
# 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 MCP server configuration (just the server object, not the wrapper)
|
||||
config = {
|
||||
"command": "uv",
|
||||
"args": args,
|
||||
}
|
||||
|
||||
# Add environment variables if provided
|
||||
if env_vars:
|
||||
config["env"] = env_vars
|
||||
|
||||
# Convert to JSON
|
||||
json_output = json.dumps(config, indent=2)
|
||||
|
||||
# Handle output
|
||||
if copy:
|
||||
try:
|
||||
import pyperclip
|
||||
|
||||
pyperclip.copy(json_output)
|
||||
print(
|
||||
f"[green]MCP configuration for '[bold]{name}[/bold]' copied to clipboard[/green]"
|
||||
)
|
||||
except ImportError:
|
||||
print(
|
||||
"[red]The `--copy` flag requires pyperclip. Please install pyperclip and try again: `pip install pyperclip`[/red]"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
# Print to stdout (for piping)
|
||||
print(json_output)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[red]❌ Failed to generate MCP configuration: {e}[/red]")
|
||||
return False
|
||||
|
||||
|
||||
def mcp_config_command(
|
||||
server_spec: Annotated[
|
||||
str, typer.Argument(help="Python file to run, optionally with :object suffix")
|
||||
],
|
||||
server_name: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Custom name for the server (defaults to server's name attribute or file name)",
|
||||
),
|
||||
] = None,
|
||||
with_editable: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--with-editable",
|
||||
"-e",
|
||||
help="Directory containing pyproject.toml to install in editable mode",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
with_packages: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--with", help="Additional packages to install, in PEP 508 format"
|
||||
),
|
||||
] = [],
|
||||
env_vars: Annotated[
|
||||
list[str],
|
||||
typer.Option(
|
||||
"--env-var", "-v", help="Environment variables in KEY=VALUE format"
|
||||
),
|
||||
] = [],
|
||||
env_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--env-file",
|
||||
"-f",
|
||||
help="Load environment variables from a .env file",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
copy: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--copy",
|
||||
help="Copy configuration to clipboard instead of printing to stdout",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Generate MCP configuration JSON for manual installation."""
|
||||
file, server_object, name, packages, env_dict = process_common_args(
|
||||
server_spec, server_name, with_packages, env_vars, env_file
|
||||
)
|
||||
|
||||
success = install_mcp_config(
|
||||
file=file,
|
||||
server_object=server_object,
|
||||
name=name,
|
||||
with_editable=with_editable,
|
||||
with_packages=packages,
|
||||
env_vars=env_dict,
|
||||
copy=copy,
|
||||
)
|
||||
|
||||
# mcp-config handles its own messaging, no generic success message needed
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
87
src/fastmcp/cli/install/shared.py
Normal file
87
src/fastmcp/cli/install/shared.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Shared utilities for install commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from rich import print
|
||||
|
||||
from fastmcp.cli.run import import_server, parse_file_path
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def parse_env_var(env_var: str) -> tuple[str, str]:
|
||||
"""Parse environment variable string in format KEY=VALUE."""
|
||||
if "=" not in env_var:
|
||||
print(
|
||||
f"[red]❌ Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]"
|
||||
)
|
||||
sys.exit(1)
|
||||
key, value = env_var.split("=", 1)
|
||||
return key.strip(), value.strip()
|
||||
|
||||
|
||||
def process_common_args(
|
||||
server_spec: str,
|
||||
server_name: str | None,
|
||||
with_packages: list[str],
|
||||
env_vars: list[str],
|
||||
env_file: Path | None,
|
||||
) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
|
||||
"""Process common arguments shared by all install commands."""
|
||||
# Parse server spec
|
||||
file, server_object = parse_file_path(server_spec)
|
||||
|
||||
logger.debug(
|
||||
"Installing server",
|
||||
extra={
|
||||
"file": str(file),
|
||||
"server_name": server_name,
|
||||
"server_object": server_object,
|
||||
"with_packages": with_packages,
|
||||
},
|
||||
)
|
||||
|
||||
# Try to import server to get its name and dependencies
|
||||
name = server_name
|
||||
server = None
|
||||
if not name:
|
||||
try:
|
||||
server = import_server(file, server_object)
|
||||
name = server.name
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.debug(
|
||||
"Could not import server (likely missing dependencies), using file name",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
name = file.stem
|
||||
|
||||
# Get server dependencies if available
|
||||
server_dependencies = getattr(server, "dependencies", []) if server else []
|
||||
if server_dependencies:
|
||||
with_packages = list(set(with_packages + server_dependencies))
|
||||
|
||||
# Process environment variables if provided
|
||||
env_dict: dict[str, str] | None = None
|
||||
if env_file or env_vars:
|
||||
env_dict = {}
|
||||
# Load from .env file if specified
|
||||
if env_file:
|
||||
try:
|
||||
env_dict |= {
|
||||
k: v for k, v in dotenv_values(env_file).items() if v is not None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[red]❌ Failed to load .env file: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
# Add command line environment variables
|
||||
for env_var in env_vars:
|
||||
key, value = parse_env_var(env_var)
|
||||
env_dict[key] = value
|
||||
|
||||
return file, server_object, name, with_packages, env_dict
|
||||
Loading…
Add table
Add a link
Reference in a new issue