fastmcp 2.13.3__py3-none-any.whl → 2.14.1__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 +0 -21
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +8 -22
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/auth/oauth.py +9 -9
- fastmcp/client/client.py +739 -136
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/messages.py +7 -5
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling/__init__.py +69 -0
- fastmcp/client/sampling/handlers/__init__.py +0 -0
- fastmcp/client/sampling/handlers/anthropic.py +387 -0
- fastmcp/client/sampling/handlers/openai.py +399 -0
- fastmcp/client/tasks.py +551 -0
- fastmcp/client/transports.py +72 -21
- fastmcp/contrib/component_manager/component_service.py +4 -20
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/__init__.py +5 -0
- fastmcp/experimental/sampling/handlers/openai.py +4 -169
- fastmcp/experimental/server/openapi/__init__.py +15 -13
- fastmcp/experimental/utilities/openapi/__init__.py +12 -38
- fastmcp/prompts/prompt.py +38 -38
- fastmcp/resources/resource.py +33 -16
- fastmcp/resources/template.py +69 -59
- fastmcp/server/auth/__init__.py +0 -9
- fastmcp/server/auth/auth.py +127 -3
- fastmcp/server/auth/oauth_proxy.py +47 -97
- fastmcp/server/auth/oidc_proxy.py +7 -0
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/oci.py +2 -2
- fastmcp/server/context.py +509 -180
- fastmcp/server/dependencies.py +464 -6
- fastmcp/server/elicitation.py +285 -47
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +15 -3
- fastmcp/server/low_level.py +56 -12
- fastmcp/server/middleware/middleware.py +2 -2
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +4 -3
- fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +53 -40
- fastmcp/server/sampling/__init__.py +10 -0
- fastmcp/server/sampling/run.py +301 -0
- fastmcp/server/sampling/sampling_tool.py +108 -0
- fastmcp/server/server.py +793 -552
- 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 +206 -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 +101 -103
- fastmcp/tools/tool.py +83 -49
- fastmcp/tools/tool_transform.py +1 -12
- fastmcp/utilities/components.py +3 -3
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
- fastmcp/utilities/tests.py +11 -5
- fastmcp/utilities/types.py +8 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
- fastmcp/client/sampling.py +0 -56
- fastmcp/experimental/sampling/handlers/base.py +0 -21
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1087
- fastmcp/server/sampling/handler.py +0 -19
- fastmcp/utilities/openapi.py +0 -1568
- /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
- /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import inspect
|
|
6
|
-
import json
|
|
7
7
|
import re
|
|
8
8
|
import secrets
|
|
9
9
|
import warnings
|
|
10
|
+
import weakref
|
|
10
11
|
from collections.abc import (
|
|
11
12
|
AsyncIterator,
|
|
12
13
|
Awaitable,
|
|
@@ -19,6 +20,7 @@ from contextlib import (
|
|
|
19
20
|
AbstractAsyncContextManager,
|
|
20
21
|
AsyncExitStack,
|
|
21
22
|
asynccontextmanager,
|
|
23
|
+
suppress,
|
|
22
24
|
)
|
|
23
25
|
from dataclasses import dataclass
|
|
24
26
|
from functools import partial
|
|
@@ -29,21 +31,25 @@ import anyio
|
|
|
29
31
|
import httpx
|
|
30
32
|
import mcp.types
|
|
31
33
|
import uvicorn
|
|
34
|
+
from docket import Docket, Worker
|
|
32
35
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
33
36
|
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
34
37
|
from mcp.server.stdio import stdio_server
|
|
38
|
+
from mcp.shared.exceptions import McpError
|
|
35
39
|
from mcp.types import (
|
|
40
|
+
METHOD_NOT_FOUND,
|
|
36
41
|
Annotations,
|
|
37
42
|
AnyFunction,
|
|
38
43
|
CallToolRequestParams,
|
|
39
44
|
ContentBlock,
|
|
45
|
+
ErrorData,
|
|
40
46
|
GetPromptResult,
|
|
41
47
|
ToolAnnotations,
|
|
42
48
|
)
|
|
43
|
-
from mcp.types import Prompt as
|
|
44
|
-
from mcp.types import Resource as
|
|
45
|
-
from mcp.types import ResourceTemplate as
|
|
46
|
-
from mcp.types import Tool as
|
|
49
|
+
from mcp.types import Prompt as SDKPrompt
|
|
50
|
+
from mcp.types import Resource as SDKResource
|
|
51
|
+
from mcp.types import ResourceTemplate as SDKResourceTemplate
|
|
52
|
+
from mcp.types import Tool as SDKTool
|
|
47
53
|
from pydantic import AnyUrl
|
|
48
54
|
from starlette.middleware import Middleware as ASGIMiddleware
|
|
49
55
|
from starlette.requests import Request
|
|
@@ -57,10 +63,11 @@ from fastmcp.mcp_config import MCPConfig
|
|
|
57
63
|
from fastmcp.prompts import Prompt
|
|
58
64
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
59
65
|
from fastmcp.prompts.prompt_manager import PromptManager
|
|
60
|
-
from fastmcp.resources.resource import Resource
|
|
66
|
+
from fastmcp.resources.resource import FunctionResource, Resource
|
|
61
67
|
from fastmcp.resources.resource_manager import ResourceManager
|
|
62
|
-
from fastmcp.resources.template import ResourceTemplate
|
|
68
|
+
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
|
|
63
69
|
from fastmcp.server.auth import AuthProvider
|
|
70
|
+
from fastmcp.server.event_store import EventStore
|
|
64
71
|
from fastmcp.server.http import (
|
|
65
72
|
StarletteWithLifespan,
|
|
66
73
|
create_sse_app,
|
|
@@ -68,6 +75,13 @@ from fastmcp.server.http import (
|
|
|
68
75
|
)
|
|
69
76
|
from fastmcp.server.low_level import LowLevelServer
|
|
70
77
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
78
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
79
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
80
|
+
from fastmcp.server.tasks.handlers import (
|
|
81
|
+
handle_prompt_as_task,
|
|
82
|
+
handle_resource_as_task,
|
|
83
|
+
handle_tool_as_task,
|
|
84
|
+
)
|
|
71
85
|
from fastmcp.settings import Settings
|
|
72
86
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
73
87
|
from fastmcp.tools.tool_manager import ToolManager
|
|
@@ -80,24 +94,34 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
80
94
|
if TYPE_CHECKING:
|
|
81
95
|
from fastmcp.client import Client
|
|
82
96
|
from fastmcp.client.client import FastMCP1Server
|
|
97
|
+
from fastmcp.client.sampling import SamplingHandler
|
|
83
98
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
84
|
-
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
85
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
86
|
-
ComponentFn as OpenAPIComponentFnNew,
|
|
87
|
-
)
|
|
88
|
-
from fastmcp.experimental.server.openapi.routing import RouteMap as RouteMapNew
|
|
89
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
90
|
-
RouteMapFn as OpenAPIRouteMapFnNew,
|
|
91
|
-
)
|
|
92
99
|
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
93
100
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
94
101
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
95
102
|
from fastmcp.server.proxy import FastMCPProxy
|
|
96
|
-
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
97
103
|
from fastmcp.tools.tool import ToolResultSerializerType
|
|
98
104
|
|
|
99
105
|
logger = get_logger(__name__)
|
|
100
106
|
|
|
107
|
+
|
|
108
|
+
def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
|
109
|
+
"""Create a wrapper function with a custom __name__ for Docket registration.
|
|
110
|
+
|
|
111
|
+
Docket uses fn.__name__ as the key for function registration and lookup.
|
|
112
|
+
When mounting servers, we need unique names to avoid collisions between
|
|
113
|
+
mounted servers that have identically-named functions.
|
|
114
|
+
"""
|
|
115
|
+
import functools
|
|
116
|
+
|
|
117
|
+
@functools.wraps(fn)
|
|
118
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
+
return await fn(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
wrapper.__name__ = name
|
|
122
|
+
return wrapper
|
|
123
|
+
|
|
124
|
+
|
|
101
125
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
102
126
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
103
127
|
|
|
@@ -158,8 +182,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
158
182
|
auth: AuthProvider | NotSetT | None = NotSet,
|
|
159
183
|
middleware: Sequence[Middleware] | None = None,
|
|
160
184
|
lifespan: LifespanCallable | None = None,
|
|
161
|
-
dependencies: list[str] | None = None,
|
|
162
|
-
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
163
185
|
mask_error_details: bool | None = None,
|
|
164
186
|
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
165
187
|
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
@@ -171,6 +193,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
171
193
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
172
194
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
173
195
|
strict_input_validation: bool | None = None,
|
|
196
|
+
tasks: bool | None = None,
|
|
174
197
|
# ---
|
|
175
198
|
# ---
|
|
176
199
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -185,15 +208,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
185
208
|
streamable_http_path: str | None = None,
|
|
186
209
|
json_response: bool | None = None,
|
|
187
210
|
stateless_http: bool | None = None,
|
|
188
|
-
sampling_handler:
|
|
211
|
+
sampling_handler: SamplingHandler | None = None,
|
|
189
212
|
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
190
213
|
):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
# Resolve server default for background task support
|
|
215
|
+
self._support_tasks_by_default: bool = tasks if tasks is not None else False
|
|
216
|
+
|
|
217
|
+
# Docket instance (set during lifespan for cross-task access)
|
|
218
|
+
self._docket = None
|
|
194
219
|
|
|
195
220
|
self._additional_http_routes: list[BaseRoute] = []
|
|
196
221
|
self._mounted_servers: list[MountedServer] = []
|
|
222
|
+
self._is_mounted: bool = (
|
|
223
|
+
False # Set to True when this server is mounted on another
|
|
224
|
+
)
|
|
197
225
|
self._tool_manager: ToolManager = ToolManager(
|
|
198
226
|
duplicate_behavior=on_duplicate_tools,
|
|
199
227
|
mask_error_details=mask_error_details,
|
|
@@ -212,6 +240,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
212
240
|
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
213
241
|
self._lifespan_result: LifespanResultT | None = None
|
|
214
242
|
self._lifespan_result_set: bool = False
|
|
243
|
+
self._started: asyncio.Event = asyncio.Event()
|
|
215
244
|
|
|
216
245
|
# Generate random ID if no name provided
|
|
217
246
|
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
@@ -259,27 +288,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
259
288
|
# Set up MCP protocol handlers
|
|
260
289
|
self._setup_handlers()
|
|
261
290
|
|
|
262
|
-
|
|
263
|
-
# TODO: Remove dependencies parameter (deprecated in v2.11.4)
|
|
264
|
-
if dependencies is not None:
|
|
265
|
-
import warnings
|
|
266
|
-
|
|
267
|
-
warnings.warn(
|
|
268
|
-
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
|
269
|
-
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
|
270
|
-
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
|
271
|
-
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
|
272
|
-
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
|
273
|
-
DeprecationWarning,
|
|
274
|
-
stacklevel=2,
|
|
275
|
-
)
|
|
276
|
-
self.dependencies: list[str] = (
|
|
277
|
-
dependencies or fastmcp.settings.server_dependencies
|
|
278
|
-
) # TODO: Remove (deprecated in v2.11.4)
|
|
279
|
-
|
|
280
|
-
self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
|
|
281
|
-
sampling_handler
|
|
282
|
-
)
|
|
291
|
+
self.sampling_handler: SamplingHandler | None = sampling_handler
|
|
283
292
|
self.sampling_handler_behavior: Literal["always", "fallback"] = (
|
|
284
293
|
sampling_handler_behavior or "fallback"
|
|
285
294
|
)
|
|
@@ -290,7 +299,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
290
299
|
else fastmcp.settings.include_fastmcp_meta
|
|
291
300
|
)
|
|
292
301
|
|
|
293
|
-
# handle deprecated settings
|
|
294
302
|
self._handle_deprecated_settings(
|
|
295
303
|
log_level=log_level,
|
|
296
304
|
debug=debug,
|
|
@@ -383,14 +391,207 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
383
391
|
else:
|
|
384
392
|
return list(self._mcp_server.icons)
|
|
385
393
|
|
|
394
|
+
@property
|
|
395
|
+
def docket(self) -> Docket | None:
|
|
396
|
+
"""Get the Docket instance if Docket support is enabled.
|
|
397
|
+
|
|
398
|
+
Returns None if Docket is not enabled or server hasn't been started yet.
|
|
399
|
+
"""
|
|
400
|
+
return self._docket
|
|
401
|
+
|
|
402
|
+
@asynccontextmanager
|
|
403
|
+
async def _docket_lifespan(self) -> AsyncIterator[None]:
|
|
404
|
+
"""Manage Docket instance and Worker for background task execution."""
|
|
405
|
+
from fastmcp import settings
|
|
406
|
+
|
|
407
|
+
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
|
|
408
|
+
from fastmcp.server.dependencies import (
|
|
409
|
+
_current_docket,
|
|
410
|
+
_current_server,
|
|
411
|
+
_current_worker,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
server_token = _current_server.set(weakref.ref(self))
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
# For directly mounted servers, the parent's Docket/Worker handles all
|
|
418
|
+
# task execution. Skip creating our own to avoid race conditions with
|
|
419
|
+
# multiple workers competing for tasks from the same queue.
|
|
420
|
+
if self._is_mounted:
|
|
421
|
+
yield
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Create Docket instance using configured name and URL
|
|
425
|
+
async with Docket(
|
|
426
|
+
name=settings.docket.name,
|
|
427
|
+
url=settings.docket.url,
|
|
428
|
+
) as docket:
|
|
429
|
+
# Store on server instance for cross-task access (FastMCPTransport)
|
|
430
|
+
self._docket = docket
|
|
431
|
+
|
|
432
|
+
# Register local task-enabled tools/prompts/resources with Docket
|
|
433
|
+
# Only function-based variants support background tasks
|
|
434
|
+
# Register components where task execution is not "forbidden"
|
|
435
|
+
for tool in self._tool_manager._tools.values():
|
|
436
|
+
if (
|
|
437
|
+
isinstance(tool, FunctionTool)
|
|
438
|
+
and tool.task_config.mode != "forbidden"
|
|
439
|
+
):
|
|
440
|
+
docket.register(tool.fn)
|
|
441
|
+
|
|
442
|
+
for prompt in self._prompt_manager._prompts.values():
|
|
443
|
+
if (
|
|
444
|
+
isinstance(prompt, FunctionPrompt)
|
|
445
|
+
and prompt.task_config.mode != "forbidden"
|
|
446
|
+
):
|
|
447
|
+
# task execution requires async fn (validated at creation time)
|
|
448
|
+
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
|
|
449
|
+
|
|
450
|
+
for resource in self._resource_manager._resources.values():
|
|
451
|
+
if (
|
|
452
|
+
isinstance(resource, FunctionResource)
|
|
453
|
+
and resource.task_config.mode != "forbidden"
|
|
454
|
+
):
|
|
455
|
+
docket.register(resource.fn)
|
|
456
|
+
|
|
457
|
+
for template in self._resource_manager._templates.values():
|
|
458
|
+
if (
|
|
459
|
+
isinstance(template, FunctionResourceTemplate)
|
|
460
|
+
and template.task_config.mode != "forbidden"
|
|
461
|
+
):
|
|
462
|
+
docket.register(template.fn)
|
|
463
|
+
|
|
464
|
+
# Also register functions from mounted servers so tasks can
|
|
465
|
+
# execute in the parent's Docket context
|
|
466
|
+
for mounted in self._mounted_servers:
|
|
467
|
+
await self._register_mounted_server_functions(
|
|
468
|
+
mounted.server, docket, mounted.prefix, mounted.tool_names
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Set Docket in ContextVar so CurrentDocket can access it
|
|
472
|
+
docket_token = _current_docket.set(docket)
|
|
473
|
+
try:
|
|
474
|
+
# Build worker kwargs from settings
|
|
475
|
+
worker_kwargs: dict[str, Any] = {
|
|
476
|
+
"concurrency": settings.docket.concurrency,
|
|
477
|
+
"redelivery_timeout": settings.docket.redelivery_timeout,
|
|
478
|
+
"reconnection_delay": settings.docket.reconnection_delay,
|
|
479
|
+
}
|
|
480
|
+
if settings.docket.worker_name:
|
|
481
|
+
worker_kwargs["name"] = settings.docket.worker_name
|
|
482
|
+
|
|
483
|
+
# Create and start Worker
|
|
484
|
+
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
|
|
485
|
+
# Set Worker in ContextVar so CurrentWorker can access it
|
|
486
|
+
worker_token = _current_worker.set(worker)
|
|
487
|
+
try:
|
|
488
|
+
worker_task = asyncio.create_task(worker.run_forever())
|
|
489
|
+
try:
|
|
490
|
+
yield
|
|
491
|
+
finally:
|
|
492
|
+
# Cancel worker task on exit with timeout to prevent hanging
|
|
493
|
+
worker_task.cancel()
|
|
494
|
+
with suppress(
|
|
495
|
+
asyncio.CancelledError, asyncio.TimeoutError
|
|
496
|
+
):
|
|
497
|
+
await asyncio.wait_for(worker_task, timeout=2.0)
|
|
498
|
+
finally:
|
|
499
|
+
_current_worker.reset(worker_token)
|
|
500
|
+
finally:
|
|
501
|
+
# Reset ContextVar
|
|
502
|
+
_current_docket.reset(docket_token)
|
|
503
|
+
# Clear instance attribute
|
|
504
|
+
self._docket = None
|
|
505
|
+
finally:
|
|
506
|
+
# Reset server ContextVar
|
|
507
|
+
_current_server.reset(server_token)
|
|
508
|
+
|
|
509
|
+
async def _register_mounted_server_functions(
|
|
510
|
+
self,
|
|
511
|
+
server: FastMCP,
|
|
512
|
+
docket: Docket,
|
|
513
|
+
prefix: str | None,
|
|
514
|
+
tool_names: dict[str, str] | None = None,
|
|
515
|
+
) -> None:
|
|
516
|
+
"""Register task-enabled functions from a mounted server with Docket.
|
|
517
|
+
|
|
518
|
+
This enables background task execution for mounted server components
|
|
519
|
+
through the parent server's Docket context.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
server: The mounted server whose functions to register
|
|
523
|
+
docket: The Docket instance to register with
|
|
524
|
+
prefix: The mount prefix to prepend to function names (matches
|
|
525
|
+
client-facing tool/prompt names)
|
|
526
|
+
tool_names: Optional mapping of original tool names to custom names
|
|
527
|
+
"""
|
|
528
|
+
# Register tools with prefixed names to avoid collisions
|
|
529
|
+
for tool in server._tool_manager._tools.values():
|
|
530
|
+
if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
|
|
531
|
+
# Apply tool_names override first, then prefix (matches get_tools logic)
|
|
532
|
+
if tool_names and tool.key in tool_names:
|
|
533
|
+
fn_name = tool_names[tool.key]
|
|
534
|
+
elif prefix:
|
|
535
|
+
fn_name = f"{prefix}_{tool.key}"
|
|
536
|
+
else:
|
|
537
|
+
fn_name = tool.key
|
|
538
|
+
named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
|
|
539
|
+
docket.register(named_fn)
|
|
540
|
+
|
|
541
|
+
# Register prompts with prefixed names
|
|
542
|
+
for prompt in server._prompt_manager._prompts.values():
|
|
543
|
+
if (
|
|
544
|
+
isinstance(prompt, FunctionPrompt)
|
|
545
|
+
and prompt.task_config.mode != "forbidden"
|
|
546
|
+
):
|
|
547
|
+
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
548
|
+
named_fn = _create_named_fn_wrapper(
|
|
549
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
|
|
550
|
+
)
|
|
551
|
+
docket.register(named_fn)
|
|
552
|
+
|
|
553
|
+
# Register resources with prefixed names (use name, not key/URI)
|
|
554
|
+
for resource in server._resource_manager._resources.values():
|
|
555
|
+
if (
|
|
556
|
+
isinstance(resource, FunctionResource)
|
|
557
|
+
and resource.task_config.mode != "forbidden"
|
|
558
|
+
):
|
|
559
|
+
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
560
|
+
named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
|
|
561
|
+
docket.register(named_fn)
|
|
562
|
+
|
|
563
|
+
# Register resource templates with prefixed names (use name, not key/URI)
|
|
564
|
+
for template in server._resource_manager._templates.values():
|
|
565
|
+
if (
|
|
566
|
+
isinstance(template, FunctionResourceTemplate)
|
|
567
|
+
and template.task_config.mode != "forbidden"
|
|
568
|
+
):
|
|
569
|
+
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
570
|
+
named_fn = _create_named_fn_wrapper(template.fn, fn_name)
|
|
571
|
+
docket.register(named_fn)
|
|
572
|
+
|
|
573
|
+
# Recursively register from nested mounted servers with accumulated prefix
|
|
574
|
+
for nested in server._mounted_servers:
|
|
575
|
+
nested_prefix = (
|
|
576
|
+
f"{prefix}_{nested.prefix}"
|
|
577
|
+
if prefix and nested.prefix
|
|
578
|
+
else (prefix or nested.prefix)
|
|
579
|
+
)
|
|
580
|
+
await self._register_mounted_server_functions(
|
|
581
|
+
nested.server, docket, nested_prefix, nested.tool_names
|
|
582
|
+
)
|
|
583
|
+
|
|
386
584
|
@asynccontextmanager
|
|
387
585
|
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
388
586
|
if self._lifespan_result_set:
|
|
389
587
|
yield
|
|
390
588
|
return
|
|
391
589
|
|
|
392
|
-
async with
|
|
393
|
-
self.
|
|
590
|
+
async with (
|
|
591
|
+
self._lifespan(self) as user_lifespan_result,
|
|
592
|
+
self._docket_lifespan(),
|
|
593
|
+
):
|
|
594
|
+
self._lifespan_result = user_lifespan_result
|
|
394
595
|
self._lifespan_result_set = True
|
|
395
596
|
|
|
396
597
|
async with AsyncExitStack[bool | None]() as stack:
|
|
@@ -399,7 +600,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
399
600
|
cm=server.server._lifespan_manager()
|
|
400
601
|
)
|
|
401
602
|
|
|
402
|
-
|
|
603
|
+
self._started.set()
|
|
604
|
+
try:
|
|
605
|
+
yield
|
|
606
|
+
finally:
|
|
607
|
+
self._started.clear()
|
|
403
608
|
|
|
404
609
|
self._lifespan_result_set = False
|
|
405
610
|
self._lifespan_result = None
|
|
@@ -464,8 +669,260 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
464
669
|
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
465
670
|
self._call_tool_mcp
|
|
466
671
|
)
|
|
467
|
-
|
|
468
|
-
self.
|
|
672
|
+
# Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
|
|
673
|
+
self._setup_read_resource_handler()
|
|
674
|
+
# Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
|
|
675
|
+
self._setup_get_prompt_handler()
|
|
676
|
+
# Register custom SEP-1686 task protocol handlers
|
|
677
|
+
self._setup_task_protocol_handlers()
|
|
678
|
+
|
|
679
|
+
def _setup_read_resource_handler(self) -> None:
|
|
680
|
+
"""
|
|
681
|
+
Set up custom read_resource handler that supports task-augmented responses.
|
|
682
|
+
|
|
683
|
+
The SDK's read_resource decorator doesn't support CreateTaskResult returns,
|
|
684
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
685
|
+
"""
|
|
686
|
+
|
|
687
|
+
async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
|
|
688
|
+
uri = req.params.uri
|
|
689
|
+
|
|
690
|
+
# Check for task metadata via SDK's request context
|
|
691
|
+
task_meta = None
|
|
692
|
+
try:
|
|
693
|
+
ctx = self._mcp_server.request_context
|
|
694
|
+
if ctx.experimental.is_task:
|
|
695
|
+
task_meta = ctx.experimental.task_metadata
|
|
696
|
+
except (AttributeError, LookupError):
|
|
697
|
+
pass
|
|
698
|
+
|
|
699
|
+
# Check for task metadata and route appropriately
|
|
700
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
701
|
+
# Get resource including from mounted servers
|
|
702
|
+
resource = await self._get_resource_or_template_or_none(str(uri))
|
|
703
|
+
if (
|
|
704
|
+
resource
|
|
705
|
+
and self._should_enable_component(resource)
|
|
706
|
+
and hasattr(resource, "task_config")
|
|
707
|
+
):
|
|
708
|
+
task_mode = resource.task_config.mode # type: ignore[union-attr]
|
|
709
|
+
|
|
710
|
+
# Enforce mode="required" - must have task metadata
|
|
711
|
+
if task_mode == "required" and not task_meta:
|
|
712
|
+
raise McpError(
|
|
713
|
+
ErrorData(
|
|
714
|
+
code=METHOD_NOT_FOUND,
|
|
715
|
+
message=f"Resource '{uri}' requires task-augmented execution",
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# Route to background if task metadata present and mode allows
|
|
720
|
+
if task_meta and task_mode != "forbidden":
|
|
721
|
+
# For FunctionResource/FunctionResourceTemplate, use Docket
|
|
722
|
+
if isinstance(
|
|
723
|
+
resource,
|
|
724
|
+
FunctionResource | FunctionResourceTemplate,
|
|
725
|
+
):
|
|
726
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
727
|
+
return await handle_resource_as_task(
|
|
728
|
+
self, str(uri), resource, task_meta_dict
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
732
|
+
# Raise error since resources don't have isError field
|
|
733
|
+
if task_meta and task_mode == "forbidden":
|
|
734
|
+
raise McpError(
|
|
735
|
+
ErrorData(
|
|
736
|
+
code=METHOD_NOT_FOUND,
|
|
737
|
+
message=f"Resource '{uri}' does not support task-augmented execution",
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# Synchronous execution
|
|
742
|
+
result = await self._read_resource_mcp(uri)
|
|
743
|
+
|
|
744
|
+
# Graceful degradation: if we got here with task_meta, something went wrong
|
|
745
|
+
# (This should be unreachable now that forbidden raises)
|
|
746
|
+
if task_meta:
|
|
747
|
+
mcp_contents = []
|
|
748
|
+
for item in result:
|
|
749
|
+
if isinstance(item.content, str):
|
|
750
|
+
mcp_contents.append(
|
|
751
|
+
mcp.types.TextResourceContents(
|
|
752
|
+
uri=uri,
|
|
753
|
+
text=item.content,
|
|
754
|
+
mimeType=item.mime_type or "text/plain",
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
elif isinstance(item.content, bytes):
|
|
758
|
+
import base64
|
|
759
|
+
|
|
760
|
+
mcp_contents.append(
|
|
761
|
+
mcp.types.BlobResourceContents(
|
|
762
|
+
uri=uri,
|
|
763
|
+
blob=base64.b64encode(item.content).decode(),
|
|
764
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
return mcp.types.ServerResult(
|
|
768
|
+
mcp.types.ReadResourceResult(
|
|
769
|
+
contents=mcp_contents,
|
|
770
|
+
_meta={
|
|
771
|
+
"modelcontextprotocol.io/task": {
|
|
772
|
+
"returned_immediately": True
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# Convert to proper ServerResult
|
|
779
|
+
if isinstance(result, mcp.types.ServerResult):
|
|
780
|
+
return result
|
|
781
|
+
|
|
782
|
+
mcp_contents = []
|
|
783
|
+
for item in result:
|
|
784
|
+
if isinstance(item.content, str):
|
|
785
|
+
mcp_contents.append(
|
|
786
|
+
mcp.types.TextResourceContents(
|
|
787
|
+
uri=uri,
|
|
788
|
+
text=item.content,
|
|
789
|
+
mimeType=item.mime_type or "text/plain",
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
elif isinstance(item.content, bytes):
|
|
793
|
+
import base64
|
|
794
|
+
|
|
795
|
+
mcp_contents.append(
|
|
796
|
+
mcp.types.BlobResourceContents(
|
|
797
|
+
uri=uri,
|
|
798
|
+
blob=base64.b64encode(item.content).decode(),
|
|
799
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
return mcp.types.ServerResult(
|
|
804
|
+
mcp.types.ReadResourceResult(contents=mcp_contents)
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
|
|
808
|
+
|
|
809
|
+
def _setup_get_prompt_handler(self) -> None:
|
|
810
|
+
"""
|
|
811
|
+
Set up custom get_prompt handler that supports task-augmented responses.
|
|
812
|
+
|
|
813
|
+
The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
|
|
814
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
815
|
+
"""
|
|
816
|
+
|
|
817
|
+
async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
|
|
818
|
+
name = req.params.name
|
|
819
|
+
arguments = req.params.arguments
|
|
820
|
+
|
|
821
|
+
# Check for task metadata via SDK's request context
|
|
822
|
+
task_meta = None
|
|
823
|
+
try:
|
|
824
|
+
ctx = self._mcp_server.request_context
|
|
825
|
+
if ctx.experimental.is_task:
|
|
826
|
+
task_meta = ctx.experimental.task_metadata
|
|
827
|
+
except (AttributeError, LookupError):
|
|
828
|
+
pass
|
|
829
|
+
|
|
830
|
+
# Check for task metadata and route appropriately
|
|
831
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
832
|
+
prompts = await self.get_prompts()
|
|
833
|
+
prompt = prompts.get(name)
|
|
834
|
+
if (
|
|
835
|
+
prompt
|
|
836
|
+
and self._should_enable_component(prompt)
|
|
837
|
+
and hasattr(prompt, "task_config")
|
|
838
|
+
and prompt.task_config
|
|
839
|
+
):
|
|
840
|
+
task_mode = prompt.task_config.mode # type: ignore[union-attr]
|
|
841
|
+
|
|
842
|
+
# Enforce mode="required" - must have task metadata
|
|
843
|
+
if task_mode == "required" and not task_meta:
|
|
844
|
+
raise McpError(
|
|
845
|
+
ErrorData(
|
|
846
|
+
code=METHOD_NOT_FOUND,
|
|
847
|
+
message=f"Prompt '{name}' requires task-augmented execution",
|
|
848
|
+
)
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
# Route to background if task metadata present and mode allows
|
|
852
|
+
if task_meta and task_mode != "forbidden":
|
|
853
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
854
|
+
result = await handle_prompt_as_task(
|
|
855
|
+
self, name, arguments, task_meta_dict
|
|
856
|
+
)
|
|
857
|
+
return mcp.types.ServerResult(result)
|
|
858
|
+
|
|
859
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
860
|
+
# Raise error since prompts don't have isError field
|
|
861
|
+
if task_meta and task_mode == "forbidden":
|
|
862
|
+
raise McpError(
|
|
863
|
+
ErrorData(
|
|
864
|
+
code=METHOD_NOT_FOUND,
|
|
865
|
+
message=f"Prompt '{name}' does not support task-augmented execution",
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# Synchronous execution
|
|
870
|
+
result = await self._get_prompt_mcp(name, arguments)
|
|
871
|
+
return mcp.types.ServerResult(result)
|
|
872
|
+
|
|
873
|
+
self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
|
|
874
|
+
|
|
875
|
+
def _setup_task_protocol_handlers(self) -> None:
|
|
876
|
+
"""Register SEP-1686 task protocol handlers with SDK."""
|
|
877
|
+
from mcp.types import (
|
|
878
|
+
CancelTaskRequest,
|
|
879
|
+
GetTaskPayloadRequest,
|
|
880
|
+
GetTaskRequest,
|
|
881
|
+
ListTasksRequest,
|
|
882
|
+
ServerResult,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
from fastmcp.server.tasks.protocol import (
|
|
886
|
+
tasks_cancel_handler,
|
|
887
|
+
tasks_get_handler,
|
|
888
|
+
tasks_list_handler,
|
|
889
|
+
tasks_result_handler,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
# Manually register handlers (SDK decorators fail with locally-defined functions)
|
|
893
|
+
# SDK expects handlers that receive Request objects and return ServerResult
|
|
894
|
+
|
|
895
|
+
async def handle_get_task(req: GetTaskRequest) -> ServerResult:
|
|
896
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
897
|
+
result = await tasks_get_handler(self, params)
|
|
898
|
+
return ServerResult(result)
|
|
899
|
+
|
|
900
|
+
async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
|
|
901
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
902
|
+
result = await tasks_result_handler(self, params)
|
|
903
|
+
return ServerResult(result)
|
|
904
|
+
|
|
905
|
+
async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
|
|
906
|
+
params = (
|
|
907
|
+
req.params.model_dump(by_alias=True, exclude_none=True)
|
|
908
|
+
if req.params
|
|
909
|
+
else {}
|
|
910
|
+
)
|
|
911
|
+
result = await tasks_list_handler(self, params)
|
|
912
|
+
return ServerResult(result)
|
|
913
|
+
|
|
914
|
+
async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
|
|
915
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
916
|
+
result = await tasks_cancel_handler(self, params)
|
|
917
|
+
return ServerResult(result)
|
|
918
|
+
|
|
919
|
+
# Register directly with SDK (same as what decorators do internally)
|
|
920
|
+
self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
|
|
921
|
+
self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
|
|
922
|
+
handle_get_task_result
|
|
923
|
+
)
|
|
924
|
+
self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
|
|
925
|
+
self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
|
|
469
926
|
|
|
470
927
|
async def _apply_middleware(
|
|
471
928
|
self,
|
|
@@ -489,7 +946,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
489
946
|
try:
|
|
490
947
|
child_tools = await mounted.server.get_tools()
|
|
491
948
|
for key, tool in child_tools.items():
|
|
492
|
-
|
|
949
|
+
# Check for manual override first, then apply prefix
|
|
950
|
+
if mounted.tool_names and key in mounted.tool_names:
|
|
951
|
+
new_key = mounted.tool_names[key]
|
|
952
|
+
elif mounted.prefix:
|
|
953
|
+
new_key = f"{mounted.prefix}_{key}"
|
|
954
|
+
else:
|
|
955
|
+
new_key = key
|
|
493
956
|
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
494
957
|
except Exception as e:
|
|
495
958
|
logger.warning(
|
|
@@ -507,6 +970,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
507
970
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
508
971
|
return tools[key]
|
|
509
972
|
|
|
973
|
+
async def _get_tool_with_task_config(self, key: str) -> Tool | None:
|
|
974
|
+
"""Get a tool by key, returning None if not found.
|
|
975
|
+
|
|
976
|
+
Used for task config checking where we need the actual tool object
|
|
977
|
+
(including from mounted servers and proxies) but don't want to raise.
|
|
978
|
+
"""
|
|
979
|
+
try:
|
|
980
|
+
return await self.get_tool(key)
|
|
981
|
+
except NotFoundError:
|
|
982
|
+
return None
|
|
983
|
+
|
|
984
|
+
async def _get_resource_or_template_or_none(
|
|
985
|
+
self, uri: str
|
|
986
|
+
) -> Resource | ResourceTemplate | None:
|
|
987
|
+
"""Get a resource or template by URI, searching recursively. Returns None if not found."""
|
|
988
|
+
try:
|
|
989
|
+
return await self.get_resource(uri)
|
|
990
|
+
except NotFoundError:
|
|
991
|
+
pass
|
|
992
|
+
|
|
993
|
+
templates = await self.get_resource_templates()
|
|
994
|
+
for template in templates.values():
|
|
995
|
+
if template.matches(uri):
|
|
996
|
+
return template
|
|
997
|
+
|
|
998
|
+
return None
|
|
999
|
+
|
|
510
1000
|
async def get_resources(self) -> dict[str, Resource]:
|
|
511
1001
|
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
512
1002
|
all_resources = dict(await self._resource_manager.get_resources())
|
|
@@ -516,9 +1006,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
516
1006
|
child_resources = await mounted.server.get_resources()
|
|
517
1007
|
for key, resource in child_resources.items():
|
|
518
1008
|
new_key = (
|
|
519
|
-
add_resource_prefix(
|
|
520
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
521
|
-
)
|
|
1009
|
+
add_resource_prefix(key, mounted.prefix)
|
|
522
1010
|
if mounted.prefix
|
|
523
1011
|
else key
|
|
524
1012
|
)
|
|
@@ -555,17 +1043,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
555
1043
|
child_templates = await mounted.server.get_resource_templates()
|
|
556
1044
|
for key, template in child_templates.items():
|
|
557
1045
|
new_key = (
|
|
558
|
-
add_resource_prefix(
|
|
559
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
560
|
-
)
|
|
1046
|
+
add_resource_prefix(key, mounted.prefix)
|
|
561
1047
|
if mounted.prefix
|
|
562
1048
|
else key
|
|
563
1049
|
)
|
|
564
|
-
update =
|
|
565
|
-
|
|
566
|
-
if
|
|
567
|
-
|
|
568
|
-
|
|
1050
|
+
update: dict[str, Any] = {}
|
|
1051
|
+
if mounted.prefix:
|
|
1052
|
+
if template.name:
|
|
1053
|
+
update["name"] = f"{mounted.prefix}_{template.name}"
|
|
1054
|
+
# Update uri_template so matches() works with prefixed URIs
|
|
1055
|
+
update["uri_template"] = new_key
|
|
569
1056
|
all_templates[new_key] = template.model_copy(
|
|
570
1057
|
key=new_key, update=update
|
|
571
1058
|
)
|
|
@@ -680,7 +1167,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
680
1167
|
|
|
681
1168
|
return routes
|
|
682
1169
|
|
|
683
|
-
async def _list_tools_mcp(self) -> list[
|
|
1170
|
+
async def _list_tools_mcp(self) -> list[SDKTool]:
|
|
684
1171
|
"""
|
|
685
1172
|
List all available tools, in the format expected by the low-level MCP
|
|
686
1173
|
server.
|
|
@@ -745,9 +1232,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
745
1232
|
if not self._should_enable_component(tool):
|
|
746
1233
|
continue
|
|
747
1234
|
|
|
748
|
-
|
|
749
|
-
if mounted.
|
|
1235
|
+
# Check for manual override first, then apply prefix
|
|
1236
|
+
if mounted.tool_names and tool.key in mounted.tool_names:
|
|
1237
|
+
key = mounted.tool_names[tool.key]
|
|
1238
|
+
elif mounted.prefix:
|
|
750
1239
|
key = f"{mounted.prefix}_{tool.key}"
|
|
1240
|
+
else:
|
|
1241
|
+
key = tool.key
|
|
1242
|
+
|
|
1243
|
+
if key != tool.key:
|
|
751
1244
|
tool = tool.model_copy(key=key)
|
|
752
1245
|
# Later mounted servers override earlier ones
|
|
753
1246
|
all_tools[key] = tool
|
|
@@ -764,7 +1257,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
764
1257
|
|
|
765
1258
|
return list(all_tools.values())
|
|
766
1259
|
|
|
767
|
-
async def _list_resources_mcp(self) -> list[
|
|
1260
|
+
async def _list_resources_mcp(self) -> list[SDKResource]:
|
|
768
1261
|
"""
|
|
769
1262
|
List all available resources, in the format expected by the low-level MCP
|
|
770
1263
|
server.
|
|
@@ -835,11 +1328,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
835
1328
|
|
|
836
1329
|
key = resource.key
|
|
837
1330
|
if mounted.prefix:
|
|
838
|
-
key = add_resource_prefix(
|
|
839
|
-
resource.key,
|
|
840
|
-
mounted.prefix,
|
|
841
|
-
mounted.resource_prefix_format,
|
|
842
|
-
)
|
|
1331
|
+
key = add_resource_prefix(resource.key, mounted.prefix)
|
|
843
1332
|
resource = resource.model_copy(
|
|
844
1333
|
key=key,
|
|
845
1334
|
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
@@ -857,7 +1346,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
857
1346
|
|
|
858
1347
|
return list(all_resources.values())
|
|
859
1348
|
|
|
860
|
-
async def _list_resource_templates_mcp(self) -> list[
|
|
1349
|
+
async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
|
|
861
1350
|
"""
|
|
862
1351
|
List all available resource templates, in the format expected by the low-level MCP
|
|
863
1352
|
server.
|
|
@@ -931,11 +1420,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
931
1420
|
|
|
932
1421
|
key = template.key
|
|
933
1422
|
if mounted.prefix:
|
|
934
|
-
key = add_resource_prefix(
|
|
935
|
-
template.key,
|
|
936
|
-
mounted.prefix,
|
|
937
|
-
mounted.resource_prefix_format,
|
|
938
|
-
)
|
|
1423
|
+
key = add_resource_prefix(template.key, mounted.prefix)
|
|
939
1424
|
template = template.model_copy(
|
|
940
1425
|
key=key,
|
|
941
1426
|
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
@@ -955,7 +1440,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
955
1440
|
|
|
956
1441
|
return list(all_templates.values())
|
|
957
1442
|
|
|
958
|
-
async def _list_prompts_mcp(self) -> list[
|
|
1443
|
+
async def _list_prompts_mcp(self) -> list[SDKPrompt]:
|
|
959
1444
|
"""
|
|
960
1445
|
List all available prompts, in the format expected by the low-level MCP
|
|
961
1446
|
server.
|
|
@@ -1025,9 +1510,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1025
1510
|
if not self._should_enable_component(prompt):
|
|
1026
1511
|
continue
|
|
1027
1512
|
|
|
1028
|
-
|
|
1513
|
+
# Apply prefix to prompt key
|
|
1029
1514
|
if mounted.prefix:
|
|
1030
1515
|
key = f"{mounted.prefix}_{prompt.key}"
|
|
1516
|
+
else:
|
|
1517
|
+
key = prompt.key
|
|
1518
|
+
|
|
1519
|
+
if key != prompt.key:
|
|
1031
1520
|
prompt = prompt.model_copy(key=key)
|
|
1032
1521
|
# Later mounted servers override earlier ones
|
|
1033
1522
|
all_prompts[key] = prompt
|
|
@@ -1054,7 +1543,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1054
1543
|
"""
|
|
1055
1544
|
Handle MCP 'callTool' requests.
|
|
1056
1545
|
|
|
1057
|
-
|
|
1546
|
+
Detects SEP-1686 task metadata and routes to background execution if supported.
|
|
1058
1547
|
|
|
1059
1548
|
Args:
|
|
1060
1549
|
key: The name of the tool to call
|
|
@@ -1069,6 +1558,65 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1069
1558
|
|
|
1070
1559
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1071
1560
|
try:
|
|
1561
|
+
# Check for SEP-1686 task metadata via request context
|
|
1562
|
+
task_meta = None
|
|
1563
|
+
try:
|
|
1564
|
+
# Access task metadata from SDK's request context
|
|
1565
|
+
ctx = self._mcp_server.request_context
|
|
1566
|
+
if ctx.experimental.is_task:
|
|
1567
|
+
task_meta = ctx.experimental.task_metadata
|
|
1568
|
+
except (AttributeError, LookupError):
|
|
1569
|
+
# No request context available - proceed without task metadata
|
|
1570
|
+
pass
|
|
1571
|
+
|
|
1572
|
+
# Get tool from local manager, mounted servers, or proxy
|
|
1573
|
+
tool = await self._get_tool_with_task_config(key)
|
|
1574
|
+
if (
|
|
1575
|
+
tool
|
|
1576
|
+
and self._should_enable_component(tool)
|
|
1577
|
+
and hasattr(tool, "task_config")
|
|
1578
|
+
):
|
|
1579
|
+
task_mode = tool.task_config.mode # type: ignore[union-attr]
|
|
1580
|
+
|
|
1581
|
+
# Enforce mode="required" - must have task metadata
|
|
1582
|
+
if task_mode == "required" and not task_meta:
|
|
1583
|
+
raise McpError(
|
|
1584
|
+
ErrorData(
|
|
1585
|
+
code=METHOD_NOT_FOUND,
|
|
1586
|
+
message=f"Tool '{key}' requires task-augmented execution",
|
|
1587
|
+
)
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
# Route to background if task metadata present and mode allows
|
|
1591
|
+
if task_meta and task_mode != "forbidden":
|
|
1592
|
+
# For FunctionTool, use Docket for background execution
|
|
1593
|
+
if isinstance(tool, FunctionTool):
|
|
1594
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
1595
|
+
return await handle_tool_as_task(
|
|
1596
|
+
self, key, arguments, task_meta_dict
|
|
1597
|
+
)
|
|
1598
|
+
# For ProxyTool/mounted tools, proceed with normal execution
|
|
1599
|
+
# They will forward task metadata to their backend
|
|
1600
|
+
|
|
1601
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
1602
|
+
# Return error result with returned_immediately=True
|
|
1603
|
+
if task_meta and task_mode == "forbidden":
|
|
1604
|
+
return mcp.types.CallToolResult(
|
|
1605
|
+
content=[
|
|
1606
|
+
mcp.types.TextContent(
|
|
1607
|
+
type="text",
|
|
1608
|
+
text=f"Tool '{key}' does not support task-augmented execution",
|
|
1609
|
+
)
|
|
1610
|
+
],
|
|
1611
|
+
isError=True,
|
|
1612
|
+
_meta={
|
|
1613
|
+
"modelcontextprotocol.io/task": {
|
|
1614
|
+
"returned_immediately": True
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
# Synchronous execution (normal path)
|
|
1072
1620
|
result = await self._call_tool_middleware(key, arguments)
|
|
1073
1621
|
return result.to_mcp_result()
|
|
1074
1622
|
except DisabledError as e:
|
|
@@ -1108,14 +1656,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1108
1656
|
# Try mounted servers in reverse order (later wins)
|
|
1109
1657
|
for mounted in reversed(self._mounted_servers):
|
|
1110
1658
|
try_name = tool_name
|
|
1111
|
-
|
|
1659
|
+
|
|
1660
|
+
# First check if tool_name is an overridden name (reverse lookup)
|
|
1661
|
+
if mounted.tool_names:
|
|
1662
|
+
for orig_key, override_name in mounted.tool_names.items():
|
|
1663
|
+
if override_name == tool_name:
|
|
1664
|
+
try_name = orig_key
|
|
1665
|
+
break
|
|
1666
|
+
else:
|
|
1667
|
+
# Not an override, try standard prefix stripping
|
|
1668
|
+
if mounted.prefix:
|
|
1669
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1670
|
+
continue
|
|
1671
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1672
|
+
elif mounted.prefix:
|
|
1112
1673
|
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1113
1674
|
continue
|
|
1114
1675
|
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1115
1676
|
|
|
1116
1677
|
try:
|
|
1117
1678
|
# First, get the tool to check if parent's filter allows it
|
|
1118
|
-
|
|
1679
|
+
# Use get_tool() instead of _tool_manager.get_tool() to support
|
|
1680
|
+
# nested mounted servers (tools mounted more than 2 levels deep)
|
|
1681
|
+
tool = await mounted.server.get_tool(try_name)
|
|
1119
1682
|
if not self._should_enable_component(tool):
|
|
1120
1683
|
# Parent filter blocks this tool, continue searching
|
|
1121
1684
|
continue
|
|
@@ -1148,6 +1711,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1148
1711
|
|
|
1149
1712
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1150
1713
|
try:
|
|
1714
|
+
# Task routing handled by custom handler
|
|
1151
1715
|
return list[ReadResourceContents](
|
|
1152
1716
|
await self._read_resource_middleware(uri)
|
|
1153
1717
|
)
|
|
@@ -1195,20 +1759,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1195
1759
|
for mounted in reversed(self._mounted_servers):
|
|
1196
1760
|
key = uri_str
|
|
1197
1761
|
if mounted.prefix:
|
|
1198
|
-
if not has_resource_prefix(
|
|
1199
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
1200
|
-
):
|
|
1762
|
+
if not has_resource_prefix(key, mounted.prefix):
|
|
1201
1763
|
continue
|
|
1202
|
-
key = remove_resource_prefix(
|
|
1203
|
-
key, mounted.prefix, mounted.resource_prefix_format
|
|
1204
|
-
)
|
|
1764
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
1205
1765
|
|
|
1766
|
+
# First, get the resource/template to check if parent's filter allows it
|
|
1767
|
+
# Use get_resource_or_template to support nested mounted servers
|
|
1768
|
+
# (resources/templates mounted more than 2 levels deep)
|
|
1769
|
+
resource = await mounted.server._get_resource_or_template_or_none(key)
|
|
1770
|
+
if resource is None:
|
|
1771
|
+
continue
|
|
1772
|
+
if not self._should_enable_component(resource):
|
|
1773
|
+
# Parent filter blocks this resource, continue searching
|
|
1774
|
+
continue
|
|
1206
1775
|
try:
|
|
1207
|
-
# First, get the resource to check if parent's filter allows it
|
|
1208
|
-
resource = await mounted.server._resource_manager.get_resource(key)
|
|
1209
|
-
if not self._should_enable_component(resource):
|
|
1210
|
-
# Parent filter blocks this resource, continue searching
|
|
1211
|
-
continue
|
|
1212
1776
|
result = list(await mounted.server._read_resource_middleware(key))
|
|
1213
1777
|
return result
|
|
1214
1778
|
except NotFoundError:
|
|
@@ -1246,6 +1810,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1246
1810
|
|
|
1247
1811
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
1248
1812
|
try:
|
|
1813
|
+
# Task routing handled by custom handler
|
|
1249
1814
|
return await self._get_prompt_middleware(name, arguments)
|
|
1250
1815
|
except DisabledError as e:
|
|
1251
1816
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
@@ -1288,7 +1853,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1288
1853
|
|
|
1289
1854
|
try:
|
|
1290
1855
|
# First, get the prompt to check if parent's filter allows it
|
|
1291
|
-
|
|
1856
|
+
# Use get_prompt() instead of _prompt_manager.get_prompt() to support
|
|
1857
|
+
# nested mounted servers (prompts mounted more than 2 levels deep)
|
|
1858
|
+
prompt = await mounted.server.get_prompt(try_name)
|
|
1292
1859
|
if not self._should_enable_component(prompt):
|
|
1293
1860
|
# Parent filter blocks this prompt, continue searching
|
|
1294
1861
|
continue
|
|
@@ -1380,6 +1947,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1380
1947
|
exclude_args: list[str] | None = None,
|
|
1381
1948
|
meta: dict[str, Any] | None = None,
|
|
1382
1949
|
enabled: bool | None = None,
|
|
1950
|
+
task: bool | TaskConfig | None = None,
|
|
1383
1951
|
) -> FunctionTool: ...
|
|
1384
1952
|
|
|
1385
1953
|
@overload
|
|
@@ -1397,6 +1965,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1397
1965
|
exclude_args: list[str] | None = None,
|
|
1398
1966
|
meta: dict[str, Any] | None = None,
|
|
1399
1967
|
enabled: bool | None = None,
|
|
1968
|
+
task: bool | TaskConfig | None = None,
|
|
1400
1969
|
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
1401
1970
|
|
|
1402
1971
|
def tool(
|
|
@@ -1413,6 +1982,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1413
1982
|
exclude_args: list[str] | None = None,
|
|
1414
1983
|
meta: dict[str, Any] | None = None,
|
|
1415
1984
|
enabled: bool | None = None,
|
|
1985
|
+
task: bool | TaskConfig | None = None,
|
|
1416
1986
|
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
|
1417
1987
|
"""Decorator to register a tool.
|
|
1418
1988
|
|
|
@@ -1435,8 +2005,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1435
2005
|
output_schema: Optional JSON schema for the tool's output
|
|
1436
2006
|
annotations: Optional annotations about the tool's behavior
|
|
1437
2007
|
exclude_args: Optional list of argument names to exclude from the tool schema.
|
|
1438
|
-
|
|
1439
|
-
injection with `Depends()` for better lifecycle management.
|
|
2008
|
+
Deprecated: Use `Depends()` for dependency injection instead.
|
|
1440
2009
|
meta: Optional meta information about the tool
|
|
1441
2010
|
enabled: Optional boolean to enable or disable the tool
|
|
1442
2011
|
|
|
@@ -1486,6 +2055,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1486
2055
|
fn = name_or_fn
|
|
1487
2056
|
tool_name = name # Use keyword name if provided, otherwise None
|
|
1488
2057
|
|
|
2058
|
+
# Resolve task parameter
|
|
2059
|
+
supports_task: bool | TaskConfig = (
|
|
2060
|
+
task if task is not None else self._support_tasks_by_default
|
|
2061
|
+
)
|
|
2062
|
+
|
|
1489
2063
|
# Register the tool immediately and return the tool object
|
|
1490
2064
|
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
1491
2065
|
tool = Tool.from_function(
|
|
@@ -1501,6 +2075,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1501
2075
|
meta=meta,
|
|
1502
2076
|
serializer=self._tool_serializer,
|
|
1503
2077
|
enabled=enabled,
|
|
2078
|
+
task=supports_task,
|
|
1504
2079
|
)
|
|
1505
2080
|
self.add_tool(tool)
|
|
1506
2081
|
return tool
|
|
@@ -1534,6 +2109,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1534
2109
|
exclude_args=exclude_args,
|
|
1535
2110
|
meta=meta,
|
|
1536
2111
|
enabled=enabled,
|
|
2112
|
+
task=task,
|
|
1537
2113
|
)
|
|
1538
2114
|
|
|
1539
2115
|
def add_resource(self, resource: Resource) -> Resource:
|
|
@@ -1580,44 +2156,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1580
2156
|
|
|
1581
2157
|
return template
|
|
1582
2158
|
|
|
1583
|
-
def add_resource_fn(
|
|
1584
|
-
self,
|
|
1585
|
-
fn: AnyFunction,
|
|
1586
|
-
uri: str,
|
|
1587
|
-
name: str | None = None,
|
|
1588
|
-
description: str | None = None,
|
|
1589
|
-
mime_type: str | None = None,
|
|
1590
|
-
tags: set[str] | None = None,
|
|
1591
|
-
) -> None:
|
|
1592
|
-
"""Add a resource or template to the server from a function.
|
|
1593
|
-
|
|
1594
|
-
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
1595
|
-
has parameters, it will be registered as a template resource.
|
|
1596
|
-
|
|
1597
|
-
Args:
|
|
1598
|
-
fn: The function to register as a resource
|
|
1599
|
-
uri: The URI for the resource
|
|
1600
|
-
name: Optional name for the resource
|
|
1601
|
-
description: Optional description of the resource
|
|
1602
|
-
mime_type: Optional MIME type for the resource
|
|
1603
|
-
tags: Optional set of tags for categorizing the resource
|
|
1604
|
-
"""
|
|
1605
|
-
# deprecated since 2.7.0
|
|
1606
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1607
|
-
warnings.warn(
|
|
1608
|
-
"The add_resource_fn method is deprecated. Use the resource decorator instead.",
|
|
1609
|
-
DeprecationWarning,
|
|
1610
|
-
stacklevel=2,
|
|
1611
|
-
)
|
|
1612
|
-
self._resource_manager.add_resource_or_template_from_fn(
|
|
1613
|
-
fn=fn,
|
|
1614
|
-
uri=uri,
|
|
1615
|
-
name=name,
|
|
1616
|
-
description=description,
|
|
1617
|
-
mime_type=mime_type,
|
|
1618
|
-
tags=tags,
|
|
1619
|
-
)
|
|
1620
|
-
|
|
1621
2159
|
def resource(
|
|
1622
2160
|
self,
|
|
1623
2161
|
uri: str,
|
|
@@ -1631,6 +2169,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1631
2169
|
enabled: bool | None = None,
|
|
1632
2170
|
annotations: Annotations | dict[str, Any] | None = None,
|
|
1633
2171
|
meta: dict[str, Any] | None = None,
|
|
2172
|
+
task: bool | TaskConfig | None = None,
|
|
1634
2173
|
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
|
1635
2174
|
"""Decorator to register a function as a resource.
|
|
1636
2175
|
|
|
@@ -1695,8 +2234,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1695
2234
|
)
|
|
1696
2235
|
|
|
1697
2236
|
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
|
|
1698
|
-
from fastmcp.server.context import Context
|
|
1699
|
-
|
|
1700
2237
|
if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
|
|
1701
2238
|
raise ValueError(
|
|
1702
2239
|
inspect.cleandoc(
|
|
@@ -1709,14 +2246,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1709
2246
|
)
|
|
1710
2247
|
)
|
|
1711
2248
|
|
|
2249
|
+
# Resolve task parameter
|
|
2250
|
+
supports_task: bool | TaskConfig = (
|
|
2251
|
+
task if task is not None else self._support_tasks_by_default
|
|
2252
|
+
)
|
|
2253
|
+
|
|
1712
2254
|
# Check if this should be a template
|
|
1713
2255
|
has_uri_params = "{" in uri and "}" in uri
|
|
1714
|
-
#
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
)
|
|
2256
|
+
# Use wrapper to check for user-facing parameters
|
|
2257
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
2258
|
+
|
|
2259
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
2260
|
+
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
|
|
1720
2261
|
|
|
1721
2262
|
if has_uri_params or has_func_params:
|
|
1722
2263
|
template = ResourceTemplate.from_function(
|
|
@@ -1731,6 +2272,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1731
2272
|
enabled=enabled,
|
|
1732
2273
|
annotations=annotations,
|
|
1733
2274
|
meta=meta,
|
|
2275
|
+
task=supports_task,
|
|
1734
2276
|
)
|
|
1735
2277
|
self.add_template(template)
|
|
1736
2278
|
return template
|
|
@@ -1747,6 +2289,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1747
2289
|
enabled=enabled,
|
|
1748
2290
|
annotations=annotations,
|
|
1749
2291
|
meta=meta,
|
|
2292
|
+
task=supports_task,
|
|
1750
2293
|
)
|
|
1751
2294
|
self.add_resource(resource)
|
|
1752
2295
|
return resource
|
|
@@ -1792,6 +2335,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1792
2335
|
tags: set[str] | None = None,
|
|
1793
2336
|
enabled: bool | None = None,
|
|
1794
2337
|
meta: dict[str, Any] | None = None,
|
|
2338
|
+
task: bool | TaskConfig | None = None,
|
|
1795
2339
|
) -> FunctionPrompt: ...
|
|
1796
2340
|
|
|
1797
2341
|
@overload
|
|
@@ -1806,6 +2350,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1806
2350
|
tags: set[str] | None = None,
|
|
1807
2351
|
enabled: bool | None = None,
|
|
1808
2352
|
meta: dict[str, Any] | None = None,
|
|
2353
|
+
task: bool | TaskConfig | None = None,
|
|
1809
2354
|
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
|
1810
2355
|
|
|
1811
2356
|
def prompt(
|
|
@@ -1819,6 +2364,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1819
2364
|
tags: set[str] | None = None,
|
|
1820
2365
|
enabled: bool | None = None,
|
|
1821
2366
|
meta: dict[str, Any] | None = None,
|
|
2367
|
+
task: bool | TaskConfig | None = None,
|
|
1822
2368
|
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
|
1823
2369
|
"""Decorator to register a prompt.
|
|
1824
2370
|
|
|
@@ -1909,6 +2455,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1909
2455
|
fn = name_or_fn
|
|
1910
2456
|
prompt_name = name # Use keyword name if provided, otherwise None
|
|
1911
2457
|
|
|
2458
|
+
# Resolve task parameter
|
|
2459
|
+
supports_task: bool | TaskConfig = (
|
|
2460
|
+
task if task is not None else self._support_tasks_by_default
|
|
2461
|
+
)
|
|
2462
|
+
|
|
1912
2463
|
# Register the prompt immediately
|
|
1913
2464
|
prompt = Prompt.from_function(
|
|
1914
2465
|
fn=fn,
|
|
@@ -1919,6 +2470,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1919
2470
|
tags=tags,
|
|
1920
2471
|
enabled=enabled,
|
|
1921
2472
|
meta=meta,
|
|
2473
|
+
task=supports_task,
|
|
1922
2474
|
)
|
|
1923
2475
|
self.add_prompt(prompt)
|
|
1924
2476
|
|
|
@@ -1950,6 +2502,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1950
2502
|
tags=tags,
|
|
1951
2503
|
enabled=enabled,
|
|
1952
2504
|
meta=meta,
|
|
2505
|
+
task=task,
|
|
1953
2506
|
)
|
|
1954
2507
|
|
|
1955
2508
|
async def run_stdio_async(
|
|
@@ -1974,11 +2527,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1974
2527
|
logger.info(
|
|
1975
2528
|
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
1976
2529
|
)
|
|
2530
|
+
|
|
2531
|
+
# Build experimental capabilities
|
|
2532
|
+
experimental_capabilities = get_task_capabilities()
|
|
2533
|
+
|
|
1977
2534
|
await self._mcp_server.run(
|
|
1978
2535
|
read_stream,
|
|
1979
2536
|
write_stream,
|
|
1980
2537
|
self._mcp_server.create_initialization_options(
|
|
1981
|
-
NotificationOptions(
|
|
2538
|
+
notification_options=NotificationOptions(
|
|
2539
|
+
tools_changed=True
|
|
2540
|
+
),
|
|
2541
|
+
experimental_capabilities=experimental_capabilities,
|
|
1982
2542
|
),
|
|
1983
2543
|
)
|
|
1984
2544
|
|
|
@@ -2061,86 +2621,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2061
2621
|
|
|
2062
2622
|
await server.serve()
|
|
2063
2623
|
|
|
2064
|
-
async def run_sse_async(
|
|
2065
|
-
self,
|
|
2066
|
-
host: str | None = None,
|
|
2067
|
-
port: int | None = None,
|
|
2068
|
-
log_level: str | None = None,
|
|
2069
|
-
path: str | None = None,
|
|
2070
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
2071
|
-
) -> None:
|
|
2072
|
-
"""Run the server using SSE transport."""
|
|
2073
|
-
|
|
2074
|
-
# Deprecated since 2.3.2
|
|
2075
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2076
|
-
warnings.warn(
|
|
2077
|
-
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
2078
|
-
"modern (non-SSE) alternative, or create an SSE app with "
|
|
2079
|
-
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
2080
|
-
DeprecationWarning,
|
|
2081
|
-
stacklevel=2,
|
|
2082
|
-
)
|
|
2083
|
-
await self.run_http_async(
|
|
2084
|
-
transport="sse",
|
|
2085
|
-
host=host,
|
|
2086
|
-
port=port,
|
|
2087
|
-
log_level=log_level,
|
|
2088
|
-
path=path,
|
|
2089
|
-
uvicorn_config=uvicorn_config,
|
|
2090
|
-
)
|
|
2091
|
-
|
|
2092
|
-
def sse_app(
|
|
2093
|
-
self,
|
|
2094
|
-
path: str | None = None,
|
|
2095
|
-
message_path: str | None = None,
|
|
2096
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2097
|
-
) -> StarletteWithLifespan:
|
|
2098
|
-
"""
|
|
2099
|
-
Create a Starlette app for the SSE server.
|
|
2100
|
-
|
|
2101
|
-
Args:
|
|
2102
|
-
path: The path to the SSE endpoint
|
|
2103
|
-
message_path: The path to the message endpoint
|
|
2104
|
-
middleware: A list of middleware to apply to the app
|
|
2105
|
-
"""
|
|
2106
|
-
# Deprecated since 2.3.2
|
|
2107
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2108
|
-
warnings.warn(
|
|
2109
|
-
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
2110
|
-
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
2111
|
-
DeprecationWarning,
|
|
2112
|
-
stacklevel=2,
|
|
2113
|
-
)
|
|
2114
|
-
return create_sse_app(
|
|
2115
|
-
server=self,
|
|
2116
|
-
message_path=message_path or self._deprecated_settings.message_path,
|
|
2117
|
-
sse_path=path or self._deprecated_settings.sse_path,
|
|
2118
|
-
auth=self.auth,
|
|
2119
|
-
debug=self._deprecated_settings.debug,
|
|
2120
|
-
middleware=middleware,
|
|
2121
|
-
)
|
|
2122
|
-
|
|
2123
|
-
def streamable_http_app(
|
|
2124
|
-
self,
|
|
2125
|
-
path: str | None = None,
|
|
2126
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
2127
|
-
) -> StarletteWithLifespan:
|
|
2128
|
-
"""
|
|
2129
|
-
Create a Starlette app for the StreamableHTTP server.
|
|
2130
|
-
|
|
2131
|
-
Args:
|
|
2132
|
-
path: The path to the StreamableHTTP endpoint
|
|
2133
|
-
middleware: A list of middleware to apply to the app
|
|
2134
|
-
"""
|
|
2135
|
-
# Deprecated since 2.3.2
|
|
2136
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2137
|
-
warnings.warn(
|
|
2138
|
-
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
2139
|
-
DeprecationWarning,
|
|
2140
|
-
stacklevel=2,
|
|
2141
|
-
)
|
|
2142
|
-
return self.http_app(path=path, middleware=middleware)
|
|
2143
|
-
|
|
2144
2624
|
def http_app(
|
|
2145
2625
|
self,
|
|
2146
2626
|
path: str | None = None,
|
|
@@ -2148,13 +2628,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2148
2628
|
json_response: bool | None = None,
|
|
2149
2629
|
stateless_http: bool | None = None,
|
|
2150
2630
|
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
2631
|
+
event_store: EventStore | None = None,
|
|
2632
|
+
retry_interval: int | None = None,
|
|
2151
2633
|
) -> StarletteWithLifespan:
|
|
2152
2634
|
"""Create a Starlette app using the specified HTTP transport.
|
|
2153
2635
|
|
|
2154
2636
|
Args:
|
|
2155
2637
|
path: The path for the HTTP endpoint
|
|
2156
2638
|
middleware: A list of middleware to apply to the app
|
|
2157
|
-
|
|
2639
|
+
json_response: Whether to use JSON response format
|
|
2640
|
+
stateless_http: Whether to use stateless mode (new transport per request)
|
|
2641
|
+
transport: Transport protocol to use - "http", "streamable-http", or "sse"
|
|
2642
|
+
event_store: Optional event store for SSE polling/resumability. When set,
|
|
2643
|
+
enables clients to reconnect and resume receiving events after
|
|
2644
|
+
server-initiated disconnections. Only used with streamable-http transport.
|
|
2645
|
+
retry_interval: Optional retry interval in milliseconds for SSE polling.
|
|
2646
|
+
Controls how quickly clients should reconnect after server-initiated
|
|
2647
|
+
disconnections. Requires event_store to be set. Only used with
|
|
2648
|
+
streamable-http transport.
|
|
2158
2649
|
|
|
2159
2650
|
Returns:
|
|
2160
2651
|
A Starlette application configured with the specified transport
|
|
@@ -2165,7 +2656,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2165
2656
|
server=self,
|
|
2166
2657
|
streamable_http_path=path
|
|
2167
2658
|
or self._deprecated_settings.streamable_http_path,
|
|
2168
|
-
event_store=
|
|
2659
|
+
event_store=event_store,
|
|
2660
|
+
retry_interval=retry_interval,
|
|
2169
2661
|
auth=self.auth,
|
|
2170
2662
|
json_response=(
|
|
2171
2663
|
json_response
|
|
@@ -2190,40 +2682,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2190
2682
|
middleware=middleware,
|
|
2191
2683
|
)
|
|
2192
2684
|
|
|
2193
|
-
async def run_streamable_http_async(
|
|
2194
|
-
self,
|
|
2195
|
-
host: str | None = None,
|
|
2196
|
-
port: int | None = None,
|
|
2197
|
-
log_level: str | None = None,
|
|
2198
|
-
path: str | None = None,
|
|
2199
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
2200
|
-
) -> None:
|
|
2201
|
-
# Deprecated since 2.3.2
|
|
2202
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2203
|
-
warnings.warn(
|
|
2204
|
-
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
2205
|
-
"Use run_http_async instead.",
|
|
2206
|
-
DeprecationWarning,
|
|
2207
|
-
stacklevel=2,
|
|
2208
|
-
)
|
|
2209
|
-
await self.run_http_async(
|
|
2210
|
-
transport="http",
|
|
2211
|
-
host=host,
|
|
2212
|
-
port=port,
|
|
2213
|
-
log_level=log_level,
|
|
2214
|
-
path=path,
|
|
2215
|
-
uvicorn_config=uvicorn_config,
|
|
2216
|
-
)
|
|
2217
|
-
|
|
2218
2685
|
def mount(
|
|
2219
2686
|
self,
|
|
2220
2687
|
server: FastMCP[LifespanResultT],
|
|
2221
2688
|
prefix: str | None = None,
|
|
2222
2689
|
as_proxy: bool | None = None,
|
|
2223
|
-
|
|
2224
|
-
tool_separator: str | None = None,
|
|
2225
|
-
resource_separator: str | None = None,
|
|
2226
|
-
prompt_separator: str | None = None,
|
|
2690
|
+
tool_names: dict[str, str] | None = None,
|
|
2227
2691
|
) -> None:
|
|
2228
2692
|
"""Mount another FastMCP server on this server with an optional prefix.
|
|
2229
2693
|
|
|
@@ -2268,56 +2732,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2268
2732
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
2269
2733
|
automatically determined based on whether the server has a custom lifespan
|
|
2270
2734
|
(True if it has a custom lifespan, False otherwise).
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2735
|
+
tool_names: Optional mapping of original tool names to custom names. Use this
|
|
2736
|
+
to override prefixed names. Keys are the original tool names from the
|
|
2737
|
+
mounted server.
|
|
2274
2738
|
"""
|
|
2275
2739
|
from fastmcp.server.proxy import FastMCPProxy
|
|
2276
2740
|
|
|
2277
|
-
# Deprecated since 2.9.0
|
|
2278
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
2279
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
2280
|
-
# is optional.
|
|
2281
|
-
if isinstance(server, str):
|
|
2282
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2283
|
-
warnings.warn(
|
|
2284
|
-
"Mount prefixes are now optional and the first positional argument "
|
|
2285
|
-
"should be the server you want to mount.",
|
|
2286
|
-
DeprecationWarning,
|
|
2287
|
-
stacklevel=2,
|
|
2288
|
-
)
|
|
2289
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
2290
|
-
|
|
2291
|
-
if tool_separator is not None:
|
|
2292
|
-
# Deprecated since 2.4.0
|
|
2293
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2294
|
-
warnings.warn(
|
|
2295
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
2296
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
2297
|
-
DeprecationWarning,
|
|
2298
|
-
stacklevel=2,
|
|
2299
|
-
)
|
|
2300
|
-
|
|
2301
|
-
if resource_separator is not None:
|
|
2302
|
-
# Deprecated since 2.4.0
|
|
2303
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2304
|
-
warnings.warn(
|
|
2305
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
2306
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
2307
|
-
DeprecationWarning,
|
|
2308
|
-
stacklevel=2,
|
|
2309
|
-
)
|
|
2310
|
-
|
|
2311
|
-
if prompt_separator is not None:
|
|
2312
|
-
# Deprecated since 2.4.0
|
|
2313
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2314
|
-
warnings.warn(
|
|
2315
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
2316
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
2317
|
-
DeprecationWarning,
|
|
2318
|
-
stacklevel=2,
|
|
2319
|
-
)
|
|
2320
|
-
|
|
2321
2741
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
2322
2742
|
# we should treat it as a proxy
|
|
2323
2743
|
if as_proxy is None:
|
|
@@ -2326,11 +2746,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2326
2746
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
2327
2747
|
server = FastMCP.as_proxy(server)
|
|
2328
2748
|
|
|
2749
|
+
# Mark the server as mounted so it skips creating its own Docket/Worker.
|
|
2750
|
+
# The parent's Docket handles task execution, avoiding race conditions
|
|
2751
|
+
# with multiple workers competing for tasks from the same queue.
|
|
2752
|
+
server._is_mounted = True
|
|
2753
|
+
|
|
2329
2754
|
# Delegate mounting to all three managers
|
|
2330
2755
|
mounted_server = MountedServer(
|
|
2331
2756
|
prefix=prefix,
|
|
2332
2757
|
server=server,
|
|
2333
|
-
|
|
2758
|
+
tool_names=tool_names,
|
|
2334
2759
|
)
|
|
2335
2760
|
self._mounted_servers.append(mounted_server)
|
|
2336
2761
|
|
|
@@ -2338,9 +2763,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2338
2763
|
self,
|
|
2339
2764
|
server: FastMCP[LifespanResultT],
|
|
2340
2765
|
prefix: str | None = None,
|
|
2341
|
-
tool_separator: str | None = None,
|
|
2342
|
-
resource_separator: str | None = None,
|
|
2343
|
-
prompt_separator: str | None = None,
|
|
2344
2766
|
) -> None:
|
|
2345
2767
|
"""
|
|
2346
2768
|
Import the MCP objects from another FastMCP server into this one,
|
|
@@ -2372,56 +2794,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2372
2794
|
server: The FastMCP server to import
|
|
2373
2795
|
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
2374
2796
|
objects are imported with their original names.
|
|
2375
|
-
|
|
2376
|
-
resource_separator: Deprecated and ignored. Prefix is now
|
|
2377
|
-
applied using the protocol://prefix/path format
|
|
2378
|
-
prompt_separator: Deprecated. Separator for prompt names.
|
|
2379
|
-
"""
|
|
2380
|
-
|
|
2381
|
-
# Deprecated since 2.9.0
|
|
2382
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
2383
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
2384
|
-
# is optional.
|
|
2385
|
-
if isinstance(server, str):
|
|
2386
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2387
|
-
warnings.warn(
|
|
2388
|
-
"Import prefixes are now optional and the first positional argument "
|
|
2389
|
-
"should be the server you want to import.",
|
|
2390
|
-
DeprecationWarning,
|
|
2391
|
-
stacklevel=2,
|
|
2392
|
-
)
|
|
2393
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
2394
|
-
|
|
2395
|
-
if tool_separator is not None:
|
|
2396
|
-
# Deprecated since 2.4.0
|
|
2397
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2398
|
-
warnings.warn(
|
|
2399
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
2400
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
2401
|
-
DeprecationWarning,
|
|
2402
|
-
stacklevel=2,
|
|
2403
|
-
)
|
|
2404
|
-
|
|
2405
|
-
if resource_separator is not None:
|
|
2406
|
-
# Deprecated since 2.4.0
|
|
2407
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2408
|
-
warnings.warn(
|
|
2409
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
2410
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
2411
|
-
DeprecationWarning,
|
|
2412
|
-
stacklevel=2,
|
|
2413
|
-
)
|
|
2414
|
-
|
|
2415
|
-
if prompt_separator is not None:
|
|
2416
|
-
# Deprecated since 2.4.0
|
|
2417
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2418
|
-
warnings.warn(
|
|
2419
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
2420
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
2421
|
-
DeprecationWarning,
|
|
2422
|
-
stacklevel=2,
|
|
2423
|
-
)
|
|
2424
|
-
|
|
2797
|
+
"""
|
|
2425
2798
|
# Import tools from the server
|
|
2426
2799
|
for key, tool in (await server.get_tools()).items():
|
|
2427
2800
|
if prefix:
|
|
@@ -2431,9 +2804,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2431
2804
|
# Import resources and templates from the server
|
|
2432
2805
|
for key, resource in (await server.get_resources()).items():
|
|
2433
2806
|
if prefix:
|
|
2434
|
-
resource_key = add_resource_prefix(
|
|
2435
|
-
key, prefix, self.resource_prefix_format
|
|
2436
|
-
)
|
|
2807
|
+
resource_key = add_resource_prefix(key, prefix)
|
|
2437
2808
|
resource = resource.model_copy(
|
|
2438
2809
|
update={"name": f"{prefix}_{resource.name}"}, key=resource_key
|
|
2439
2810
|
)
|
|
@@ -2441,9 +2812,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2441
2812
|
|
|
2442
2813
|
for key, template in (await server.get_resource_templates()).items():
|
|
2443
2814
|
if prefix:
|
|
2444
|
-
template_key = add_resource_prefix(
|
|
2445
|
-
key, prefix, self.resource_prefix_format
|
|
2446
|
-
)
|
|
2815
|
+
template_key = add_resource_prefix(key, prefix)
|
|
2447
2816
|
template = template.model_copy(
|
|
2448
2817
|
update={"name": f"{prefix}_{template.name}"}, key=template_key
|
|
2449
2818
|
)
|
|
@@ -2476,66 +2845,46 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2476
2845
|
cls,
|
|
2477
2846
|
openapi_spec: dict[str, Any],
|
|
2478
2847
|
client: httpx.AsyncClient,
|
|
2479
|
-
route_maps: list[RouteMap] |
|
|
2480
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
2481
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2848
|
+
route_maps: list[RouteMap] | None = None,
|
|
2849
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2850
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2482
2851
|
mcp_names: dict[str, str] | None = None,
|
|
2483
2852
|
tags: set[str] | None = None,
|
|
2484
2853
|
**settings: Any,
|
|
2485
|
-
) -> FastMCPOpenAPI
|
|
2854
|
+
) -> FastMCPOpenAPI:
|
|
2486
2855
|
"""
|
|
2487
2856
|
Create a FastMCP server from an OpenAPI specification.
|
|
2488
2857
|
"""
|
|
2858
|
+
from .openapi import FastMCPOpenAPI
|
|
2489
2859
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
mcp_names=mcp_names,
|
|
2501
|
-
tags=tags,
|
|
2502
|
-
**settings,
|
|
2503
|
-
)
|
|
2504
|
-
else:
|
|
2505
|
-
logger.info(
|
|
2506
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2507
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2508
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2509
|
-
)
|
|
2510
|
-
from .openapi import FastMCPOpenAPI
|
|
2511
|
-
|
|
2512
|
-
return FastMCPOpenAPI(
|
|
2513
|
-
openapi_spec=openapi_spec,
|
|
2514
|
-
client=client,
|
|
2515
|
-
route_maps=cast(Any, route_maps),
|
|
2516
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2517
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2518
|
-
mcp_names=mcp_names,
|
|
2519
|
-
tags=tags,
|
|
2520
|
-
**settings,
|
|
2521
|
-
)
|
|
2860
|
+
return FastMCPOpenAPI(
|
|
2861
|
+
openapi_spec=openapi_spec,
|
|
2862
|
+
client=client,
|
|
2863
|
+
route_maps=route_maps,
|
|
2864
|
+
route_map_fn=route_map_fn,
|
|
2865
|
+
mcp_component_fn=mcp_component_fn,
|
|
2866
|
+
mcp_names=mcp_names,
|
|
2867
|
+
tags=tags,
|
|
2868
|
+
**settings,
|
|
2869
|
+
)
|
|
2522
2870
|
|
|
2523
2871
|
@classmethod
|
|
2524
2872
|
def from_fastapi(
|
|
2525
2873
|
cls,
|
|
2526
2874
|
app: Any,
|
|
2527
2875
|
name: str | None = None,
|
|
2528
|
-
route_maps: list[RouteMap] |
|
|
2529
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
2530
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2876
|
+
route_maps: list[RouteMap] | None = None,
|
|
2877
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2878
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2531
2879
|
mcp_names: dict[str, str] | None = None,
|
|
2532
2880
|
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
2533
2881
|
tags: set[str] | None = None,
|
|
2534
2882
|
**settings: Any,
|
|
2535
|
-
) -> FastMCPOpenAPI
|
|
2883
|
+
) -> FastMCPOpenAPI:
|
|
2536
2884
|
"""
|
|
2537
2885
|
Create a FastMCP server from a FastAPI application.
|
|
2538
2886
|
"""
|
|
2887
|
+
from .openapi import FastMCPOpenAPI
|
|
2539
2888
|
|
|
2540
2889
|
if httpx_client_kwargs is None:
|
|
2541
2890
|
httpx_client_kwargs = {}
|
|
@@ -2548,40 +2897,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2548
2897
|
|
|
2549
2898
|
name = name or app.title
|
|
2550
2899
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
mcp_names=mcp_names,
|
|
2563
|
-
tags=tags,
|
|
2564
|
-
**settings,
|
|
2565
|
-
)
|
|
2566
|
-
else:
|
|
2567
|
-
logger.info(
|
|
2568
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2569
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2570
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2571
|
-
)
|
|
2572
|
-
from .openapi import FastMCPOpenAPI
|
|
2573
|
-
|
|
2574
|
-
return FastMCPOpenAPI(
|
|
2575
|
-
openapi_spec=app.openapi(),
|
|
2576
|
-
client=client,
|
|
2577
|
-
name=name,
|
|
2578
|
-
route_maps=cast(Any, route_maps),
|
|
2579
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2580
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2581
|
-
mcp_names=mcp_names,
|
|
2582
|
-
tags=tags,
|
|
2583
|
-
**settings,
|
|
2584
|
-
)
|
|
2900
|
+
return FastMCPOpenAPI(
|
|
2901
|
+
openapi_spec=app.openapi(),
|
|
2902
|
+
client=client,
|
|
2903
|
+
name=name,
|
|
2904
|
+
route_maps=route_maps,
|
|
2905
|
+
route_map_fn=route_map_fn,
|
|
2906
|
+
mcp_component_fn=mcp_component_fn,
|
|
2907
|
+
mcp_names=mcp_names,
|
|
2908
|
+
tags=tags,
|
|
2909
|
+
**settings,
|
|
2910
|
+
)
|
|
2585
2911
|
|
|
2586
2912
|
@classmethod
|
|
2587
2913
|
def as_proxy(
|
|
@@ -2643,23 +2969,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2643
2969
|
|
|
2644
2970
|
return FastMCPProxy(client_factory=client_factory, **settings)
|
|
2645
2971
|
|
|
2646
|
-
@classmethod
|
|
2647
|
-
def from_client(
|
|
2648
|
-
cls, client: Client[ClientTransportT], **settings: Any
|
|
2649
|
-
) -> FastMCPProxy:
|
|
2650
|
-
"""
|
|
2651
|
-
Create a FastMCP proxy server from a FastMCP client.
|
|
2652
|
-
"""
|
|
2653
|
-
# Deprecated since 2.3.5
|
|
2654
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2655
|
-
warnings.warn(
|
|
2656
|
-
"FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
|
|
2657
|
-
DeprecationWarning,
|
|
2658
|
-
stacklevel=2,
|
|
2659
|
-
)
|
|
2660
|
-
|
|
2661
|
-
return cls.as_proxy(client, **settings)
|
|
2662
|
-
|
|
2663
2972
|
def _should_enable_component(
|
|
2664
2973
|
self,
|
|
2665
2974
|
component: FastMCPComponent,
|
|
@@ -2706,13 +3015,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2706
3015
|
class MountedServer:
|
|
2707
3016
|
prefix: str | None
|
|
2708
3017
|
server: FastMCP[Any]
|
|
2709
|
-
|
|
3018
|
+
tool_names: dict[str, str] | None = None
|
|
2710
3019
|
|
|
2711
3020
|
|
|
2712
|
-
def add_resource_prefix(
|
|
2713
|
-
|
|
2714
|
-
) -> str:
|
|
2715
|
-
"""Add a prefix to a resource URI.
|
|
3021
|
+
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
3022
|
+
"""Add a prefix to a resource URI using path formatting (resource://prefix/path).
|
|
2716
3023
|
|
|
2717
3024
|
Args:
|
|
2718
3025
|
uri: The original resource URI
|
|
@@ -2722,16 +3029,10 @@ def add_resource_prefix(
|
|
|
2722
3029
|
The resource URI with the prefix added
|
|
2723
3030
|
|
|
2724
3031
|
Examples:
|
|
2725
|
-
With new style:
|
|
2726
3032
|
```python
|
|
2727
3033
|
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2728
3034
|
"resource://prefix/path/to/resource"
|
|
2729
3035
|
```
|
|
2730
|
-
With legacy style:
|
|
2731
|
-
```python
|
|
2732
|
-
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2733
|
-
"prefix+resource://path/to/resource"
|
|
2734
|
-
```
|
|
2735
3036
|
With absolute path:
|
|
2736
3037
|
```python
|
|
2737
3038
|
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
@@ -2744,54 +3045,32 @@ def add_resource_prefix(
|
|
|
2744
3045
|
if not prefix:
|
|
2745
3046
|
return uri
|
|
2746
3047
|
|
|
2747
|
-
#
|
|
2748
|
-
|
|
2749
|
-
if
|
|
2750
|
-
|
|
3048
|
+
# Split the URI into protocol and path
|
|
3049
|
+
match = URI_PATTERN.match(uri)
|
|
3050
|
+
if not match:
|
|
3051
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2751
3052
|
|
|
2752
|
-
|
|
2753
|
-
# Legacy style: prefix+protocol://path
|
|
2754
|
-
return f"{prefix}+{uri}"
|
|
2755
|
-
elif prefix_format == "path":
|
|
2756
|
-
# New style: protocol://prefix/path
|
|
2757
|
-
# Split the URI into protocol and path
|
|
2758
|
-
match = URI_PATTERN.match(uri)
|
|
2759
|
-
if not match:
|
|
2760
|
-
raise ValueError(
|
|
2761
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2762
|
-
)
|
|
2763
|
-
|
|
2764
|
-
protocol, path = match.groups()
|
|
3053
|
+
protocol, path = match.groups()
|
|
2765
3054
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
else:
|
|
2769
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3055
|
+
# Add the prefix to the path
|
|
3056
|
+
return f"{protocol}{prefix}/{path}"
|
|
2770
3057
|
|
|
2771
3058
|
|
|
2772
|
-
def remove_resource_prefix(
|
|
2773
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2774
|
-
) -> str:
|
|
3059
|
+
def remove_resource_prefix(uri: str, prefix: str) -> str:
|
|
2775
3060
|
"""Remove a prefix from a resource URI.
|
|
2776
3061
|
|
|
2777
3062
|
Args:
|
|
2778
3063
|
uri: The resource URI with a prefix
|
|
2779
3064
|
prefix: The prefix to remove
|
|
2780
|
-
|
|
3065
|
+
|
|
2781
3066
|
Returns:
|
|
2782
3067
|
The resource URI with the prefix removed
|
|
2783
3068
|
|
|
2784
3069
|
Examples:
|
|
2785
|
-
With new style:
|
|
2786
3070
|
```python
|
|
2787
3071
|
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2788
3072
|
"resource://path/to/resource"
|
|
2789
3073
|
```
|
|
2790
|
-
With legacy style:
|
|
2791
|
-
```python
|
|
2792
|
-
remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2793
|
-
"resource://path/to/resource"
|
|
2794
|
-
```
|
|
2795
3074
|
With absolute path:
|
|
2796
3075
|
```python
|
|
2797
3076
|
remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
@@ -2804,41 +3083,24 @@ def remove_resource_prefix(
|
|
|
2804
3083
|
if not prefix:
|
|
2805
3084
|
return uri
|
|
2806
3085
|
|
|
2807
|
-
|
|
2808
|
-
|
|
3086
|
+
# Split the URI into protocol and path
|
|
3087
|
+
match = URI_PATTERN.match(uri)
|
|
3088
|
+
if not match:
|
|
3089
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2809
3090
|
|
|
2810
|
-
|
|
2811
|
-
# Legacy style: prefix+protocol://path
|
|
2812
|
-
legacy_prefix = f"{prefix}+"
|
|
2813
|
-
if uri.startswith(legacy_prefix):
|
|
2814
|
-
return uri[len(legacy_prefix) :]
|
|
2815
|
-
return uri
|
|
2816
|
-
elif prefix_format == "path":
|
|
2817
|
-
# New style: protocol://prefix/path
|
|
2818
|
-
# Split the URI into protocol and path
|
|
2819
|
-
match = URI_PATTERN.match(uri)
|
|
2820
|
-
if not match:
|
|
2821
|
-
raise ValueError(
|
|
2822
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2823
|
-
)
|
|
3091
|
+
protocol, path = match.groups()
|
|
2824
3092
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
if not path_match:
|
|
2831
|
-
return uri
|
|
3093
|
+
# Check if the path starts with the prefix followed by a /
|
|
3094
|
+
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
3095
|
+
path_match = re.match(prefix_pattern, path)
|
|
3096
|
+
if not path_match:
|
|
3097
|
+
return uri
|
|
2832
3098
|
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
else:
|
|
2836
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3099
|
+
# Return the URI without the prefix
|
|
3100
|
+
return f"{protocol}{path_match.group(1)}"
|
|
2837
3101
|
|
|
2838
3102
|
|
|
2839
|
-
def has_resource_prefix(
|
|
2840
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2841
|
-
) -> bool:
|
|
3103
|
+
def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
2842
3104
|
"""Check if a resource URI has a specific prefix.
|
|
2843
3105
|
|
|
2844
3106
|
Args:
|
|
@@ -2849,16 +3111,10 @@ def has_resource_prefix(
|
|
|
2849
3111
|
True if the URI has the specified prefix, False otherwise
|
|
2850
3112
|
|
|
2851
3113
|
Examples:
|
|
2852
|
-
With new style:
|
|
2853
3114
|
```python
|
|
2854
3115
|
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2855
3116
|
True
|
|
2856
3117
|
```
|
|
2857
|
-
With legacy style:
|
|
2858
|
-
```python
|
|
2859
|
-
has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2860
|
-
True
|
|
2861
|
-
```
|
|
2862
3118
|
With other path:
|
|
2863
3119
|
```python
|
|
2864
3120
|
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
@@ -2871,28 +3127,13 @@ def has_resource_prefix(
|
|
|
2871
3127
|
if not prefix:
|
|
2872
3128
|
return False
|
|
2873
3129
|
|
|
2874
|
-
#
|
|
2875
|
-
|
|
2876
|
-
if
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
if prefix_format == "protocol":
|
|
2880
|
-
# Legacy style: prefix+protocol://path
|
|
2881
|
-
legacy_prefix = f"{prefix}+"
|
|
2882
|
-
return uri.startswith(legacy_prefix)
|
|
2883
|
-
elif prefix_format == "path":
|
|
2884
|
-
# New style: protocol://prefix/path
|
|
2885
|
-
# Split the URI into protocol and path
|
|
2886
|
-
match = URI_PATTERN.match(uri)
|
|
2887
|
-
if not match:
|
|
2888
|
-
raise ValueError(
|
|
2889
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2890
|
-
)
|
|
3130
|
+
# Split the URI into protocol and path
|
|
3131
|
+
match = URI_PATTERN.match(uri)
|
|
3132
|
+
if not match:
|
|
3133
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2891
3134
|
|
|
2892
|
-
|
|
3135
|
+
_, path = match.groups()
|
|
2893
3136
|
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
else:
|
|
2898
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3137
|
+
# Check if the path starts with the prefix followed by a /
|
|
3138
|
+
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
3139
|
+
return bool(re.match(prefix_pattern, path))
|