fastmcp 2.5.1__py3-none-any.whl → 2.5.2__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 +1 -3
- fastmcp/client/client.py +83 -15
- fastmcp/client/transports.py +179 -15
- fastmcp/resources/template.py +1 -1
- fastmcp/server/dependencies.py +17 -6
- fastmcp/server/server.py +43 -16
- fastmcp/settings.py +7 -0
- fastmcp/tools/tool.py +4 -2
- {fastmcp-2.5.1.dist-info → fastmcp-2.5.2.dist-info}/METADATA +1 -1
- {fastmcp-2.5.1.dist-info → fastmcp-2.5.2.dist-info}/RECORD +13 -13
- {fastmcp-2.5.1.dist-info → fastmcp-2.5.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.5.1.dist-info → fastmcp-2.5.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.5.1.dist-info → fastmcp-2.5.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -50,9 +50,7 @@ def _get_npx_command():
|
|
|
50
50
|
def _parse_env_var(env_var: str) -> tuple[str, str]:
|
|
51
51
|
"""Parse environment variable string in format KEY=VALUE."""
|
|
52
52
|
if "=" not in env_var:
|
|
53
|
-
logger.error(
|
|
54
|
-
f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
|
|
55
|
-
)
|
|
53
|
+
logger.error("Invalid environment variable format. Must be KEY=VALUE")
|
|
56
54
|
sys.exit(1)
|
|
57
55
|
key, value = env_var.split("=", 1)
|
|
58
56
|
return key.strip(), value.strip()
|
fastmcp/client/client.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, cast
|
|
4
|
+
from typing import Any, Generic, cast, overload
|
|
5
5
|
|
|
6
6
|
import anyio
|
|
7
7
|
import mcp.types
|
|
@@ -9,6 +9,7 @@ from exceptiongroup import catch
|
|
|
9
9
|
from mcp import ClientSession
|
|
10
10
|
from pydantic import AnyUrl
|
|
11
11
|
|
|
12
|
+
import fastmcp
|
|
12
13
|
from fastmcp.client.logging import (
|
|
13
14
|
LogHandler,
|
|
14
15
|
MessageHandler,
|
|
@@ -27,7 +28,18 @@ from fastmcp.server import FastMCP
|
|
|
27
28
|
from fastmcp.utilities.exceptions import get_catch_handlers
|
|
28
29
|
from fastmcp.utilities.mcp_config import MCPConfig
|
|
29
30
|
|
|
30
|
-
from .transports import
|
|
31
|
+
from .transports import (
|
|
32
|
+
ClientTransportT,
|
|
33
|
+
FastMCP1Server,
|
|
34
|
+
FastMCPTransport,
|
|
35
|
+
MCPConfigTransport,
|
|
36
|
+
NodeStdioTransport,
|
|
37
|
+
PythonStdioTransport,
|
|
38
|
+
SessionKwargs,
|
|
39
|
+
SSETransport,
|
|
40
|
+
StreamableHttpTransport,
|
|
41
|
+
infer_transport,
|
|
42
|
+
)
|
|
31
43
|
|
|
32
44
|
__all__ = [
|
|
33
45
|
"Client",
|
|
@@ -40,13 +52,13 @@ __all__ = [
|
|
|
40
52
|
]
|
|
41
53
|
|
|
42
54
|
|
|
43
|
-
class Client:
|
|
55
|
+
class Client(Generic[ClientTransportT]):
|
|
44
56
|
"""
|
|
45
57
|
MCP client that delegates connection management to a Transport instance.
|
|
46
58
|
|
|
47
59
|
The Client class is responsible for MCP protocol logic, while the Transport
|
|
48
|
-
handles connection establishment and management. Client provides methods
|
|
49
|
-
|
|
60
|
+
handles connection establishment and management. Client provides methods for
|
|
61
|
+
working with resources, prompts, tools and other MCP capabilities.
|
|
50
62
|
|
|
51
63
|
Args:
|
|
52
64
|
transport: Connection source specification, which can be:
|
|
@@ -62,24 +74,60 @@ class Client:
|
|
|
62
74
|
message_handler: Optional handler for protocol messages
|
|
63
75
|
progress_handler: Optional handler for progress notifications
|
|
64
76
|
timeout: Optional timeout for requests (seconds or timedelta)
|
|
77
|
+
init_timeout: Optional timeout for initial connection (seconds or timedelta).
|
|
78
|
+
Set to 0 to disable. If None, uses the value in the FastMCP global settings.
|
|
65
79
|
|
|
66
80
|
Examples:
|
|
67
|
-
```python
|
|
68
|
-
|
|
69
|
-
client = Client("http://localhost:8080")
|
|
81
|
+
```python # Connect to FastMCP server client =
|
|
82
|
+
Client("http://localhost:8080")
|
|
70
83
|
|
|
71
84
|
async with client:
|
|
72
|
-
# List available resources
|
|
73
|
-
resources = await client.list_resources()
|
|
85
|
+
# List available resources resources = await client.list_resources()
|
|
74
86
|
|
|
75
|
-
# Call a tool
|
|
76
|
-
|
|
87
|
+
# Call a tool result = await client.call_tool("my_tool", {"param":
|
|
88
|
+
"value"})
|
|
77
89
|
```
|
|
78
90
|
"""
|
|
79
91
|
|
|
92
|
+
@overload
|
|
93
|
+
def __new__(
|
|
94
|
+
cls,
|
|
95
|
+
transport: ClientTransportT,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> "Client[ClientTransportT]": ...
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def __new__(
|
|
101
|
+
cls, transport: AnyUrl, **kwargs
|
|
102
|
+
) -> "Client[SSETransport|StreamableHttpTransport]": ...
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def __new__(
|
|
106
|
+
cls, transport: FastMCP | FastMCP1Server, **kwargs
|
|
107
|
+
) -> "Client[FastMCPTransport]": ...
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def __new__(
|
|
111
|
+
cls, transport: Path, **kwargs
|
|
112
|
+
) -> "Client[PythonStdioTransport|NodeStdioTransport]": ...
|
|
113
|
+
|
|
114
|
+
@overload
|
|
115
|
+
def __new__(
|
|
116
|
+
cls, transport: MCPConfig | dict[str, Any], **kwargs
|
|
117
|
+
) -> "Client[MCPConfigTransport]": ...
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def __new__(
|
|
121
|
+
cls, transport: str, **kwargs
|
|
122
|
+
) -> "Client[PythonStdioTransport|NodeStdioTransport|SSETransport|StreamableHttpTransport]": ...
|
|
123
|
+
|
|
124
|
+
def __new__(cls, transport, **kwargs) -> "Client":
|
|
125
|
+
instance = super().__new__(cls)
|
|
126
|
+
return instance
|
|
127
|
+
|
|
80
128
|
def __init__(
|
|
81
129
|
self,
|
|
82
|
-
transport:
|
|
130
|
+
transport: ClientTransportT
|
|
83
131
|
| FastMCP
|
|
84
132
|
| AnyUrl
|
|
85
133
|
| Path
|
|
@@ -93,8 +141,9 @@ class Client:
|
|
|
93
141
|
message_handler: MessageHandler | None = None,
|
|
94
142
|
progress_handler: ProgressHandler | None = None,
|
|
95
143
|
timeout: datetime.timedelta | float | int | None = None,
|
|
144
|
+
init_timeout: datetime.timedelta | float | int | None = None,
|
|
96
145
|
):
|
|
97
|
-
self.transport = infer_transport(transport)
|
|
146
|
+
self.transport = cast(ClientTransportT, infer_transport(transport))
|
|
98
147
|
self._session: ClientSession | None = None
|
|
99
148
|
self._exit_stack: AsyncExitStack | None = None
|
|
100
149
|
self._nesting_counter: int = 0
|
|
@@ -111,6 +160,17 @@ class Client:
|
|
|
111
160
|
if isinstance(timeout, int | float):
|
|
112
161
|
timeout = datetime.timedelta(seconds=timeout)
|
|
113
162
|
|
|
163
|
+
# handle init handshake timeout
|
|
164
|
+
if init_timeout is None:
|
|
165
|
+
init_timeout = fastmcp.settings.settings.client_init_timeout
|
|
166
|
+
if isinstance(init_timeout, datetime.timedelta):
|
|
167
|
+
init_timeout = init_timeout.total_seconds()
|
|
168
|
+
elif not init_timeout:
|
|
169
|
+
init_timeout = None
|
|
170
|
+
else:
|
|
171
|
+
init_timeout = float(init_timeout)
|
|
172
|
+
self._init_timeout = init_timeout
|
|
173
|
+
|
|
114
174
|
self._session_kwargs: SessionKwargs = {
|
|
115
175
|
"sampling_callback": None,
|
|
116
176
|
"list_roots_callback": None,
|
|
@@ -134,6 +194,7 @@ class Client:
|
|
|
134
194
|
raise RuntimeError(
|
|
135
195
|
"Client is not connected. Use the 'async with client:' context manager first."
|
|
136
196
|
)
|
|
197
|
+
|
|
137
198
|
return self._session
|
|
138
199
|
|
|
139
200
|
@property
|
|
@@ -168,9 +229,11 @@ class Client:
|
|
|
168
229
|
self._session = session
|
|
169
230
|
# Initialize the session
|
|
170
231
|
try:
|
|
171
|
-
with anyio.fail_after(
|
|
232
|
+
with anyio.fail_after(self._init_timeout):
|
|
172
233
|
self._initialize_result = await self._session.initialize()
|
|
173
234
|
yield
|
|
235
|
+
except anyio.ClosedResourceError:
|
|
236
|
+
raise RuntimeError("Server session was closed unexpectedly")
|
|
174
237
|
except TimeoutError:
|
|
175
238
|
raise RuntimeError("Failed to initialize server session")
|
|
176
239
|
finally:
|
|
@@ -203,6 +266,11 @@ class Client:
|
|
|
203
266
|
finally:
|
|
204
267
|
self._exit_stack = None
|
|
205
268
|
|
|
269
|
+
async def close(self):
|
|
270
|
+
await self.transport.close()
|
|
271
|
+
self._session = None
|
|
272
|
+
self._initialize_result = None
|
|
273
|
+
|
|
206
274
|
# --- MCP Client Methods ---
|
|
207
275
|
|
|
208
276
|
async def ping(self) -> bool:
|
fastmcp/client/transports.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import asyncio
|
|
2
3
|
import contextlib
|
|
3
4
|
import datetime
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import sys
|
|
8
|
+
import warnings
|
|
7
9
|
from collections.abc import AsyncIterator
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
|
11
|
+
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast, overload
|
|
10
12
|
|
|
11
13
|
from mcp import ClientSession, StdioServerParameters
|
|
12
14
|
from mcp.client.session import (
|
|
@@ -35,6 +37,9 @@ if TYPE_CHECKING:
|
|
|
35
37
|
|
|
36
38
|
logger = get_logger(__name__)
|
|
37
39
|
|
|
40
|
+
# TypeVar for preserving specific ClientTransport subclass types
|
|
41
|
+
ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
class SessionKwargs(TypedDict, total=False):
|
|
40
45
|
"""Keyword arguments for the MCP ClientSession constructor."""
|
|
@@ -83,11 +88,21 @@ class ClientTransport(abc.ABC):
|
|
|
83
88
|
# Basic representation for subclasses
|
|
84
89
|
return f"<{self.__class__.__name__}>"
|
|
85
90
|
|
|
91
|
+
async def close(self):
|
|
92
|
+
"""Close the transport."""
|
|
93
|
+
pass
|
|
94
|
+
|
|
86
95
|
|
|
87
96
|
class WSTransport(ClientTransport):
|
|
88
97
|
"""Transport implementation that connects to an MCP server via WebSockets."""
|
|
89
98
|
|
|
90
99
|
def __init__(self, url: str | AnyUrl):
|
|
100
|
+
# we never really used this transport, so it can be removed at any time
|
|
101
|
+
warnings.warn(
|
|
102
|
+
"WSTransport is a deprecated MCP transport and will be removed in a future version. Use StreamableHttpTransport instead.",
|
|
103
|
+
DeprecationWarning,
|
|
104
|
+
stacklevel=2,
|
|
105
|
+
)
|
|
91
106
|
if isinstance(url, AnyUrl):
|
|
92
107
|
url = str(url)
|
|
93
108
|
if not isinstance(url, str) or not url.startswith("ws"):
|
|
@@ -224,6 +239,7 @@ class StdioTransport(ClientTransport):
|
|
|
224
239
|
args: list[str],
|
|
225
240
|
env: dict[str, str] | None = None,
|
|
226
241
|
cwd: str | None = None,
|
|
242
|
+
keep_alive: bool | None = None,
|
|
227
243
|
):
|
|
228
244
|
"""
|
|
229
245
|
Initialize a Stdio transport.
|
|
@@ -233,25 +249,90 @@ class StdioTransport(ClientTransport):
|
|
|
233
249
|
args: The arguments to pass to the command
|
|
234
250
|
env: Environment variables to set for the subprocess
|
|
235
251
|
cwd: Current working directory for the subprocess
|
|
252
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
253
|
+
Defaults to True. When True, the subprocess remains active
|
|
254
|
+
after the connection context exits, allowing reuse in
|
|
255
|
+
subsequent connections.
|
|
236
256
|
"""
|
|
237
257
|
self.command = command
|
|
238
258
|
self.args = args
|
|
239
259
|
self.env = env
|
|
240
260
|
self.cwd = cwd
|
|
261
|
+
if keep_alive is None:
|
|
262
|
+
keep_alive = True
|
|
263
|
+
self.keep_alive = keep_alive
|
|
264
|
+
|
|
265
|
+
self._session: ClientSession | None = None
|
|
266
|
+
self._connect_task: asyncio.Task | None = None
|
|
267
|
+
self._ready_event = asyncio.Event()
|
|
268
|
+
self._stop_event = asyncio.Event()
|
|
241
269
|
|
|
242
270
|
@contextlib.asynccontextmanager
|
|
243
271
|
async def connect_session(
|
|
244
272
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
245
273
|
) -> AsyncIterator[ClientSession]:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
274
|
+
try:
|
|
275
|
+
await self.connect(**session_kwargs)
|
|
276
|
+
assert self._session is not None
|
|
277
|
+
yield self._session
|
|
278
|
+
finally:
|
|
279
|
+
if not self.keep_alive:
|
|
280
|
+
await self.disconnect()
|
|
281
|
+
else:
|
|
282
|
+
logger.debug("Stdio transport has keep_alive=True, not disconnecting")
|
|
283
|
+
|
|
284
|
+
async def connect(
|
|
285
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
286
|
+
) -> ClientSession | None:
|
|
287
|
+
if self._connect_task is not None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
async def _connect_task():
|
|
291
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
292
|
+
try:
|
|
293
|
+
server_params = StdioServerParameters(
|
|
294
|
+
command=self.command, args=self.args, env=self.env, cwd=self.cwd
|
|
295
|
+
)
|
|
296
|
+
transport = await stack.enter_async_context(
|
|
297
|
+
stdio_client(server_params)
|
|
298
|
+
)
|
|
299
|
+
read_stream, write_stream = transport
|
|
300
|
+
self._session = await stack.enter_async_context(
|
|
301
|
+
ClientSession(read_stream, write_stream, **session_kwargs)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
logger.debug("Stdio transport connected")
|
|
305
|
+
self._ready_event.set()
|
|
306
|
+
|
|
307
|
+
# Wait until disconnect is requested (stop_event is set)
|
|
308
|
+
await self._stop_event.wait()
|
|
309
|
+
finally:
|
|
310
|
+
# Clean up client on exit
|
|
311
|
+
self._session = None
|
|
312
|
+
logger.debug("Stdio transport disconnected")
|
|
313
|
+
|
|
314
|
+
# start the connection task
|
|
315
|
+
self._connect_task = asyncio.create_task(_connect_task())
|
|
316
|
+
# wait for the client to be ready before returning
|
|
317
|
+
await self._ready_event.wait()
|
|
318
|
+
|
|
319
|
+
async def disconnect(self):
|
|
320
|
+
if self._connect_task is None:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# signal the connection task to stop
|
|
324
|
+
self._stop_event.set()
|
|
325
|
+
|
|
326
|
+
# wait for the connection task to finish cleanly
|
|
327
|
+
await self._connect_task
|
|
328
|
+
|
|
329
|
+
# reset variables and events for potential future reconnects
|
|
330
|
+
self._connect_task = None
|
|
331
|
+
self._stop_event = asyncio.Event()
|
|
332
|
+
self._ready_event = asyncio.Event()
|
|
333
|
+
|
|
334
|
+
async def close(self):
|
|
335
|
+
await self.disconnect()
|
|
255
336
|
|
|
256
337
|
def __repr__(self) -> str:
|
|
257
338
|
return (
|
|
@@ -269,6 +350,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
269
350
|
env: dict[str, str] | None = None,
|
|
270
351
|
cwd: str | None = None,
|
|
271
352
|
python_cmd: str = sys.executable,
|
|
353
|
+
keep_alive: bool | None = None,
|
|
272
354
|
):
|
|
273
355
|
"""
|
|
274
356
|
Initialize a Python transport.
|
|
@@ -279,6 +361,10 @@ class PythonStdioTransport(StdioTransport):
|
|
|
279
361
|
env: Environment variables to set for the subprocess
|
|
280
362
|
cwd: Current working directory for the subprocess
|
|
281
363
|
python_cmd: Python command to use (default: "python")
|
|
364
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
365
|
+
Defaults to True. When True, the subprocess remains active
|
|
366
|
+
after the connection context exits, allowing reuse in
|
|
367
|
+
subsequent connections.
|
|
282
368
|
"""
|
|
283
369
|
script_path = Path(script_path).resolve()
|
|
284
370
|
if not script_path.is_file():
|
|
@@ -290,7 +376,13 @@ class PythonStdioTransport(StdioTransport):
|
|
|
290
376
|
if args:
|
|
291
377
|
full_args.extend(args)
|
|
292
378
|
|
|
293
|
-
super().__init__(
|
|
379
|
+
super().__init__(
|
|
380
|
+
command=python_cmd,
|
|
381
|
+
args=full_args,
|
|
382
|
+
env=env,
|
|
383
|
+
cwd=cwd,
|
|
384
|
+
keep_alive=keep_alive,
|
|
385
|
+
)
|
|
294
386
|
self.script_path = script_path
|
|
295
387
|
|
|
296
388
|
|
|
@@ -303,6 +395,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
303
395
|
args: list[str] | None = None,
|
|
304
396
|
env: dict[str, str] | None = None,
|
|
305
397
|
cwd: str | None = None,
|
|
398
|
+
keep_alive: bool | None = None,
|
|
306
399
|
):
|
|
307
400
|
script_path = Path(script_path).resolve()
|
|
308
401
|
if not script_path.is_file():
|
|
@@ -311,7 +404,11 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
311
404
|
raise ValueError(f"Not a Python script: {script_path}")
|
|
312
405
|
|
|
313
406
|
super().__init__(
|
|
314
|
-
command="fastmcp",
|
|
407
|
+
command="fastmcp",
|
|
408
|
+
args=["run", str(script_path)],
|
|
409
|
+
env=env,
|
|
410
|
+
cwd=cwd,
|
|
411
|
+
keep_alive=keep_alive,
|
|
315
412
|
)
|
|
316
413
|
self.script_path = script_path
|
|
317
414
|
|
|
@@ -326,6 +423,7 @@ class NodeStdioTransport(StdioTransport):
|
|
|
326
423
|
env: dict[str, str] | None = None,
|
|
327
424
|
cwd: str | None = None,
|
|
328
425
|
node_cmd: str = "node",
|
|
426
|
+
keep_alive: bool | None = None,
|
|
329
427
|
):
|
|
330
428
|
"""
|
|
331
429
|
Initialize a Node transport.
|
|
@@ -336,6 +434,10 @@ class NodeStdioTransport(StdioTransport):
|
|
|
336
434
|
env: Environment variables to set for the subprocess
|
|
337
435
|
cwd: Current working directory for the subprocess
|
|
338
436
|
node_cmd: Node.js command to use (default: "node")
|
|
437
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
438
|
+
Defaults to True. When True, the subprocess remains active
|
|
439
|
+
after the connection context exits, allowing reuse in
|
|
440
|
+
subsequent connections.
|
|
339
441
|
"""
|
|
340
442
|
script_path = Path(script_path).resolve()
|
|
341
443
|
if not script_path.is_file():
|
|
@@ -347,7 +449,9 @@ class NodeStdioTransport(StdioTransport):
|
|
|
347
449
|
if args:
|
|
348
450
|
full_args.extend(args)
|
|
349
451
|
|
|
350
|
-
super().__init__(
|
|
452
|
+
super().__init__(
|
|
453
|
+
command=node_cmd, args=full_args, env=env, cwd=cwd, keep_alive=keep_alive
|
|
454
|
+
)
|
|
351
455
|
self.script_path = script_path
|
|
352
456
|
|
|
353
457
|
|
|
@@ -363,6 +467,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
363
467
|
with_packages: list[str] | None = None,
|
|
364
468
|
from_package: str | None = None,
|
|
365
469
|
env_vars: dict[str, str] | None = None,
|
|
470
|
+
keep_alive: bool | None = None,
|
|
366
471
|
):
|
|
367
472
|
"""
|
|
368
473
|
Initialize a Uvx transport.
|
|
@@ -375,6 +480,10 @@ class UvxStdioTransport(StdioTransport):
|
|
|
375
480
|
with_packages: Additional packages to include
|
|
376
481
|
from_package: Package to install the tool from
|
|
377
482
|
env_vars: Additional environment variables
|
|
483
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
484
|
+
Defaults to True. When True, the subprocess remains active
|
|
485
|
+
after the connection context exits, allowing reuse in
|
|
486
|
+
subsequent connections.
|
|
378
487
|
"""
|
|
379
488
|
# Basic validation
|
|
380
489
|
if project_directory and not Path(project_directory).exists():
|
|
@@ -402,7 +511,13 @@ class UvxStdioTransport(StdioTransport):
|
|
|
402
511
|
env = os.environ.copy()
|
|
403
512
|
env.update(env_vars)
|
|
404
513
|
|
|
405
|
-
super().__init__(
|
|
514
|
+
super().__init__(
|
|
515
|
+
command="uvx",
|
|
516
|
+
args=uvx_args,
|
|
517
|
+
env=env,
|
|
518
|
+
cwd=project_directory,
|
|
519
|
+
keep_alive=keep_alive,
|
|
520
|
+
)
|
|
406
521
|
self.tool_name = tool_name
|
|
407
522
|
|
|
408
523
|
|
|
@@ -416,6 +531,7 @@ class NpxStdioTransport(StdioTransport):
|
|
|
416
531
|
project_directory: str | None = None,
|
|
417
532
|
env_vars: dict[str, str] | None = None,
|
|
418
533
|
use_package_lock: bool = True,
|
|
534
|
+
keep_alive: bool | None = None,
|
|
419
535
|
):
|
|
420
536
|
"""
|
|
421
537
|
Initialize an Npx transport.
|
|
@@ -426,6 +542,10 @@ class NpxStdioTransport(StdioTransport):
|
|
|
426
542
|
project_directory: Project directory with package.json
|
|
427
543
|
env_vars: Additional environment variables
|
|
428
544
|
use_package_lock: Whether to use package-lock.json (--prefer-offline)
|
|
545
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
546
|
+
Defaults to True. When True, the subprocess remains active
|
|
547
|
+
after the connection context exits, allowing reuse in
|
|
548
|
+
subsequent connections.
|
|
429
549
|
"""
|
|
430
550
|
# verify npx is installed
|
|
431
551
|
if shutil.which("npx") is None:
|
|
@@ -453,7 +573,13 @@ class NpxStdioTransport(StdioTransport):
|
|
|
453
573
|
env = os.environ.copy()
|
|
454
574
|
env.update(env_vars)
|
|
455
575
|
|
|
456
|
-
super().__init__(
|
|
576
|
+
super().__init__(
|
|
577
|
+
command="npx",
|
|
578
|
+
args=npx_args,
|
|
579
|
+
env=env,
|
|
580
|
+
cwd=project_directory,
|
|
581
|
+
keep_alive=keep_alive,
|
|
582
|
+
)
|
|
457
583
|
self.package = package
|
|
458
584
|
|
|
459
585
|
|
|
@@ -575,6 +701,44 @@ class MCPConfigTransport(ClientTransport):
|
|
|
575
701
|
return f"<MCPConfig(config='{self.config}')>"
|
|
576
702
|
|
|
577
703
|
|
|
704
|
+
@overload
|
|
705
|
+
def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@overload
|
|
709
|
+
def infer_transport(transport: FastMCPServer) -> FastMCPTransport: ...
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
@overload
|
|
713
|
+
def infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@overload
|
|
717
|
+
def infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@overload
|
|
721
|
+
def infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@overload
|
|
725
|
+
def infer_transport(
|
|
726
|
+
transport: AnyUrl,
|
|
727
|
+
) -> SSETransport | StreamableHttpTransport: ...
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@overload
|
|
731
|
+
def infer_transport(
|
|
732
|
+
transport: str,
|
|
733
|
+
) -> (
|
|
734
|
+
PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport
|
|
735
|
+
): ...
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@overload
|
|
739
|
+
def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...
|
|
740
|
+
|
|
741
|
+
|
|
578
742
|
def infer_transport(
|
|
579
743
|
transport: ClientTransport
|
|
580
744
|
| FastMCPServer
|
fastmcp/resources/template.py
CHANGED
|
@@ -148,7 +148,7 @@ class ResourceTemplate(BaseModel):
|
|
|
148
148
|
f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
description = description or fn.__doc__
|
|
151
|
+
description = description or fn.__doc__
|
|
152
152
|
|
|
153
153
|
if not inspect.isroutine(fn):
|
|
154
154
|
fn = fn.__call__
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -48,12 +48,23 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
|
|
|
48
48
|
if include_all:
|
|
49
49
|
exclude_headers = set()
|
|
50
50
|
else:
|
|
51
|
-
exclude_headers = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
exclude_headers = {
|
|
52
|
+
"host",
|
|
53
|
+
"content-length",
|
|
54
|
+
"connection",
|
|
55
|
+
"transfer-encoding",
|
|
56
|
+
"upgrade",
|
|
57
|
+
"te",
|
|
58
|
+
"keep-alive",
|
|
59
|
+
"expect",
|
|
60
|
+
# Proxy-related headers
|
|
61
|
+
"proxy-authenticate",
|
|
62
|
+
"proxy-authorization",
|
|
63
|
+
"proxy-connection",
|
|
64
|
+
}
|
|
65
|
+
# (just in case)
|
|
66
|
+
if not all(h.lower() == h for h in exclude_headers):
|
|
67
|
+
raise ValueError("Excluded headers must be lowercase")
|
|
57
68
|
headers = {}
|
|
58
69
|
|
|
59
70
|
try:
|
fastmcp/server/server.py
CHANGED
|
@@ -62,7 +62,7 @@ from fastmcp.utilities.mcp_config import MCPConfig
|
|
|
62
62
|
|
|
63
63
|
if TYPE_CHECKING:
|
|
64
64
|
from fastmcp.client import Client
|
|
65
|
-
from fastmcp.client.transports import ClientTransport
|
|
65
|
+
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
66
66
|
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
67
67
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
68
68
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
@@ -257,9 +257,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
257
257
|
"""Get all registered tools, indexed by registered key."""
|
|
258
258
|
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
|
259
259
|
tools: dict[str, Tool] = {}
|
|
260
|
-
for server in self._mounted_servers.
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
for prefix, server in self._mounted_servers.items():
|
|
261
|
+
try:
|
|
262
|
+
server_tools = await server.get_tools()
|
|
263
|
+
tools.update(server_tools)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(
|
|
266
|
+
f"Failed to get tools from mounted server '{prefix}': {e}"
|
|
267
|
+
)
|
|
268
|
+
continue
|
|
263
269
|
tools.update(self._tool_manager.get_tools())
|
|
264
270
|
self._cache.set("tools", tools)
|
|
265
271
|
return tools
|
|
@@ -268,9 +274,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
268
274
|
"""Get all registered resources, indexed by registered key."""
|
|
269
275
|
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
|
270
276
|
resources: dict[str, Resource] = {}
|
|
271
|
-
for server in self._mounted_servers.
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
for prefix, server in self._mounted_servers.items():
|
|
278
|
+
try:
|
|
279
|
+
server_resources = await server.get_resources()
|
|
280
|
+
resources.update(server_resources)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.warning(
|
|
283
|
+
f"Failed to get resources from mounted server '{prefix}': {e}"
|
|
284
|
+
)
|
|
285
|
+
continue
|
|
274
286
|
resources.update(self._resource_manager.get_resources())
|
|
275
287
|
self._cache.set("resources", resources)
|
|
276
288
|
return resources
|
|
@@ -281,9 +293,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
281
293
|
templates := self._cache.get("resource_templates")
|
|
282
294
|
) is self._cache.NOT_FOUND:
|
|
283
295
|
templates: dict[str, ResourceTemplate] = {}
|
|
284
|
-
for server in self._mounted_servers.
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
for prefix, server in self._mounted_servers.items():
|
|
297
|
+
try:
|
|
298
|
+
server_templates = await server.get_resource_templates()
|
|
299
|
+
templates.update(server_templates)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.warning(
|
|
302
|
+
"Failed to get resource templates from mounted server "
|
|
303
|
+
f"'{prefix}': {e}"
|
|
304
|
+
)
|
|
305
|
+
continue
|
|
287
306
|
templates.update(self._resource_manager.get_templates())
|
|
288
307
|
self._cache.set("resource_templates", templates)
|
|
289
308
|
return templates
|
|
@@ -294,9 +313,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
294
313
|
"""
|
|
295
314
|
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
|
296
315
|
prompts: dict[str, Prompt] = {}
|
|
297
|
-
for server in self._mounted_servers.
|
|
298
|
-
|
|
299
|
-
|
|
316
|
+
for prefix, server in self._mounted_servers.items():
|
|
317
|
+
try:
|
|
318
|
+
server_prompts = await server.get_prompts()
|
|
319
|
+
prompts.update(server_prompts)
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.warning(
|
|
322
|
+
f"Failed to get prompts from mounted server '{prefix}': {e}"
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
300
325
|
prompts.update(self._prompt_manager.get_prompts())
|
|
301
326
|
self._cache.set("prompts", prompts)
|
|
302
327
|
return prompts
|
|
@@ -802,7 +827,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
827
|
"""
|
|
803
828
|
host = host or self.settings.host
|
|
804
829
|
port = port or self.settings.port
|
|
805
|
-
default_log_level_to_use = log_level or self.settings.log_level.lower()
|
|
830
|
+
default_log_level_to_use = (log_level or self.settings.log_level).lower()
|
|
806
831
|
|
|
807
832
|
app = self.http_app(path=path, transport=transport, middleware=middleware)
|
|
808
833
|
|
|
@@ -1263,7 +1288,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1263
1288
|
@classmethod
|
|
1264
1289
|
def as_proxy(
|
|
1265
1290
|
cls,
|
|
1266
|
-
backend: Client
|
|
1291
|
+
backend: Client[ClientTransportT]
|
|
1267
1292
|
| ClientTransport
|
|
1268
1293
|
| FastMCP[Any]
|
|
1269
1294
|
| AnyUrl
|
|
@@ -1291,7 +1316,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1291
1316
|
return FastMCPProxy(client=client, **settings)
|
|
1292
1317
|
|
|
1293
1318
|
@classmethod
|
|
1294
|
-
def from_client(
|
|
1319
|
+
def from_client(
|
|
1320
|
+
cls, client: Client[ClientTransportT], **settings: Any
|
|
1321
|
+
) -> FastMCPProxy:
|
|
1295
1322
|
"""
|
|
1296
1323
|
Create a FastMCP proxy server from a FastMCP client.
|
|
1297
1324
|
"""
|
fastmcp/settings.py
CHANGED
|
@@ -87,6 +87,13 @@ class Settings(BaseSettings):
|
|
|
87
87
|
),
|
|
88
88
|
] = False
|
|
89
89
|
|
|
90
|
+
client_init_timeout: Annotated[
|
|
91
|
+
float | None,
|
|
92
|
+
Field(
|
|
93
|
+
description="The timeout for the client's initialization handshake, in seconds. Set to None or 0 to disable.",
|
|
94
|
+
),
|
|
95
|
+
] = None
|
|
96
|
+
|
|
90
97
|
@model_validator(mode="after")
|
|
91
98
|
def setup_logging(self) -> Self:
|
|
92
99
|
"""Finalize the settings."""
|
fastmcp/tools/tool.py
CHANGED
|
@@ -36,7 +36,9 @@ class Tool(BaseModel):
|
|
|
36
36
|
|
|
37
37
|
fn: Callable[..., Any]
|
|
38
38
|
name: str = Field(description="Name of the tool")
|
|
39
|
-
description: str = Field(
|
|
39
|
+
description: str | None = Field(
|
|
40
|
+
default=None, description="Description of what the tool does"
|
|
41
|
+
)
|
|
40
42
|
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
41
43
|
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
42
44
|
default_factory=set, description="Tags for the tool"
|
|
@@ -74,7 +76,7 @@ class Tool(BaseModel):
|
|
|
74
76
|
if func_name == "<lambda>":
|
|
75
77
|
raise ValueError("You must provide a name for lambda functions")
|
|
76
78
|
|
|
77
|
-
func_doc = description or fn.__doc__
|
|
79
|
+
func_doc = description or fn.__doc__
|
|
78
80
|
|
|
79
81
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
80
82
|
if not inspect.isroutine(fn):
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
fastmcp/__init__.py,sha256=yTAqLZORsPqbr7AE0ayw6zIYBeMlxQlI-3HE2WqbvHk,435
|
|
2
2
|
fastmcp/exceptions.py,sha256=YvaKqOT3w0boXF9ylIoaSIzW9XiQ1qLFG1LZq6B60H8,680
|
|
3
3
|
fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
fastmcp/settings.py,sha256=
|
|
4
|
+
fastmcp/settings.py,sha256=aVOLK-QfhGr_0mPLVzBmeUxyS9_w8gSHAjMmRqEoEow,5577
|
|
5
5
|
fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
|
|
6
6
|
fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
|
|
7
|
-
fastmcp/cli/cli.py,sha256=
|
|
7
|
+
fastmcp/cli/cli.py,sha256=CQxpRTXgnQQynGJLEV5g1FnLMaiWoiUgefnMZ7VxS4o,12367
|
|
8
8
|
fastmcp/cli/run.py,sha256=o7Ge6JZKXYwlY2vYdMNoVX8agBchAaeU_73iPndojIM,5351
|
|
9
9
|
fastmcp/client/__init__.py,sha256=Ri8GFHolIKOZnXaMzIc3VpkLcEqAmOoYGCKgmSk6NnE,550
|
|
10
10
|
fastmcp/client/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
fastmcp/client/client.py,sha256=
|
|
11
|
+
fastmcp/client/client.py,sha256=JxFC_YUOrDPZwiDbD0JgCHQsOy1F8Rup7hnege96OIc,23021
|
|
12
12
|
fastmcp/client/logging.py,sha256=hOPRailZUp89RUck6V4HPaWVZinVrNY8HD4hD0dd-fE,822
|
|
13
13
|
fastmcp/client/progress.py,sha256=WjLLDbUKMsx8DK-fqO7AGsXb83ak-6BMrLvzzznGmcI,1043
|
|
14
14
|
fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
|
|
15
15
|
fastmcp/client/sampling.py,sha256=UlDHxnd6k_HoU8RA3ob0g8-e6haJBc9u27N_v291QoI,1698
|
|
16
|
-
fastmcp/client/transports.py,sha256=
|
|
16
|
+
fastmcp/client/transports.py,sha256=G1MQ7bHkmQbbni4ZWwMJs-opbPdqpyKYdg7TYkkjLbU,29986
|
|
17
17
|
fastmcp/contrib/README.md,sha256=rKknYSI1T192UvSszqwwDlQ2eYQpxywrNTLoj177SYU,878
|
|
18
18
|
fastmcp/contrib/bulk_tool_caller/README.md,sha256=5aUUY1TSFKtz1pvTLSDqkUCkGkuqMfMZNsLeaNqEgAc,1960
|
|
19
19
|
fastmcp/contrib/bulk_tool_caller/__init__.py,sha256=xvGSSaUXTQrc31erBoi1Gh7BikgOliETDiYVTP3rLxY,75
|
|
@@ -31,17 +31,17 @@ fastmcp/prompts/prompt_manager.py,sha256=qptEhZHMwc8XxQd5lTQg8iIb5MiTZVsNaux_XLv
|
|
|
31
31
|
fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
|
|
32
32
|
fastmcp/resources/resource.py,sha256=Rx1My_fi1f-oqnQ9R_v7ejopAk4BJDfbB75-s4d31dM,2492
|
|
33
33
|
fastmcp/resources/resource_manager.py,sha256=nsgCR3lo9t4Q0QR6txPfAas2upqIb8P8ZlqWAfV9Qc0,11344
|
|
34
|
-
fastmcp/resources/template.py,sha256=
|
|
34
|
+
fastmcp/resources/template.py,sha256=u0_-yNMmZfnl5DqtSRndGbGBrm7JgbzBU8IUd0hrEWE,7523
|
|
35
35
|
fastmcp/resources/types.py,sha256=5fUFvzRlekNjtfihtq8S-fT0alKoNfclzrugqeM5JRE,6366
|
|
36
36
|
fastmcp/server/__init__.py,sha256=bMD4aQD4yJqLz7-mudoNsyeV8UgQfRAg3PRwPvwTEds,119
|
|
37
37
|
fastmcp/server/context.py,sha256=yN1e0LsnCl7cEpr9WlbvFhSf8oE56kKb-20m8h2SsBY,10171
|
|
38
|
-
fastmcp/server/dependencies.py,sha256=
|
|
38
|
+
fastmcp/server/dependencies.py,sha256=4kdJLvWn-lMU7uPIJ-Np1RHBwvkbU7Dc31ZdsGTA9_I,2093
|
|
39
39
|
fastmcp/server/http.py,sha256=wZWUrLvKITlvkxQoggJ9RyvynCUMEJqqMMsvX7Hmb9o,12807
|
|
40
40
|
fastmcp/server/openapi.py,sha256=9qXSuEl671sT1F7nSM3SiD5KANGqHUhiL1BBdCnuCcU,39153
|
|
41
41
|
fastmcp/server/proxy.py,sha256=mt3eM6TQWfnZD5XehmTXisskZ4CBbsWyjRPjprlTjBY,9653
|
|
42
|
-
fastmcp/server/server.py,sha256=
|
|
42
|
+
fastmcp/server/server.py,sha256=TGC8ysEtA-fpuFzaqfvCstfWYJks5ClrCQl6zyYfLZM,58237
|
|
43
43
|
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
44
|
-
fastmcp/tools/tool.py,sha256=
|
|
44
|
+
fastmcp/tools/tool.py,sha256=Qx1sQ-D_llZIETPea8KoRn_vOjYgyriDqi0hpd_pRP8,7832
|
|
45
45
|
fastmcp/tools/tool_manager.py,sha256=785vKYlJ9B2B5ThXFhuXYB4VNY4h0283-_AAdy1hEfk,4430
|
|
46
46
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
47
47
|
fastmcp/utilities/cache.py,sha256=aV3oZ-ZhMgLSM9iAotlUlEy5jFvGXrVo0Y5Bj4PBtqY,707
|
|
@@ -53,8 +53,8 @@ fastmcp/utilities/mcp_config.py,sha256=_wY3peaFDEgyOBkJ_Tb8sETk3mtdwtw1053q7ry0z
|
|
|
53
53
|
fastmcp/utilities/openapi.py,sha256=QQos4vP59HQ8vPDTKftWOIVv_zmW30mNxYSXVU7JUbY,38441
|
|
54
54
|
fastmcp/utilities/tests.py,sha256=teyHcl3j7WGfYJ6m42VuQYB_IVpGvPdFqIpC-UxsN78,3369
|
|
55
55
|
fastmcp/utilities/types.py,sha256=6CcqAQ1QqCO2HGSFlPS6FO5JRWnacjCcO2-EhyEnZV0,4400
|
|
56
|
-
fastmcp-2.5.
|
|
57
|
-
fastmcp-2.5.
|
|
58
|
-
fastmcp-2.5.
|
|
59
|
-
fastmcp-2.5.
|
|
60
|
-
fastmcp-2.5.
|
|
56
|
+
fastmcp-2.5.2.dist-info/METADATA,sha256=EU1bb1c0HO8jLeLAlTo-UCPA619yiw1kiAiFNA8bMMk,16510
|
|
57
|
+
fastmcp-2.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
58
|
+
fastmcp-2.5.2.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
59
|
+
fastmcp-2.5.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
60
|
+
fastmcp-2.5.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|