fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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 +30 -138
- fastmcp/cli/run.py +179 -0
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/client.py +131 -24
- fastmcp/client/logging.py +8 -0
- fastmcp/client/progress.py +38 -0
- fastmcp/client/transports.py +80 -64
- fastmcp/exceptions.py +2 -0
- fastmcp/prompts/prompt.py +12 -6
- fastmcp/resources/resource_manager.py +22 -1
- fastmcp/resources/template.py +21 -17
- fastmcp/resources/types.py +25 -27
- fastmcp/server/context.py +6 -3
- fastmcp/server/http.py +47 -14
- fastmcp/server/openapi.py +14 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +159 -96
- fastmcp/settings.py +55 -29
- fastmcp/tools/tool.py +45 -45
- fastmcp/tools/tool_manager.py +27 -2
- fastmcp/utilities/exceptions.py +49 -0
- fastmcp/utilities/json_schema.py +78 -17
- fastmcp/utilities/logging.py +11 -6
- fastmcp/utilities/openapi.py +122 -7
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/METADATA +3 -3
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/RECORD +29 -27
- fastmcp/low_level/sse_server_transport.py +0 -104
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -17,6 +17,7 @@ from typer import Context, Exit
|
|
|
17
17
|
|
|
18
18
|
import fastmcp
|
|
19
19
|
from fastmcp.cli import claude
|
|
20
|
+
from fastmcp.cli import run as run_module
|
|
20
21
|
from fastmcp.utilities.logging import get_logger
|
|
21
22
|
|
|
22
23
|
logger = get_logger("cli")
|
|
@@ -58,7 +59,7 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def _build_uv_command(
|
|
61
|
-
|
|
62
|
+
server_spec: str,
|
|
62
63
|
with_editable: Path | None = None,
|
|
63
64
|
with_packages: list[str] | None = None,
|
|
64
65
|
) -> list[str]:
|
|
@@ -76,106 +77,10 @@ def _build_uv_command(
|
|
|
76
77
|
cmd.extend(["--with", pkg])
|
|
77
78
|
|
|
78
79
|
# Add mcp run command
|
|
79
|
-
cmd.extend(["fastmcp", "run",
|
|
80
|
+
cmd.extend(["fastmcp", "run", server_spec])
|
|
80
81
|
return cmd
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
|
|
84
|
-
"""Parse a file path that may include a server object specification.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
file_spec: Path to file, optionally with :object suffix
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Tuple of (file_path, server_object)
|
|
91
|
-
"""
|
|
92
|
-
# First check if we have a Windows path (e.g., C:\...)
|
|
93
|
-
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
|
|
94
|
-
|
|
95
|
-
# Split on the last colon, but only if it's not part of the Windows drive letter
|
|
96
|
-
# and there's actually another colon in the string after the drive letter
|
|
97
|
-
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
|
|
98
|
-
file_str, server_object = file_spec.rsplit(":", 1)
|
|
99
|
-
else:
|
|
100
|
-
file_str, server_object = file_spec, None
|
|
101
|
-
|
|
102
|
-
# Resolve the file path
|
|
103
|
-
file_path = Path(file_str).expanduser().resolve()
|
|
104
|
-
if not file_path.exists():
|
|
105
|
-
logger.error(f"File not found: {file_path}")
|
|
106
|
-
sys.exit(1)
|
|
107
|
-
if not file_path.is_file():
|
|
108
|
-
logger.error(f"Not a file: {file_path}")
|
|
109
|
-
sys.exit(1)
|
|
110
|
-
|
|
111
|
-
return file_path, server_object
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _import_server(file: Path, server_object: str | None = None):
|
|
115
|
-
"""Import a MCP server from a file.
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
file: Path to the file
|
|
119
|
-
server_object: Optional object name in format "module:object" or just "object"
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
The server object
|
|
123
|
-
"""
|
|
124
|
-
# Add parent directory to Python path so imports can be resolved
|
|
125
|
-
file_dir = str(file.parent)
|
|
126
|
-
if file_dir not in sys.path:
|
|
127
|
-
sys.path.insert(0, file_dir)
|
|
128
|
-
|
|
129
|
-
# Import the module
|
|
130
|
-
spec = importlib.util.spec_from_file_location("server_module", file)
|
|
131
|
-
if not spec or not spec.loader:
|
|
132
|
-
logger.error("Could not load module", extra={"file": str(file)})
|
|
133
|
-
sys.exit(1)
|
|
134
|
-
|
|
135
|
-
module = importlib.util.module_from_spec(spec)
|
|
136
|
-
spec.loader.exec_module(module)
|
|
137
|
-
|
|
138
|
-
# If no object specified, try common server names
|
|
139
|
-
if not server_object:
|
|
140
|
-
# Look for the most common server object names
|
|
141
|
-
for name in ["mcp", "server", "app"]:
|
|
142
|
-
if hasattr(module, name):
|
|
143
|
-
return getattr(module, name)
|
|
144
|
-
|
|
145
|
-
logger.error(
|
|
146
|
-
f"No server object found in {file}. Please either:\n"
|
|
147
|
-
"1. Use a standard variable name (mcp, server, or app)\n"
|
|
148
|
-
"2. Specify the object name with file:object syntax",
|
|
149
|
-
extra={"file": str(file)},
|
|
150
|
-
)
|
|
151
|
-
sys.exit(1)
|
|
152
|
-
|
|
153
|
-
# Handle module:object syntax
|
|
154
|
-
if ":" in server_object:
|
|
155
|
-
module_name, object_name = server_object.split(":", 1)
|
|
156
|
-
try:
|
|
157
|
-
server_module = importlib.import_module(module_name)
|
|
158
|
-
server = getattr(server_module, object_name, None)
|
|
159
|
-
except ImportError:
|
|
160
|
-
logger.error(
|
|
161
|
-
f"Could not import module '{module_name}'",
|
|
162
|
-
extra={"file": str(file)},
|
|
163
|
-
)
|
|
164
|
-
sys.exit(1)
|
|
165
|
-
else:
|
|
166
|
-
# Just object name
|
|
167
|
-
server = getattr(module, server_object, None)
|
|
168
|
-
|
|
169
|
-
if server is None:
|
|
170
|
-
logger.error(
|
|
171
|
-
f"Server object '{server_object}' not found",
|
|
172
|
-
extra={"file": str(file)},
|
|
173
|
-
)
|
|
174
|
-
sys.exit(1)
|
|
175
|
-
|
|
176
|
-
return server
|
|
177
|
-
|
|
178
|
-
|
|
179
84
|
@app.command()
|
|
180
85
|
def version(ctx: Context):
|
|
181
86
|
if ctx.resilient_parsing:
|
|
@@ -201,7 +106,7 @@ def version(ctx: Context):
|
|
|
201
106
|
|
|
202
107
|
@app.command()
|
|
203
108
|
def dev(
|
|
204
|
-
|
|
109
|
+
server_spec: str = typer.Argument(
|
|
205
110
|
...,
|
|
206
111
|
help="Python file to run, optionally with :object suffix",
|
|
207
112
|
),
|
|
@@ -246,7 +151,7 @@ def dev(
|
|
|
246
151
|
] = None,
|
|
247
152
|
) -> None:
|
|
248
153
|
"""Run a MCP server with the MCP Inspector."""
|
|
249
|
-
file, server_object =
|
|
154
|
+
file, server_object = run_module.parse_file_path(server_spec)
|
|
250
155
|
|
|
251
156
|
logger.debug(
|
|
252
157
|
"Starting dev server",
|
|
@@ -262,8 +167,8 @@ def dev(
|
|
|
262
167
|
|
|
263
168
|
try:
|
|
264
169
|
# Import server to get dependencies
|
|
265
|
-
server =
|
|
266
|
-
if hasattr(server, "dependencies"):
|
|
170
|
+
server = run_module.import_server(file, server_object)
|
|
171
|
+
if hasattr(server, "dependencies") and server.dependencies is not None:
|
|
267
172
|
with_packages = list(set(with_packages + server.dependencies))
|
|
268
173
|
|
|
269
174
|
env_vars = {}
|
|
@@ -285,7 +190,7 @@ def dev(
|
|
|
285
190
|
if inspector_version:
|
|
286
191
|
inspector_cmd += f"@{inspector_version}"
|
|
287
192
|
|
|
288
|
-
uv_cmd = _build_uv_command(
|
|
193
|
+
uv_cmd = _build_uv_command(server_spec, with_editable, with_packages)
|
|
289
194
|
|
|
290
195
|
# Run the MCP Inspector command with shell=True on Windows
|
|
291
196
|
shell = sys.platform == "win32"
|
|
@@ -318,9 +223,9 @@ def dev(
|
|
|
318
223
|
|
|
319
224
|
@app.command()
|
|
320
225
|
def run(
|
|
321
|
-
|
|
226
|
+
server_spec: str = typer.Argument(
|
|
322
227
|
...,
|
|
323
|
-
help="Python file
|
|
228
|
+
help="Python file, object specification (file:obj), or URL",
|
|
324
229
|
),
|
|
325
230
|
transport: Annotated[
|
|
326
231
|
str | None,
|
|
@@ -354,22 +259,20 @@ def run(
|
|
|
354
259
|
),
|
|
355
260
|
] = None,
|
|
356
261
|
) -> None:
|
|
357
|
-
"""Run a MCP server.
|
|
262
|
+
"""Run a MCP server or connect to a remote one.
|
|
358
263
|
|
|
359
|
-
The server can be specified in
|
|
360
|
-
1. Module approach: server.py - runs the module directly,
|
|
361
|
-
2. Import approach: server.py:app - imports and runs the specified server object.\n
|
|
264
|
+
The server can be specified in three ways:
|
|
265
|
+
1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app.\n
|
|
266
|
+
2. Import approach: server.py:app - imports and runs the specified server object.\n
|
|
267
|
+
3. URL approach: http://server-url - connects to a remote server and creates a proxy.\n\n
|
|
362
268
|
|
|
363
269
|
Note: This command runs the server directly. You are responsible for ensuring
|
|
364
270
|
all dependencies are available.
|
|
365
271
|
"""
|
|
366
|
-
file, server_object = _parse_file_path(file_spec)
|
|
367
|
-
|
|
368
272
|
logger.debug(
|
|
369
|
-
"Running server",
|
|
273
|
+
"Running server or client",
|
|
370
274
|
extra={
|
|
371
|
-
"
|
|
372
|
-
"server_object": server_object,
|
|
275
|
+
"server_spec": server_spec,
|
|
373
276
|
"transport": transport,
|
|
374
277
|
"host": host,
|
|
375
278
|
"port": port,
|
|
@@ -378,29 +281,18 @@ def run(
|
|
|
378
281
|
)
|
|
379
282
|
|
|
380
283
|
try:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if transport:
|
|
389
|
-
kwargs["transport"] = transport
|
|
390
|
-
if host:
|
|
391
|
-
kwargs["host"] = host
|
|
392
|
-
if port:
|
|
393
|
-
kwargs["port"] = port
|
|
394
|
-
if log_level:
|
|
395
|
-
kwargs["log_level"] = log_level
|
|
396
|
-
|
|
397
|
-
server.run(**kwargs)
|
|
398
|
-
|
|
284
|
+
run_module.run_command(
|
|
285
|
+
server_spec=server_spec,
|
|
286
|
+
transport=transport,
|
|
287
|
+
host=host,
|
|
288
|
+
port=port,
|
|
289
|
+
log_level=log_level,
|
|
290
|
+
)
|
|
399
291
|
except Exception as e:
|
|
400
292
|
logger.error(
|
|
401
|
-
f"Failed to run
|
|
293
|
+
f"Failed to run: {e}",
|
|
402
294
|
extra={
|
|
403
|
-
"
|
|
295
|
+
"server_spec": server_spec,
|
|
404
296
|
"error": str(e),
|
|
405
297
|
},
|
|
406
298
|
)
|
|
@@ -409,7 +301,7 @@ def run(
|
|
|
409
301
|
|
|
410
302
|
@app.command()
|
|
411
303
|
def install(
|
|
412
|
-
|
|
304
|
+
server_spec: str = typer.Argument(
|
|
413
305
|
...,
|
|
414
306
|
help="Python file to run, optionally with :object suffix",
|
|
415
307
|
),
|
|
@@ -466,7 +358,7 @@ def install(
|
|
|
466
358
|
Environment variables are preserved once added and only updated if new values
|
|
467
359
|
are explicitly provided.
|
|
468
360
|
"""
|
|
469
|
-
file, server_object =
|
|
361
|
+
file, server_object = run_module.parse_file_path(server_spec)
|
|
470
362
|
|
|
471
363
|
logger.debug(
|
|
472
364
|
"Installing server",
|
|
@@ -489,7 +381,7 @@ def install(
|
|
|
489
381
|
server = None
|
|
490
382
|
if not name:
|
|
491
383
|
try:
|
|
492
|
-
server =
|
|
384
|
+
server = run_module.import_server(file, server_object)
|
|
493
385
|
name = server.name
|
|
494
386
|
except (ImportError, ModuleNotFoundError) as e:
|
|
495
387
|
logger.debug(
|
|
@@ -526,7 +418,7 @@ def install(
|
|
|
526
418
|
env_dict[key] = value
|
|
527
419
|
|
|
528
420
|
if claude.update_claude_config(
|
|
529
|
-
|
|
421
|
+
server_spec,
|
|
530
422
|
name,
|
|
531
423
|
with_editable=with_editable,
|
|
532
424
|
with_packages=with_packages,
|
fastmcp/cli/run.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""FastMCP run command implementation."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from fastmcp.utilities.logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger("cli.run")
|
|
12
|
+
|
|
13
|
+
TransportType = Literal["stdio", "streamable-http", "sse"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_url(path: str) -> bool:
|
|
17
|
+
"""Check if a string is a URL."""
|
|
18
|
+
url_pattern = re.compile(r"^https?://")
|
|
19
|
+
return bool(url_pattern.match(path))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
|
|
23
|
+
"""Parse a file path that may include a server object specification.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
server_spec: Path to file, optionally with :object suffix
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (file_path, server_object)
|
|
30
|
+
"""
|
|
31
|
+
# First check if we have a Windows path (e.g., C:\...)
|
|
32
|
+
has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
|
|
33
|
+
|
|
34
|
+
# Split on the last colon, but only if it's not part of the Windows drive letter
|
|
35
|
+
# and there's actually another colon in the string after the drive letter
|
|
36
|
+
if ":" in (server_spec[2:] if has_windows_drive else server_spec):
|
|
37
|
+
file_str, server_object = server_spec.rsplit(":", 1)
|
|
38
|
+
else:
|
|
39
|
+
file_str, server_object = server_spec, None
|
|
40
|
+
|
|
41
|
+
# Resolve the file path
|
|
42
|
+
file_path = Path(file_str).expanduser().resolve()
|
|
43
|
+
if not file_path.exists():
|
|
44
|
+
logger.error(f"File not found: {file_path}")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
if not file_path.is_file():
|
|
47
|
+
logger.error(f"Not a file: {file_path}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
return file_path, server_object
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def import_server(file: Path, server_object: str | None = None) -> Any:
|
|
54
|
+
"""Import a MCP server from a file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file: Path to the file
|
|
58
|
+
server_object: Optional object name in format "module:object" or just "object"
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The server object
|
|
62
|
+
"""
|
|
63
|
+
# Add parent directory to Python path so imports can be resolved
|
|
64
|
+
file_dir = str(file.parent)
|
|
65
|
+
if file_dir not in sys.path:
|
|
66
|
+
sys.path.insert(0, file_dir)
|
|
67
|
+
|
|
68
|
+
# Import the module
|
|
69
|
+
spec = importlib.util.spec_from_file_location("server_module", file)
|
|
70
|
+
if not spec or not spec.loader:
|
|
71
|
+
logger.error("Could not load module", extra={"file": str(file)})
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
module = importlib.util.module_from_spec(spec)
|
|
75
|
+
spec.loader.exec_module(module)
|
|
76
|
+
|
|
77
|
+
# If no object specified, try common server names
|
|
78
|
+
if not server_object:
|
|
79
|
+
# Look for the most common server object names
|
|
80
|
+
for name in ["mcp", "server", "app"]:
|
|
81
|
+
if hasattr(module, name):
|
|
82
|
+
return getattr(module, name)
|
|
83
|
+
|
|
84
|
+
logger.error(
|
|
85
|
+
f"No server object found in {file}. Please either:\n"
|
|
86
|
+
"1. Use a standard variable name (mcp, server, or app)\n"
|
|
87
|
+
"2. Specify the object name with file:object syntax",
|
|
88
|
+
extra={"file": str(file)},
|
|
89
|
+
)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
# Handle module:object syntax
|
|
93
|
+
if ":" in server_object:
|
|
94
|
+
module_name, object_name = server_object.split(":", 1)
|
|
95
|
+
try:
|
|
96
|
+
server_module = importlib.import_module(module_name)
|
|
97
|
+
server = getattr(server_module, object_name, None)
|
|
98
|
+
except ImportError:
|
|
99
|
+
logger.error(
|
|
100
|
+
f"Could not import module '{module_name}'",
|
|
101
|
+
extra={"file": str(file)},
|
|
102
|
+
)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
else:
|
|
105
|
+
# Just object name
|
|
106
|
+
server = getattr(module, server_object, None)
|
|
107
|
+
|
|
108
|
+
if server is None:
|
|
109
|
+
logger.error(
|
|
110
|
+
f"Server object '{server_object}' not found",
|
|
111
|
+
extra={"file": str(file)},
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
return server
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_client_server(url: str) -> Any:
|
|
119
|
+
"""Create a FastMCP server from a client URL.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
url: The URL to connect to
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A FastMCP server instance
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
import fastmcp
|
|
129
|
+
|
|
130
|
+
client = fastmcp.Client(url)
|
|
131
|
+
server = fastmcp.FastMCP.from_client(client)
|
|
132
|
+
return server
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to create client for URL {url}: {e}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_command(
|
|
139
|
+
server_spec: str,
|
|
140
|
+
transport: str | None = None,
|
|
141
|
+
host: str | None = None,
|
|
142
|
+
port: int | None = None,
|
|
143
|
+
log_level: str | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Run a MCP server or connect to a remote one.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
server_spec: Python file, object specification (file:obj), or URL
|
|
149
|
+
transport: Transport protocol to use
|
|
150
|
+
host: Host to bind to when using http transport
|
|
151
|
+
port: Port to bind to when using http transport
|
|
152
|
+
log_level: Log level
|
|
153
|
+
"""
|
|
154
|
+
if is_url(server_spec):
|
|
155
|
+
# Handle URL case
|
|
156
|
+
server = create_client_server(server_spec)
|
|
157
|
+
logger.debug(f"Created client proxy server for {server_spec}")
|
|
158
|
+
else:
|
|
159
|
+
# Handle file case
|
|
160
|
+
file, server_object = parse_file_path(server_spec)
|
|
161
|
+
server = import_server(file, server_object)
|
|
162
|
+
logger.debug(f'Found server "{server.name}" in {file}')
|
|
163
|
+
|
|
164
|
+
# Run the server
|
|
165
|
+
kwargs = {}
|
|
166
|
+
if transport:
|
|
167
|
+
kwargs["transport"] = transport
|
|
168
|
+
if host:
|
|
169
|
+
kwargs["host"] = host
|
|
170
|
+
if port:
|
|
171
|
+
kwargs["port"] = port
|
|
172
|
+
if log_level:
|
|
173
|
+
kwargs["log_level"] = log_level
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
server.run(**kwargs)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Failed to run server: {e}")
|
|
179
|
+
sys.exit(1)
|
fastmcp/client/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .transports import (
|
|
|
9
9
|
UvxStdioTransport,
|
|
10
10
|
NpxStdioTransport,
|
|
11
11
|
FastMCPTransport,
|
|
12
|
+
StreamableHttpTransport,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
@@ -22,4 +23,5 @@ __all__ = [
|
|
|
22
23
|
"UvxStdioTransport",
|
|
23
24
|
"NpxStdioTransport",
|
|
24
25
|
"FastMCPTransport",
|
|
26
|
+
"StreamableHttpTransport",
|
|
25
27
|
]
|