fastmcp 2.12.5__py3-none-any.whl → 2.14.0__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/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/transports.py
CHANGED
|
@@ -6,9 +6,9 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
8
|
import warnings
|
|
9
|
-
from collections.abc import AsyncIterator
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Literal, TypeVar, cast, overload
|
|
11
|
+
from typing import Any, Literal, TextIO, TypeVar, cast, overload
|
|
12
12
|
|
|
13
13
|
import anyio
|
|
14
14
|
import httpx
|
|
@@ -36,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
|
|
|
36
36
|
from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
|
|
37
37
|
from fastmcp.server.dependencies import get_http_headers
|
|
38
38
|
from fastmcp.server.server import FastMCP
|
|
39
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
39
40
|
from fastmcp.utilities.logging import get_logger
|
|
40
41
|
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
41
42
|
|
|
@@ -46,16 +47,16 @@ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
|
|
|
46
47
|
|
|
47
48
|
__all__ = [
|
|
48
49
|
"ClientTransport",
|
|
49
|
-
"SSETransport",
|
|
50
|
-
"StreamableHttpTransport",
|
|
51
|
-
"StdioTransport",
|
|
52
|
-
"PythonStdioTransport",
|
|
53
50
|
"FastMCPStdioTransport",
|
|
51
|
+
"FastMCPTransport",
|
|
54
52
|
"NodeStdioTransport",
|
|
55
|
-
"UvxStdioTransport",
|
|
56
|
-
"UvStdioTransport",
|
|
57
53
|
"NpxStdioTransport",
|
|
58
|
-
"
|
|
54
|
+
"PythonStdioTransport",
|
|
55
|
+
"SSETransport",
|
|
56
|
+
"StdioTransport",
|
|
57
|
+
"StreamableHttpTransport",
|
|
58
|
+
"UvStdioTransport",
|
|
59
|
+
"UvxStdioTransport",
|
|
59
60
|
"infer_transport",
|
|
60
61
|
]
|
|
61
62
|
|
|
@@ -109,9 +110,8 @@ class ClientTransport(abc.ABC):
|
|
|
109
110
|
# Basic representation for subclasses
|
|
110
111
|
return f"<{self.__class__.__name__}>"
|
|
111
112
|
|
|
112
|
-
async def close(self):
|
|
113
|
+
async def close(self): # noqa: B027
|
|
113
114
|
"""Close the transport."""
|
|
114
|
-
pass
|
|
115
115
|
|
|
116
116
|
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
117
117
|
if auth is not None:
|
|
@@ -141,10 +141,10 @@ class WSTransport(ClientTransport):
|
|
|
141
141
|
) -> AsyncIterator[ClientSession]:
|
|
142
142
|
try:
|
|
143
143
|
from mcp.client.websocket import websocket_client
|
|
144
|
-
except ImportError:
|
|
144
|
+
except ImportError as e:
|
|
145
145
|
raise ImportError(
|
|
146
146
|
"The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
|
|
147
|
-
)
|
|
147
|
+
) from e
|
|
148
148
|
|
|
149
149
|
async with websocket_client(self.url) as transport:
|
|
150
150
|
read_stream, write_stream = transport
|
|
@@ -178,8 +178,8 @@ class SSETransport(ClientTransport):
|
|
|
178
178
|
|
|
179
179
|
self.url = url
|
|
180
180
|
self.headers = headers or {}
|
|
181
|
-
self._set_auth(auth)
|
|
182
181
|
self.httpx_client_factory = httpx_client_factory
|
|
182
|
+
self._set_auth(auth)
|
|
183
183
|
|
|
184
184
|
if isinstance(sse_read_timeout, int | float):
|
|
185
185
|
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
|
|
@@ -187,7 +187,7 @@ class SSETransport(ClientTransport):
|
|
|
187
187
|
|
|
188
188
|
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
189
189
|
if auth == "oauth":
|
|
190
|
-
auth = OAuth(self.url)
|
|
190
|
+
auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
|
|
191
191
|
elif isinstance(auth, str):
|
|
192
192
|
auth = BearerAuth(auth)
|
|
193
193
|
self.auth = auth
|
|
@@ -207,7 +207,7 @@ class SSETransport(ClientTransport):
|
|
|
207
207
|
# instead we simply leave the kwarg out if it's not provided
|
|
208
208
|
if self.sse_read_timeout is not None:
|
|
209
209
|
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
|
|
210
|
-
if session_kwargs.get("read_timeout_seconds"
|
|
210
|
+
if session_kwargs.get("read_timeout_seconds") is not None:
|
|
211
211
|
read_timeout_seconds = cast(
|
|
212
212
|
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
|
|
213
213
|
)
|
|
@@ -248,16 +248,18 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
248
248
|
|
|
249
249
|
self.url = url
|
|
250
250
|
self.headers = headers or {}
|
|
251
|
-
self._set_auth(auth)
|
|
252
251
|
self.httpx_client_factory = httpx_client_factory
|
|
252
|
+
self._set_auth(auth)
|
|
253
253
|
|
|
254
254
|
if isinstance(sse_read_timeout, int | float):
|
|
255
255
|
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
|
|
256
256
|
self.sse_read_timeout = sse_read_timeout
|
|
257
257
|
|
|
258
|
+
self._get_session_id_cb: Callable[[], str | None] | None = None
|
|
259
|
+
|
|
258
260
|
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
259
261
|
if auth == "oauth":
|
|
260
|
-
auth = OAuth(self.url)
|
|
262
|
+
auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
|
|
261
263
|
elif isinstance(auth, str):
|
|
262
264
|
auth = BearerAuth(auth)
|
|
263
265
|
self.auth = auth
|
|
@@ -277,7 +279,7 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
277
279
|
# instead we simply leave the kwarg out if it's not provided
|
|
278
280
|
if self.sse_read_timeout is not None:
|
|
279
281
|
client_kwargs["sse_read_timeout"] = self.sse_read_timeout
|
|
280
|
-
if session_kwargs.get("read_timeout_seconds"
|
|
282
|
+
if session_kwargs.get("read_timeout_seconds") is not None:
|
|
281
283
|
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
282
284
|
|
|
283
285
|
if self.httpx_client_factory is not None:
|
|
@@ -288,12 +290,25 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
288
290
|
auth=self.auth,
|
|
289
291
|
**client_kwargs,
|
|
290
292
|
) as transport:
|
|
291
|
-
read_stream, write_stream,
|
|
293
|
+
read_stream, write_stream, get_session_id = transport
|
|
294
|
+
self._get_session_id_cb = get_session_id
|
|
292
295
|
async with ClientSession(
|
|
293
296
|
read_stream, write_stream, **session_kwargs
|
|
294
297
|
) as session:
|
|
295
298
|
yield session
|
|
296
299
|
|
|
300
|
+
def get_session_id(self) -> str | None:
|
|
301
|
+
if self._get_session_id_cb:
|
|
302
|
+
try:
|
|
303
|
+
return self._get_session_id_cb()
|
|
304
|
+
except Exception:
|
|
305
|
+
return None
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
async def close(self):
|
|
309
|
+
# Reset the session id callback
|
|
310
|
+
self._get_session_id_cb = None
|
|
311
|
+
|
|
297
312
|
def __repr__(self) -> str:
|
|
298
313
|
return f"<StreamableHttpTransport(url='{self.url}')>"
|
|
299
314
|
|
|
@@ -313,6 +328,7 @@ class StdioTransport(ClientTransport):
|
|
|
313
328
|
env: dict[str, str] | None = None,
|
|
314
329
|
cwd: str | None = None,
|
|
315
330
|
keep_alive: bool | None = None,
|
|
331
|
+
log_file: Path | TextIO | None = None,
|
|
316
332
|
):
|
|
317
333
|
"""
|
|
318
334
|
Initialize a Stdio transport.
|
|
@@ -326,6 +342,11 @@ class StdioTransport(ClientTransport):
|
|
|
326
342
|
Defaults to True. When True, the subprocess remains active
|
|
327
343
|
after the connection context exits, allowing reuse in
|
|
328
344
|
subsequent connections.
|
|
345
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
346
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
347
|
+
if not provided. When a Path is provided, the file will be created
|
|
348
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
349
|
+
errors will be written to this file instead of appearing in the console.
|
|
329
350
|
"""
|
|
330
351
|
self.command = command
|
|
331
352
|
self.args = args
|
|
@@ -334,6 +355,7 @@ class StdioTransport(ClientTransport):
|
|
|
334
355
|
if keep_alive is None:
|
|
335
356
|
keep_alive = True
|
|
336
357
|
self.keep_alive = keep_alive
|
|
358
|
+
self.log_file = log_file
|
|
337
359
|
|
|
338
360
|
self._session: ClientSession | None = None
|
|
339
361
|
self._connect_task: asyncio.Task | None = None
|
|
@@ -368,7 +390,9 @@ class StdioTransport(ClientTransport):
|
|
|
368
390
|
args=self.args,
|
|
369
391
|
env=self.env,
|
|
370
392
|
cwd=self.cwd,
|
|
371
|
-
|
|
393
|
+
log_file=self.log_file,
|
|
394
|
+
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
|
|
395
|
+
session_kwargs=session_kwargs, # type: ignore[arg-type]
|
|
372
396
|
ready_event=self._ready_event,
|
|
373
397
|
stop_event=self._stop_event,
|
|
374
398
|
session_future=session_future,
|
|
@@ -421,6 +445,7 @@ async def _stdio_transport_connect_task(
|
|
|
421
445
|
args: list[str],
|
|
422
446
|
env: dict[str, str] | None,
|
|
423
447
|
cwd: str | None,
|
|
448
|
+
log_file: Path | TextIO | None,
|
|
424
449
|
session_kwargs: SessionKwargs,
|
|
425
450
|
ready_event: anyio.Event,
|
|
426
451
|
stop_event: anyio.Event,
|
|
@@ -438,7 +463,18 @@ async def _stdio_transport_connect_task(
|
|
|
438
463
|
env=env,
|
|
439
464
|
cwd=cwd,
|
|
440
465
|
)
|
|
441
|
-
|
|
466
|
+
# Handle log_file: Path needs to be opened, TextIO used as-is
|
|
467
|
+
if log_file is None:
|
|
468
|
+
log_file_handle = sys.stderr
|
|
469
|
+
elif isinstance(log_file, Path):
|
|
470
|
+
log_file_handle = stack.enter_context(log_file.open("a"))
|
|
471
|
+
else:
|
|
472
|
+
# Must be TextIO - use it directly
|
|
473
|
+
log_file_handle = log_file
|
|
474
|
+
|
|
475
|
+
transport = await stack.enter_async_context(
|
|
476
|
+
stdio_client(server_params, errlog=log_file_handle)
|
|
477
|
+
)
|
|
442
478
|
read_stream, write_stream = transport
|
|
443
479
|
session_future.set_result(
|
|
444
480
|
await stack.enter_async_context(
|
|
@@ -471,6 +507,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
471
507
|
cwd: str | None = None,
|
|
472
508
|
python_cmd: str = sys.executable,
|
|
473
509
|
keep_alive: bool | None = None,
|
|
510
|
+
log_file: Path | TextIO | None = None,
|
|
474
511
|
):
|
|
475
512
|
"""
|
|
476
513
|
Initialize a Python transport.
|
|
@@ -485,6 +522,11 @@ class PythonStdioTransport(StdioTransport):
|
|
|
485
522
|
Defaults to True. When True, the subprocess remains active
|
|
486
523
|
after the connection context exits, allowing reuse in
|
|
487
524
|
subsequent connections.
|
|
525
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
526
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
527
|
+
if not provided. When a Path is provided, the file will be created
|
|
528
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
529
|
+
errors will be written to this file instead of appearing in the console.
|
|
488
530
|
"""
|
|
489
531
|
script_path = Path(script_path).resolve()
|
|
490
532
|
if not script_path.is_file():
|
|
@@ -502,6 +544,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
502
544
|
env=env,
|
|
503
545
|
cwd=cwd,
|
|
504
546
|
keep_alive=keep_alive,
|
|
547
|
+
log_file=log_file,
|
|
505
548
|
)
|
|
506
549
|
self.script_path = script_path
|
|
507
550
|
|
|
@@ -516,6 +559,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
516
559
|
env: dict[str, str] | None = None,
|
|
517
560
|
cwd: str | None = None,
|
|
518
561
|
keep_alive: bool | None = None,
|
|
562
|
+
log_file: Path | TextIO | None = None,
|
|
519
563
|
):
|
|
520
564
|
script_path = Path(script_path).resolve()
|
|
521
565
|
if not script_path.is_file():
|
|
@@ -529,6 +573,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
529
573
|
env=env,
|
|
530
574
|
cwd=cwd,
|
|
531
575
|
keep_alive=keep_alive,
|
|
576
|
+
log_file=log_file,
|
|
532
577
|
)
|
|
533
578
|
self.script_path = script_path
|
|
534
579
|
|
|
@@ -544,6 +589,7 @@ class NodeStdioTransport(StdioTransport):
|
|
|
544
589
|
cwd: str | None = None,
|
|
545
590
|
node_cmd: str = "node",
|
|
546
591
|
keep_alive: bool | None = None,
|
|
592
|
+
log_file: Path | TextIO | None = None,
|
|
547
593
|
):
|
|
548
594
|
"""
|
|
549
595
|
Initialize a Node transport.
|
|
@@ -558,6 +604,11 @@ class NodeStdioTransport(StdioTransport):
|
|
|
558
604
|
Defaults to True. When True, the subprocess remains active
|
|
559
605
|
after the connection context exits, allowing reuse in
|
|
560
606
|
subsequent connections.
|
|
607
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
608
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
609
|
+
if not provided. When a Path is provided, the file will be created
|
|
610
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
611
|
+
errors will be written to this file instead of appearing in the console.
|
|
561
612
|
"""
|
|
562
613
|
script_path = Path(script_path).resolve()
|
|
563
614
|
if not script_path.is_file():
|
|
@@ -570,7 +621,12 @@ class NodeStdioTransport(StdioTransport):
|
|
|
570
621
|
full_args.extend(args)
|
|
571
622
|
|
|
572
623
|
super().__init__(
|
|
573
|
-
command=node_cmd,
|
|
624
|
+
command=node_cmd,
|
|
625
|
+
args=full_args,
|
|
626
|
+
env=env,
|
|
627
|
+
cwd=cwd,
|
|
628
|
+
keep_alive=keep_alive,
|
|
629
|
+
log_file=log_file,
|
|
574
630
|
)
|
|
575
631
|
self.script_path = script_path
|
|
576
632
|
|
|
@@ -583,15 +639,15 @@ class UvStdioTransport(StdioTransport):
|
|
|
583
639
|
command: str,
|
|
584
640
|
args: list[str] | None = None,
|
|
585
641
|
module: bool = False,
|
|
586
|
-
project_directory:
|
|
642
|
+
project_directory: Path | None = None,
|
|
587
643
|
python_version: str | None = None,
|
|
588
644
|
with_packages: list[str] | None = None,
|
|
589
|
-
with_requirements:
|
|
645
|
+
with_requirements: Path | None = None,
|
|
590
646
|
env_vars: dict[str, str] | None = None,
|
|
591
647
|
keep_alive: bool | None = None,
|
|
592
648
|
):
|
|
593
649
|
# Basic validation
|
|
594
|
-
if project_directory and not
|
|
650
|
+
if project_directory and not project_directory.exists():
|
|
595
651
|
raise NotADirectoryError(
|
|
596
652
|
f"Project directory not found: {project_directory}"
|
|
597
653
|
)
|
|
@@ -707,6 +763,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
707
763
|
env: dict[str, str] | None = None
|
|
708
764
|
if env_vars:
|
|
709
765
|
env = os.environ.copy()
|
|
766
|
+
env.update(env_vars)
|
|
710
767
|
|
|
711
768
|
super().__init__(
|
|
712
769
|
command="uvx",
|
|
@@ -809,13 +866,25 @@ class FastMCPTransport(ClientTransport):
|
|
|
809
866
|
client_read, client_write = client_streams
|
|
810
867
|
server_read, server_write = server_streams
|
|
811
868
|
|
|
812
|
-
#
|
|
813
|
-
|
|
869
|
+
# Capture exceptions to re-raise after task group cleanup.
|
|
870
|
+
# anyio task groups can suppress exceptions when cancel_scope.cancel()
|
|
871
|
+
# is called during cleanup, so we capture and re-raise manually.
|
|
872
|
+
exception_to_raise: BaseException | None = None
|
|
873
|
+
|
|
874
|
+
async with (
|
|
875
|
+
anyio.create_task_group() as tg,
|
|
876
|
+
_enter_server_lifespan(server=self.server),
|
|
877
|
+
):
|
|
878
|
+
# Build experimental capabilities
|
|
879
|
+
experimental_capabilities = get_task_capabilities()
|
|
880
|
+
|
|
814
881
|
tg.start_soon(
|
|
815
882
|
lambda: self.server._mcp_server.run(
|
|
816
883
|
server_read,
|
|
817
884
|
server_write,
|
|
818
|
-
self.server._mcp_server.create_initialization_options(
|
|
885
|
+
self.server._mcp_server.create_initialization_options(
|
|
886
|
+
experimental_capabilities=experimental_capabilities
|
|
887
|
+
),
|
|
819
888
|
raise_exceptions=self.raise_exceptions,
|
|
820
889
|
)
|
|
821
890
|
)
|
|
@@ -827,13 +896,31 @@ class FastMCPTransport(ClientTransport):
|
|
|
827
896
|
**session_kwargs,
|
|
828
897
|
) as client_session:
|
|
829
898
|
yield client_session
|
|
899
|
+
except BaseException as e:
|
|
900
|
+
exception_to_raise = e
|
|
830
901
|
finally:
|
|
831
902
|
tg.cancel_scope.cancel()
|
|
832
903
|
|
|
904
|
+
# Re-raise after task group has exited cleanly
|
|
905
|
+
if exception_to_raise is not None:
|
|
906
|
+
raise exception_to_raise
|
|
907
|
+
|
|
833
908
|
def __repr__(self) -> str:
|
|
834
909
|
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
835
910
|
|
|
836
911
|
|
|
912
|
+
@contextlib.asynccontextmanager
|
|
913
|
+
async def _enter_server_lifespan(
|
|
914
|
+
server: FastMCP | FastMCP1Server,
|
|
915
|
+
) -> AsyncIterator[None]:
|
|
916
|
+
"""Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
|
|
917
|
+
if isinstance(server, FastMCP):
|
|
918
|
+
async with server._lifespan_manager():
|
|
919
|
+
yield
|
|
920
|
+
else:
|
|
921
|
+
yield
|
|
922
|
+
|
|
923
|
+
|
|
837
924
|
class MCPConfigTransport(ClientTransport):
|
|
838
925
|
"""Transport for connecting to one or more MCP servers defined in an MCPConfig.
|
|
839
926
|
|
|
@@ -897,7 +984,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
897
984
|
|
|
898
985
|
# if there's exactly one server, create a client for that server
|
|
899
986
|
elif len(self.config.mcpServers) == 1:
|
|
900
|
-
self.transport =
|
|
987
|
+
self.transport = next(iter(self.config.mcpServers.values())).to_transport()
|
|
901
988
|
self._underlying_transports.append(self.transport)
|
|
902
989
|
|
|
903
990
|
# otherwise create a composite client
|
|
@@ -97,11 +97,11 @@ def make_endpoint(action, component, config):
|
|
|
97
97
|
return JSONResponse(
|
|
98
98
|
{"message": f"{action.capitalize()}d {component}: {name}"}
|
|
99
99
|
)
|
|
100
|
-
except NotFoundError:
|
|
100
|
+
except NotFoundError as e:
|
|
101
101
|
raise StarletteHTTPException(
|
|
102
102
|
status_code=404,
|
|
103
103
|
detail=f"Unknown {component}: {name}",
|
|
104
|
-
)
|
|
104
|
+
) from e
|
|
105
105
|
|
|
106
106
|
return endpoint
|
|
107
107
|
|
|
@@ -41,7 +41,7 @@ class ComponentService:
|
|
|
41
41
|
return tool
|
|
42
42
|
|
|
43
43
|
# 2. Check mounted servers using the filtered protocol path.
|
|
44
|
-
for mounted in reversed(self.
|
|
44
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
45
45
|
if mounted.prefix:
|
|
46
46
|
if key.startswith(f"{mounted.prefix}_"):
|
|
47
47
|
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -70,7 +70,7 @@ class ComponentService:
|
|
|
70
70
|
return tool
|
|
71
71
|
|
|
72
72
|
# 2. Check mounted servers using the filtered protocol path.
|
|
73
|
-
for mounted in reversed(self.
|
|
73
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
74
74
|
if mounted.prefix:
|
|
75
75
|
if key.startswith(f"{mounted.prefix}_"):
|
|
76
76
|
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -103,18 +103,10 @@ class ComponentService:
|
|
|
103
103
|
return template
|
|
104
104
|
|
|
105
105
|
# 2. Check mounted servers using the filtered protocol path.
|
|
106
|
-
for mounted in reversed(self.
|
|
106
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
107
107
|
if mounted.prefix:
|
|
108
|
-
if has_resource_prefix(
|
|
109
|
-
key,
|
|
110
|
-
mounted.prefix,
|
|
111
|
-
mounted.resource_prefix_format,
|
|
112
|
-
):
|
|
113
|
-
key = remove_resource_prefix(
|
|
114
|
-
key,
|
|
115
|
-
mounted.prefix,
|
|
116
|
-
mounted.resource_prefix_format,
|
|
117
|
-
)
|
|
108
|
+
if has_resource_prefix(key, mounted.prefix):
|
|
109
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
118
110
|
mounted_service = ComponentService(mounted.server)
|
|
119
111
|
mounted_resource: (
|
|
120
112
|
Resource | ResourceTemplate
|
|
@@ -146,18 +138,10 @@ class ComponentService:
|
|
|
146
138
|
return template
|
|
147
139
|
|
|
148
140
|
# 2. Check mounted servers using the filtered protocol path.
|
|
149
|
-
for mounted in reversed(self.
|
|
141
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
150
142
|
if mounted.prefix:
|
|
151
|
-
if has_resource_prefix(
|
|
152
|
-
key,
|
|
153
|
-
mounted.prefix,
|
|
154
|
-
mounted.resource_prefix_format,
|
|
155
|
-
):
|
|
156
|
-
key = remove_resource_prefix(
|
|
157
|
-
key,
|
|
158
|
-
mounted.prefix,
|
|
159
|
-
mounted.resource_prefix_format,
|
|
160
|
-
)
|
|
143
|
+
if has_resource_prefix(key, mounted.prefix):
|
|
144
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
161
145
|
mounted_service = ComponentService(mounted.server)
|
|
162
146
|
mounted_resource: (
|
|
163
147
|
Resource | ResourceTemplate
|
|
@@ -185,7 +169,7 @@ class ComponentService:
|
|
|
185
169
|
return prompt
|
|
186
170
|
|
|
187
171
|
# 2. Check mounted servers using the filtered protocol path.
|
|
188
|
-
for mounted in reversed(self.
|
|
172
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
189
173
|
if mounted.prefix:
|
|
190
174
|
if key.startswith(f"{mounted.prefix}_"):
|
|
191
175
|
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -213,7 +197,7 @@ class ComponentService:
|
|
|
213
197
|
return prompt
|
|
214
198
|
|
|
215
199
|
# 2. Check mounted servers using the filtered protocol path.
|
|
216
|
-
for mounted in reversed(self.
|
|
200
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
217
201
|
if mounted.prefix:
|
|
218
202
|
if key.startswith(f"{mounted.prefix}_"):
|
|
219
203
|
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -11,12 +11,15 @@ Tools:
|
|
|
11
11
|
* [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
|
|
12
12
|
* [annotations](https://gofastmcp.com/servers/tools#annotations-2)
|
|
13
13
|
* [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
|
|
14
|
+
* [meta](https://gofastmcp.com/servers/tools#param-meta)
|
|
14
15
|
|
|
15
16
|
Prompts:
|
|
16
17
|
* [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
|
|
18
|
+
* [meta](https://gofastmcp.com/servers/prompts#param-meta)
|
|
17
19
|
|
|
18
20
|
Resources:
|
|
19
21
|
* [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)
|
|
22
|
+
* [meta](https://gofastmcp.com/servers/resources#param-meta)
|
|
20
23
|
|
|
21
24
|
## Usage
|
|
22
25
|
|
|
@@ -78,7 +81,16 @@ class MyComponent(MCPMixin):
|
|
|
78
81
|
if delete_all:
|
|
79
82
|
return "99 records deleted. I bet you're not a tool :)"
|
|
80
83
|
return "Tool executed, but you might be a tool!"
|
|
81
|
-
|
|
84
|
+
|
|
85
|
+
# example tool w/ meta
|
|
86
|
+
@mcp_tool(
|
|
87
|
+
name="data_tool",
|
|
88
|
+
description="Fetches user data from database",
|
|
89
|
+
meta={"version": "2.0", "category": "database", "author": "dev-team"}
|
|
90
|
+
)
|
|
91
|
+
def data_tool_method(self, user_id: int):
|
|
92
|
+
return f"Fetching data for user {user_id}"
|
|
93
|
+
|
|
82
94
|
@mcp_resource(uri="component://data")
|
|
83
95
|
def resource_method(self):
|
|
84
96
|
return {"data": "some data"}
|
|
@@ -88,6 +100,15 @@ class MyComponent(MCPMixin):
|
|
|
88
100
|
def resource_method(self):
|
|
89
101
|
return {"data": "some data"}
|
|
90
102
|
|
|
103
|
+
# example resource w/meta and title
|
|
104
|
+
@mcp_resource(
|
|
105
|
+
uri="component://config",
|
|
106
|
+
title="Data resource Title,
|
|
107
|
+
meta={"internal": True, "cache_ttl": 3600, "priority": "high"}
|
|
108
|
+
)
|
|
109
|
+
def config_resource_method(self):
|
|
110
|
+
return {"config": "data"}
|
|
111
|
+
|
|
91
112
|
# prompt
|
|
92
113
|
@mcp_prompt(name="A prompt")
|
|
93
114
|
def prompt_method(self, name):
|
|
@@ -98,6 +119,16 @@ class MyComponent(MCPMixin):
|
|
|
98
119
|
def prompt_method(self, name):
|
|
99
120
|
return f"What's up {name}?"
|
|
100
121
|
|
|
122
|
+
# example prompt w/title and meta
|
|
123
|
+
@mcp_prompt(
|
|
124
|
+
name="analysis_prompt",
|
|
125
|
+
title="Data Analysis Prompt",
|
|
126
|
+
description="Analyzes data patterns",
|
|
127
|
+
meta={"complexity": "high", "domain": "analytics", "requires_context": True}
|
|
128
|
+
)
|
|
129
|
+
def analysis_prompt_method(self, dataset: str):
|
|
130
|
+
return f"Analyze the patterns in {dataset}"
|
|
131
|
+
|
|
101
132
|
mcp_server = FastMCP()
|
|
102
133
|
component = MyComponent()
|
|
103
134
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
-
from mcp.types import ToolAnnotations
|
|
6
|
+
from mcp.types import Annotations, ToolAnnotations
|
|
7
7
|
|
|
8
8
|
from fastmcp.prompts.prompt import Prompt
|
|
9
9
|
from fastmcp.resources.resource import Resource
|
|
@@ -29,6 +29,7 @@ def mcp_tool(
|
|
|
29
29
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
30
30
|
exclude_args: list[str] | None = None,
|
|
31
31
|
serializer: Callable[[Any], str] | None = None,
|
|
32
|
+
meta: dict[str, Any] | None = None,
|
|
32
33
|
enabled: bool | None = None,
|
|
33
34
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
34
35
|
"""Decorator to mark a method as an MCP tool for later registration."""
|
|
@@ -41,6 +42,7 @@ def mcp_tool(
|
|
|
41
42
|
"annotations": annotations,
|
|
42
43
|
"exclude_args": exclude_args,
|
|
43
44
|
"serializer": serializer,
|
|
45
|
+
"meta": meta,
|
|
44
46
|
"enabled": enabled,
|
|
45
47
|
}
|
|
46
48
|
call_args = {k: v for k, v in call_args.items() if v is not None}
|
|
@@ -54,9 +56,12 @@ def mcp_resource(
|
|
|
54
56
|
uri: str,
|
|
55
57
|
*,
|
|
56
58
|
name: str | None = None,
|
|
59
|
+
title: str | None = None,
|
|
57
60
|
description: str | None = None,
|
|
58
61
|
mime_type: str | None = None,
|
|
59
62
|
tags: set[str] | None = None,
|
|
63
|
+
annotations: Annotations | None = None,
|
|
64
|
+
meta: dict[str, Any] | None = None,
|
|
60
65
|
enabled: bool | None = None,
|
|
61
66
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
62
67
|
"""Decorator to mark a method as an MCP resource for later registration."""
|
|
@@ -65,9 +70,12 @@ def mcp_resource(
|
|
|
65
70
|
call_args = {
|
|
66
71
|
"uri": uri,
|
|
67
72
|
"name": name or get_fn_name(func),
|
|
73
|
+
"title": title,
|
|
68
74
|
"description": description,
|
|
69
75
|
"mime_type": mime_type,
|
|
70
76
|
"tags": tags,
|
|
77
|
+
"annotations": annotations,
|
|
78
|
+
"meta": meta,
|
|
71
79
|
"enabled": enabled,
|
|
72
80
|
}
|
|
73
81
|
call_args = {k: v for k, v in call_args.items() if v is not None}
|
|
@@ -81,8 +89,10 @@ def mcp_resource(
|
|
|
81
89
|
|
|
82
90
|
def mcp_prompt(
|
|
83
91
|
name: str | None = None,
|
|
92
|
+
title: str | None = None,
|
|
84
93
|
description: str | None = None,
|
|
85
94
|
tags: set[str] | None = None,
|
|
95
|
+
meta: dict[str, Any] | None = None,
|
|
86
96
|
enabled: bool | None = None,
|
|
87
97
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
88
98
|
"""Decorator to mark a method as an MCP prompt for later registration."""
|
|
@@ -90,8 +100,10 @@ def mcp_prompt(
|
|
|
90
100
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
91
101
|
call_args = {
|
|
92
102
|
"name": name or get_fn_name(func),
|
|
103
|
+
"title": title,
|
|
93
104
|
"description": description,
|
|
94
105
|
"tags": tags,
|
|
106
|
+
"meta": meta,
|
|
95
107
|
"enabled": enabled,
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -151,7 +163,6 @@ class MCPMixin:
|
|
|
151
163
|
tool = Tool.from_function(
|
|
152
164
|
fn=method,
|
|
153
165
|
name=registration_info.get("name"),
|
|
154
|
-
title=registration_info.get("title"),
|
|
155
166
|
description=registration_info.get("description"),
|
|
156
167
|
tags=registration_info.get("tags"),
|
|
157
168
|
annotations=registration_info.get("annotations"),
|
|
@@ -195,6 +206,7 @@ class MCPMixin:
|
|
|
195
206
|
fn=method,
|
|
196
207
|
uri=registration_info["uri"],
|
|
197
208
|
name=registration_info.get("name"),
|
|
209
|
+
title=registration_info.get("title"),
|
|
198
210
|
description=registration_info.get("description"),
|
|
199
211
|
mime_type=registration_info.get("mime_type"),
|
|
200
212
|
tags=registration_info.get("tags"),
|
fastmcp/dependencies.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Dependency injection exports for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module re-exports dependency injection symbols from Docket and FastMCP
|
|
4
|
+
to provide a clean, centralized import location for all dependency-related
|
|
5
|
+
functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from docket import Depends
|
|
9
|
+
|
|
10
|
+
from fastmcp.server.dependencies import (
|
|
11
|
+
CurrentContext,
|
|
12
|
+
CurrentDocket,
|
|
13
|
+
CurrentFastMCP,
|
|
14
|
+
CurrentWorker,
|
|
15
|
+
Progress,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CurrentContext",
|
|
20
|
+
"CurrentDocket",
|
|
21
|
+
"CurrentFastMCP",
|
|
22
|
+
"CurrentWorker",
|
|
23
|
+
"Depends",
|
|
24
|
+
"Progress",
|
|
25
|
+
]
|