youtube-summarizer/venv311/lib/python3.11/site-packages/fastmcp/cli/install/cursor.py

238 lines
6.7 KiB
Python

"""Cursor integration for FastMCP install using Cyclopts."""
import base64
import subprocess
import sys
from pathlib import Path
from typing import Annotated
import cyclopts
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__)
def generate_cursor_deeplink(
server_name: str,
server_config: StdioMCPServer,
) -> str:
"""Generate a Cursor deeplink for installing the MCP server.
Args:
server_name: Name of the server
server_config: Server configuration
Returns:
Deeplink URL that can be clicked to install the server
"""
# Create the configuration structure expected by Cursor
# Base64 encode the configuration (URL-safe for query parameter)
config_json = server_config.model_dump_json(exclude_none=True)
config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()
# Generate the deeplink URL
deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}"
return deeplink
def open_deeplink(deeplink: str) -> bool:
"""Attempt to open a deeplink URL using the system's default handler.
Args:
deeplink: The deeplink URL to open
Returns:
True if the command succeeded, False otherwise
"""
try:
if sys.platform == "darwin": # macOS
subprocess.run(["open", deeplink], check=True, capture_output=True)
elif sys.platform == "win32": # Windows
subprocess.run(
["start", deeplink], shell=True, check=True, capture_output=True
)
else: # Linux and others
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def install_cursor(
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 Cursor.
Args:
file: Path to the server file
server_object: Optional server object name (for :object suffix)
name: Name for the server in Cursor
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
"""
# 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])
# Create server configuration
server_config = StdioMCPServer(
command="uv",
args=args,
env=env_vars or {},
)
# Generate deeplink
deeplink = generate_cursor_deeplink(name, server_config)
print(f"[blue]Opening Cursor to install '{name}'[/blue]")
if open_deeplink(deeplink):
print("[green]Cursor should now open with the installation dialog[/green]")
return True
else:
print(
"[red]Could not open Cursor automatically.[/red]\n"
f"[blue]Please copy this link and open it in Cursor: {deeplink}[/blue]"
)
return False
async def cursor_command(
server_spec: str,
*,
server_name: Annotated[
str | None,
cyclopts.Parameter(
name=["--name", "-n"],
help="Custom name for the server in Cursor",
),
] = 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 Cursor.
Args:
server_spec: Python file to install, optionally with :object suffix
"""
file, server_object, name, with_packages, env_dict = await 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=with_packages,
env_vars=env_dict,
python_version=python,
with_requirements=with_requirements,
project=project,
)
if not success:
sys.exit(1)