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.
- fastmcp/cli/cli.py +102 -225
- fastmcp/cli/install/__init__.py +20 -0
- fastmcp/cli/install/claude_code.py +186 -0
- fastmcp/cli/install/claude_desktop.py +186 -0
- fastmcp/cli/install/cursor.py +196 -0
- fastmcp/cli/install/mcp_config.py +165 -0
- fastmcp/cli/install/shared.py +85 -0
- fastmcp/cli/run.py +13 -4
- fastmcp/client/client.py +230 -124
- fastmcp/client/transports.py +1 -1
- fastmcp/mcp_config.py +282 -0
- fastmcp/prompts/prompt.py +2 -4
- fastmcp/resources/resource.py +2 -2
- fastmcp/resources/template.py +1 -1
- fastmcp/server/openapi.py +40 -9
- fastmcp/server/proxy.py +101 -48
- fastmcp/server/server.py +32 -3
- fastmcp/tools/tool.py +3 -2
- fastmcp/tools/tool_transform.py +5 -6
- fastmcp/utilities/json_schema.py +14 -3
- fastmcp/utilities/openapi.py +92 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/METADATA +4 -3
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/RECORD +26 -20
- fastmcp/utilities/mcp_config.py +0 -103
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -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:
|
|
171
|
+
transport: TransportType | None = None,
|
|
168
172
|
host: str | None = None,
|
|
169
173
|
port: int | None = None,
|
|
170
|
-
|
|
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
|
|