fastmcp 2.10.2__py3-none-any.whl → 2.10.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ """Claude Desktop integration for FastMCP install using Cyclopts."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import cyclopts
9
+ from rich import print
10
+
11
+ from fastmcp.mcp_config import StdioMCPServer, update_config_file
12
+ from fastmcp.utilities.logging import get_logger
13
+
14
+ from .shared import process_common_args
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ def get_claude_config_path() -> Path | None:
20
+ """Get the Claude config directory based on platform."""
21
+ if sys.platform == "win32":
22
+ path = Path(Path.home(), "AppData", "Roaming", "Claude")
23
+ elif sys.platform == "darwin":
24
+ path = Path(Path.home(), "Library", "Application Support", "Claude")
25
+ elif sys.platform.startswith("linux"):
26
+ path = Path(
27
+ os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
28
+ )
29
+ else:
30
+ return None
31
+
32
+ if path.exists():
33
+ return path
34
+ return None
35
+
36
+
37
+ def install_claude_desktop(
38
+ file: Path,
39
+ server_object: str | None,
40
+ name: str,
41
+ *,
42
+ with_editable: Path | None = None,
43
+ with_packages: list[str] | None = None,
44
+ env_vars: dict[str, str] | None = None,
45
+ ) -> bool:
46
+ """Install FastMCP server in Claude Desktop.
47
+
48
+ Args:
49
+ file: Path to the server file
50
+ server_object: Optional server object name (for :object suffix)
51
+ name: Name for the server in Claude's config
52
+ with_editable: Optional directory to install in editable mode
53
+ with_packages: Optional list of additional packages to install
54
+ env_vars: Optional dictionary of environment variables
55
+
56
+ Returns:
57
+ True if installation was successful, False otherwise
58
+ """
59
+ config_dir = get_claude_config_path()
60
+ if not config_dir:
61
+ print(
62
+ "[red]Claude Desktop config directory not found.[/red]\n"
63
+ "[blue]Please ensure Claude Desktop is installed and has been run at least once to initialize its config.[/blue]"
64
+ )
65
+ return False
66
+
67
+ config_file = config_dir / "claude_desktop_config.json"
68
+
69
+ # Build uv run command
70
+ args = ["run"]
71
+
72
+ # Collect all packages in a set to deduplicate
73
+ packages = {"fastmcp"}
74
+ if with_packages:
75
+ packages.update(pkg for pkg in with_packages if pkg)
76
+
77
+ # Add all packages with --with
78
+ for pkg in sorted(packages):
79
+ args.extend(["--with", pkg])
80
+
81
+ if with_editable:
82
+ args.extend(["--with-editable", str(with_editable)])
83
+
84
+ # Build server spec from parsed components
85
+ if server_object:
86
+ server_spec = f"{file.resolve()}:{server_object}"
87
+ else:
88
+ server_spec = str(file.resolve())
89
+
90
+ # Add fastmcp run command
91
+ args.extend(["fastmcp", "run", server_spec])
92
+
93
+ # Create server configuration
94
+ server_config = StdioMCPServer(
95
+ command="uv",
96
+ args=args,
97
+ env=env_vars or {},
98
+ )
99
+
100
+ try:
101
+ # Handle environment variable merging manually since we need to preserve existing config
102
+ if config_file.exists():
103
+ import json
104
+
105
+ content = config_file.read_text().strip()
106
+ if content:
107
+ config = json.loads(content)
108
+ if "mcpServers" in config and name in config["mcpServers"]:
109
+ existing_env = config["mcpServers"][name].get("env", {})
110
+ if env_vars:
111
+ # New vars take precedence over existing ones
112
+ merged_env = {**existing_env, **env_vars}
113
+ else:
114
+ merged_env = existing_env
115
+ server_config.env = merged_env
116
+
117
+ # Update configuration with correct function signature
118
+ update_config_file(config_file, name, server_config)
119
+ print(f"[green]Successfully installed '{name}' in Claude Desktop[/green]")
120
+ return True
121
+ except Exception as e:
122
+ print(f"[red]Failed to install server: {e}[/red]")
123
+ return False
124
+
125
+
126
+ def claude_desktop_command(
127
+ server_spec: str,
128
+ *,
129
+ server_name: Annotated[
130
+ str | None,
131
+ cyclopts.Parameter(
132
+ name=["--server-name", "-n"],
133
+ help="Custom name for the server in Claude Desktop's config",
134
+ ),
135
+ ] = None,
136
+ with_editable: Annotated[
137
+ Path | None,
138
+ cyclopts.Parameter(
139
+ name=["--with-editable", "-e"],
140
+ help="Directory with pyproject.toml to install in editable mode",
141
+ ),
142
+ ] = None,
143
+ with_packages: Annotated[
144
+ list[str],
145
+ cyclopts.Parameter(
146
+ "--with",
147
+ help="Additional packages to install",
148
+ negative=False,
149
+ ),
150
+ ] = [],
151
+ env_vars: Annotated[
152
+ list[str],
153
+ cyclopts.Parameter(
154
+ "--env",
155
+ help="Environment variables in KEY=VALUE format",
156
+ negative=False,
157
+ ),
158
+ ] = [],
159
+ env_file: Annotated[
160
+ Path | None,
161
+ cyclopts.Parameter(
162
+ "--env-file",
163
+ help="Load environment variables from .env file",
164
+ ),
165
+ ] = None,
166
+ ) -> None:
167
+ """Install an MCP server in Claude Desktop.
168
+
169
+ Args:
170
+ server_spec: Python file to install, optionally with :object suffix
171
+ """
172
+ file, server_object, name, with_packages, env_dict = process_common_args(
173
+ server_spec, server_name, with_packages, env_vars, env_file
174
+ )
175
+
176
+ success = install_claude_desktop(
177
+ file=file,
178
+ server_object=server_object,
179
+ name=name,
180
+ with_editable=with_editable,
181
+ with_packages=with_packages,
182
+ env_vars=env_dict,
183
+ )
184
+
185
+ if not success:
186
+ sys.exit(1)
@@ -0,0 +1,196 @@
1
+ """Cursor integration for FastMCP install using Cyclopts."""
2
+
3
+ import base64
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import cyclopts
10
+ from rich import print
11
+
12
+ from fastmcp.mcp_config import StdioMCPServer
13
+ from fastmcp.utilities.logging import get_logger
14
+
15
+ from .shared import process_common_args
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ def generate_cursor_deeplink(
21
+ server_name: str,
22
+ server_config: StdioMCPServer,
23
+ ) -> str:
24
+ """Generate a Cursor deeplink for installing the MCP server.
25
+
26
+ Args:
27
+ server_name: Name of the server
28
+ server_config: Server configuration
29
+
30
+ Returns:
31
+ Deeplink URL that can be clicked to install the server
32
+ """
33
+ # Create the configuration structure expected by Cursor
34
+ # Base64 encode the configuration (URL-safe for query parameter)
35
+ config_json = server_config.model_dump_json(exclude_none=True)
36
+ config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()
37
+
38
+ # Generate the deeplink URL
39
+ deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}"
40
+
41
+ return deeplink
42
+
43
+
44
+ def open_deeplink(deeplink: str) -> bool:
45
+ """Attempt to open a deeplink URL using the system's default handler.
46
+
47
+ Args:
48
+ deeplink: The deeplink URL to open
49
+
50
+ Returns:
51
+ True if the command succeeded, False otherwise
52
+ """
53
+ try:
54
+ if sys.platform == "darwin": # macOS
55
+ subprocess.run(["open", deeplink], check=True, capture_output=True)
56
+ elif sys.platform == "win32": # Windows
57
+ subprocess.run(
58
+ ["start", deeplink], shell=True, check=True, capture_output=True
59
+ )
60
+ else: # Linux and others
61
+ subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
62
+ return True
63
+ except (subprocess.CalledProcessError, FileNotFoundError):
64
+ return False
65
+
66
+
67
+ def install_cursor(
68
+ file: Path,
69
+ server_object: str | None,
70
+ name: str,
71
+ *,
72
+ with_editable: Path | None = None,
73
+ with_packages: list[str] | None = None,
74
+ env_vars: dict[str, str] | None = None,
75
+ ) -> bool:
76
+ """Install FastMCP server in Cursor.
77
+
78
+ Args:
79
+ file: Path to the server file
80
+ server_object: Optional server object name (for :object suffix)
81
+ name: Name for the server in Cursor
82
+ with_editable: Optional directory to install in editable mode
83
+ with_packages: Optional list of additional packages to install
84
+ env_vars: Optional dictionary of environment variables
85
+
86
+ Returns:
87
+ True if installation was successful, False otherwise
88
+ """
89
+ # Build uv run command
90
+ args = ["run"]
91
+
92
+ # Collect all packages in a set to deduplicate
93
+ packages = {"fastmcp"}
94
+ if with_packages:
95
+ packages.update(pkg for pkg in with_packages if pkg)
96
+
97
+ # Add all packages with --with
98
+ for pkg in sorted(packages):
99
+ args.extend(["--with", pkg])
100
+
101
+ if with_editable:
102
+ args.extend(["--with-editable", str(with_editable)])
103
+
104
+ # Build server spec from parsed components
105
+ if server_object:
106
+ server_spec = f"{file.resolve()}:{server_object}"
107
+ else:
108
+ server_spec = str(file.resolve())
109
+
110
+ # Add fastmcp run command
111
+ args.extend(["fastmcp", "run", server_spec])
112
+
113
+ # Create server configuration
114
+ server_config = StdioMCPServer(
115
+ command="uv",
116
+ args=args,
117
+ env=env_vars or {},
118
+ )
119
+
120
+ # Generate deeplink
121
+ deeplink = generate_cursor_deeplink(name, server_config)
122
+
123
+ print(f"[blue]Opening Cursor to install '{name}'[/blue]")
124
+
125
+ if open_deeplink(deeplink):
126
+ print("[green]Cursor should now open with the installation dialog[/green]")
127
+ return True
128
+ else:
129
+ print(
130
+ "[red]Could not open Cursor automatically.[/red]\n"
131
+ f"[blue]Please copy this link and open it in Cursor: {deeplink}[/blue]"
132
+ )
133
+ return False
134
+
135
+
136
+ def cursor_command(
137
+ server_spec: str,
138
+ *,
139
+ server_name: Annotated[
140
+ str | None,
141
+ cyclopts.Parameter(
142
+ name=["--server-name", "-n"],
143
+ help="Custom name for the server in Cursor",
144
+ ),
145
+ ] = None,
146
+ with_editable: Annotated[
147
+ Path | None,
148
+ cyclopts.Parameter(
149
+ name=["--with-editable", "-e"],
150
+ help="Directory with pyproject.toml to install in editable mode",
151
+ ),
152
+ ] = None,
153
+ with_packages: Annotated[
154
+ list[str],
155
+ cyclopts.Parameter(
156
+ "--with",
157
+ help="Additional packages to install",
158
+ negative=False,
159
+ ),
160
+ ] = [],
161
+ env_vars: Annotated[
162
+ list[str],
163
+ cyclopts.Parameter(
164
+ "--env",
165
+ help="Environment variables in KEY=VALUE format",
166
+ negative=False,
167
+ ),
168
+ ] = [],
169
+ env_file: Annotated[
170
+ Path | None,
171
+ cyclopts.Parameter(
172
+ "--env-file",
173
+ help="Load environment variables from .env file",
174
+ ),
175
+ ] = None,
176
+ ) -> None:
177
+ """Install an MCP server in Cursor.
178
+
179
+ Args:
180
+ server_spec: Python file to install, optionally with :object suffix
181
+ """
182
+ file, server_object, name, with_packages, env_dict = process_common_args(
183
+ server_spec, server_name, with_packages, env_vars, env_file
184
+ )
185
+
186
+ success = install_cursor(
187
+ file=file,
188
+ server_object=server_object,
189
+ name=name,
190
+ with_editable=with_editable,
191
+ with_packages=with_packages,
192
+ env_vars=env_dict,
193
+ )
194
+
195
+ if not success:
196
+ sys.exit(1)
@@ -0,0 +1,165 @@
1
+ """MCP configuration JSON generation for FastMCP install using Cyclopts."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import cyclopts
9
+ import pyperclip
10
+ from rich import print
11
+
12
+ from fastmcp.utilities.logging import get_logger
13
+
14
+ from .shared import process_common_args
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ def install_mcp_config(
20
+ file: Path,
21
+ server_object: str | None,
22
+ name: str,
23
+ *,
24
+ with_editable: Path | None = None,
25
+ with_packages: list[str] | None = None,
26
+ env_vars: dict[str, str] | None = None,
27
+ copy: bool = False,
28
+ ) -> bool:
29
+ """Generate MCP configuration JSON for manual installation.
30
+
31
+ Args:
32
+ file: Path to the server file
33
+ server_object: Optional server object name (for :object suffix)
34
+ name: Name for the server in MCP config
35
+ with_editable: Optional directory to install in editable mode
36
+ with_packages: Optional list of additional packages to install
37
+ env_vars: Optional dictionary of environment variables
38
+ copy: If True, copy to clipboard instead of printing to stdout
39
+
40
+ Returns:
41
+ True if generation was successful, False otherwise
42
+ """
43
+ try:
44
+ # Build uv run command
45
+ args = ["run"]
46
+
47
+ # Collect all packages in a set to deduplicate
48
+ packages = {"fastmcp"}
49
+ if with_packages:
50
+ packages.update(pkg for pkg in with_packages if pkg)
51
+
52
+ # Add all packages with --with
53
+ for pkg in sorted(packages):
54
+ args.extend(["--with", pkg])
55
+
56
+ if with_editable:
57
+ args.extend(["--with-editable", str(with_editable)])
58
+
59
+ # Build server spec from parsed components
60
+ if server_object:
61
+ server_spec = f"{file.resolve()}:{server_object}"
62
+ else:
63
+ server_spec = str(file.resolve())
64
+
65
+ # Add fastmcp run command
66
+ args.extend(["fastmcp", "run", server_spec])
67
+
68
+ # Build MCP server configuration (just the server object, not the wrapper)
69
+ config = {
70
+ "command": "uv",
71
+ "args": args,
72
+ }
73
+
74
+ # Add environment variables if provided
75
+ if env_vars:
76
+ config["env"] = env_vars
77
+
78
+ # Convert to JSON
79
+ json_output = json.dumps(config, indent=2)
80
+
81
+ # Handle output
82
+ if copy:
83
+ pyperclip.copy(json_output)
84
+ print(f"[green]MCP configuration for '{name}' copied to clipboard[/green]")
85
+ else:
86
+ # Print to stdout (for piping)
87
+ print(json_output)
88
+
89
+ return True
90
+
91
+ except Exception as e:
92
+ print(f"[red]Failed to generate MCP configuration: {e}[/red]")
93
+ return False
94
+
95
+
96
+ def mcp_config_command(
97
+ server_spec: str,
98
+ *,
99
+ server_name: Annotated[
100
+ str | None,
101
+ cyclopts.Parameter(
102
+ name=["--server-name", "-n"],
103
+ help="Custom name for the server in MCP config",
104
+ ),
105
+ ] = None,
106
+ with_editable: Annotated[
107
+ Path | None,
108
+ cyclopts.Parameter(
109
+ name=["--with-editable", "-e"],
110
+ help="Directory with pyproject.toml to install in editable mode",
111
+ ),
112
+ ] = None,
113
+ with_packages: Annotated[
114
+ list[str],
115
+ cyclopts.Parameter(
116
+ "--with",
117
+ help="Additional packages to install",
118
+ negative=False,
119
+ ),
120
+ ] = [],
121
+ env_vars: Annotated[
122
+ list[str],
123
+ cyclopts.Parameter(
124
+ "--env",
125
+ help="Environment variables in KEY=VALUE format",
126
+ negative=False,
127
+ ),
128
+ ] = [],
129
+ env_file: Annotated[
130
+ Path | None,
131
+ cyclopts.Parameter(
132
+ "--env-file",
133
+ help="Load environment variables from .env file",
134
+ ),
135
+ ] = None,
136
+ copy: Annotated[
137
+ bool,
138
+ cyclopts.Parameter(
139
+ "--copy",
140
+ help="Copy configuration to clipboard instead of printing to stdout",
141
+ negative=False,
142
+ ),
143
+ ] = False,
144
+ ) -> None:
145
+ """Generate MCP configuration JSON for manual installation.
146
+
147
+ Args:
148
+ server_spec: Python file to install, optionally with :object suffix
149
+ """
150
+ file, server_object, name, packages, env_dict = process_common_args(
151
+ server_spec, server_name, with_packages, env_vars, env_file
152
+ )
153
+
154
+ success = install_mcp_config(
155
+ file=file,
156
+ server_object=server_object,
157
+ name=name,
158
+ with_editable=with_editable,
159
+ with_packages=packages,
160
+ env_vars=env_dict,
161
+ copy=copy,
162
+ )
163
+
164
+ if not success:
165
+ sys.exit(1)
@@ -0,0 +1,85 @@
1
+ """Shared utilities for install commands."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from dotenv import dotenv_values
7
+ from rich import print
8
+
9
+ from fastmcp.cli.run import import_server, parse_file_path
10
+ from fastmcp.utilities.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def parse_env_var(env_var: str) -> tuple[str, str]:
16
+ """Parse environment variable string in format KEY=VALUE."""
17
+ if "=" not in env_var:
18
+ print(
19
+ f"[red]Invalid environment variable format: '[bold]{env_var}[/bold]'. Must be KEY=VALUE[/red]"
20
+ )
21
+ sys.exit(1)
22
+ key, value = env_var.split("=", 1)
23
+ return key.strip(), value.strip()
24
+
25
+
26
+ def process_common_args(
27
+ server_spec: str,
28
+ server_name: str | None,
29
+ with_packages: list[str],
30
+ env_vars: list[str],
31
+ env_file: Path | None,
32
+ ) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
33
+ """Process common arguments shared by all install commands."""
34
+ # Parse server spec
35
+ file, server_object = parse_file_path(server_spec)
36
+
37
+ logger.debug(
38
+ "Installing server",
39
+ extra={
40
+ "file": str(file),
41
+ "server_name": server_name,
42
+ "server_object": server_object,
43
+ "with_packages": with_packages,
44
+ },
45
+ )
46
+
47
+ # Try to import server to get its name and dependencies
48
+ name = server_name
49
+ server = None
50
+ if not name:
51
+ try:
52
+ server = import_server(file, server_object)
53
+ name = server.name
54
+ except (ImportError, ModuleNotFoundError) as e:
55
+ logger.debug(
56
+ "Could not import server (likely missing dependencies), using file name",
57
+ extra={"error": str(e)},
58
+ )
59
+ name = file.stem
60
+
61
+ # Get server dependencies if available
62
+ server_dependencies = getattr(server, "dependencies", []) if server else []
63
+ if server_dependencies:
64
+ with_packages = list(set(with_packages + server_dependencies))
65
+
66
+ # Process environment variables if provided
67
+ env_dict: dict[str, str] | None = None
68
+ if env_file or env_vars:
69
+ env_dict = {}
70
+ # Load from .env file if specified
71
+ if env_file:
72
+ try:
73
+ env_dict |= {
74
+ k: v for k, v in dotenv_values(env_file).items() if v is not None
75
+ }
76
+ except Exception as e:
77
+ print(f"[red]Failed to load .env file: {e}[/red]")
78
+ sys.exit(1)
79
+
80
+ # Add command line environment variables
81
+ for env_var in env_vars:
82
+ key, value = parse_env_var(env_var)
83
+ env_dict[key] = value
84
+
85
+ return file, server_object, name, with_packages, env_dict
fastmcp/cli/run.py CHANGED
@@ -1,15 +1,19 @@
1
- """FastMCP run command implementation."""
1
+ """FastMCP run command implementation with enhanced type hints."""
2
2
 
3
3
  import importlib.util
4
4
  import re
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Literal
8
8
 
9
9
  from fastmcp.utilities.logging import get_logger
10
10
 
11
11
  logger = get_logger("cli.run")
12
12
 
13
+ # Type aliases for better type safety
14
+ TransportType = Literal["stdio", "http", "sse", "streamable-http"]
15
+ LogLevelType = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
16
+
13
17
 
14
18
  def is_url(path: str) -> bool:
15
19
  """Check if a string is a URL."""
@@ -164,10 +168,11 @@ def import_server_with_args(
164
168
 
165
169
  def run_command(
166
170
  server_spec: str,
167
- transport: str | None = None,
171
+ transport: TransportType | None = None,
168
172
  host: str | None = None,
169
173
  port: int | None = None,
170
- log_level: str | None = None,
174
+ path: str | None = None,
175
+ log_level: LogLevelType | None = None,
171
176
  server_args: list[str] | None = None,
172
177
  show_banner: bool = True,
173
178
  ) -> None:
@@ -178,8 +183,10 @@ def run_command(
178
183
  transport: Transport protocol to use
179
184
  host: Host to bind to when using http transport
180
185
  port: Port to bind to when using http transport
186
+ path: Path to bind to when using http transport
181
187
  log_level: Log level
182
188
  server_args: Additional arguments to pass to the server
189
+ show_banner: Whether to show the server banner
183
190
  """
184
191
  if is_url(server_spec):
185
192
  # Handle URL case
@@ -199,6 +206,8 @@ def run_command(
199
206
  kwargs["host"] = host
200
207
  if port:
201
208
  kwargs["port"] = port
209
+ if path:
210
+ kwargs["path"] = path
202
211
  if log_level:
203
212
  kwargs["log_level"] = log_level
204
213