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/server/server.py
CHANGED
|
@@ -2,13 +2,26 @@
|
|
|
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
|
-
|
|
11
|
-
from
|
|
10
|
+
import weakref
|
|
11
|
+
from collections.abc import (
|
|
12
|
+
AsyncIterator,
|
|
13
|
+
Awaitable,
|
|
14
|
+
Callable,
|
|
15
|
+
Collection,
|
|
16
|
+
Mapping,
|
|
17
|
+
Sequence,
|
|
18
|
+
)
|
|
19
|
+
from contextlib import (
|
|
20
|
+
AbstractAsyncContextManager,
|
|
21
|
+
AsyncExitStack,
|
|
22
|
+
asynccontextmanager,
|
|
23
|
+
suppress,
|
|
24
|
+
)
|
|
12
25
|
from dataclasses import dataclass
|
|
13
26
|
from functools import partial
|
|
14
27
|
from pathlib import Path
|
|
@@ -18,14 +31,18 @@ import anyio
|
|
|
18
31
|
import httpx
|
|
19
32
|
import mcp.types
|
|
20
33
|
import uvicorn
|
|
34
|
+
from docket import Docket, Worker
|
|
21
35
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
22
36
|
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
23
37
|
from mcp.server.stdio import stdio_server
|
|
38
|
+
from mcp.shared.exceptions import McpError
|
|
24
39
|
from mcp.types import (
|
|
40
|
+
METHOD_NOT_FOUND,
|
|
25
41
|
Annotations,
|
|
26
42
|
AnyFunction,
|
|
27
43
|
CallToolRequestParams,
|
|
28
44
|
ContentBlock,
|
|
45
|
+
ErrorData,
|
|
29
46
|
GetPromptResult,
|
|
30
47
|
ToolAnnotations,
|
|
31
48
|
)
|
|
@@ -43,11 +60,14 @@ import fastmcp
|
|
|
43
60
|
import fastmcp.server
|
|
44
61
|
from fastmcp.exceptions import DisabledError, NotFoundError
|
|
45
62
|
from fastmcp.mcp_config import MCPConfig
|
|
46
|
-
from fastmcp.prompts import Prompt
|
|
63
|
+
from fastmcp.prompts import Prompt
|
|
47
64
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
48
|
-
from fastmcp.
|
|
49
|
-
from fastmcp.resources.
|
|
65
|
+
from fastmcp.prompts.prompt_manager import PromptManager
|
|
66
|
+
from fastmcp.resources.resource import FunctionResource, Resource
|
|
67
|
+
from fastmcp.resources.resource_manager import ResourceManager
|
|
68
|
+
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
|
|
50
69
|
from fastmcp.server.auth import AuthProvider
|
|
70
|
+
from fastmcp.server.event_store import EventStore
|
|
51
71
|
from fastmcp.server.http import (
|
|
52
72
|
StarletteWithLifespan,
|
|
53
73
|
create_sse_app,
|
|
@@ -55,9 +75,16 @@ from fastmcp.server.http import (
|
|
|
55
75
|
)
|
|
56
76
|
from fastmcp.server.low_level import LowLevelServer
|
|
57
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
|
+
)
|
|
58
85
|
from fastmcp.settings import Settings
|
|
59
|
-
from fastmcp.tools import ToolManager
|
|
60
86
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
87
|
+
from fastmcp.tools.tool_manager import ToolManager
|
|
61
88
|
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
62
89
|
from fastmcp.utilities.cli import log_server_banner
|
|
63
90
|
from fastmcp.utilities.components import FastMCPComponent
|
|
@@ -66,29 +93,45 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
66
93
|
|
|
67
94
|
if TYPE_CHECKING:
|
|
68
95
|
from fastmcp.client import Client
|
|
69
|
-
from fastmcp.client.
|
|
96
|
+
from fastmcp.client.client import FastMCP1Server
|
|
70
97
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
71
|
-
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
72
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
73
|
-
ComponentFn as OpenAPIComponentFnNew,
|
|
74
|
-
)
|
|
75
|
-
from fastmcp.experimental.server.openapi.routing import RouteMap as RouteMapNew
|
|
76
|
-
from fastmcp.experimental.server.openapi.routing import (
|
|
77
|
-
RouteMapFn as OpenAPIRouteMapFnNew,
|
|
78
|
-
)
|
|
79
98
|
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
80
99
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
81
100
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
82
101
|
from fastmcp.server.proxy import FastMCPProxy
|
|
102
|
+
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
103
|
+
from fastmcp.tools.tool import ToolResultSerializerType
|
|
83
104
|
|
|
84
105
|
logger = get_logger(__name__)
|
|
85
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
|
+
|
|
86
125
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
87
126
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
88
127
|
|
|
89
128
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
90
129
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
91
130
|
|
|
131
|
+
LifespanCallable = Callable[
|
|
132
|
+
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
133
|
+
]
|
|
134
|
+
|
|
92
135
|
|
|
93
136
|
@asynccontextmanager
|
|
94
137
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -98,26 +141,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
|
|
|
98
141
|
server: The server instance this lifespan is managing
|
|
99
142
|
|
|
100
143
|
Returns:
|
|
101
|
-
An empty
|
|
144
|
+
An empty dictionary as the lifespan result.
|
|
102
145
|
"""
|
|
103
146
|
yield {}
|
|
104
147
|
|
|
105
148
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
lifespan: Callable[
|
|
109
|
-
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
110
|
-
],
|
|
149
|
+
def _lifespan_proxy(
|
|
150
|
+
fastmcp_server: FastMCP[LifespanResultT],
|
|
111
151
|
) -> Callable[
|
|
112
152
|
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
113
153
|
]:
|
|
114
154
|
@asynccontextmanager
|
|
115
155
|
async def wrap(
|
|
116
|
-
|
|
156
|
+
low_level_server: LowLevelServer[LifespanResultT],
|
|
117
157
|
) -> AsyncIterator[LifespanResultT]:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
if fastmcp_server._lifespan is default_lifespan:
|
|
159
|
+
yield {}
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if not fastmcp_server._lifespan_result_set:
|
|
163
|
+
raise RuntimeError(
|
|
164
|
+
"FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
|
|
165
|
+
+ " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
yield fastmcp_server._lifespan_result
|
|
121
169
|
|
|
122
170
|
return wrap
|
|
123
171
|
|
|
@@ -129,27 +177,23 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
129
177
|
instructions: str | None = None,
|
|
130
178
|
*,
|
|
131
179
|
version: str | None = None,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
AbstractAsyncContextManager[LifespanResultT],
|
|
138
|
-
]
|
|
139
|
-
| None
|
|
140
|
-
) = None,
|
|
141
|
-
dependencies: list[str] | None = None,
|
|
142
|
-
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
180
|
+
website_url: str | None = None,
|
|
181
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
182
|
+
auth: AuthProvider | NotSetT | None = NotSet,
|
|
183
|
+
middleware: Sequence[Middleware] | None = None,
|
|
184
|
+
lifespan: LifespanCallable | None = None,
|
|
143
185
|
mask_error_details: bool | None = None,
|
|
144
|
-
tools:
|
|
145
|
-
tool_transformations:
|
|
146
|
-
tool_serializer:
|
|
147
|
-
include_tags:
|
|
148
|
-
exclude_tags:
|
|
186
|
+
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
187
|
+
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
188
|
+
tool_serializer: ToolResultSerializerType | None = None,
|
|
189
|
+
include_tags: Collection[str] | None = None,
|
|
190
|
+
exclude_tags: Collection[str] | None = None,
|
|
149
191
|
include_fastmcp_meta: bool | None = None,
|
|
150
192
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
151
193
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
152
194
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
195
|
+
strict_input_validation: bool | None = None,
|
|
196
|
+
tasks: bool | None = None,
|
|
153
197
|
# ---
|
|
154
198
|
# ---
|
|
155
199
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -167,38 +211,48 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
167
211
|
sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
|
|
168
212
|
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
169
213
|
):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
173
219
|
|
|
174
220
|
self._additional_http_routes: list[BaseRoute] = []
|
|
175
221
|
self._mounted_servers: list[MountedServer] = []
|
|
176
|
-
self.
|
|
222
|
+
self._is_mounted: bool = (
|
|
223
|
+
False # Set to True when this server is mounted on another
|
|
224
|
+
)
|
|
225
|
+
self._tool_manager: ToolManager = ToolManager(
|
|
177
226
|
duplicate_behavior=on_duplicate_tools,
|
|
178
227
|
mask_error_details=mask_error_details,
|
|
179
228
|
transformations=tool_transformations,
|
|
180
229
|
)
|
|
181
|
-
self._resource_manager = ResourceManager(
|
|
230
|
+
self._resource_manager: ResourceManager = ResourceManager(
|
|
182
231
|
duplicate_behavior=on_duplicate_resources,
|
|
183
232
|
mask_error_details=mask_error_details,
|
|
184
233
|
)
|
|
185
|
-
self._prompt_manager = PromptManager(
|
|
234
|
+
self._prompt_manager: PromptManager = PromptManager(
|
|
186
235
|
duplicate_behavior=on_duplicate_prompts,
|
|
187
236
|
mask_error_details=mask_error_details,
|
|
188
237
|
)
|
|
189
|
-
self._tool_serializer = tool_serializer
|
|
238
|
+
self._tool_serializer: Callable[[Any], str] | None = tool_serializer
|
|
239
|
+
|
|
240
|
+
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
241
|
+
self._lifespan_result: LifespanResultT | None = None
|
|
242
|
+
self._lifespan_result_set: bool = False
|
|
243
|
+
self._started: asyncio.Event = asyncio.Event()
|
|
190
244
|
|
|
191
|
-
if lifespan is None:
|
|
192
|
-
self._has_lifespan = False
|
|
193
|
-
lifespan = default_lifespan
|
|
194
|
-
else:
|
|
195
|
-
self._has_lifespan = True
|
|
196
245
|
# Generate random ID if no name provided
|
|
197
|
-
self._mcp_server = LowLevelServer[
|
|
246
|
+
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
247
|
+
LifespanResultT
|
|
248
|
+
](
|
|
249
|
+
fastmcp=self,
|
|
198
250
|
name=name or self.generate_name(),
|
|
199
|
-
version=version,
|
|
251
|
+
version=version or fastmcp.__version__,
|
|
200
252
|
instructions=instructions,
|
|
201
|
-
|
|
253
|
+
website_url=website_url,
|
|
254
|
+
icons=icons,
|
|
255
|
+
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
202
256
|
)
|
|
203
257
|
|
|
204
258
|
# if auth is `NotSet`, try to create a provider from the environment
|
|
@@ -208,7 +262,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
208
262
|
auth = fastmcp.settings.server_auth_class()
|
|
209
263
|
else:
|
|
210
264
|
auth = None
|
|
211
|
-
self.auth = cast(AuthProvider | None, auth)
|
|
265
|
+
self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
|
|
212
266
|
|
|
213
267
|
if tools:
|
|
214
268
|
for tool in tools:
|
|
@@ -216,42 +270,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
216
270
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
217
271
|
self.add_tool(tool)
|
|
218
272
|
|
|
219
|
-
self.include_tags =
|
|
220
|
-
|
|
273
|
+
self.include_tags: set[str] | None = (
|
|
274
|
+
set(include_tags) if include_tags is not None else None
|
|
275
|
+
)
|
|
276
|
+
self.exclude_tags: set[str] | None = (
|
|
277
|
+
set(exclude_tags) if exclude_tags is not None else None
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self.strict_input_validation: bool = (
|
|
281
|
+
strict_input_validation
|
|
282
|
+
if strict_input_validation is not None
|
|
283
|
+
else fastmcp.settings.strict_input_validation
|
|
284
|
+
)
|
|
221
285
|
|
|
222
|
-
self.middleware = middleware or []
|
|
286
|
+
self.middleware: list[Middleware] = list(middleware or [])
|
|
223
287
|
|
|
224
288
|
# Set up MCP protocol handlers
|
|
225
289
|
self._setup_handlers()
|
|
226
290
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
|
234
|
-
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
|
235
|
-
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
|
236
|
-
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
|
237
|
-
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
|
238
|
-
DeprecationWarning,
|
|
239
|
-
stacklevel=2,
|
|
240
|
-
)
|
|
241
|
-
self.dependencies = (
|
|
242
|
-
dependencies or fastmcp.settings.server_dependencies
|
|
243
|
-
) # TODO: Remove (deprecated in v2.11.4)
|
|
244
|
-
|
|
245
|
-
self.sampling_handler = sampling_handler
|
|
246
|
-
self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
|
|
291
|
+
self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
|
|
292
|
+
sampling_handler
|
|
293
|
+
)
|
|
294
|
+
self.sampling_handler_behavior: Literal["always", "fallback"] = (
|
|
295
|
+
sampling_handler_behavior or "fallback"
|
|
296
|
+
)
|
|
247
297
|
|
|
248
|
-
self.include_fastmcp_meta = (
|
|
298
|
+
self.include_fastmcp_meta: bool = (
|
|
249
299
|
include_fastmcp_meta
|
|
250
300
|
if include_fastmcp_meta is not None
|
|
251
301
|
else fastmcp.settings.include_fastmcp_meta
|
|
252
302
|
)
|
|
253
303
|
|
|
254
|
-
# handle deprecated settings
|
|
255
304
|
self._handle_deprecated_settings(
|
|
256
305
|
log_level=log_level,
|
|
257
306
|
debug=debug,
|
|
@@ -333,6 +382,225 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
333
382
|
def version(self) -> str | None:
|
|
334
383
|
return self._mcp_server.version
|
|
335
384
|
|
|
385
|
+
@property
|
|
386
|
+
def website_url(self) -> str | None:
|
|
387
|
+
return self._mcp_server.website_url
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def icons(self) -> list[mcp.types.Icon]:
|
|
391
|
+
if self._mcp_server.icons is None:
|
|
392
|
+
return []
|
|
393
|
+
else:
|
|
394
|
+
return list(self._mcp_server.icons)
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def docket(self) -> Docket | None:
|
|
398
|
+
"""Get the Docket instance if Docket support is enabled.
|
|
399
|
+
|
|
400
|
+
Returns None if Docket is not enabled or server hasn't been started yet.
|
|
401
|
+
"""
|
|
402
|
+
return self._docket
|
|
403
|
+
|
|
404
|
+
@asynccontextmanager
|
|
405
|
+
async def _docket_lifespan(self) -> AsyncIterator[None]:
|
|
406
|
+
"""Manage Docket instance and Worker for background task execution."""
|
|
407
|
+
from fastmcp import settings
|
|
408
|
+
|
|
409
|
+
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
|
|
410
|
+
from fastmcp.server.dependencies import (
|
|
411
|
+
_current_docket,
|
|
412
|
+
_current_server,
|
|
413
|
+
_current_worker,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
server_token = _current_server.set(weakref.ref(self))
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
# For directly mounted servers, the parent's Docket/Worker handles all
|
|
420
|
+
# task execution. Skip creating our own to avoid race conditions with
|
|
421
|
+
# multiple workers competing for tasks from the same queue.
|
|
422
|
+
if self._is_mounted:
|
|
423
|
+
yield
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
# Create Docket instance using configured name and URL
|
|
427
|
+
async with Docket(
|
|
428
|
+
name=settings.docket.name,
|
|
429
|
+
url=settings.docket.url,
|
|
430
|
+
) as docket:
|
|
431
|
+
# Store on server instance for cross-task access (FastMCPTransport)
|
|
432
|
+
self._docket = docket
|
|
433
|
+
|
|
434
|
+
# Register local task-enabled tools/prompts/resources with Docket
|
|
435
|
+
# Only function-based variants support background tasks
|
|
436
|
+
# Register components where task execution is not "forbidden"
|
|
437
|
+
for tool in self._tool_manager._tools.values():
|
|
438
|
+
if (
|
|
439
|
+
isinstance(tool, FunctionTool)
|
|
440
|
+
and tool.task_config.mode != "forbidden"
|
|
441
|
+
):
|
|
442
|
+
docket.register(tool.fn)
|
|
443
|
+
|
|
444
|
+
for prompt in self._prompt_manager._prompts.values():
|
|
445
|
+
if (
|
|
446
|
+
isinstance(prompt, FunctionPrompt)
|
|
447
|
+
and prompt.task_config.mode != "forbidden"
|
|
448
|
+
):
|
|
449
|
+
# task execution requires async fn (validated at creation time)
|
|
450
|
+
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
|
|
451
|
+
|
|
452
|
+
for resource in self._resource_manager._resources.values():
|
|
453
|
+
if (
|
|
454
|
+
isinstance(resource, FunctionResource)
|
|
455
|
+
and resource.task_config.mode != "forbidden"
|
|
456
|
+
):
|
|
457
|
+
docket.register(resource.fn)
|
|
458
|
+
|
|
459
|
+
for template in self._resource_manager._templates.values():
|
|
460
|
+
if (
|
|
461
|
+
isinstance(template, FunctionResourceTemplate)
|
|
462
|
+
and template.task_config.mode != "forbidden"
|
|
463
|
+
):
|
|
464
|
+
docket.register(template.fn)
|
|
465
|
+
|
|
466
|
+
# Also register functions from mounted servers so tasks can
|
|
467
|
+
# execute in the parent's Docket context
|
|
468
|
+
for mounted in self._mounted_servers:
|
|
469
|
+
await self._register_mounted_server_functions(
|
|
470
|
+
mounted.server, docket, mounted.prefix
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Set Docket in ContextVar so CurrentDocket can access it
|
|
474
|
+
docket_token = _current_docket.set(docket)
|
|
475
|
+
try:
|
|
476
|
+
# Build worker kwargs from settings
|
|
477
|
+
worker_kwargs: dict[str, Any] = {
|
|
478
|
+
"concurrency": settings.docket.concurrency,
|
|
479
|
+
"redelivery_timeout": settings.docket.redelivery_timeout,
|
|
480
|
+
"reconnection_delay": settings.docket.reconnection_delay,
|
|
481
|
+
}
|
|
482
|
+
if settings.docket.worker_name:
|
|
483
|
+
worker_kwargs["name"] = settings.docket.worker_name
|
|
484
|
+
|
|
485
|
+
# Create and start Worker
|
|
486
|
+
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
|
|
487
|
+
# Set Worker in ContextVar so CurrentWorker can access it
|
|
488
|
+
worker_token = _current_worker.set(worker)
|
|
489
|
+
try:
|
|
490
|
+
worker_task = asyncio.create_task(worker.run_forever())
|
|
491
|
+
try:
|
|
492
|
+
yield
|
|
493
|
+
finally:
|
|
494
|
+
# Cancel worker task on exit with timeout to prevent hanging
|
|
495
|
+
worker_task.cancel()
|
|
496
|
+
with suppress(
|
|
497
|
+
asyncio.CancelledError, asyncio.TimeoutError
|
|
498
|
+
):
|
|
499
|
+
await asyncio.wait_for(worker_task, timeout=2.0)
|
|
500
|
+
finally:
|
|
501
|
+
_current_worker.reset(worker_token)
|
|
502
|
+
finally:
|
|
503
|
+
# Reset ContextVar
|
|
504
|
+
_current_docket.reset(docket_token)
|
|
505
|
+
# Clear instance attribute
|
|
506
|
+
self._docket = None
|
|
507
|
+
finally:
|
|
508
|
+
# Reset server ContextVar
|
|
509
|
+
_current_server.reset(server_token)
|
|
510
|
+
|
|
511
|
+
async def _register_mounted_server_functions(
|
|
512
|
+
self, server: FastMCP, docket: Docket, prefix: str | None
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Register task-enabled functions from a mounted server with Docket.
|
|
515
|
+
|
|
516
|
+
This enables background task execution for mounted server components
|
|
517
|
+
through the parent server's Docket context.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
server: The mounted server whose functions to register
|
|
521
|
+
docket: The Docket instance to register with
|
|
522
|
+
prefix: The mount prefix to prepend to function names (matches
|
|
523
|
+
client-facing tool/prompt names)
|
|
524
|
+
"""
|
|
525
|
+
# Register tools with prefixed names to avoid collisions
|
|
526
|
+
for tool in server._tool_manager._tools.values():
|
|
527
|
+
if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
|
|
528
|
+
# Use same naming as client-facing tool keys
|
|
529
|
+
fn_name = f"{prefix}_{tool.key}" if prefix else tool.key
|
|
530
|
+
named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
|
|
531
|
+
docket.register(named_fn)
|
|
532
|
+
|
|
533
|
+
# Register prompts with prefixed names
|
|
534
|
+
for prompt in server._prompt_manager._prompts.values():
|
|
535
|
+
if (
|
|
536
|
+
isinstance(prompt, FunctionPrompt)
|
|
537
|
+
and prompt.task_config.mode != "forbidden"
|
|
538
|
+
):
|
|
539
|
+
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
540
|
+
named_fn = _create_named_fn_wrapper(
|
|
541
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
|
|
542
|
+
)
|
|
543
|
+
docket.register(named_fn)
|
|
544
|
+
|
|
545
|
+
# Register resources with prefixed names (use name, not key/URI)
|
|
546
|
+
for resource in server._resource_manager._resources.values():
|
|
547
|
+
if (
|
|
548
|
+
isinstance(resource, FunctionResource)
|
|
549
|
+
and resource.task_config.mode != "forbidden"
|
|
550
|
+
):
|
|
551
|
+
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
552
|
+
named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
|
|
553
|
+
docket.register(named_fn)
|
|
554
|
+
|
|
555
|
+
# Register resource templates with prefixed names (use name, not key/URI)
|
|
556
|
+
for template in server._resource_manager._templates.values():
|
|
557
|
+
if (
|
|
558
|
+
isinstance(template, FunctionResourceTemplate)
|
|
559
|
+
and template.task_config.mode != "forbidden"
|
|
560
|
+
):
|
|
561
|
+
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
562
|
+
named_fn = _create_named_fn_wrapper(template.fn, fn_name)
|
|
563
|
+
docket.register(named_fn)
|
|
564
|
+
|
|
565
|
+
# Recursively register from nested mounted servers with accumulated prefix
|
|
566
|
+
for nested in server._mounted_servers:
|
|
567
|
+
nested_prefix = (
|
|
568
|
+
f"{prefix}_{nested.prefix}"
|
|
569
|
+
if prefix and nested.prefix
|
|
570
|
+
else (prefix or nested.prefix)
|
|
571
|
+
)
|
|
572
|
+
await self._register_mounted_server_functions(
|
|
573
|
+
nested.server, docket, nested_prefix
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@asynccontextmanager
|
|
577
|
+
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
578
|
+
if self._lifespan_result_set:
|
|
579
|
+
yield
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
async with (
|
|
583
|
+
self._lifespan(self) as user_lifespan_result,
|
|
584
|
+
self._docket_lifespan(),
|
|
585
|
+
):
|
|
586
|
+
self._lifespan_result = user_lifespan_result
|
|
587
|
+
self._lifespan_result_set = True
|
|
588
|
+
|
|
589
|
+
async with AsyncExitStack[bool | None]() as stack:
|
|
590
|
+
for server in self._mounted_servers:
|
|
591
|
+
await stack.enter_async_context(
|
|
592
|
+
cm=server.server._lifespan_manager()
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
self._started.set()
|
|
596
|
+
try:
|
|
597
|
+
yield
|
|
598
|
+
finally:
|
|
599
|
+
self._started.clear()
|
|
600
|
+
|
|
601
|
+
self._lifespan_result_set = False
|
|
602
|
+
self._lifespan_result = None
|
|
603
|
+
|
|
336
604
|
async def run_async(
|
|
337
605
|
self,
|
|
338
606
|
transport: Transport | None = None,
|
|
@@ -386,13 +654,267 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
386
654
|
|
|
387
655
|
def _setup_handlers(self) -> None:
|
|
388
656
|
"""Set up core MCP protocol handlers."""
|
|
389
|
-
self._mcp_server.list_tools()(self.
|
|
390
|
-
self._mcp_server.list_resources()(self.
|
|
391
|
-
self._mcp_server.list_resource_templates()(self.
|
|
392
|
-
self._mcp_server.list_prompts()(self.
|
|
393
|
-
self._mcp_server.call_tool(
|
|
394
|
-
|
|
395
|
-
|
|
657
|
+
self._mcp_server.list_tools()(self._list_tools_mcp)
|
|
658
|
+
self._mcp_server.list_resources()(self._list_resources_mcp)
|
|
659
|
+
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
|
660
|
+
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
|
661
|
+
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
662
|
+
self._call_tool_mcp
|
|
663
|
+
)
|
|
664
|
+
# Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
|
|
665
|
+
self._setup_read_resource_handler()
|
|
666
|
+
# Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
|
|
667
|
+
self._setup_get_prompt_handler()
|
|
668
|
+
# Register custom SEP-1686 task protocol handlers
|
|
669
|
+
self._setup_task_protocol_handlers()
|
|
670
|
+
|
|
671
|
+
def _setup_read_resource_handler(self) -> None:
|
|
672
|
+
"""
|
|
673
|
+
Set up custom read_resource handler that supports task-augmented responses.
|
|
674
|
+
|
|
675
|
+
The SDK's read_resource decorator doesn't support CreateTaskResult returns,
|
|
676
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
|
|
680
|
+
uri = req.params.uri
|
|
681
|
+
|
|
682
|
+
# Check for task metadata via SDK's request context
|
|
683
|
+
task_meta = None
|
|
684
|
+
try:
|
|
685
|
+
ctx = self._mcp_server.request_context
|
|
686
|
+
if ctx.experimental.is_task:
|
|
687
|
+
task_meta = ctx.experimental.task_metadata
|
|
688
|
+
except (AttributeError, LookupError):
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
# Check for task metadata and route appropriately
|
|
692
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
693
|
+
# Get resource including from mounted servers
|
|
694
|
+
resource = await self._get_resource_or_template_or_none(str(uri))
|
|
695
|
+
if (
|
|
696
|
+
resource
|
|
697
|
+
and self._should_enable_component(resource)
|
|
698
|
+
and hasattr(resource, "task_config")
|
|
699
|
+
):
|
|
700
|
+
task_mode = resource.task_config.mode # type: ignore[union-attr]
|
|
701
|
+
|
|
702
|
+
# Enforce mode="required" - must have task metadata
|
|
703
|
+
if task_mode == "required" and not task_meta:
|
|
704
|
+
raise McpError(
|
|
705
|
+
ErrorData(
|
|
706
|
+
code=METHOD_NOT_FOUND,
|
|
707
|
+
message=f"Resource '{uri}' requires task-augmented execution",
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Route to background if task metadata present and mode allows
|
|
712
|
+
if task_meta and task_mode != "forbidden":
|
|
713
|
+
# For FunctionResource/FunctionResourceTemplate, use Docket
|
|
714
|
+
if isinstance(
|
|
715
|
+
resource,
|
|
716
|
+
FunctionResource | FunctionResourceTemplate,
|
|
717
|
+
):
|
|
718
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
719
|
+
return await handle_resource_as_task(
|
|
720
|
+
self, str(uri), resource, task_meta_dict
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
724
|
+
# Raise error since resources don't have isError field
|
|
725
|
+
if task_meta and task_mode == "forbidden":
|
|
726
|
+
raise McpError(
|
|
727
|
+
ErrorData(
|
|
728
|
+
code=METHOD_NOT_FOUND,
|
|
729
|
+
message=f"Resource '{uri}' does not support task-augmented execution",
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Synchronous execution
|
|
734
|
+
result = await self._read_resource_mcp(uri)
|
|
735
|
+
|
|
736
|
+
# Graceful degradation: if we got here with task_meta, something went wrong
|
|
737
|
+
# (This should be unreachable now that forbidden raises)
|
|
738
|
+
if task_meta:
|
|
739
|
+
mcp_contents = []
|
|
740
|
+
for item in result:
|
|
741
|
+
if isinstance(item.content, str):
|
|
742
|
+
mcp_contents.append(
|
|
743
|
+
mcp.types.TextResourceContents(
|
|
744
|
+
uri=uri,
|
|
745
|
+
text=item.content,
|
|
746
|
+
mimeType=item.mime_type or "text/plain",
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
elif isinstance(item.content, bytes):
|
|
750
|
+
import base64
|
|
751
|
+
|
|
752
|
+
mcp_contents.append(
|
|
753
|
+
mcp.types.BlobResourceContents(
|
|
754
|
+
uri=uri,
|
|
755
|
+
blob=base64.b64encode(item.content).decode(),
|
|
756
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
return mcp.types.ServerResult(
|
|
760
|
+
mcp.types.ReadResourceResult(
|
|
761
|
+
contents=mcp_contents,
|
|
762
|
+
_meta={
|
|
763
|
+
"modelcontextprotocol.io/task": {
|
|
764
|
+
"returned_immediately": True
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Convert to proper ServerResult
|
|
771
|
+
if isinstance(result, mcp.types.ServerResult):
|
|
772
|
+
return result
|
|
773
|
+
|
|
774
|
+
mcp_contents = []
|
|
775
|
+
for item in result:
|
|
776
|
+
if isinstance(item.content, str):
|
|
777
|
+
mcp_contents.append(
|
|
778
|
+
mcp.types.TextResourceContents(
|
|
779
|
+
uri=uri,
|
|
780
|
+
text=item.content,
|
|
781
|
+
mimeType=item.mime_type or "text/plain",
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
elif isinstance(item.content, bytes):
|
|
785
|
+
import base64
|
|
786
|
+
|
|
787
|
+
mcp_contents.append(
|
|
788
|
+
mcp.types.BlobResourceContents(
|
|
789
|
+
uri=uri,
|
|
790
|
+
blob=base64.b64encode(item.content).decode(),
|
|
791
|
+
mimeType=item.mime_type or "application/octet-stream",
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
return mcp.types.ServerResult(
|
|
796
|
+
mcp.types.ReadResourceResult(contents=mcp_contents)
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
|
|
800
|
+
|
|
801
|
+
def _setup_get_prompt_handler(self) -> None:
|
|
802
|
+
"""
|
|
803
|
+
Set up custom get_prompt handler that supports task-augmented responses.
|
|
804
|
+
|
|
805
|
+
The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
|
|
806
|
+
so we register a custom handler that checks request_context.experimental.is_task.
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
|
|
810
|
+
name = req.params.name
|
|
811
|
+
arguments = req.params.arguments
|
|
812
|
+
|
|
813
|
+
# Check for task metadata via SDK's request context
|
|
814
|
+
task_meta = None
|
|
815
|
+
try:
|
|
816
|
+
ctx = self._mcp_server.request_context
|
|
817
|
+
if ctx.experimental.is_task:
|
|
818
|
+
task_meta = ctx.experimental.task_metadata
|
|
819
|
+
except (AttributeError, LookupError):
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
# Check for task metadata and route appropriately
|
|
823
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
824
|
+
prompts = await self.get_prompts()
|
|
825
|
+
prompt = prompts.get(name)
|
|
826
|
+
if (
|
|
827
|
+
prompt
|
|
828
|
+
and self._should_enable_component(prompt)
|
|
829
|
+
and hasattr(prompt, "task_config")
|
|
830
|
+
and prompt.task_config
|
|
831
|
+
):
|
|
832
|
+
task_mode = prompt.task_config.mode # type: ignore[union-attr]
|
|
833
|
+
|
|
834
|
+
# Enforce mode="required" - must have task metadata
|
|
835
|
+
if task_mode == "required" and not task_meta:
|
|
836
|
+
raise McpError(
|
|
837
|
+
ErrorData(
|
|
838
|
+
code=METHOD_NOT_FOUND,
|
|
839
|
+
message=f"Prompt '{name}' requires task-augmented execution",
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
# Route to background if task metadata present and mode allows
|
|
844
|
+
if task_meta and task_mode != "forbidden":
|
|
845
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
846
|
+
result = await handle_prompt_as_task(
|
|
847
|
+
self, name, arguments, task_meta_dict
|
|
848
|
+
)
|
|
849
|
+
return mcp.types.ServerResult(result)
|
|
850
|
+
|
|
851
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
852
|
+
# Raise error since prompts don't have isError field
|
|
853
|
+
if task_meta and task_mode == "forbidden":
|
|
854
|
+
raise McpError(
|
|
855
|
+
ErrorData(
|
|
856
|
+
code=METHOD_NOT_FOUND,
|
|
857
|
+
message=f"Prompt '{name}' does not support task-augmented execution",
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
# Synchronous execution
|
|
862
|
+
result = await self._get_prompt_mcp(name, arguments)
|
|
863
|
+
return mcp.types.ServerResult(result)
|
|
864
|
+
|
|
865
|
+
self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
|
|
866
|
+
|
|
867
|
+
def _setup_task_protocol_handlers(self) -> None:
|
|
868
|
+
"""Register SEP-1686 task protocol handlers with SDK."""
|
|
869
|
+
from mcp.types import (
|
|
870
|
+
CancelTaskRequest,
|
|
871
|
+
GetTaskPayloadRequest,
|
|
872
|
+
GetTaskRequest,
|
|
873
|
+
ListTasksRequest,
|
|
874
|
+
ServerResult,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
from fastmcp.server.tasks.protocol import (
|
|
878
|
+
tasks_cancel_handler,
|
|
879
|
+
tasks_get_handler,
|
|
880
|
+
tasks_list_handler,
|
|
881
|
+
tasks_result_handler,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# Manually register handlers (SDK decorators fail with locally-defined functions)
|
|
885
|
+
# SDK expects handlers that receive Request objects and return ServerResult
|
|
886
|
+
|
|
887
|
+
async def handle_get_task(req: GetTaskRequest) -> ServerResult:
|
|
888
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
889
|
+
result = await tasks_get_handler(self, params)
|
|
890
|
+
return ServerResult(result)
|
|
891
|
+
|
|
892
|
+
async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
|
|
893
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
894
|
+
result = await tasks_result_handler(self, params)
|
|
895
|
+
return ServerResult(result)
|
|
896
|
+
|
|
897
|
+
async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
|
|
898
|
+
params = (
|
|
899
|
+
req.params.model_dump(by_alias=True, exclude_none=True)
|
|
900
|
+
if req.params
|
|
901
|
+
else {}
|
|
902
|
+
)
|
|
903
|
+
result = await tasks_list_handler(self, params)
|
|
904
|
+
return ServerResult(result)
|
|
905
|
+
|
|
906
|
+
async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
|
|
907
|
+
params = req.params.model_dump(by_alias=True, exclude_none=True)
|
|
908
|
+
result = await tasks_cancel_handler(self, params)
|
|
909
|
+
return ServerResult(result)
|
|
910
|
+
|
|
911
|
+
# Register directly with SDK (same as what decorators do internally)
|
|
912
|
+
self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
|
|
913
|
+
self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
|
|
914
|
+
handle_get_task_result
|
|
915
|
+
)
|
|
916
|
+
self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
|
|
917
|
+
self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
|
|
396
918
|
|
|
397
919
|
async def _apply_middleware(
|
|
398
920
|
self,
|
|
@@ -409,8 +931,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
409
931
|
self.middleware.append(middleware)
|
|
410
932
|
|
|
411
933
|
async def get_tools(self) -> dict[str, Tool]:
|
|
412
|
-
"""Get all
|
|
413
|
-
|
|
934
|
+
"""Get all tools (unfiltered), including mounted servers, indexed by key."""
|
|
935
|
+
all_tools = dict(await self._tool_manager.get_tools())
|
|
936
|
+
|
|
937
|
+
for mounted in self._mounted_servers:
|
|
938
|
+
try:
|
|
939
|
+
child_tools = await mounted.server.get_tools()
|
|
940
|
+
for key, tool in child_tools.items():
|
|
941
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
942
|
+
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
943
|
+
except Exception as e:
|
|
944
|
+
logger.warning(
|
|
945
|
+
f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
|
|
946
|
+
)
|
|
947
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
948
|
+
raise
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
return all_tools
|
|
414
952
|
|
|
415
953
|
async def get_tool(self, key: str) -> Tool:
|
|
416
954
|
tools = await self.get_tools()
|
|
@@ -418,9 +956,63 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
418
956
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
419
957
|
return tools[key]
|
|
420
958
|
|
|
959
|
+
async def _get_tool_with_task_config(self, key: str) -> Tool | None:
|
|
960
|
+
"""Get a tool by key, returning None if not found.
|
|
961
|
+
|
|
962
|
+
Used for task config checking where we need the actual tool object
|
|
963
|
+
(including from mounted servers and proxies) but don't want to raise.
|
|
964
|
+
"""
|
|
965
|
+
try:
|
|
966
|
+
return await self.get_tool(key)
|
|
967
|
+
except NotFoundError:
|
|
968
|
+
return None
|
|
969
|
+
|
|
970
|
+
async def _get_resource_or_template_or_none(
|
|
971
|
+
self, uri: str
|
|
972
|
+
) -> Resource | ResourceTemplate | None:
|
|
973
|
+
"""Get a resource or template by URI, searching recursively. Returns None if not found."""
|
|
974
|
+
try:
|
|
975
|
+
return await self.get_resource(uri)
|
|
976
|
+
except NotFoundError:
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
templates = await self.get_resource_templates()
|
|
980
|
+
for template in templates.values():
|
|
981
|
+
if template.matches(uri):
|
|
982
|
+
return template
|
|
983
|
+
|
|
984
|
+
return None
|
|
985
|
+
|
|
421
986
|
async def get_resources(self) -> dict[str, Resource]:
|
|
422
|
-
"""Get all
|
|
423
|
-
|
|
987
|
+
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
988
|
+
all_resources = dict(await self._resource_manager.get_resources())
|
|
989
|
+
|
|
990
|
+
for mounted in self._mounted_servers:
|
|
991
|
+
try:
|
|
992
|
+
child_resources = await mounted.server.get_resources()
|
|
993
|
+
for key, resource in child_resources.items():
|
|
994
|
+
new_key = (
|
|
995
|
+
add_resource_prefix(key, mounted.prefix)
|
|
996
|
+
if mounted.prefix
|
|
997
|
+
else key
|
|
998
|
+
)
|
|
999
|
+
update = (
|
|
1000
|
+
{"name": f"{mounted.prefix}_{resource.name}"}
|
|
1001
|
+
if mounted.prefix and resource.name
|
|
1002
|
+
else {}
|
|
1003
|
+
)
|
|
1004
|
+
all_resources[new_key] = resource.model_copy(
|
|
1005
|
+
key=new_key, update=update
|
|
1006
|
+
)
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
logger.warning(
|
|
1009
|
+
f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
|
|
1010
|
+
)
|
|
1011
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1012
|
+
raise
|
|
1013
|
+
continue
|
|
1014
|
+
|
|
1015
|
+
return all_resources
|
|
424
1016
|
|
|
425
1017
|
async def get_resource(self, key: str) -> Resource:
|
|
426
1018
|
resources = await self.get_resources()
|
|
@@ -429,8 +1021,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
429
1021
|
return resources[key]
|
|
430
1022
|
|
|
431
1023
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
432
|
-
"""Get all
|
|
433
|
-
|
|
1024
|
+
"""Get all resource templates (unfiltered), including mounted servers, indexed by key."""
|
|
1025
|
+
all_templates = dict(await self._resource_manager.get_resource_templates())
|
|
1026
|
+
|
|
1027
|
+
for mounted in self._mounted_servers:
|
|
1028
|
+
try:
|
|
1029
|
+
child_templates = await mounted.server.get_resource_templates()
|
|
1030
|
+
for key, template in child_templates.items():
|
|
1031
|
+
new_key = (
|
|
1032
|
+
add_resource_prefix(key, mounted.prefix)
|
|
1033
|
+
if mounted.prefix
|
|
1034
|
+
else key
|
|
1035
|
+
)
|
|
1036
|
+
update: dict[str, Any] = {}
|
|
1037
|
+
if mounted.prefix:
|
|
1038
|
+
if template.name:
|
|
1039
|
+
update["name"] = f"{mounted.prefix}_{template.name}"
|
|
1040
|
+
# Update uri_template so matches() works with prefixed URIs
|
|
1041
|
+
update["uri_template"] = new_key
|
|
1042
|
+
all_templates[new_key] = template.model_copy(
|
|
1043
|
+
key=new_key, update=update
|
|
1044
|
+
)
|
|
1045
|
+
except Exception as e:
|
|
1046
|
+
logger.warning(
|
|
1047
|
+
f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
|
|
1048
|
+
)
|
|
1049
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1050
|
+
raise
|
|
1051
|
+
continue
|
|
1052
|
+
|
|
1053
|
+
return all_templates
|
|
434
1054
|
|
|
435
1055
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
436
1056
|
"""Get a registered resource template by key."""
|
|
@@ -440,10 +1060,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
440
1060
|
return templates[key]
|
|
441
1061
|
|
|
442
1062
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
443
|
-
"""
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
1063
|
+
"""Get all prompts (unfiltered), including mounted servers, indexed by key."""
|
|
1064
|
+
all_prompts = dict(await self._prompt_manager.get_prompts())
|
|
1065
|
+
|
|
1066
|
+
for mounted in self._mounted_servers:
|
|
1067
|
+
try:
|
|
1068
|
+
child_prompts = await mounted.server.get_prompts()
|
|
1069
|
+
for key, prompt in child_prompts.items():
|
|
1070
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
1071
|
+
all_prompts[new_key] = prompt.model_copy(key=new_key)
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
logger.warning(
|
|
1074
|
+
f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
|
|
1075
|
+
)
|
|
1076
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1077
|
+
raise
|
|
1078
|
+
continue
|
|
1079
|
+
|
|
1080
|
+
return all_prompts
|
|
447
1081
|
|
|
448
1082
|
async def get_prompt(self, key: str) -> Prompt:
|
|
449
1083
|
prompts = await self.get_prompts()
|
|
@@ -519,11 +1153,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
519
1153
|
|
|
520
1154
|
return routes
|
|
521
1155
|
|
|
522
|
-
async def
|
|
1156
|
+
async def _list_tools_mcp(self) -> list[MCPTool]:
|
|
1157
|
+
"""
|
|
1158
|
+
List all available tools, in the format expected by the low-level MCP
|
|
1159
|
+
server.
|
|
1160
|
+
"""
|
|
523
1161
|
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
524
1162
|
|
|
525
1163
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
|
-
tools = await self.
|
|
1164
|
+
tools = await self._list_tools_middleware()
|
|
527
1165
|
return [
|
|
528
1166
|
tool.to_mcp_tool(
|
|
529
1167
|
name=tool.key,
|
|
@@ -532,24 +1170,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
532
1170
|
for tool in tools
|
|
533
1171
|
]
|
|
534
1172
|
|
|
535
|
-
async def
|
|
1173
|
+
async def _list_tools_middleware(self) -> list[Tool]:
|
|
536
1174
|
"""
|
|
537
|
-
List all available tools,
|
|
538
|
-
server.
|
|
1175
|
+
List all available tools, applying MCP middleware.
|
|
539
1176
|
"""
|
|
540
1177
|
|
|
541
|
-
async def _handler(
|
|
542
|
-
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
543
|
-
) -> list[Tool]:
|
|
544
|
-
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
|
545
|
-
|
|
546
|
-
mcp_tools: list[Tool] = []
|
|
547
|
-
for tool in tools:
|
|
548
|
-
if self._should_enable_component(tool):
|
|
549
|
-
mcp_tools.append(tool)
|
|
550
|
-
|
|
551
|
-
return mcp_tools
|
|
552
|
-
|
|
553
1178
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
554
1179
|
# Create the middleware context.
|
|
555
1180
|
mw_context = MiddlewareContext(
|
|
@@ -561,13 +1186,66 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
561
1186
|
)
|
|
562
1187
|
|
|
563
1188
|
# Apply the middleware chain.
|
|
564
|
-
return
|
|
1189
|
+
return list(
|
|
1190
|
+
await self._apply_middleware(
|
|
1191
|
+
context=mw_context, call_next=self._list_tools
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
565
1194
|
|
|
566
|
-
async def
|
|
1195
|
+
async def _list_tools(
|
|
1196
|
+
self,
|
|
1197
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
1198
|
+
) -> list[Tool]:
|
|
1199
|
+
"""
|
|
1200
|
+
List all available tools.
|
|
1201
|
+
"""
|
|
1202
|
+
# 1. Get local tools and filter them
|
|
1203
|
+
local_tools = await self._tool_manager.get_tools()
|
|
1204
|
+
filtered_local = [
|
|
1205
|
+
tool for tool in local_tools.values() if self._should_enable_component(tool)
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
# 2. Get tools from mounted servers
|
|
1209
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1210
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1211
|
+
all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
|
|
1212
|
+
|
|
1213
|
+
for mounted in self._mounted_servers:
|
|
1214
|
+
try:
|
|
1215
|
+
child_tools = await mounted.server._list_tools_middleware()
|
|
1216
|
+
for tool in child_tools:
|
|
1217
|
+
# Apply parent server's filtering to mounted components
|
|
1218
|
+
if not self._should_enable_component(tool):
|
|
1219
|
+
continue
|
|
1220
|
+
|
|
1221
|
+
key = tool.key
|
|
1222
|
+
if mounted.prefix:
|
|
1223
|
+
key = f"{mounted.prefix}_{tool.key}"
|
|
1224
|
+
tool = tool.model_copy(key=key)
|
|
1225
|
+
# Later mounted servers override earlier ones
|
|
1226
|
+
all_tools[key] = tool
|
|
1227
|
+
except Exception as e:
|
|
1228
|
+
server_name = getattr(
|
|
1229
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1230
|
+
)
|
|
1231
|
+
logger.warning(
|
|
1232
|
+
f"Failed to list tools from mounted server {server_name!r}: {e}"
|
|
1233
|
+
)
|
|
1234
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1235
|
+
raise
|
|
1236
|
+
continue
|
|
1237
|
+
|
|
1238
|
+
return list(all_tools.values())
|
|
1239
|
+
|
|
1240
|
+
async def _list_resources_mcp(self) -> list[MCPResource]:
|
|
1241
|
+
"""
|
|
1242
|
+
List all available resources, in the format expected by the low-level MCP
|
|
1243
|
+
server.
|
|
1244
|
+
"""
|
|
567
1245
|
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
568
1246
|
|
|
569
1247
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
570
|
-
resources = await self.
|
|
1248
|
+
resources = await self._list_resources_middleware()
|
|
571
1249
|
return [
|
|
572
1250
|
resource.to_mcp_resource(
|
|
573
1251
|
uri=resource.key,
|
|
@@ -576,25 +1254,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
576
1254
|
for resource in resources
|
|
577
1255
|
]
|
|
578
1256
|
|
|
579
|
-
async def
|
|
1257
|
+
async def _list_resources_middleware(self) -> list[Resource]:
|
|
580
1258
|
"""
|
|
581
|
-
List all available resources,
|
|
582
|
-
server.
|
|
583
|
-
|
|
1259
|
+
List all available resources, applying MCP middleware.
|
|
584
1260
|
"""
|
|
585
1261
|
|
|
586
|
-
async def _handler(
|
|
587
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
588
|
-
) -> list[Resource]:
|
|
589
|
-
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
|
590
|
-
|
|
591
|
-
mcp_resources: list[Resource] = []
|
|
592
|
-
for resource in resources:
|
|
593
|
-
if self._should_enable_component(resource):
|
|
594
|
-
mcp_resources.append(resource)
|
|
595
|
-
|
|
596
|
-
return mcp_resources
|
|
597
|
-
|
|
598
1262
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
599
1263
|
# Create the middleware context.
|
|
600
1264
|
mw_context = MiddlewareContext(
|
|
@@ -606,13 +1270,71 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
606
1270
|
)
|
|
607
1271
|
|
|
608
1272
|
# Apply the middleware chain.
|
|
609
|
-
return
|
|
1273
|
+
return list(
|
|
1274
|
+
await self._apply_middleware(
|
|
1275
|
+
context=mw_context, call_next=self._list_resources
|
|
1276
|
+
)
|
|
1277
|
+
)
|
|
610
1278
|
|
|
611
|
-
async def
|
|
1279
|
+
async def _list_resources(
|
|
1280
|
+
self,
|
|
1281
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
1282
|
+
) -> list[Resource]:
|
|
1283
|
+
"""
|
|
1284
|
+
List all available resources.
|
|
1285
|
+
"""
|
|
1286
|
+
# 1. Filter local resources
|
|
1287
|
+
local_resources = await self._resource_manager.get_resources()
|
|
1288
|
+
filtered_local = [
|
|
1289
|
+
resource
|
|
1290
|
+
for resource in local_resources.values()
|
|
1291
|
+
if self._should_enable_component(resource)
|
|
1292
|
+
]
|
|
1293
|
+
|
|
1294
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
1295
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1296
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1297
|
+
all_resources: dict[str, Resource] = {
|
|
1298
|
+
resource.key: resource for resource in filtered_local
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
for mounted in self._mounted_servers:
|
|
1302
|
+
try:
|
|
1303
|
+
child_resources = await mounted.server._list_resources_middleware()
|
|
1304
|
+
for resource in child_resources:
|
|
1305
|
+
# Apply parent server's filtering to mounted components
|
|
1306
|
+
if not self._should_enable_component(resource):
|
|
1307
|
+
continue
|
|
1308
|
+
|
|
1309
|
+
key = resource.key
|
|
1310
|
+
if mounted.prefix:
|
|
1311
|
+
key = add_resource_prefix(resource.key, mounted.prefix)
|
|
1312
|
+
resource = resource.model_copy(
|
|
1313
|
+
key=key,
|
|
1314
|
+
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
1315
|
+
)
|
|
1316
|
+
# Later mounted servers override earlier ones
|
|
1317
|
+
all_resources[key] = resource
|
|
1318
|
+
except Exception as e:
|
|
1319
|
+
server_name = getattr(
|
|
1320
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1321
|
+
)
|
|
1322
|
+
logger.warning(f"Failed to list resources from {server_name!r}: {e}")
|
|
1323
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1324
|
+
raise
|
|
1325
|
+
continue
|
|
1326
|
+
|
|
1327
|
+
return list(all_resources.values())
|
|
1328
|
+
|
|
1329
|
+
async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
|
|
1330
|
+
"""
|
|
1331
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
1332
|
+
server.
|
|
1333
|
+
"""
|
|
612
1334
|
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
613
1335
|
|
|
614
1336
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
615
|
-
templates = await self.
|
|
1337
|
+
templates = await self._list_resource_templates_middleware()
|
|
616
1338
|
return [
|
|
617
1339
|
template.to_mcp_template(
|
|
618
1340
|
uriTemplate=template.key,
|
|
@@ -621,25 +1343,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
621
1343
|
for template in templates
|
|
622
1344
|
]
|
|
623
1345
|
|
|
624
|
-
async def
|
|
1346
|
+
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
|
625
1347
|
"""
|
|
626
|
-
List all available resource templates,
|
|
627
|
-
server.
|
|
1348
|
+
List all available resource templates, applying MCP middleware.
|
|
628
1349
|
|
|
629
1350
|
"""
|
|
630
1351
|
|
|
631
|
-
async def _handler(
|
|
632
|
-
context: MiddlewareContext[dict[str, Any]],
|
|
633
|
-
) -> list[ResourceTemplate]:
|
|
634
|
-
templates = await self._resource_manager.list_resource_templates()
|
|
635
|
-
|
|
636
|
-
mcp_templates: list[ResourceTemplate] = []
|
|
637
|
-
for template in templates:
|
|
638
|
-
if self._should_enable_component(template):
|
|
639
|
-
mcp_templates.append(template)
|
|
640
|
-
|
|
641
|
-
return mcp_templates
|
|
642
|
-
|
|
643
1352
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
644
1353
|
# Create the middleware context.
|
|
645
1354
|
mw_context = MiddlewareContext(
|
|
@@ -651,13 +1360,75 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
651
1360
|
)
|
|
652
1361
|
|
|
653
1362
|
# Apply the middleware chain.
|
|
654
|
-
return
|
|
1363
|
+
return list(
|
|
1364
|
+
await self._apply_middleware(
|
|
1365
|
+
context=mw_context, call_next=self._list_resource_templates
|
|
1366
|
+
)
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
async def _list_resource_templates(
|
|
1370
|
+
self,
|
|
1371
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
1372
|
+
) -> list[ResourceTemplate]:
|
|
1373
|
+
"""
|
|
1374
|
+
List all available resource templates.
|
|
1375
|
+
"""
|
|
1376
|
+
# 1. Filter local templates
|
|
1377
|
+
local_templates = await self._resource_manager.get_resource_templates()
|
|
1378
|
+
filtered_local = [
|
|
1379
|
+
template
|
|
1380
|
+
for template in local_templates.values()
|
|
1381
|
+
if self._should_enable_component(template)
|
|
1382
|
+
]
|
|
1383
|
+
|
|
1384
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
1385
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1386
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1387
|
+
all_templates: dict[str, ResourceTemplate] = {
|
|
1388
|
+
template.key: template for template in filtered_local
|
|
1389
|
+
}
|
|
655
1390
|
|
|
656
|
-
|
|
1391
|
+
for mounted in self._mounted_servers:
|
|
1392
|
+
try:
|
|
1393
|
+
child_templates = (
|
|
1394
|
+
await mounted.server._list_resource_templates_middleware()
|
|
1395
|
+
)
|
|
1396
|
+
for template in child_templates:
|
|
1397
|
+
# Apply parent server's filtering to mounted components
|
|
1398
|
+
if not self._should_enable_component(template):
|
|
1399
|
+
continue
|
|
1400
|
+
|
|
1401
|
+
key = template.key
|
|
1402
|
+
if mounted.prefix:
|
|
1403
|
+
key = add_resource_prefix(template.key, mounted.prefix)
|
|
1404
|
+
template = template.model_copy(
|
|
1405
|
+
key=key,
|
|
1406
|
+
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
1407
|
+
)
|
|
1408
|
+
# Later mounted servers override earlier ones
|
|
1409
|
+
all_templates[key] = template
|
|
1410
|
+
except Exception as e:
|
|
1411
|
+
server_name = getattr(
|
|
1412
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1413
|
+
)
|
|
1414
|
+
logger.warning(
|
|
1415
|
+
f"Failed to list resource templates from {server_name!r}: {e}"
|
|
1416
|
+
)
|
|
1417
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1418
|
+
raise
|
|
1419
|
+
continue
|
|
1420
|
+
|
|
1421
|
+
return list(all_templates.values())
|
|
1422
|
+
|
|
1423
|
+
async def _list_prompts_mcp(self) -> list[MCPPrompt]:
|
|
1424
|
+
"""
|
|
1425
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
1426
|
+
server.
|
|
1427
|
+
"""
|
|
657
1428
|
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
658
1429
|
|
|
659
1430
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
660
|
-
prompts = await self.
|
|
1431
|
+
prompts = await self._list_prompts_middleware()
|
|
661
1432
|
return [
|
|
662
1433
|
prompt.to_mcp_prompt(
|
|
663
1434
|
name=prompt.key,
|
|
@@ -666,25 +1437,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
666
1437
|
for prompt in prompts
|
|
667
1438
|
]
|
|
668
1439
|
|
|
669
|
-
async def
|
|
1440
|
+
async def _list_prompts_middleware(self) -> list[Prompt]:
|
|
670
1441
|
"""
|
|
671
|
-
List all available prompts,
|
|
672
|
-
server.
|
|
1442
|
+
List all available prompts, applying MCP middleware.
|
|
673
1443
|
|
|
674
1444
|
"""
|
|
675
1445
|
|
|
676
|
-
async def _handler(
|
|
677
|
-
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
678
|
-
) -> list[Prompt]:
|
|
679
|
-
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
|
680
|
-
|
|
681
|
-
mcp_prompts: list[Prompt] = []
|
|
682
|
-
for prompt in prompts:
|
|
683
|
-
if self._should_enable_component(prompt):
|
|
684
|
-
mcp_prompts.append(prompt)
|
|
685
|
-
|
|
686
|
-
return mcp_prompts
|
|
687
|
-
|
|
688
1446
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
689
1447
|
# Create the middleware context.
|
|
690
1448
|
mw_context = MiddlewareContext(
|
|
@@ -696,15 +1454,72 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
696
1454
|
)
|
|
697
1455
|
|
|
698
1456
|
# Apply the middleware chain.
|
|
699
|
-
return
|
|
1457
|
+
return list(
|
|
1458
|
+
await self._apply_middleware(
|
|
1459
|
+
context=mw_context, call_next=self._list_prompts
|
|
1460
|
+
)
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
async def _list_prompts(
|
|
1464
|
+
self,
|
|
1465
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
1466
|
+
) -> list[Prompt]:
|
|
1467
|
+
"""
|
|
1468
|
+
List all available prompts.
|
|
1469
|
+
"""
|
|
1470
|
+
# 1. Filter local prompts
|
|
1471
|
+
local_prompts = await self._prompt_manager.get_prompts()
|
|
1472
|
+
filtered_local = [
|
|
1473
|
+
prompt
|
|
1474
|
+
for prompt in local_prompts.values()
|
|
1475
|
+
if self._should_enable_component(prompt)
|
|
1476
|
+
]
|
|
1477
|
+
|
|
1478
|
+
# 2. Get from mounted servers
|
|
1479
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1480
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1481
|
+
all_prompts: dict[str, Prompt] = {
|
|
1482
|
+
prompt.key: prompt for prompt in filtered_local
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
for mounted in self._mounted_servers:
|
|
1486
|
+
try:
|
|
1487
|
+
child_prompts = await mounted.server._list_prompts_middleware()
|
|
1488
|
+
for prompt in child_prompts:
|
|
1489
|
+
# Apply parent server's filtering to mounted components
|
|
1490
|
+
if not self._should_enable_component(prompt):
|
|
1491
|
+
continue
|
|
1492
|
+
|
|
1493
|
+
key = prompt.key
|
|
1494
|
+
if mounted.prefix:
|
|
1495
|
+
key = f"{mounted.prefix}_{prompt.key}"
|
|
1496
|
+
prompt = prompt.model_copy(key=key)
|
|
1497
|
+
# Later mounted servers override earlier ones
|
|
1498
|
+
all_prompts[key] = prompt
|
|
1499
|
+
except Exception as e:
|
|
1500
|
+
server_name = getattr(
|
|
1501
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1502
|
+
)
|
|
1503
|
+
logger.warning(
|
|
1504
|
+
f"Failed to list prompts from mounted server {server_name!r}: {e}"
|
|
1505
|
+
)
|
|
1506
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1507
|
+
raise
|
|
1508
|
+
continue
|
|
1509
|
+
|
|
1510
|
+
return list(all_prompts.values())
|
|
700
1511
|
|
|
701
|
-
async def
|
|
1512
|
+
async def _call_tool_mcp(
|
|
702
1513
|
self, key: str, arguments: dict[str, Any]
|
|
703
|
-
) ->
|
|
1514
|
+
) -> (
|
|
1515
|
+
list[ContentBlock]
|
|
1516
|
+
| tuple[list[ContentBlock], dict[str, Any]]
|
|
1517
|
+
| mcp.types.CallToolResult
|
|
1518
|
+
):
|
|
704
1519
|
"""
|
|
705
1520
|
Handle MCP 'callTool' requests.
|
|
706
1521
|
|
|
707
|
-
|
|
1522
|
+
Detects SEP-1686 task metadata and routes to background execution if supported.
|
|
708
1523
|
|
|
709
1524
|
Args:
|
|
710
1525
|
key: The name of the tool to call
|
|
@@ -719,29 +1534,81 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
719
1534
|
|
|
720
1535
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
721
1536
|
try:
|
|
722
|
-
|
|
1537
|
+
# Check for SEP-1686 task metadata via request context
|
|
1538
|
+
task_meta = None
|
|
1539
|
+
try:
|
|
1540
|
+
# Access task metadata from SDK's request context
|
|
1541
|
+
ctx = self._mcp_server.request_context
|
|
1542
|
+
if ctx.experimental.is_task:
|
|
1543
|
+
task_meta = ctx.experimental.task_metadata
|
|
1544
|
+
except (AttributeError, LookupError):
|
|
1545
|
+
# No request context available - proceed without task metadata
|
|
1546
|
+
pass
|
|
1547
|
+
|
|
1548
|
+
# Get tool from local manager, mounted servers, or proxy
|
|
1549
|
+
tool = await self._get_tool_with_task_config(key)
|
|
1550
|
+
if (
|
|
1551
|
+
tool
|
|
1552
|
+
and self._should_enable_component(tool)
|
|
1553
|
+
and hasattr(tool, "task_config")
|
|
1554
|
+
):
|
|
1555
|
+
task_mode = tool.task_config.mode # type: ignore[union-attr]
|
|
1556
|
+
|
|
1557
|
+
# Enforce mode="required" - must have task metadata
|
|
1558
|
+
if task_mode == "required" and not task_meta:
|
|
1559
|
+
raise McpError(
|
|
1560
|
+
ErrorData(
|
|
1561
|
+
code=METHOD_NOT_FOUND,
|
|
1562
|
+
message=f"Tool '{key}' requires task-augmented execution",
|
|
1563
|
+
)
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
# Route to background if task metadata present and mode allows
|
|
1567
|
+
if task_meta and task_mode != "forbidden":
|
|
1568
|
+
# For FunctionTool, use Docket for background execution
|
|
1569
|
+
if isinstance(tool, FunctionTool):
|
|
1570
|
+
task_meta_dict = task_meta.model_dump(exclude_none=True)
|
|
1571
|
+
return await handle_tool_as_task(
|
|
1572
|
+
self, key, arguments, task_meta_dict
|
|
1573
|
+
)
|
|
1574
|
+
# For ProxyTool/mounted tools, proceed with normal execution
|
|
1575
|
+
# They will forward task metadata to their backend
|
|
1576
|
+
|
|
1577
|
+
# Forbidden mode: task requested but mode="forbidden"
|
|
1578
|
+
# Return error result with returned_immediately=True
|
|
1579
|
+
if task_meta and task_mode == "forbidden":
|
|
1580
|
+
return mcp.types.CallToolResult(
|
|
1581
|
+
content=[
|
|
1582
|
+
mcp.types.TextContent(
|
|
1583
|
+
type="text",
|
|
1584
|
+
text=f"Tool '{key}' does not support task-augmented execution",
|
|
1585
|
+
)
|
|
1586
|
+
],
|
|
1587
|
+
isError=True,
|
|
1588
|
+
_meta={
|
|
1589
|
+
"modelcontextprotocol.io/task": {
|
|
1590
|
+
"returned_immediately": True
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
# Synchronous execution (normal path)
|
|
1596
|
+
result = await self._call_tool_middleware(key, arguments)
|
|
723
1597
|
return result.to_mcp_result()
|
|
724
|
-
except DisabledError:
|
|
725
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
726
|
-
except NotFoundError:
|
|
727
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
1598
|
+
except DisabledError as e:
|
|
1599
|
+
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
1600
|
+
except NotFoundError as e:
|
|
1601
|
+
raise NotFoundError(f"Unknown tool: {key}") from e
|
|
728
1602
|
|
|
729
|
-
async def
|
|
1603
|
+
async def _call_tool_middleware(
|
|
1604
|
+
self,
|
|
1605
|
+
key: str,
|
|
1606
|
+
arguments: dict[str, Any],
|
|
1607
|
+
) -> ToolResult:
|
|
730
1608
|
"""
|
|
731
1609
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
732
1610
|
"""
|
|
733
1611
|
|
|
734
|
-
async def _handler(
|
|
735
|
-
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
736
|
-
) -> ToolResult:
|
|
737
|
-
tool = await self._tool_manager.get_tool(context.message.name)
|
|
738
|
-
if not self._should_enable_component(tool):
|
|
739
|
-
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
|
740
|
-
|
|
741
|
-
return await self._tool_manager.call_tool(
|
|
742
|
-
key=context.message.name, arguments=context.message.arguments or {}
|
|
743
|
-
)
|
|
744
|
-
|
|
745
1612
|
mw_context = MiddlewareContext[CallToolRequestParams](
|
|
746
1613
|
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
747
1614
|
source="client",
|
|
@@ -749,9 +1616,55 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
749
1616
|
method="tools/call",
|
|
750
1617
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
751
1618
|
)
|
|
752
|
-
return await self._apply_middleware(
|
|
1619
|
+
return await self._apply_middleware(
|
|
1620
|
+
context=mw_context, call_next=self._call_tool
|
|
1621
|
+
)
|
|
753
1622
|
|
|
754
|
-
async def
|
|
1623
|
+
async def _call_tool(
|
|
1624
|
+
self,
|
|
1625
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
1626
|
+
) -> ToolResult:
|
|
1627
|
+
"""
|
|
1628
|
+
Call a tool
|
|
1629
|
+
"""
|
|
1630
|
+
tool_name = context.message.name
|
|
1631
|
+
|
|
1632
|
+
# Try mounted servers in reverse order (later wins)
|
|
1633
|
+
for mounted in reversed(self._mounted_servers):
|
|
1634
|
+
try_name = tool_name
|
|
1635
|
+
if mounted.prefix:
|
|
1636
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1637
|
+
continue
|
|
1638
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1639
|
+
|
|
1640
|
+
try:
|
|
1641
|
+
# First, get the tool to check if parent's filter allows it
|
|
1642
|
+
# Use get_tool() instead of _tool_manager.get_tool() to support
|
|
1643
|
+
# nested mounted servers (tools mounted more than 2 levels deep)
|
|
1644
|
+
tool = await mounted.server.get_tool(try_name)
|
|
1645
|
+
if not self._should_enable_component(tool):
|
|
1646
|
+
# Parent filter blocks this tool, continue searching
|
|
1647
|
+
continue
|
|
1648
|
+
|
|
1649
|
+
return await mounted.server._call_tool_middleware(
|
|
1650
|
+
try_name, context.message.arguments or {}
|
|
1651
|
+
)
|
|
1652
|
+
except NotFoundError:
|
|
1653
|
+
continue
|
|
1654
|
+
|
|
1655
|
+
# Try local tools last (mounted servers override local)
|
|
1656
|
+
try:
|
|
1657
|
+
tool = await self._tool_manager.get_tool(tool_name)
|
|
1658
|
+
if self._should_enable_component(tool):
|
|
1659
|
+
return await self._tool_manager.call_tool(
|
|
1660
|
+
key=tool_name, arguments=context.message.arguments or {}
|
|
1661
|
+
)
|
|
1662
|
+
except NotFoundError:
|
|
1663
|
+
pass
|
|
1664
|
+
|
|
1665
|
+
raise NotFoundError(f"Unknown tool: {tool_name!r}")
|
|
1666
|
+
|
|
1667
|
+
async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
755
1668
|
"""
|
|
756
1669
|
Handle MCP 'readResource' requests.
|
|
757
1670
|
|
|
@@ -761,39 +1674,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
761
1674
|
|
|
762
1675
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
763
1676
|
try:
|
|
764
|
-
|
|
765
|
-
|
|
1677
|
+
# Task routing handled by custom handler
|
|
1678
|
+
return list[ReadResourceContents](
|
|
1679
|
+
await self._read_resource_middleware(uri)
|
|
1680
|
+
)
|
|
1681
|
+
except DisabledError as e:
|
|
766
1682
|
# convert to NotFoundError to avoid leaking resource presence
|
|
767
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
768
|
-
except NotFoundError:
|
|
1683
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
1684
|
+
except NotFoundError as e:
|
|
769
1685
|
# standardize NotFound message
|
|
770
|
-
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
1686
|
+
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
|
|
771
1687
|
|
|
772
|
-
async def
|
|
1688
|
+
async def _read_resource_middleware(
|
|
1689
|
+
self,
|
|
1690
|
+
uri: AnyUrl | str,
|
|
1691
|
+
) -> list[ReadResourceContents]:
|
|
773
1692
|
"""
|
|
774
1693
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
775
1694
|
"""
|
|
776
1695
|
|
|
777
|
-
async def _handler(
|
|
778
|
-
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
779
|
-
) -> list[ReadResourceContents]:
|
|
780
|
-
resource = await self._resource_manager.get_resource(context.message.uri)
|
|
781
|
-
if not self._should_enable_component(resource):
|
|
782
|
-
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
|
783
|
-
|
|
784
|
-
content = await self._resource_manager.read_resource(context.message.uri)
|
|
785
|
-
return [
|
|
786
|
-
ReadResourceContents(
|
|
787
|
-
content=content,
|
|
788
|
-
mime_type=resource.mime_type,
|
|
789
|
-
)
|
|
790
|
-
]
|
|
791
|
-
|
|
792
1696
|
# Convert string URI to AnyUrl if needed
|
|
793
|
-
if isinstance(uri, str)
|
|
794
|
-
uri_param = AnyUrl(uri)
|
|
795
|
-
else:
|
|
796
|
-
uri_param = uri
|
|
1697
|
+
uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
|
|
797
1698
|
|
|
798
1699
|
mw_context = MiddlewareContext(
|
|
799
1700
|
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
|
|
@@ -802,9 +1703,61 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
1703
|
method="resources/read",
|
|
803
1704
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
804
1705
|
)
|
|
805
|
-
return
|
|
1706
|
+
return list(
|
|
1707
|
+
await self._apply_middleware(
|
|
1708
|
+
context=mw_context, call_next=self._read_resource
|
|
1709
|
+
)
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
async def _read_resource(
|
|
1713
|
+
self,
|
|
1714
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
1715
|
+
) -> list[ReadResourceContents]:
|
|
1716
|
+
"""
|
|
1717
|
+
Read a resource
|
|
1718
|
+
"""
|
|
1719
|
+
uri_str = str(context.message.uri)
|
|
1720
|
+
|
|
1721
|
+
# Try mounted servers in reverse order (later wins)
|
|
1722
|
+
for mounted in reversed(self._mounted_servers):
|
|
1723
|
+
key = uri_str
|
|
1724
|
+
if mounted.prefix:
|
|
1725
|
+
if not has_resource_prefix(key, mounted.prefix):
|
|
1726
|
+
continue
|
|
1727
|
+
key = remove_resource_prefix(key, mounted.prefix)
|
|
1728
|
+
|
|
1729
|
+
# First, get the resource/template to check if parent's filter allows it
|
|
1730
|
+
# Use get_resource_or_template to support nested mounted servers
|
|
1731
|
+
# (resources/templates mounted more than 2 levels deep)
|
|
1732
|
+
resource = await mounted.server._get_resource_or_template_or_none(key)
|
|
1733
|
+
if resource is None:
|
|
1734
|
+
continue
|
|
1735
|
+
if not self._should_enable_component(resource):
|
|
1736
|
+
# Parent filter blocks this resource, continue searching
|
|
1737
|
+
continue
|
|
1738
|
+
try:
|
|
1739
|
+
result = list(await mounted.server._read_resource_middleware(key))
|
|
1740
|
+
return result
|
|
1741
|
+
except NotFoundError:
|
|
1742
|
+
continue
|
|
1743
|
+
|
|
1744
|
+
# Try local resources last (mounted servers override local)
|
|
1745
|
+
try:
|
|
1746
|
+
resource = await self._resource_manager.get_resource(uri_str)
|
|
1747
|
+
if self._should_enable_component(resource):
|
|
1748
|
+
content = await self._resource_manager.read_resource(uri_str)
|
|
1749
|
+
return [
|
|
1750
|
+
ReadResourceContents(
|
|
1751
|
+
content=content,
|
|
1752
|
+
mime_type=resource.mime_type,
|
|
1753
|
+
)
|
|
1754
|
+
]
|
|
1755
|
+
except NotFoundError:
|
|
1756
|
+
pass
|
|
806
1757
|
|
|
807
|
-
|
|
1758
|
+
raise NotFoundError(f"Unknown resource: {uri_str!r}")
|
|
1759
|
+
|
|
1760
|
+
async def _get_prompt_mcp(
|
|
808
1761
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
809
1762
|
) -> GetPromptResult:
|
|
810
1763
|
"""
|
|
@@ -820,32 +1773,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
820
1773
|
|
|
821
1774
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
822
1775
|
try:
|
|
823
|
-
|
|
824
|
-
|
|
1776
|
+
# Task routing handled by custom handler
|
|
1777
|
+
return await self._get_prompt_middleware(name, arguments)
|
|
1778
|
+
except DisabledError as e:
|
|
825
1779
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
826
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
827
|
-
except NotFoundError:
|
|
1780
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
1781
|
+
except NotFoundError as e:
|
|
828
1782
|
# standardize NotFound message
|
|
829
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
1783
|
+
raise NotFoundError(f"Unknown prompt: {name}") from e
|
|
830
1784
|
|
|
831
|
-
async def
|
|
1785
|
+
async def _get_prompt_middleware(
|
|
832
1786
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
833
1787
|
) -> GetPromptResult:
|
|
834
1788
|
"""
|
|
835
1789
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
836
1790
|
"""
|
|
837
1791
|
|
|
838
|
-
async def _handler(
|
|
839
|
-
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
840
|
-
) -> GetPromptResult:
|
|
841
|
-
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
|
842
|
-
if not self._should_enable_component(prompt):
|
|
843
|
-
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
|
844
|
-
|
|
845
|
-
return await self._prompt_manager.render_prompt(
|
|
846
|
-
name=context.message.name, arguments=context.message.arguments
|
|
847
|
-
)
|
|
848
|
-
|
|
849
1792
|
mw_context = MiddlewareContext(
|
|
850
1793
|
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
851
1794
|
source="client",
|
|
@@ -853,7 +1796,49 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
853
1796
|
method="prompts/get",
|
|
854
1797
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
855
1798
|
)
|
|
856
|
-
return await self._apply_middleware(
|
|
1799
|
+
return await self._apply_middleware(
|
|
1800
|
+
context=mw_context, call_next=self._get_prompt
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
async def _get_prompt(
|
|
1804
|
+
self,
|
|
1805
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
1806
|
+
) -> GetPromptResult:
|
|
1807
|
+
name = context.message.name
|
|
1808
|
+
|
|
1809
|
+
# Try mounted servers in reverse order (later wins)
|
|
1810
|
+
for mounted in reversed(self._mounted_servers):
|
|
1811
|
+
try_name = name
|
|
1812
|
+
if mounted.prefix:
|
|
1813
|
+
if not name.startswith(f"{mounted.prefix}_"):
|
|
1814
|
+
continue
|
|
1815
|
+
try_name = name[len(mounted.prefix) + 1 :]
|
|
1816
|
+
|
|
1817
|
+
try:
|
|
1818
|
+
# First, get the prompt to check if parent's filter allows it
|
|
1819
|
+
# Use get_prompt() instead of _prompt_manager.get_prompt() to support
|
|
1820
|
+
# nested mounted servers (prompts mounted more than 2 levels deep)
|
|
1821
|
+
prompt = await mounted.server.get_prompt(try_name)
|
|
1822
|
+
if not self._should_enable_component(prompt):
|
|
1823
|
+
# Parent filter blocks this prompt, continue searching
|
|
1824
|
+
continue
|
|
1825
|
+
return await mounted.server._get_prompt_middleware(
|
|
1826
|
+
try_name, context.message.arguments
|
|
1827
|
+
)
|
|
1828
|
+
except NotFoundError:
|
|
1829
|
+
continue
|
|
1830
|
+
|
|
1831
|
+
# Try local prompts last (mounted servers override local)
|
|
1832
|
+
try:
|
|
1833
|
+
prompt = await self._prompt_manager.get_prompt(name)
|
|
1834
|
+
if self._should_enable_component(prompt):
|
|
1835
|
+
return await self._prompt_manager.render_prompt(
|
|
1836
|
+
name=name, arguments=context.message.arguments
|
|
1837
|
+
)
|
|
1838
|
+
except NotFoundError:
|
|
1839
|
+
pass
|
|
1840
|
+
|
|
1841
|
+
raise NotFoundError(f"Unknown prompt: {name!r}")
|
|
857
1842
|
|
|
858
1843
|
def add_tool(self, tool: Tool) -> Tool:
|
|
859
1844
|
"""Add a tool to the server.
|
|
@@ -918,12 +1903,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
918
1903
|
name: str | None = None,
|
|
919
1904
|
title: str | None = None,
|
|
920
1905
|
description: str | None = None,
|
|
1906
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
921
1907
|
tags: set[str] | None = None,
|
|
922
|
-
output_schema: dict[str, Any] |
|
|
1908
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
923
1909
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
924
1910
|
exclude_args: list[str] | None = None,
|
|
925
1911
|
meta: dict[str, Any] | None = None,
|
|
926
1912
|
enabled: bool | None = None,
|
|
1913
|
+
task: bool | TaskConfig | None = None,
|
|
927
1914
|
) -> FunctionTool: ...
|
|
928
1915
|
|
|
929
1916
|
@overload
|
|
@@ -934,12 +1921,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
934
1921
|
name: str | None = None,
|
|
935
1922
|
title: str | None = None,
|
|
936
1923
|
description: str | None = None,
|
|
1924
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
937
1925
|
tags: set[str] | None = None,
|
|
938
|
-
output_schema: dict[str, Any] |
|
|
1926
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
939
1927
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
940
1928
|
exclude_args: list[str] | None = None,
|
|
941
1929
|
meta: dict[str, Any] | None = None,
|
|
942
1930
|
enabled: bool | None = None,
|
|
1931
|
+
task: bool | TaskConfig | None = None,
|
|
943
1932
|
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
944
1933
|
|
|
945
1934
|
def tool(
|
|
@@ -949,12 +1938,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
1938
|
name: str | None = None,
|
|
950
1939
|
title: str | None = None,
|
|
951
1940
|
description: str | None = None,
|
|
1941
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
952
1942
|
tags: set[str] | None = None,
|
|
953
|
-
output_schema: dict[str, Any] |
|
|
1943
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
954
1944
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
955
1945
|
exclude_args: list[str] | None = None,
|
|
956
1946
|
meta: dict[str, Any] | None = None,
|
|
957
1947
|
enabled: bool | None = None,
|
|
1948
|
+
task: bool | TaskConfig | None = None,
|
|
958
1949
|
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
|
959
1950
|
"""Decorator to register a tool.
|
|
960
1951
|
|
|
@@ -976,7 +1967,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
976
1967
|
tags: Optional set of tags for categorizing the tool
|
|
977
1968
|
output_schema: Optional JSON schema for the tool's output
|
|
978
1969
|
annotations: Optional annotations about the tool's behavior
|
|
979
|
-
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
1970
|
+
exclude_args: Optional list of argument names to exclude from the tool schema.
|
|
1971
|
+
Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
|
|
1972
|
+
injection with `Depends()` for better lifecycle management.
|
|
980
1973
|
meta: Optional meta information about the tool
|
|
981
1974
|
enabled: Optional boolean to enable or disable the tool
|
|
982
1975
|
|
|
@@ -1026,19 +2019,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1026
2019
|
fn = name_or_fn
|
|
1027
2020
|
tool_name = name # Use keyword name if provided, otherwise None
|
|
1028
2021
|
|
|
2022
|
+
# Resolve task parameter
|
|
2023
|
+
supports_task: bool | TaskConfig = (
|
|
2024
|
+
task if task is not None else self._support_tasks_by_default
|
|
2025
|
+
)
|
|
2026
|
+
|
|
1029
2027
|
# Register the tool immediately and return the tool object
|
|
2028
|
+
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
|
1030
2029
|
tool = Tool.from_function(
|
|
1031
2030
|
fn,
|
|
1032
2031
|
name=tool_name,
|
|
1033
2032
|
title=title,
|
|
1034
2033
|
description=description,
|
|
2034
|
+
icons=icons,
|
|
1035
2035
|
tags=tags,
|
|
1036
2036
|
output_schema=output_schema,
|
|
1037
|
-
annotations=
|
|
2037
|
+
annotations=annotations,
|
|
1038
2038
|
exclude_args=exclude_args,
|
|
1039
2039
|
meta=meta,
|
|
1040
2040
|
serializer=self._tool_serializer,
|
|
1041
2041
|
enabled=enabled,
|
|
2042
|
+
task=supports_task,
|
|
1042
2043
|
)
|
|
1043
2044
|
self.add_tool(tool)
|
|
1044
2045
|
return tool
|
|
@@ -1065,12 +2066,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1065
2066
|
name=tool_name,
|
|
1066
2067
|
title=title,
|
|
1067
2068
|
description=description,
|
|
2069
|
+
icons=icons,
|
|
1068
2070
|
tags=tags,
|
|
1069
2071
|
output_schema=output_schema,
|
|
1070
2072
|
annotations=annotations,
|
|
1071
2073
|
exclude_args=exclude_args,
|
|
1072
2074
|
meta=meta,
|
|
1073
2075
|
enabled=enabled,
|
|
2076
|
+
task=task,
|
|
1074
2077
|
)
|
|
1075
2078
|
|
|
1076
2079
|
def add_resource(self, resource: Resource) -> Resource:
|
|
@@ -1117,44 +2120,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1117
2120
|
|
|
1118
2121
|
return template
|
|
1119
2122
|
|
|
1120
|
-
def add_resource_fn(
|
|
1121
|
-
self,
|
|
1122
|
-
fn: AnyFunction,
|
|
1123
|
-
uri: str,
|
|
1124
|
-
name: str | None = None,
|
|
1125
|
-
description: str | None = None,
|
|
1126
|
-
mime_type: str | None = None,
|
|
1127
|
-
tags: set[str] | None = None,
|
|
1128
|
-
) -> None:
|
|
1129
|
-
"""Add a resource or template to the server from a function.
|
|
1130
|
-
|
|
1131
|
-
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
1132
|
-
has parameters, it will be registered as a template resource.
|
|
1133
|
-
|
|
1134
|
-
Args:
|
|
1135
|
-
fn: The function to register as a resource
|
|
1136
|
-
uri: The URI for the resource
|
|
1137
|
-
name: Optional name for the resource
|
|
1138
|
-
description: Optional description of the resource
|
|
1139
|
-
mime_type: Optional MIME type for the resource
|
|
1140
|
-
tags: Optional set of tags for categorizing the resource
|
|
1141
|
-
"""
|
|
1142
|
-
# deprecated since 2.7.0
|
|
1143
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1144
|
-
warnings.warn(
|
|
1145
|
-
"The add_resource_fn method is deprecated. Use the resource decorator instead.",
|
|
1146
|
-
DeprecationWarning,
|
|
1147
|
-
stacklevel=2,
|
|
1148
|
-
)
|
|
1149
|
-
self._resource_manager.add_resource_or_template_from_fn(
|
|
1150
|
-
fn=fn,
|
|
1151
|
-
uri=uri,
|
|
1152
|
-
name=name,
|
|
1153
|
-
description=description,
|
|
1154
|
-
mime_type=mime_type,
|
|
1155
|
-
tags=tags,
|
|
1156
|
-
)
|
|
1157
|
-
|
|
1158
2123
|
def resource(
|
|
1159
2124
|
self,
|
|
1160
2125
|
uri: str,
|
|
@@ -1162,11 +2127,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1162
2127
|
name: str | None = None,
|
|
1163
2128
|
title: str | None = None,
|
|
1164
2129
|
description: str | None = None,
|
|
2130
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1165
2131
|
mime_type: str | None = None,
|
|
1166
2132
|
tags: set[str] | None = None,
|
|
1167
2133
|
enabled: bool | None = None,
|
|
1168
2134
|
annotations: Annotations | dict[str, Any] | None = None,
|
|
1169
2135
|
meta: dict[str, Any] | None = None,
|
|
2136
|
+
task: bool | TaskConfig | None = None,
|
|
1170
2137
|
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
|
1171
2138
|
"""Decorator to register a function as a resource.
|
|
1172
2139
|
|
|
@@ -1231,8 +2198,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1231
2198
|
)
|
|
1232
2199
|
|
|
1233
2200
|
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
|
|
1234
|
-
from fastmcp.server.context import Context
|
|
1235
|
-
|
|
1236
2201
|
if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
|
|
1237
2202
|
raise ValueError(
|
|
1238
2203
|
inspect.cleandoc(
|
|
@@ -1245,14 +2210,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1245
2210
|
)
|
|
1246
2211
|
)
|
|
1247
2212
|
|
|
2213
|
+
# Resolve task parameter
|
|
2214
|
+
supports_task: bool | TaskConfig = (
|
|
2215
|
+
task if task is not None else self._support_tasks_by_default
|
|
2216
|
+
)
|
|
2217
|
+
|
|
1248
2218
|
# Check if this should be a template
|
|
1249
2219
|
has_uri_params = "{" in uri and "}" in uri
|
|
1250
|
-
#
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
)
|
|
2220
|
+
# Use wrapper to check for user-facing parameters
|
|
2221
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
2222
|
+
|
|
2223
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
2224
|
+
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
|
|
1256
2225
|
|
|
1257
2226
|
if has_uri_params or has_func_params:
|
|
1258
2227
|
template = ResourceTemplate.from_function(
|
|
@@ -1261,11 +2230,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1261
2230
|
name=name,
|
|
1262
2231
|
title=title,
|
|
1263
2232
|
description=description,
|
|
2233
|
+
icons=icons,
|
|
1264
2234
|
mime_type=mime_type,
|
|
1265
2235
|
tags=tags,
|
|
1266
2236
|
enabled=enabled,
|
|
1267
|
-
annotations=
|
|
2237
|
+
annotations=annotations,
|
|
1268
2238
|
meta=meta,
|
|
2239
|
+
task=supports_task,
|
|
1269
2240
|
)
|
|
1270
2241
|
self.add_template(template)
|
|
1271
2242
|
return template
|
|
@@ -1276,11 +2247,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1276
2247
|
name=name,
|
|
1277
2248
|
title=title,
|
|
1278
2249
|
description=description,
|
|
2250
|
+
icons=icons,
|
|
1279
2251
|
mime_type=mime_type,
|
|
1280
2252
|
tags=tags,
|
|
1281
2253
|
enabled=enabled,
|
|
1282
|
-
annotations=
|
|
2254
|
+
annotations=annotations,
|
|
1283
2255
|
meta=meta,
|
|
2256
|
+
task=supports_task,
|
|
1284
2257
|
)
|
|
1285
2258
|
self.add_resource(resource)
|
|
1286
2259
|
return resource
|
|
@@ -1322,9 +2295,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1322
2295
|
name: str | None = None,
|
|
1323
2296
|
title: str | None = None,
|
|
1324
2297
|
description: str | None = None,
|
|
2298
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1325
2299
|
tags: set[str] | None = None,
|
|
1326
2300
|
enabled: bool | None = None,
|
|
1327
2301
|
meta: dict[str, Any] | None = None,
|
|
2302
|
+
task: bool | TaskConfig | None = None,
|
|
1328
2303
|
) -> FunctionPrompt: ...
|
|
1329
2304
|
|
|
1330
2305
|
@overload
|
|
@@ -1335,9 +2310,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1335
2310
|
name: str | None = None,
|
|
1336
2311
|
title: str | None = None,
|
|
1337
2312
|
description: str | None = None,
|
|
2313
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1338
2314
|
tags: set[str] | None = None,
|
|
1339
2315
|
enabled: bool | None = None,
|
|
1340
2316
|
meta: dict[str, Any] | None = None,
|
|
2317
|
+
task: bool | TaskConfig | None = None,
|
|
1341
2318
|
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
|
1342
2319
|
|
|
1343
2320
|
def prompt(
|
|
@@ -1347,9 +2324,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1347
2324
|
name: str | None = None,
|
|
1348
2325
|
title: str | None = None,
|
|
1349
2326
|
description: str | None = None,
|
|
2327
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1350
2328
|
tags: set[str] | None = None,
|
|
1351
2329
|
enabled: bool | None = None,
|
|
1352
2330
|
meta: dict[str, Any] | None = None,
|
|
2331
|
+
task: bool | TaskConfig | None = None,
|
|
1353
2332
|
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
|
1354
2333
|
"""Decorator to register a prompt.
|
|
1355
2334
|
|
|
@@ -1440,15 +2419,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1440
2419
|
fn = name_or_fn
|
|
1441
2420
|
prompt_name = name # Use keyword name if provided, otherwise None
|
|
1442
2421
|
|
|
2422
|
+
# Resolve task parameter
|
|
2423
|
+
supports_task: bool | TaskConfig = (
|
|
2424
|
+
task if task is not None else self._support_tasks_by_default
|
|
2425
|
+
)
|
|
2426
|
+
|
|
1443
2427
|
# Register the prompt immediately
|
|
1444
2428
|
prompt = Prompt.from_function(
|
|
1445
2429
|
fn=fn,
|
|
1446
2430
|
name=prompt_name,
|
|
1447
2431
|
title=title,
|
|
1448
2432
|
description=description,
|
|
2433
|
+
icons=icons,
|
|
1449
2434
|
tags=tags,
|
|
1450
2435
|
enabled=enabled,
|
|
1451
2436
|
meta=meta,
|
|
2437
|
+
task=supports_task,
|
|
1452
2438
|
)
|
|
1453
2439
|
self.add_prompt(prompt)
|
|
1454
2440
|
|
|
@@ -1476,9 +2462,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1476
2462
|
name=prompt_name,
|
|
1477
2463
|
title=title,
|
|
1478
2464
|
description=description,
|
|
2465
|
+
icons=icons,
|
|
1479
2466
|
tags=tags,
|
|
1480
2467
|
enabled=enabled,
|
|
1481
2468
|
meta=meta,
|
|
2469
|
+
task=task,
|
|
1482
2470
|
)
|
|
1483
2471
|
|
|
1484
2472
|
async def run_stdio_async(
|
|
@@ -1498,15 +2486,25 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1498
2486
|
)
|
|
1499
2487
|
|
|
1500
2488
|
with temporary_log_level(log_level):
|
|
1501
|
-
async with
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
)
|
|
1509
|
-
|
|
2489
|
+
async with self._lifespan_manager():
|
|
2490
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
2491
|
+
logger.info(
|
|
2492
|
+
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
# Build experimental capabilities
|
|
2496
|
+
experimental_capabilities = get_task_capabilities()
|
|
2497
|
+
|
|
2498
|
+
await self._mcp_server.run(
|
|
2499
|
+
read_stream,
|
|
2500
|
+
write_stream,
|
|
2501
|
+
self._mcp_server.create_initialization_options(
|
|
2502
|
+
notification_options=NotificationOptions(
|
|
2503
|
+
tools_changed=True
|
|
2504
|
+
),
|
|
2505
|
+
experimental_capabilities=experimental_capabilities,
|
|
2506
|
+
),
|
|
2507
|
+
)
|
|
1510
2508
|
|
|
1511
2509
|
async def run_http_async(
|
|
1512
2510
|
self,
|
|
@@ -1518,6 +2516,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1518
2516
|
path: str | None = None,
|
|
1519
2517
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1520
2518
|
middleware: list[ASGIMiddleware] | None = None,
|
|
2519
|
+
json_response: bool | None = None,
|
|
1521
2520
|
stateless_http: bool | None = None,
|
|
1522
2521
|
) -> None:
|
|
1523
2522
|
"""Run the server using HTTP transport.
|
|
@@ -1530,6 +2529,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1530
2529
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1531
2530
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1532
2531
|
middleware: A list of middleware to apply to the app
|
|
2532
|
+
json_response: Whether to use JSON response format (defaults to settings.json_response)
|
|
1533
2533
|
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
1534
2534
|
"""
|
|
1535
2535
|
host = host or self._deprecated_settings.host
|
|
@@ -1542,6 +2542,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1542
2542
|
path=path,
|
|
1543
2543
|
transport=transport,
|
|
1544
2544
|
middleware=middleware,
|
|
2545
|
+
json_response=json_response,
|
|
1545
2546
|
stateless_http=stateless_http,
|
|
1546
2547
|
)
|
|
1547
2548
|
|
|
@@ -1561,106 +2562,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1561
2562
|
port=port,
|
|
1562
2563
|
path=server_path,
|
|
1563
2564
|
)
|
|
1564
|
-
|
|
2565
|
+
uvicorn_config_from_user = uvicorn_config or {}
|
|
1565
2566
|
|
|
1566
2567
|
config_kwargs: dict[str, Any] = {
|
|
1567
2568
|
"timeout_graceful_shutdown": 0,
|
|
1568
2569
|
"lifespan": "on",
|
|
2570
|
+
"ws": "websockets-sansio",
|
|
1569
2571
|
}
|
|
1570
|
-
config_kwargs.update(
|
|
2572
|
+
config_kwargs.update(uvicorn_config_from_user)
|
|
1571
2573
|
|
|
1572
2574
|
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
|
|
1573
2575
|
config_kwargs["log_level"] = default_log_level_to_use
|
|
1574
2576
|
|
|
1575
2577
|
with temporary_log_level(log_level):
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
await server.serve()
|
|
1584
|
-
|
|
1585
|
-
async def run_sse_async(
|
|
1586
|
-
self,
|
|
1587
|
-
host: str | None = None,
|
|
1588
|
-
port: int | None = None,
|
|
1589
|
-
log_level: str | None = None,
|
|
1590
|
-
path: str | None = None,
|
|
1591
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
1592
|
-
) -> None:
|
|
1593
|
-
"""Run the server using SSE transport."""
|
|
1594
|
-
|
|
1595
|
-
# Deprecated since 2.3.2
|
|
1596
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1597
|
-
warnings.warn(
|
|
1598
|
-
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
1599
|
-
"modern (non-SSE) alternative, or create an SSE app with "
|
|
1600
|
-
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
1601
|
-
DeprecationWarning,
|
|
1602
|
-
stacklevel=2,
|
|
1603
|
-
)
|
|
1604
|
-
await self.run_http_async(
|
|
1605
|
-
transport="sse",
|
|
1606
|
-
host=host,
|
|
1607
|
-
port=port,
|
|
1608
|
-
log_level=log_level,
|
|
1609
|
-
path=path,
|
|
1610
|
-
uvicorn_config=uvicorn_config,
|
|
1611
|
-
)
|
|
1612
|
-
|
|
1613
|
-
def sse_app(
|
|
1614
|
-
self,
|
|
1615
|
-
path: str | None = None,
|
|
1616
|
-
message_path: str | None = None,
|
|
1617
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
1618
|
-
) -> StarletteWithLifespan:
|
|
1619
|
-
"""
|
|
1620
|
-
Create a Starlette app for the SSE server.
|
|
1621
|
-
|
|
1622
|
-
Args:
|
|
1623
|
-
path: The path to the SSE endpoint
|
|
1624
|
-
message_path: The path to the message endpoint
|
|
1625
|
-
middleware: A list of middleware to apply to the app
|
|
1626
|
-
"""
|
|
1627
|
-
# Deprecated since 2.3.2
|
|
1628
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1629
|
-
warnings.warn(
|
|
1630
|
-
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
1631
|
-
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
1632
|
-
DeprecationWarning,
|
|
1633
|
-
stacklevel=2,
|
|
1634
|
-
)
|
|
1635
|
-
return create_sse_app(
|
|
1636
|
-
server=self,
|
|
1637
|
-
message_path=message_path or self._deprecated_settings.message_path,
|
|
1638
|
-
sse_path=path or self._deprecated_settings.sse_path,
|
|
1639
|
-
auth=self.auth,
|
|
1640
|
-
debug=self._deprecated_settings.debug,
|
|
1641
|
-
middleware=middleware,
|
|
1642
|
-
)
|
|
1643
|
-
|
|
1644
|
-
def streamable_http_app(
|
|
1645
|
-
self,
|
|
1646
|
-
path: str | None = None,
|
|
1647
|
-
middleware: list[ASGIMiddleware] | None = None,
|
|
1648
|
-
) -> StarletteWithLifespan:
|
|
1649
|
-
"""
|
|
1650
|
-
Create a Starlette app for the StreamableHTTP server.
|
|
2578
|
+
async with self._lifespan_manager():
|
|
2579
|
+
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
2580
|
+
server = uvicorn.Server(config)
|
|
2581
|
+
path = app.state.path.lstrip("/") # type: ignore
|
|
2582
|
+
logger.info(
|
|
2583
|
+
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
2584
|
+
)
|
|
1651
2585
|
|
|
1652
|
-
|
|
1653
|
-
path: The path to the StreamableHTTP endpoint
|
|
1654
|
-
middleware: A list of middleware to apply to the app
|
|
1655
|
-
"""
|
|
1656
|
-
# Deprecated since 2.3.2
|
|
1657
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1658
|
-
warnings.warn(
|
|
1659
|
-
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
1660
|
-
DeprecationWarning,
|
|
1661
|
-
stacklevel=2,
|
|
1662
|
-
)
|
|
1663
|
-
return self.http_app(path=path, middleware=middleware)
|
|
2586
|
+
await server.serve()
|
|
1664
2587
|
|
|
1665
2588
|
def http_app(
|
|
1666
2589
|
self,
|
|
@@ -1669,13 +2592,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1669
2592
|
json_response: bool | None = None,
|
|
1670
2593
|
stateless_http: bool | None = None,
|
|
1671
2594
|
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
2595
|
+
event_store: EventStore | None = None,
|
|
2596
|
+
retry_interval: int | None = None,
|
|
1672
2597
|
) -> StarletteWithLifespan:
|
|
1673
2598
|
"""Create a Starlette app using the specified HTTP transport.
|
|
1674
2599
|
|
|
1675
2600
|
Args:
|
|
1676
2601
|
path: The path for the HTTP endpoint
|
|
1677
2602
|
middleware: A list of middleware to apply to the app
|
|
1678
|
-
|
|
2603
|
+
json_response: Whether to use JSON response format
|
|
2604
|
+
stateless_http: Whether to use stateless mode (new transport per request)
|
|
2605
|
+
transport: Transport protocol to use - "http", "streamable-http", or "sse"
|
|
2606
|
+
event_store: Optional event store for SSE polling/resumability. When set,
|
|
2607
|
+
enables clients to reconnect and resume receiving events after
|
|
2608
|
+
server-initiated disconnections. Only used with streamable-http transport.
|
|
2609
|
+
retry_interval: Optional retry interval in milliseconds for SSE polling.
|
|
2610
|
+
Controls how quickly clients should reconnect after server-initiated
|
|
2611
|
+
disconnections. Requires event_store to be set. Only used with
|
|
2612
|
+
streamable-http transport.
|
|
1679
2613
|
|
|
1680
2614
|
Returns:
|
|
1681
2615
|
A Starlette application configured with the specified transport
|
|
@@ -1686,7 +2620,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1686
2620
|
server=self,
|
|
1687
2621
|
streamable_http_path=path
|
|
1688
2622
|
or self._deprecated_settings.streamable_http_path,
|
|
1689
|
-
event_store=
|
|
2623
|
+
event_store=event_store,
|
|
2624
|
+
retry_interval=retry_interval,
|
|
1690
2625
|
auth=self.auth,
|
|
1691
2626
|
json_response=(
|
|
1692
2627
|
json_response
|
|
@@ -1711,40 +2646,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1711
2646
|
middleware=middleware,
|
|
1712
2647
|
)
|
|
1713
2648
|
|
|
1714
|
-
async def run_streamable_http_async(
|
|
1715
|
-
self,
|
|
1716
|
-
host: str | None = None,
|
|
1717
|
-
port: int | None = None,
|
|
1718
|
-
log_level: str | None = None,
|
|
1719
|
-
path: str | None = None,
|
|
1720
|
-
uvicorn_config: dict[str, Any] | None = None,
|
|
1721
|
-
) -> None:
|
|
1722
|
-
# Deprecated since 2.3.2
|
|
1723
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1724
|
-
warnings.warn(
|
|
1725
|
-
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
1726
|
-
"Use run_http_async instead.",
|
|
1727
|
-
DeprecationWarning,
|
|
1728
|
-
stacklevel=2,
|
|
1729
|
-
)
|
|
1730
|
-
await self.run_http_async(
|
|
1731
|
-
transport="http",
|
|
1732
|
-
host=host,
|
|
1733
|
-
port=port,
|
|
1734
|
-
log_level=log_level,
|
|
1735
|
-
path=path,
|
|
1736
|
-
uvicorn_config=uvicorn_config,
|
|
1737
|
-
)
|
|
1738
|
-
|
|
1739
2649
|
def mount(
|
|
1740
2650
|
self,
|
|
1741
2651
|
server: FastMCP[LifespanResultT],
|
|
1742
2652
|
prefix: str | None = None,
|
|
1743
2653
|
as_proxy: bool | None = None,
|
|
1744
|
-
*,
|
|
1745
|
-
tool_separator: str | None = None,
|
|
1746
|
-
resource_separator: str | None = None,
|
|
1747
|
-
prompt_separator: str | None = None,
|
|
1748
2654
|
) -> None:
|
|
1749
2655
|
"""Mount another FastMCP server on this server with an optional prefix.
|
|
1750
2656
|
|
|
@@ -1789,82 +2695,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1789
2695
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
1790
2696
|
automatically determined based on whether the server has a custom lifespan
|
|
1791
2697
|
(True if it has a custom lifespan, False otherwise).
|
|
1792
|
-
tool_separator: Deprecated. Separator character for tool names.
|
|
1793
|
-
resource_separator: Deprecated. Separator character for resource URIs.
|
|
1794
|
-
prompt_separator: Deprecated. Separator character for prompt names.
|
|
1795
2698
|
"""
|
|
1796
2699
|
from fastmcp.server.proxy import FastMCPProxy
|
|
1797
2700
|
|
|
1798
|
-
# Deprecated since 2.9.0
|
|
1799
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1800
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
1801
|
-
# is optional.
|
|
1802
|
-
if isinstance(server, str):
|
|
1803
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1804
|
-
warnings.warn(
|
|
1805
|
-
"Mount prefixes are now optional and the first positional argument "
|
|
1806
|
-
"should be the server you want to mount.",
|
|
1807
|
-
DeprecationWarning,
|
|
1808
|
-
stacklevel=2,
|
|
1809
|
-
)
|
|
1810
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1811
|
-
|
|
1812
|
-
if tool_separator is not None:
|
|
1813
|
-
# Deprecated since 2.4.0
|
|
1814
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1815
|
-
warnings.warn(
|
|
1816
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1817
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1818
|
-
DeprecationWarning,
|
|
1819
|
-
stacklevel=2,
|
|
1820
|
-
)
|
|
1821
|
-
|
|
1822
|
-
if resource_separator is not None:
|
|
1823
|
-
# Deprecated since 2.4.0
|
|
1824
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1825
|
-
warnings.warn(
|
|
1826
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
1827
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1828
|
-
DeprecationWarning,
|
|
1829
|
-
stacklevel=2,
|
|
1830
|
-
)
|
|
1831
|
-
|
|
1832
|
-
if prompt_separator is not None:
|
|
1833
|
-
# Deprecated since 2.4.0
|
|
1834
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1835
|
-
warnings.warn(
|
|
1836
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1837
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1838
|
-
DeprecationWarning,
|
|
1839
|
-
stacklevel=2,
|
|
1840
|
-
)
|
|
1841
|
-
|
|
1842
2701
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1843
2702
|
# we should treat it as a proxy
|
|
1844
2703
|
if as_proxy is None:
|
|
1845
|
-
as_proxy = server.
|
|
2704
|
+
as_proxy = server._lifespan != default_lifespan
|
|
1846
2705
|
|
|
1847
2706
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1848
2707
|
server = FastMCP.as_proxy(server)
|
|
1849
2708
|
|
|
2709
|
+
# Mark the server as mounted so it skips creating its own Docket/Worker.
|
|
2710
|
+
# The parent's Docket handles task execution, avoiding race conditions
|
|
2711
|
+
# with multiple workers competing for tasks from the same queue.
|
|
2712
|
+
server._is_mounted = True
|
|
2713
|
+
|
|
1850
2714
|
# Delegate mounting to all three managers
|
|
1851
2715
|
mounted_server = MountedServer(
|
|
1852
2716
|
prefix=prefix,
|
|
1853
2717
|
server=server,
|
|
1854
|
-
resource_prefix_format=self.resource_prefix_format,
|
|
1855
2718
|
)
|
|
1856
2719
|
self._mounted_servers.append(mounted_server)
|
|
1857
|
-
self._tool_manager.mount(mounted_server)
|
|
1858
|
-
self._resource_manager.mount(mounted_server)
|
|
1859
|
-
self._prompt_manager.mount(mounted_server)
|
|
1860
2720
|
|
|
1861
2721
|
async def import_server(
|
|
1862
2722
|
self,
|
|
1863
2723
|
server: FastMCP[LifespanResultT],
|
|
1864
2724
|
prefix: str | None = None,
|
|
1865
|
-
tool_separator: str | None = None,
|
|
1866
|
-
resource_separator: str | None = None,
|
|
1867
|
-
prompt_separator: str | None = None,
|
|
1868
2725
|
) -> None:
|
|
1869
2726
|
"""
|
|
1870
2727
|
Import the MCP objects from another FastMCP server into this one,
|
|
@@ -1896,56 +2753,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1896
2753
|
server: The FastMCP server to import
|
|
1897
2754
|
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
1898
2755
|
objects are imported with their original names.
|
|
1899
|
-
|
|
1900
|
-
resource_separator: Deprecated and ignored. Prefix is now
|
|
1901
|
-
applied using the protocol://prefix/path format
|
|
1902
|
-
prompt_separator: Deprecated. Separator for prompt names.
|
|
1903
|
-
"""
|
|
1904
|
-
|
|
1905
|
-
# Deprecated since 2.9.0
|
|
1906
|
-
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1907
|
-
# second was the server. Here we swap them if needed now that the prefix
|
|
1908
|
-
# is optional.
|
|
1909
|
-
if isinstance(server, str):
|
|
1910
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1911
|
-
warnings.warn(
|
|
1912
|
-
"Import prefixes are now optional and the first positional argument "
|
|
1913
|
-
"should be the server you want to import.",
|
|
1914
|
-
DeprecationWarning,
|
|
1915
|
-
stacklevel=2,
|
|
1916
|
-
)
|
|
1917
|
-
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1918
|
-
|
|
1919
|
-
if tool_separator is not None:
|
|
1920
|
-
# Deprecated since 2.4.0
|
|
1921
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1922
|
-
warnings.warn(
|
|
1923
|
-
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1924
|
-
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1925
|
-
DeprecationWarning,
|
|
1926
|
-
stacklevel=2,
|
|
1927
|
-
)
|
|
1928
|
-
|
|
1929
|
-
if resource_separator is not None:
|
|
1930
|
-
# Deprecated since 2.4.0
|
|
1931
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1932
|
-
warnings.warn(
|
|
1933
|
-
"The resource_separator parameter is deprecated and ignored. "
|
|
1934
|
-
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1935
|
-
DeprecationWarning,
|
|
1936
|
-
stacklevel=2,
|
|
1937
|
-
)
|
|
1938
|
-
|
|
1939
|
-
if prompt_separator is not None:
|
|
1940
|
-
# Deprecated since 2.4.0
|
|
1941
|
-
if fastmcp.settings.deprecation_warnings:
|
|
1942
|
-
warnings.warn(
|
|
1943
|
-
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1944
|
-
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1945
|
-
DeprecationWarning,
|
|
1946
|
-
stacklevel=2,
|
|
1947
|
-
)
|
|
1948
|
-
|
|
2756
|
+
"""
|
|
1949
2757
|
# Import tools from the server
|
|
1950
2758
|
for key, tool in (await server.get_tools()).items():
|
|
1951
2759
|
if prefix:
|
|
@@ -1955,9 +2763,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1955
2763
|
# Import resources and templates from the server
|
|
1956
2764
|
for key, resource in (await server.get_resources()).items():
|
|
1957
2765
|
if prefix:
|
|
1958
|
-
resource_key = add_resource_prefix(
|
|
1959
|
-
key, prefix, self.resource_prefix_format
|
|
1960
|
-
)
|
|
2766
|
+
resource_key = add_resource_prefix(key, prefix)
|
|
1961
2767
|
resource = resource.model_copy(
|
|
1962
2768
|
update={"name": f"{prefix}_{resource.name}"}, key=resource_key
|
|
1963
2769
|
)
|
|
@@ -1965,9 +2771,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1965
2771
|
|
|
1966
2772
|
for key, template in (await server.get_resource_templates()).items():
|
|
1967
2773
|
if prefix:
|
|
1968
|
-
template_key = add_resource_prefix(
|
|
1969
|
-
key, prefix, self.resource_prefix_format
|
|
1970
|
-
)
|
|
2774
|
+
template_key = add_resource_prefix(key, prefix)
|
|
1971
2775
|
template = template.model_copy(
|
|
1972
2776
|
update={"name": f"{prefix}_{template.name}"}, key=template_key
|
|
1973
2777
|
)
|
|
@@ -1979,6 +2783,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1979
2783
|
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1980
2784
|
self._prompt_manager.add_prompt(prompt)
|
|
1981
2785
|
|
|
2786
|
+
if server._lifespan != default_lifespan:
|
|
2787
|
+
from warnings import warn
|
|
2788
|
+
|
|
2789
|
+
warn(
|
|
2790
|
+
message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
|
|
2791
|
+
category=RuntimeWarning,
|
|
2792
|
+
stacklevel=2,
|
|
2793
|
+
)
|
|
2794
|
+
|
|
1982
2795
|
if prefix:
|
|
1983
2796
|
logger.debug(
|
|
1984
2797
|
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|
|
@@ -1991,66 +2804,46 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1991
2804
|
cls,
|
|
1992
2805
|
openapi_spec: dict[str, Any],
|
|
1993
2806
|
client: httpx.AsyncClient,
|
|
1994
|
-
route_maps: list[RouteMap] |
|
|
1995
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
1996
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2807
|
+
route_maps: list[RouteMap] | None = None,
|
|
2808
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2809
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
1997
2810
|
mcp_names: dict[str, str] | None = None,
|
|
1998
2811
|
tags: set[str] | None = None,
|
|
1999
2812
|
**settings: Any,
|
|
2000
|
-
) -> FastMCPOpenAPI
|
|
2813
|
+
) -> FastMCPOpenAPI:
|
|
2001
2814
|
"""
|
|
2002
2815
|
Create a FastMCP server from an OpenAPI specification.
|
|
2003
2816
|
"""
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
tags=tags,
|
|
2017
|
-
**settings,
|
|
2018
|
-
)
|
|
2019
|
-
else:
|
|
2020
|
-
logger.info(
|
|
2021
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2022
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2023
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2024
|
-
)
|
|
2025
|
-
from .openapi import FastMCPOpenAPI
|
|
2026
|
-
|
|
2027
|
-
return FastMCPOpenAPI(
|
|
2028
|
-
openapi_spec=openapi_spec,
|
|
2029
|
-
client=client,
|
|
2030
|
-
route_maps=cast(Any, route_maps),
|
|
2031
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2032
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2033
|
-
mcp_names=mcp_names,
|
|
2034
|
-
tags=tags,
|
|
2035
|
-
**settings,
|
|
2036
|
-
)
|
|
2817
|
+
from .openapi import FastMCPOpenAPI
|
|
2818
|
+
|
|
2819
|
+
return FastMCPOpenAPI(
|
|
2820
|
+
openapi_spec=openapi_spec,
|
|
2821
|
+
client=client,
|
|
2822
|
+
route_maps=route_maps,
|
|
2823
|
+
route_map_fn=route_map_fn,
|
|
2824
|
+
mcp_component_fn=mcp_component_fn,
|
|
2825
|
+
mcp_names=mcp_names,
|
|
2826
|
+
tags=tags,
|
|
2827
|
+
**settings,
|
|
2828
|
+
)
|
|
2037
2829
|
|
|
2038
2830
|
@classmethod
|
|
2039
2831
|
def from_fastapi(
|
|
2040
2832
|
cls,
|
|
2041
2833
|
app: Any,
|
|
2042
2834
|
name: str | None = None,
|
|
2043
|
-
route_maps: list[RouteMap] |
|
|
2044
|
-
route_map_fn: OpenAPIRouteMapFn |
|
|
2045
|
-
mcp_component_fn: OpenAPIComponentFn |
|
|
2835
|
+
route_maps: list[RouteMap] | None = None,
|
|
2836
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
2837
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
2046
2838
|
mcp_names: dict[str, str] | None = None,
|
|
2047
2839
|
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
2048
2840
|
tags: set[str] | None = None,
|
|
2049
2841
|
**settings: Any,
|
|
2050
|
-
) -> FastMCPOpenAPI
|
|
2842
|
+
) -> FastMCPOpenAPI:
|
|
2051
2843
|
"""
|
|
2052
2844
|
Create a FastMCP server from a FastAPI application.
|
|
2053
2845
|
"""
|
|
2846
|
+
from .openapi import FastMCPOpenAPI
|
|
2054
2847
|
|
|
2055
2848
|
if httpx_client_kwargs is None:
|
|
2056
2849
|
httpx_client_kwargs = {}
|
|
@@ -2063,40 +2856,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2063
2856
|
|
|
2064
2857
|
name = name or app.title
|
|
2065
2858
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
mcp_names=mcp_names,
|
|
2078
|
-
tags=tags,
|
|
2079
|
-
**settings,
|
|
2080
|
-
)
|
|
2081
|
-
else:
|
|
2082
|
-
logger.info(
|
|
2083
|
-
"Using legacy OpenAPI parser. To use the new parser, set "
|
|
2084
|
-
"FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
|
|
2085
|
-
"was introduced for testing in 2.11 and will become the default soon."
|
|
2086
|
-
)
|
|
2087
|
-
from .openapi import FastMCPOpenAPI
|
|
2088
|
-
|
|
2089
|
-
return FastMCPOpenAPI(
|
|
2090
|
-
openapi_spec=app.openapi(),
|
|
2091
|
-
client=client,
|
|
2092
|
-
name=name,
|
|
2093
|
-
route_maps=cast(Any, route_maps),
|
|
2094
|
-
route_map_fn=cast(Any, route_map_fn),
|
|
2095
|
-
mcp_component_fn=cast(Any, mcp_component_fn),
|
|
2096
|
-
mcp_names=mcp_names,
|
|
2097
|
-
tags=tags,
|
|
2098
|
-
**settings,
|
|
2099
|
-
)
|
|
2859
|
+
return FastMCPOpenAPI(
|
|
2860
|
+
openapi_spec=app.openapi(),
|
|
2861
|
+
client=client,
|
|
2862
|
+
name=name,
|
|
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
|
+
)
|
|
2100
2870
|
|
|
2101
2871
|
@classmethod
|
|
2102
2872
|
def as_proxy(
|
|
@@ -2105,6 +2875,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2105
2875
|
Client[ClientTransportT]
|
|
2106
2876
|
| ClientTransport
|
|
2107
2877
|
| FastMCP[Any]
|
|
2878
|
+
| FastMCP1Server
|
|
2108
2879
|
| AnyUrl
|
|
2109
2880
|
| Path
|
|
2110
2881
|
| MCPConfig
|
|
@@ -2129,8 +2900,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2129
2900
|
# - Connected clients: reuse existing session for all requests
|
|
2130
2901
|
# - Disconnected clients: create fresh sessions per request for isolation
|
|
2131
2902
|
if client.is_connected():
|
|
2132
|
-
|
|
2133
|
-
|
|
2903
|
+
proxy_logger = get_logger(__name__)
|
|
2904
|
+
proxy_logger.info(
|
|
2134
2905
|
"Proxy detected connected client - reusing existing session for all requests. "
|
|
2135
2906
|
"This may cause context mixing in concurrent scenarios."
|
|
2136
2907
|
)
|
|
@@ -2147,7 +2918,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2147
2918
|
|
|
2148
2919
|
client_factory = fresh_client_factory
|
|
2149
2920
|
else:
|
|
2150
|
-
base_client = ProxyClient(backend)
|
|
2921
|
+
base_client = ProxyClient(backend) # type: ignore
|
|
2151
2922
|
|
|
2152
2923
|
# Fresh client created from transport - use fresh sessions per request
|
|
2153
2924
|
def proxy_client_factory():
|
|
@@ -2157,23 +2928,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2157
2928
|
|
|
2158
2929
|
return FastMCPProxy(client_factory=client_factory, **settings)
|
|
2159
2930
|
|
|
2160
|
-
@classmethod
|
|
2161
|
-
def from_client(
|
|
2162
|
-
cls, client: Client[ClientTransportT], **settings: Any
|
|
2163
|
-
) -> FastMCPProxy:
|
|
2164
|
-
"""
|
|
2165
|
-
Create a FastMCP proxy server from a FastMCP client.
|
|
2166
|
-
"""
|
|
2167
|
-
# Deprecated since 2.3.5
|
|
2168
|
-
if fastmcp.settings.deprecation_warnings:
|
|
2169
|
-
warnings.warn(
|
|
2170
|
-
"FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
|
|
2171
|
-
DeprecationWarning,
|
|
2172
|
-
stacklevel=2,
|
|
2173
|
-
)
|
|
2174
|
-
|
|
2175
|
-
return cls.as_proxy(client, **settings)
|
|
2176
|
-
|
|
2177
2931
|
def _should_enable_component(
|
|
2178
2932
|
self,
|
|
2179
2933
|
component: FastMCPComponent,
|
|
@@ -2202,10 +2956,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2202
2956
|
return False
|
|
2203
2957
|
|
|
2204
2958
|
if self.include_tags is not None:
|
|
2205
|
-
|
|
2206
|
-
return True
|
|
2207
|
-
else:
|
|
2208
|
-
return False
|
|
2959
|
+
return bool(any(itag in component.tags for itag in self.include_tags))
|
|
2209
2960
|
|
|
2210
2961
|
return True
|
|
2211
2962
|
|
|
@@ -2223,13 +2974,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2223
2974
|
class MountedServer:
|
|
2224
2975
|
prefix: str | None
|
|
2225
2976
|
server: FastMCP[Any]
|
|
2226
|
-
resource_prefix_format: Literal["protocol", "path"] | None = None
|
|
2227
2977
|
|
|
2228
2978
|
|
|
2229
|
-
def add_resource_prefix(
|
|
2230
|
-
|
|
2231
|
-
) -> str:
|
|
2232
|
-
"""Add a prefix to a resource URI.
|
|
2979
|
+
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
2980
|
+
"""Add a prefix to a resource URI using path formatting (resource://prefix/path).
|
|
2233
2981
|
|
|
2234
2982
|
Args:
|
|
2235
2983
|
uri: The original resource URI
|
|
@@ -2239,16 +2987,10 @@ def add_resource_prefix(
|
|
|
2239
2987
|
The resource URI with the prefix added
|
|
2240
2988
|
|
|
2241
2989
|
Examples:
|
|
2242
|
-
With new style:
|
|
2243
2990
|
```python
|
|
2244
2991
|
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2245
2992
|
"resource://prefix/path/to/resource"
|
|
2246
2993
|
```
|
|
2247
|
-
With legacy style:
|
|
2248
|
-
```python
|
|
2249
|
-
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
2250
|
-
"prefix+resource://path/to/resource"
|
|
2251
|
-
```
|
|
2252
2994
|
With absolute path:
|
|
2253
2995
|
```python
|
|
2254
2996
|
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
@@ -2261,54 +3003,32 @@ def add_resource_prefix(
|
|
|
2261
3003
|
if not prefix:
|
|
2262
3004
|
return uri
|
|
2263
3005
|
|
|
2264
|
-
#
|
|
2265
|
-
|
|
2266
|
-
if
|
|
2267
|
-
|
|
3006
|
+
# Split the URI into protocol and path
|
|
3007
|
+
match = URI_PATTERN.match(uri)
|
|
3008
|
+
if not match:
|
|
3009
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2268
3010
|
|
|
2269
|
-
|
|
2270
|
-
# Legacy style: prefix+protocol://path
|
|
2271
|
-
return f"{prefix}+{uri}"
|
|
2272
|
-
elif prefix_format == "path":
|
|
2273
|
-
# New style: protocol://prefix/path
|
|
2274
|
-
# Split the URI into protocol and path
|
|
2275
|
-
match = URI_PATTERN.match(uri)
|
|
2276
|
-
if not match:
|
|
2277
|
-
raise ValueError(
|
|
2278
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2279
|
-
)
|
|
3011
|
+
protocol, path = match.groups()
|
|
2280
3012
|
|
|
2281
|
-
|
|
3013
|
+
# Add the prefix to the path
|
|
3014
|
+
return f"{protocol}{prefix}/{path}"
|
|
2282
3015
|
|
|
2283
|
-
# Add the prefix to the path
|
|
2284
|
-
return f"{protocol}{prefix}/{path}"
|
|
2285
|
-
else:
|
|
2286
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
2287
3016
|
|
|
2288
|
-
|
|
2289
|
-
def remove_resource_prefix(
|
|
2290
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2291
|
-
) -> str:
|
|
3017
|
+
def remove_resource_prefix(uri: str, prefix: str) -> str:
|
|
2292
3018
|
"""Remove a prefix from a resource URI.
|
|
2293
3019
|
|
|
2294
3020
|
Args:
|
|
2295
3021
|
uri: The resource URI with a prefix
|
|
2296
3022
|
prefix: The prefix to remove
|
|
2297
|
-
|
|
3023
|
+
|
|
2298
3024
|
Returns:
|
|
2299
3025
|
The resource URI with the prefix removed
|
|
2300
3026
|
|
|
2301
3027
|
Examples:
|
|
2302
|
-
With new style:
|
|
2303
3028
|
```python
|
|
2304
3029
|
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2305
3030
|
"resource://path/to/resource"
|
|
2306
3031
|
```
|
|
2307
|
-
With legacy style:
|
|
2308
|
-
```python
|
|
2309
|
-
remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2310
|
-
"resource://path/to/resource"
|
|
2311
|
-
```
|
|
2312
3032
|
With absolute path:
|
|
2313
3033
|
```python
|
|
2314
3034
|
remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
@@ -2321,41 +3041,24 @@ def remove_resource_prefix(
|
|
|
2321
3041
|
if not prefix:
|
|
2322
3042
|
return uri
|
|
2323
3043
|
|
|
2324
|
-
|
|
2325
|
-
|
|
3044
|
+
# Split the URI into protocol and path
|
|
3045
|
+
match = URI_PATTERN.match(uri)
|
|
3046
|
+
if not match:
|
|
3047
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2326
3048
|
|
|
2327
|
-
|
|
2328
|
-
# Legacy style: prefix+protocol://path
|
|
2329
|
-
legacy_prefix = f"{prefix}+"
|
|
2330
|
-
if uri.startswith(legacy_prefix):
|
|
2331
|
-
return uri[len(legacy_prefix) :]
|
|
2332
|
-
return uri
|
|
2333
|
-
elif prefix_format == "path":
|
|
2334
|
-
# New style: protocol://prefix/path
|
|
2335
|
-
# Split the URI into protocol and path
|
|
2336
|
-
match = URI_PATTERN.match(uri)
|
|
2337
|
-
if not match:
|
|
2338
|
-
raise ValueError(
|
|
2339
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2340
|
-
)
|
|
2341
|
-
|
|
2342
|
-
protocol, path = match.groups()
|
|
3049
|
+
protocol, path = match.groups()
|
|
2343
3050
|
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
3051
|
+
# Check if the path starts with the prefix followed by a /
|
|
3052
|
+
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
3053
|
+
path_match = re.match(prefix_pattern, path)
|
|
3054
|
+
if not path_match:
|
|
3055
|
+
return uri
|
|
2349
3056
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
else:
|
|
2353
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3057
|
+
# Return the URI without the prefix
|
|
3058
|
+
return f"{protocol}{path_match.group(1)}"
|
|
2354
3059
|
|
|
2355
3060
|
|
|
2356
|
-
def has_resource_prefix(
|
|
2357
|
-
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
2358
|
-
) -> bool:
|
|
3061
|
+
def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
2359
3062
|
"""Check if a resource URI has a specific prefix.
|
|
2360
3063
|
|
|
2361
3064
|
Args:
|
|
@@ -2366,16 +3069,10 @@ def has_resource_prefix(
|
|
|
2366
3069
|
True if the URI has the specified prefix, False otherwise
|
|
2367
3070
|
|
|
2368
3071
|
Examples:
|
|
2369
|
-
With new style:
|
|
2370
3072
|
```python
|
|
2371
3073
|
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2372
3074
|
True
|
|
2373
3075
|
```
|
|
2374
|
-
With legacy style:
|
|
2375
|
-
```python
|
|
2376
|
-
has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2377
|
-
True
|
|
2378
|
-
```
|
|
2379
3076
|
With other path:
|
|
2380
3077
|
```python
|
|
2381
3078
|
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
@@ -2388,28 +3085,13 @@ def has_resource_prefix(
|
|
|
2388
3085
|
if not prefix:
|
|
2389
3086
|
return False
|
|
2390
3087
|
|
|
2391
|
-
#
|
|
2392
|
-
|
|
2393
|
-
if
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
if prefix_format == "protocol":
|
|
2397
|
-
# Legacy style: prefix+protocol://path
|
|
2398
|
-
legacy_prefix = f"{prefix}+"
|
|
2399
|
-
return uri.startswith(legacy_prefix)
|
|
2400
|
-
elif prefix_format == "path":
|
|
2401
|
-
# New style: protocol://prefix/path
|
|
2402
|
-
# Split the URI into protocol and path
|
|
2403
|
-
match = URI_PATTERN.match(uri)
|
|
2404
|
-
if not match:
|
|
2405
|
-
raise ValueError(
|
|
2406
|
-
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
2407
|
-
)
|
|
3088
|
+
# Split the URI into protocol and path
|
|
3089
|
+
match = URI_PATTERN.match(uri)
|
|
3090
|
+
if not match:
|
|
3091
|
+
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
|
2408
3092
|
|
|
2409
|
-
|
|
3093
|
+
_, path = match.groups()
|
|
2410
3094
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
else:
|
|
2415
|
-
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
3095
|
+
# Check if the path starts with the prefix followed by a /
|
|
3096
|
+
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
3097
|
+
return bool(re.match(prefix_pattern, path))
|