fastmcp 2.12.4__py3-none-any.whl → 2.13.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/cli/cli.py +7 -6
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/transports.py +77 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1256 -242
- fastmcp/server/auth/oidc_proxy.py +23 -6
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +178 -127
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +30 -9
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +32 -14
- fastmcp/server/context.py +122 -36
- fastmcp/server/http.py +58 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +683 -207
- fastmcp/settings.py +24 -10
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +62 -22
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +617 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -7,7 +7,14 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import secrets
|
|
9
9
|
import warnings
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import (
|
|
11
|
+
AsyncIterator,
|
|
12
|
+
Awaitable,
|
|
13
|
+
Callable,
|
|
14
|
+
Collection,
|
|
15
|
+
Mapping,
|
|
16
|
+
Sequence,
|
|
17
|
+
)
|
|
11
18
|
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
|
|
12
19
|
from dataclasses import dataclass
|
|
13
20
|
from functools import partial
|
|
@@ -43,9 +50,11 @@ import fastmcp
|
|
|
43
50
|
import fastmcp.server
|
|
44
51
|
from fastmcp.exceptions import DisabledError, NotFoundError
|
|
45
52
|
from fastmcp.mcp_config import MCPConfig
|
|
46
|
-
from fastmcp.prompts import Prompt
|
|
53
|
+
from fastmcp.prompts import Prompt
|
|
47
54
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
48
|
-
from fastmcp.
|
|
55
|
+
from fastmcp.prompts.prompt_manager import PromptManager
|
|
56
|
+
from fastmcp.resources.resource import Resource
|
|
57
|
+
from fastmcp.resources.resource_manager import ResourceManager
|
|
49
58
|
from fastmcp.resources.template import ResourceTemplate
|
|
50
59
|
from fastmcp.server.auth import AuthProvider
|
|
51
60
|
from fastmcp.server.http import (
|
|
@@ -56,8 +65,8 @@ from fastmcp.server.http import (
|
|
|
56
65
|
from fastmcp.server.low_level import LowLevelServer
|
|
57
66
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
58
67
|
from fastmcp.settings import Settings
|
|
59
|
-
from fastmcp.tools import ToolManager
|
|
60
68
|
from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
|
|
69
|
+
from fastmcp.tools.tool_manager import ToolManager
|
|
61
70
|
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
62
71
|
from fastmcp.utilities.cli import log_server_banner
|
|
63
72
|
from fastmcp.utilities.components import FastMCPComponent
|
|
@@ -66,7 +75,6 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
66
75
|
|
|
67
76
|
if TYPE_CHECKING:
|
|
68
77
|
from fastmcp.client import Client
|
|
69
|
-
from fastmcp.client.sampling import ServerSamplingHandler
|
|
70
78
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
71
79
|
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
72
80
|
from fastmcp.experimental.server.openapi.routing import (
|
|
@@ -80,6 +88,8 @@ if TYPE_CHECKING:
|
|
|
80
88
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
81
89
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
82
90
|
from fastmcp.server.proxy import FastMCPProxy
|
|
91
|
+
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
92
|
+
from fastmcp.tools.tool import ToolResultSerializerType
|
|
83
93
|
|
|
84
94
|
logger = get_logger(__name__)
|
|
85
95
|
|
|
@@ -89,6 +99,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
|
89
99
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
90
100
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
91
101
|
|
|
102
|
+
LifespanCallable = Callable[
|
|
103
|
+
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
104
|
+
]
|
|
105
|
+
|
|
92
106
|
|
|
93
107
|
@asynccontextmanager
|
|
94
108
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -98,26 +112,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
|
|
|
98
112
|
server: The server instance this lifespan is managing
|
|
99
113
|
|
|
100
114
|
Returns:
|
|
101
|
-
An empty
|
|
115
|
+
An empty dictionary as the lifespan result.
|
|
102
116
|
"""
|
|
103
117
|
yield {}
|
|
104
118
|
|
|
105
119
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
lifespan: Callable[
|
|
109
|
-
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
110
|
-
],
|
|
120
|
+
def _lifespan_proxy(
|
|
121
|
+
fastmcp_server: FastMCP[LifespanResultT],
|
|
111
122
|
) -> Callable[
|
|
112
123
|
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
113
124
|
]:
|
|
114
125
|
@asynccontextmanager
|
|
115
126
|
async def wrap(
|
|
116
|
-
|
|
127
|
+
low_level_server: LowLevelServer[LifespanResultT],
|
|
117
128
|
) -> AsyncIterator[LifespanResultT]:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
if fastmcp_server._lifespan is default_lifespan:
|
|
130
|
+
yield {}
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if not fastmcp_server._lifespan_result_set:
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
"FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
|
|
136
|
+
+ " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
yield fastmcp_server._lifespan_result
|
|
121
140
|
|
|
122
141
|
return wrap
|
|
123
142
|
|
|
@@ -129,27 +148,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
129
148
|
instructions: str | None = None,
|
|
130
149
|
*,
|
|
131
150
|
version: str | None = None,
|
|
151
|
+
website_url: str | None = None,
|
|
152
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
132
153
|
auth: AuthProvider | None | NotSetT = NotSet,
|
|
133
|
-
middleware:
|
|
134
|
-
lifespan:
|
|
135
|
-
Callable[
|
|
136
|
-
[FastMCP[LifespanResultT]],
|
|
137
|
-
AbstractAsyncContextManager[LifespanResultT],
|
|
138
|
-
]
|
|
139
|
-
| None
|
|
140
|
-
) = None,
|
|
154
|
+
middleware: Sequence[Middleware] | None = None,
|
|
155
|
+
lifespan: LifespanCallable | None = None,
|
|
141
156
|
dependencies: list[str] | None = None,
|
|
142
157
|
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
143
158
|
mask_error_details: bool | None = None,
|
|
144
|
-
tools:
|
|
145
|
-
tool_transformations:
|
|
146
|
-
tool_serializer:
|
|
147
|
-
include_tags:
|
|
148
|
-
exclude_tags:
|
|
159
|
+
tools: Sequence[Tool | Callable[..., Any]] | None = None,
|
|
160
|
+
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
161
|
+
tool_serializer: ToolResultSerializerType | None = None,
|
|
162
|
+
include_tags: Collection[str] | None = None,
|
|
163
|
+
exclude_tags: Collection[str] | None = None,
|
|
149
164
|
include_fastmcp_meta: bool | None = None,
|
|
150
165
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
151
166
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
152
167
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
168
|
+
strict_input_validation: bool | None = None,
|
|
153
169
|
# ---
|
|
154
170
|
# ---
|
|
155
171
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -173,32 +189,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
173
189
|
|
|
174
190
|
self._additional_http_routes: list[BaseRoute] = []
|
|
175
191
|
self._mounted_servers: list[MountedServer] = []
|
|
176
|
-
self._tool_manager = ToolManager(
|
|
192
|
+
self._tool_manager: ToolManager = ToolManager(
|
|
177
193
|
duplicate_behavior=on_duplicate_tools,
|
|
178
194
|
mask_error_details=mask_error_details,
|
|
179
195
|
transformations=tool_transformations,
|
|
180
196
|
)
|
|
181
|
-
self._resource_manager = ResourceManager(
|
|
197
|
+
self._resource_manager: ResourceManager = ResourceManager(
|
|
182
198
|
duplicate_behavior=on_duplicate_resources,
|
|
183
199
|
mask_error_details=mask_error_details,
|
|
184
200
|
)
|
|
185
|
-
self._prompt_manager = PromptManager(
|
|
201
|
+
self._prompt_manager: PromptManager = PromptManager(
|
|
186
202
|
duplicate_behavior=on_duplicate_prompts,
|
|
187
203
|
mask_error_details=mask_error_details,
|
|
188
204
|
)
|
|
189
|
-
self._tool_serializer = tool_serializer
|
|
205
|
+
self._tool_serializer: Callable[[Any], str] | None = tool_serializer
|
|
206
|
+
|
|
207
|
+
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
208
|
+
self._lifespan_result: LifespanResultT | None = None
|
|
209
|
+
self._lifespan_result_set: bool = False
|
|
190
210
|
|
|
191
|
-
if lifespan is None:
|
|
192
|
-
self._has_lifespan = False
|
|
193
|
-
lifespan = default_lifespan
|
|
194
|
-
else:
|
|
195
|
-
self._has_lifespan = True
|
|
196
211
|
# Generate random ID if no name provided
|
|
197
|
-
self._mcp_server = LowLevelServer[
|
|
212
|
+
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
|
|
213
|
+
LifespanResultT
|
|
214
|
+
](
|
|
215
|
+
fastmcp=self,
|
|
198
216
|
name=name or self.generate_name(),
|
|
199
|
-
version=version,
|
|
217
|
+
version=version or fastmcp.__version__,
|
|
200
218
|
instructions=instructions,
|
|
201
|
-
|
|
219
|
+
website_url=website_url,
|
|
220
|
+
icons=icons,
|
|
221
|
+
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
202
222
|
)
|
|
203
223
|
|
|
204
224
|
# if auth is `NotSet`, try to create a provider from the environment
|
|
@@ -208,7 +228,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
208
228
|
auth = fastmcp.settings.server_auth_class()
|
|
209
229
|
else:
|
|
210
230
|
auth = None
|
|
211
|
-
self.auth = cast(AuthProvider | None, auth)
|
|
231
|
+
self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
|
|
212
232
|
|
|
213
233
|
if tools:
|
|
214
234
|
for tool in tools:
|
|
@@ -216,10 +236,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
216
236
|
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
|
217
237
|
self.add_tool(tool)
|
|
218
238
|
|
|
219
|
-
self.include_tags =
|
|
220
|
-
|
|
239
|
+
self.include_tags: set[str] | None = (
|
|
240
|
+
set(include_tags) if include_tags is not None else None
|
|
241
|
+
)
|
|
242
|
+
self.exclude_tags: set[str] | None = (
|
|
243
|
+
set(exclude_tags) if exclude_tags is not None else None
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.strict_input_validation: bool = (
|
|
247
|
+
strict_input_validation
|
|
248
|
+
if strict_input_validation is not None
|
|
249
|
+
else fastmcp.settings.strict_input_validation
|
|
250
|
+
)
|
|
221
251
|
|
|
222
|
-
self.middleware = middleware or []
|
|
252
|
+
self.middleware: list[Middleware] = list(middleware or [])
|
|
223
253
|
|
|
224
254
|
# Set up MCP protocol handlers
|
|
225
255
|
self._setup_handlers()
|
|
@@ -238,14 +268,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
238
268
|
DeprecationWarning,
|
|
239
269
|
stacklevel=2,
|
|
240
270
|
)
|
|
241
|
-
self.dependencies = (
|
|
271
|
+
self.dependencies: list[str] = (
|
|
242
272
|
dependencies or fastmcp.settings.server_dependencies
|
|
243
273
|
) # TODO: Remove (deprecated in v2.11.4)
|
|
244
274
|
|
|
245
|
-
self.sampling_handler =
|
|
246
|
-
|
|
275
|
+
self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
|
|
276
|
+
sampling_handler
|
|
277
|
+
)
|
|
278
|
+
self.sampling_handler_behavior: Literal["always", "fallback"] = (
|
|
279
|
+
sampling_handler_behavior or "fallback"
|
|
280
|
+
)
|
|
247
281
|
|
|
248
|
-
self.include_fastmcp_meta = (
|
|
282
|
+
self.include_fastmcp_meta: bool = (
|
|
249
283
|
include_fastmcp_meta
|
|
250
284
|
if include_fastmcp_meta is not None
|
|
251
285
|
else fastmcp.settings.include_fastmcp_meta
|
|
@@ -333,6 +367,38 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
333
367
|
def version(self) -> str | None:
|
|
334
368
|
return self._mcp_server.version
|
|
335
369
|
|
|
370
|
+
@property
|
|
371
|
+
def website_url(self) -> str | None:
|
|
372
|
+
return self._mcp_server.website_url
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def icons(self) -> list[mcp.types.Icon]:
|
|
376
|
+
if self._mcp_server.icons is None:
|
|
377
|
+
return []
|
|
378
|
+
else:
|
|
379
|
+
return list(self._mcp_server.icons)
|
|
380
|
+
|
|
381
|
+
@asynccontextmanager
|
|
382
|
+
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
383
|
+
if self._lifespan_result_set:
|
|
384
|
+
yield
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
async with self._lifespan(self) as lifespan_result:
|
|
388
|
+
self._lifespan_result = lifespan_result
|
|
389
|
+
self._lifespan_result_set = True
|
|
390
|
+
|
|
391
|
+
async with AsyncExitStack[bool | None]() as stack:
|
|
392
|
+
for server in self._mounted_servers:
|
|
393
|
+
await stack.enter_async_context(
|
|
394
|
+
cm=server.server._lifespan_manager()
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
yield
|
|
398
|
+
|
|
399
|
+
self._lifespan_result_set = False
|
|
400
|
+
self._lifespan_result = None
|
|
401
|
+
|
|
336
402
|
async def run_async(
|
|
337
403
|
self,
|
|
338
404
|
transport: Transport | None = None,
|
|
@@ -386,13 +452,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
386
452
|
|
|
387
453
|
def _setup_handlers(self) -> None:
|
|
388
454
|
"""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
|
-
|
|
455
|
+
self._mcp_server.list_tools()(self._list_tools_mcp)
|
|
456
|
+
self._mcp_server.list_resources()(self._list_resources_mcp)
|
|
457
|
+
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
|
458
|
+
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
|
459
|
+
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
460
|
+
self._call_tool_mcp
|
|
461
|
+
)
|
|
462
|
+
self._mcp_server.read_resource()(self._read_resource_mcp)
|
|
463
|
+
self._mcp_server.get_prompt()(self._get_prompt_mcp)
|
|
396
464
|
|
|
397
465
|
async def _apply_middleware(
|
|
398
466
|
self,
|
|
@@ -409,8 +477,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
409
477
|
self.middleware.append(middleware)
|
|
410
478
|
|
|
411
479
|
async def get_tools(self) -> dict[str, Tool]:
|
|
412
|
-
"""Get all
|
|
413
|
-
|
|
480
|
+
"""Get all tools (unfiltered), including mounted servers, indexed by key."""
|
|
481
|
+
all_tools = dict(await self._tool_manager.get_tools())
|
|
482
|
+
|
|
483
|
+
for mounted in self._mounted_servers:
|
|
484
|
+
try:
|
|
485
|
+
child_tools = await mounted.server.get_tools()
|
|
486
|
+
for key, tool in child_tools.items():
|
|
487
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
488
|
+
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
logger.warning(
|
|
491
|
+
f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
|
|
492
|
+
)
|
|
493
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
494
|
+
raise
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
return all_tools
|
|
414
498
|
|
|
415
499
|
async def get_tool(self, key: str) -> Tool:
|
|
416
500
|
tools = await self.get_tools()
|
|
@@ -419,8 +503,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
419
503
|
return tools[key]
|
|
420
504
|
|
|
421
505
|
async def get_resources(self) -> dict[str, Resource]:
|
|
422
|
-
"""Get all
|
|
423
|
-
|
|
506
|
+
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
507
|
+
all_resources = dict(await self._resource_manager.get_resources())
|
|
508
|
+
|
|
509
|
+
for mounted in self._mounted_servers:
|
|
510
|
+
try:
|
|
511
|
+
child_resources = await mounted.server.get_resources()
|
|
512
|
+
for key, resource in child_resources.items():
|
|
513
|
+
new_key = (
|
|
514
|
+
add_resource_prefix(
|
|
515
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
516
|
+
)
|
|
517
|
+
if mounted.prefix
|
|
518
|
+
else key
|
|
519
|
+
)
|
|
520
|
+
update = (
|
|
521
|
+
{"name": f"{mounted.prefix}_{resource.name}"}
|
|
522
|
+
if mounted.prefix and resource.name
|
|
523
|
+
else {}
|
|
524
|
+
)
|
|
525
|
+
all_resources[new_key] = resource.model_copy(
|
|
526
|
+
key=new_key, update=update
|
|
527
|
+
)
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.warning(
|
|
530
|
+
f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
|
|
531
|
+
)
|
|
532
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
533
|
+
raise
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
return all_resources
|
|
424
537
|
|
|
425
538
|
async def get_resource(self, key: str) -> Resource:
|
|
426
539
|
resources = await self.get_resources()
|
|
@@ -429,8 +542,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
429
542
|
return resources[key]
|
|
430
543
|
|
|
431
544
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
432
|
-
"""Get all
|
|
433
|
-
|
|
545
|
+
"""Get all resource templates (unfiltered), including mounted servers, indexed by key."""
|
|
546
|
+
all_templates = dict(await self._resource_manager.get_resource_templates())
|
|
547
|
+
|
|
548
|
+
for mounted in self._mounted_servers:
|
|
549
|
+
try:
|
|
550
|
+
child_templates = await mounted.server.get_resource_templates()
|
|
551
|
+
for key, template in child_templates.items():
|
|
552
|
+
new_key = (
|
|
553
|
+
add_resource_prefix(
|
|
554
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
555
|
+
)
|
|
556
|
+
if mounted.prefix
|
|
557
|
+
else key
|
|
558
|
+
)
|
|
559
|
+
update = (
|
|
560
|
+
{"name": f"{mounted.prefix}_{template.name}"}
|
|
561
|
+
if mounted.prefix and template.name
|
|
562
|
+
else {}
|
|
563
|
+
)
|
|
564
|
+
all_templates[new_key] = template.model_copy(
|
|
565
|
+
key=new_key, update=update
|
|
566
|
+
)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.warning(
|
|
569
|
+
f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
|
|
570
|
+
)
|
|
571
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
572
|
+
raise
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
return all_templates
|
|
434
576
|
|
|
435
577
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
436
578
|
"""Get a registered resource template by key."""
|
|
@@ -440,10 +582,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
440
582
|
return templates[key]
|
|
441
583
|
|
|
442
584
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
443
|
-
"""
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
585
|
+
"""Get all prompts (unfiltered), including mounted servers, indexed by key."""
|
|
586
|
+
all_prompts = dict(await self._prompt_manager.get_prompts())
|
|
587
|
+
|
|
588
|
+
for mounted in self._mounted_servers:
|
|
589
|
+
try:
|
|
590
|
+
child_prompts = await mounted.server.get_prompts()
|
|
591
|
+
for key, prompt in child_prompts.items():
|
|
592
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
593
|
+
all_prompts[new_key] = prompt.model_copy(key=new_key)
|
|
594
|
+
except Exception as e:
|
|
595
|
+
logger.warning(
|
|
596
|
+
f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
|
|
597
|
+
)
|
|
598
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
599
|
+
raise
|
|
600
|
+
continue
|
|
601
|
+
|
|
602
|
+
return all_prompts
|
|
447
603
|
|
|
448
604
|
async def get_prompt(self, key: str) -> Prompt:
|
|
449
605
|
prompts = await self.get_prompts()
|
|
@@ -519,11 +675,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
519
675
|
|
|
520
676
|
return routes
|
|
521
677
|
|
|
522
|
-
async def
|
|
678
|
+
async def _list_tools_mcp(self) -> list[MCPTool]:
|
|
679
|
+
"""
|
|
680
|
+
List all available tools, in the format expected by the low-level MCP
|
|
681
|
+
server.
|
|
682
|
+
"""
|
|
523
683
|
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
524
684
|
|
|
525
685
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
|
-
tools = await self.
|
|
686
|
+
tools = await self._list_tools_middleware()
|
|
527
687
|
return [
|
|
528
688
|
tool.to_mcp_tool(
|
|
529
689
|
name=tool.key,
|
|
@@ -532,24 +692,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
532
692
|
for tool in tools
|
|
533
693
|
]
|
|
534
694
|
|
|
535
|
-
async def
|
|
695
|
+
async def _list_tools_middleware(self) -> list[Tool]:
|
|
536
696
|
"""
|
|
537
|
-
List all available tools,
|
|
538
|
-
server.
|
|
697
|
+
List all available tools, applying MCP middleware.
|
|
539
698
|
"""
|
|
540
699
|
|
|
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
700
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
554
701
|
# Create the middleware context.
|
|
555
702
|
mw_context = MiddlewareContext(
|
|
@@ -561,13 +708,66 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
561
708
|
)
|
|
562
709
|
|
|
563
710
|
# Apply the middleware chain.
|
|
564
|
-
return
|
|
711
|
+
return list(
|
|
712
|
+
await self._apply_middleware(
|
|
713
|
+
context=mw_context, call_next=self._list_tools
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
async def _list_tools(
|
|
718
|
+
self,
|
|
719
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
720
|
+
) -> list[Tool]:
|
|
721
|
+
"""
|
|
722
|
+
List all available tools.
|
|
723
|
+
"""
|
|
724
|
+
# 1. Get local tools and filter them
|
|
725
|
+
local_tools = await self._tool_manager.get_tools()
|
|
726
|
+
filtered_local = [
|
|
727
|
+
tool for tool in local_tools.values() if self._should_enable_component(tool)
|
|
728
|
+
]
|
|
729
|
+
|
|
730
|
+
# 2. Get tools from mounted servers
|
|
731
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
732
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
733
|
+
all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
|
|
734
|
+
|
|
735
|
+
for mounted in self._mounted_servers:
|
|
736
|
+
try:
|
|
737
|
+
child_tools = await mounted.server._list_tools_middleware()
|
|
738
|
+
for tool in child_tools:
|
|
739
|
+
# Apply parent server's filtering to mounted components
|
|
740
|
+
if not self._should_enable_component(tool):
|
|
741
|
+
continue
|
|
742
|
+
|
|
743
|
+
key = tool.key
|
|
744
|
+
if mounted.prefix:
|
|
745
|
+
key = f"{mounted.prefix}_{tool.key}"
|
|
746
|
+
tool = tool.model_copy(key=key)
|
|
747
|
+
# Later mounted servers override earlier ones
|
|
748
|
+
all_tools[key] = tool
|
|
749
|
+
except Exception as e:
|
|
750
|
+
server_name = getattr(
|
|
751
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
752
|
+
)
|
|
753
|
+
logger.warning(
|
|
754
|
+
f"Failed to list tools from mounted server {server_name!r}: {e}"
|
|
755
|
+
)
|
|
756
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
757
|
+
raise
|
|
758
|
+
continue
|
|
759
|
+
|
|
760
|
+
return list(all_tools.values())
|
|
565
761
|
|
|
566
|
-
async def
|
|
762
|
+
async def _list_resources_mcp(self) -> list[MCPResource]:
|
|
763
|
+
"""
|
|
764
|
+
List all available resources, in the format expected by the low-level MCP
|
|
765
|
+
server.
|
|
766
|
+
"""
|
|
567
767
|
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
568
768
|
|
|
569
769
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
570
|
-
resources = await self.
|
|
770
|
+
resources = await self._list_resources_middleware()
|
|
571
771
|
return [
|
|
572
772
|
resource.to_mcp_resource(
|
|
573
773
|
uri=resource.key,
|
|
@@ -576,25 +776,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
576
776
|
for resource in resources
|
|
577
777
|
]
|
|
578
778
|
|
|
579
|
-
async def
|
|
779
|
+
async def _list_resources_middleware(self) -> list[Resource]:
|
|
580
780
|
"""
|
|
581
|
-
List all available resources,
|
|
582
|
-
server.
|
|
583
|
-
|
|
781
|
+
List all available resources, applying MCP middleware.
|
|
584
782
|
"""
|
|
585
783
|
|
|
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
784
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
599
785
|
# Create the middleware context.
|
|
600
786
|
mw_context = MiddlewareContext(
|
|
@@ -606,13 +792,75 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
606
792
|
)
|
|
607
793
|
|
|
608
794
|
# Apply the middleware chain.
|
|
609
|
-
return
|
|
795
|
+
return list(
|
|
796
|
+
await self._apply_middleware(
|
|
797
|
+
context=mw_context, call_next=self._list_resources
|
|
798
|
+
)
|
|
799
|
+
)
|
|
610
800
|
|
|
611
|
-
async def
|
|
801
|
+
async def _list_resources(
|
|
802
|
+
self,
|
|
803
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
804
|
+
) -> list[Resource]:
|
|
805
|
+
"""
|
|
806
|
+
List all available resources.
|
|
807
|
+
"""
|
|
808
|
+
# 1. Filter local resources
|
|
809
|
+
local_resources = await self._resource_manager.get_resources()
|
|
810
|
+
filtered_local = [
|
|
811
|
+
resource
|
|
812
|
+
for resource in local_resources.values()
|
|
813
|
+
if self._should_enable_component(resource)
|
|
814
|
+
]
|
|
815
|
+
|
|
816
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
817
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
818
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
819
|
+
all_resources: dict[str, Resource] = {
|
|
820
|
+
resource.key: resource for resource in filtered_local
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for mounted in self._mounted_servers:
|
|
824
|
+
try:
|
|
825
|
+
child_resources = await mounted.server._list_resources_middleware()
|
|
826
|
+
for resource in child_resources:
|
|
827
|
+
# Apply parent server's filtering to mounted components
|
|
828
|
+
if not self._should_enable_component(resource):
|
|
829
|
+
continue
|
|
830
|
+
|
|
831
|
+
key = resource.key
|
|
832
|
+
if mounted.prefix:
|
|
833
|
+
key = add_resource_prefix(
|
|
834
|
+
resource.key,
|
|
835
|
+
mounted.prefix,
|
|
836
|
+
mounted.resource_prefix_format,
|
|
837
|
+
)
|
|
838
|
+
resource = resource.model_copy(
|
|
839
|
+
key=key,
|
|
840
|
+
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
841
|
+
)
|
|
842
|
+
# Later mounted servers override earlier ones
|
|
843
|
+
all_resources[key] = resource
|
|
844
|
+
except Exception as e:
|
|
845
|
+
server_name = getattr(
|
|
846
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
847
|
+
)
|
|
848
|
+
logger.warning(f"Failed to list resources from {server_name!r}: {e}")
|
|
849
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
850
|
+
raise
|
|
851
|
+
continue
|
|
852
|
+
|
|
853
|
+
return list(all_resources.values())
|
|
854
|
+
|
|
855
|
+
async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
|
|
856
|
+
"""
|
|
857
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
858
|
+
server.
|
|
859
|
+
"""
|
|
612
860
|
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
613
861
|
|
|
614
862
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
615
|
-
templates = await self.
|
|
863
|
+
templates = await self._list_resource_templates_middleware()
|
|
616
864
|
return [
|
|
617
865
|
template.to_mcp_template(
|
|
618
866
|
uriTemplate=template.key,
|
|
@@ -621,25 +869,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
621
869
|
for template in templates
|
|
622
870
|
]
|
|
623
871
|
|
|
624
|
-
async def
|
|
872
|
+
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
|
625
873
|
"""
|
|
626
|
-
List all available resource templates,
|
|
627
|
-
server.
|
|
874
|
+
List all available resource templates, applying MCP middleware.
|
|
628
875
|
|
|
629
876
|
"""
|
|
630
877
|
|
|
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
878
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
644
879
|
# Create the middleware context.
|
|
645
880
|
mw_context = MiddlewareContext(
|
|
@@ -651,13 +886,79 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
651
886
|
)
|
|
652
887
|
|
|
653
888
|
# Apply the middleware chain.
|
|
654
|
-
return
|
|
889
|
+
return list(
|
|
890
|
+
await self._apply_middleware(
|
|
891
|
+
context=mw_context, call_next=self._list_resource_templates
|
|
892
|
+
)
|
|
893
|
+
)
|
|
655
894
|
|
|
656
|
-
async def
|
|
895
|
+
async def _list_resource_templates(
|
|
896
|
+
self,
|
|
897
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
898
|
+
) -> list[ResourceTemplate]:
|
|
899
|
+
"""
|
|
900
|
+
List all available resource templates.
|
|
901
|
+
"""
|
|
902
|
+
# 1. Filter local templates
|
|
903
|
+
local_templates = await self._resource_manager.get_resource_templates()
|
|
904
|
+
filtered_local = [
|
|
905
|
+
template
|
|
906
|
+
for template in local_templates.values()
|
|
907
|
+
if self._should_enable_component(template)
|
|
908
|
+
]
|
|
909
|
+
|
|
910
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
911
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
912
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
913
|
+
all_templates: dict[str, ResourceTemplate] = {
|
|
914
|
+
template.key: template for template in filtered_local
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for mounted in self._mounted_servers:
|
|
918
|
+
try:
|
|
919
|
+
child_templates = (
|
|
920
|
+
await mounted.server._list_resource_templates_middleware()
|
|
921
|
+
)
|
|
922
|
+
for template in child_templates:
|
|
923
|
+
# Apply parent server's filtering to mounted components
|
|
924
|
+
if not self._should_enable_component(template):
|
|
925
|
+
continue
|
|
926
|
+
|
|
927
|
+
key = template.key
|
|
928
|
+
if mounted.prefix:
|
|
929
|
+
key = add_resource_prefix(
|
|
930
|
+
template.key,
|
|
931
|
+
mounted.prefix,
|
|
932
|
+
mounted.resource_prefix_format,
|
|
933
|
+
)
|
|
934
|
+
template = template.model_copy(
|
|
935
|
+
key=key,
|
|
936
|
+
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
937
|
+
)
|
|
938
|
+
# Later mounted servers override earlier ones
|
|
939
|
+
all_templates[key] = template
|
|
940
|
+
except Exception as e:
|
|
941
|
+
server_name = getattr(
|
|
942
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
943
|
+
)
|
|
944
|
+
logger.warning(
|
|
945
|
+
f"Failed to list resource templates from {server_name!r}: {e}"
|
|
946
|
+
)
|
|
947
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
948
|
+
raise
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
return list(all_templates.values())
|
|
952
|
+
|
|
953
|
+
async def _list_prompts_mcp(self) -> list[MCPPrompt]:
|
|
954
|
+
"""
|
|
955
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
956
|
+
server.
|
|
957
|
+
"""
|
|
657
958
|
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
658
959
|
|
|
659
960
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
660
|
-
prompts = await self.
|
|
961
|
+
prompts = await self._list_prompts_middleware()
|
|
661
962
|
return [
|
|
662
963
|
prompt.to_mcp_prompt(
|
|
663
964
|
name=prompt.key,
|
|
@@ -666,25 +967,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
666
967
|
for prompt in prompts
|
|
667
968
|
]
|
|
668
969
|
|
|
669
|
-
async def
|
|
970
|
+
async def _list_prompts_middleware(self) -> list[Prompt]:
|
|
670
971
|
"""
|
|
671
|
-
List all available prompts,
|
|
672
|
-
server.
|
|
972
|
+
List all available prompts, applying MCP middleware.
|
|
673
973
|
|
|
674
974
|
"""
|
|
675
975
|
|
|
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
976
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
689
977
|
# Create the middleware context.
|
|
690
978
|
mw_context = MiddlewareContext(
|
|
@@ -696,9 +984,62 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
696
984
|
)
|
|
697
985
|
|
|
698
986
|
# Apply the middleware chain.
|
|
699
|
-
return
|
|
987
|
+
return list(
|
|
988
|
+
await self._apply_middleware(
|
|
989
|
+
context=mw_context, call_next=self._list_prompts
|
|
990
|
+
)
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
async def _list_prompts(
|
|
994
|
+
self,
|
|
995
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
996
|
+
) -> list[Prompt]:
|
|
997
|
+
"""
|
|
998
|
+
List all available prompts.
|
|
999
|
+
"""
|
|
1000
|
+
# 1. Filter local prompts
|
|
1001
|
+
local_prompts = await self._prompt_manager.get_prompts()
|
|
1002
|
+
filtered_local = [
|
|
1003
|
+
prompt
|
|
1004
|
+
for prompt in local_prompts.values()
|
|
1005
|
+
if self._should_enable_component(prompt)
|
|
1006
|
+
]
|
|
1007
|
+
|
|
1008
|
+
# 2. Get from mounted servers
|
|
1009
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
1010
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
1011
|
+
all_prompts: dict[str, Prompt] = {
|
|
1012
|
+
prompt.key: prompt for prompt in filtered_local
|
|
1013
|
+
}
|
|
700
1014
|
|
|
701
|
-
|
|
1015
|
+
for mounted in self._mounted_servers:
|
|
1016
|
+
try:
|
|
1017
|
+
child_prompts = await mounted.server._list_prompts_middleware()
|
|
1018
|
+
for prompt in child_prompts:
|
|
1019
|
+
# Apply parent server's filtering to mounted components
|
|
1020
|
+
if not self._should_enable_component(prompt):
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
key = prompt.key
|
|
1024
|
+
if mounted.prefix:
|
|
1025
|
+
key = f"{mounted.prefix}_{prompt.key}"
|
|
1026
|
+
prompt = prompt.model_copy(key=key)
|
|
1027
|
+
# Later mounted servers override earlier ones
|
|
1028
|
+
all_prompts[key] = prompt
|
|
1029
|
+
except Exception as e:
|
|
1030
|
+
server_name = getattr(
|
|
1031
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1032
|
+
)
|
|
1033
|
+
logger.warning(
|
|
1034
|
+
f"Failed to list prompts from mounted server {server_name!r}: {e}"
|
|
1035
|
+
)
|
|
1036
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1037
|
+
raise
|
|
1038
|
+
continue
|
|
1039
|
+
|
|
1040
|
+
return list(all_prompts.values())
|
|
1041
|
+
|
|
1042
|
+
async def _call_tool_mcp(
|
|
702
1043
|
self, key: str, arguments: dict[str, Any]
|
|
703
1044
|
) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
|
|
704
1045
|
"""
|
|
@@ -719,29 +1060,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
719
1060
|
|
|
720
1061
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
721
1062
|
try:
|
|
722
|
-
result = await self.
|
|
1063
|
+
result = await self._call_tool_middleware(key, arguments)
|
|
723
1064
|
return result.to_mcp_result()
|
|
724
1065
|
except DisabledError:
|
|
725
1066
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
726
1067
|
except NotFoundError:
|
|
727
1068
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
728
1069
|
|
|
729
|
-
async def
|
|
1070
|
+
async def _call_tool_middleware(
|
|
1071
|
+
self,
|
|
1072
|
+
key: str,
|
|
1073
|
+
arguments: dict[str, Any],
|
|
1074
|
+
) -> ToolResult:
|
|
730
1075
|
"""
|
|
731
1076
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
732
1077
|
"""
|
|
733
1078
|
|
|
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
1079
|
mw_context = MiddlewareContext[CallToolRequestParams](
|
|
746
1080
|
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
747
1081
|
source="client",
|
|
@@ -749,9 +1083,53 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
749
1083
|
method="tools/call",
|
|
750
1084
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
751
1085
|
)
|
|
752
|
-
return await self._apply_middleware(
|
|
1086
|
+
return await self._apply_middleware(
|
|
1087
|
+
context=mw_context, call_next=self._call_tool
|
|
1088
|
+
)
|
|
753
1089
|
|
|
754
|
-
async def
|
|
1090
|
+
async def _call_tool(
|
|
1091
|
+
self,
|
|
1092
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
1093
|
+
) -> ToolResult:
|
|
1094
|
+
"""
|
|
1095
|
+
Call a tool
|
|
1096
|
+
"""
|
|
1097
|
+
tool_name = context.message.name
|
|
1098
|
+
|
|
1099
|
+
# Try mounted servers in reverse order (later wins)
|
|
1100
|
+
for mounted in reversed(self._mounted_servers):
|
|
1101
|
+
try_name = tool_name
|
|
1102
|
+
if mounted.prefix:
|
|
1103
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1104
|
+
continue
|
|
1105
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1106
|
+
|
|
1107
|
+
try:
|
|
1108
|
+
# First, get the tool to check if parent's filter allows it
|
|
1109
|
+
tool = await mounted.server._tool_manager.get_tool(try_name)
|
|
1110
|
+
if not self._should_enable_component(tool):
|
|
1111
|
+
# Parent filter blocks this tool, continue searching
|
|
1112
|
+
continue
|
|
1113
|
+
|
|
1114
|
+
return await mounted.server._call_tool_middleware(
|
|
1115
|
+
try_name, context.message.arguments or {}
|
|
1116
|
+
)
|
|
1117
|
+
except NotFoundError:
|
|
1118
|
+
continue
|
|
1119
|
+
|
|
1120
|
+
# Try local tools last (mounted servers override local)
|
|
1121
|
+
try:
|
|
1122
|
+
tool = await self._tool_manager.get_tool(tool_name)
|
|
1123
|
+
if self._should_enable_component(tool):
|
|
1124
|
+
return await self._tool_manager.call_tool(
|
|
1125
|
+
key=tool_name, arguments=context.message.arguments or {}
|
|
1126
|
+
)
|
|
1127
|
+
except NotFoundError:
|
|
1128
|
+
pass
|
|
1129
|
+
|
|
1130
|
+
raise NotFoundError(f"Unknown tool: {tool_name!r}")
|
|
1131
|
+
|
|
1132
|
+
async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
755
1133
|
"""
|
|
756
1134
|
Handle MCP 'readResource' requests.
|
|
757
1135
|
|
|
@@ -761,7 +1139,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
761
1139
|
|
|
762
1140
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
763
1141
|
try:
|
|
764
|
-
return
|
|
1142
|
+
return list[ReadResourceContents](
|
|
1143
|
+
await self._read_resource_middleware(uri)
|
|
1144
|
+
)
|
|
765
1145
|
except DisabledError:
|
|
766
1146
|
# convert to NotFoundError to avoid leaking resource presence
|
|
767
1147
|
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
@@ -769,26 +1149,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
769
1149
|
# standardize NotFound message
|
|
770
1150
|
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
771
1151
|
|
|
772
|
-
async def
|
|
1152
|
+
async def _read_resource_middleware(
|
|
1153
|
+
self,
|
|
1154
|
+
uri: AnyUrl | str,
|
|
1155
|
+
) -> list[ReadResourceContents]:
|
|
773
1156
|
"""
|
|
774
1157
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
775
1158
|
"""
|
|
776
1159
|
|
|
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
1160
|
# Convert string URI to AnyUrl if needed
|
|
793
1161
|
if isinstance(uri, str):
|
|
794
1162
|
uri_param = AnyUrl(uri)
|
|
@@ -802,9 +1170,61 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
1170
|
method="resources/read",
|
|
803
1171
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
804
1172
|
)
|
|
805
|
-
return
|
|
1173
|
+
return list(
|
|
1174
|
+
await self._apply_middleware(
|
|
1175
|
+
context=mw_context, call_next=self._read_resource
|
|
1176
|
+
)
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
async def _read_resource(
|
|
1180
|
+
self,
|
|
1181
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
1182
|
+
) -> list[ReadResourceContents]:
|
|
1183
|
+
"""
|
|
1184
|
+
Read a resource
|
|
1185
|
+
"""
|
|
1186
|
+
uri_str = str(context.message.uri)
|
|
1187
|
+
|
|
1188
|
+
# Try mounted servers in reverse order (later wins)
|
|
1189
|
+
for mounted in reversed(self._mounted_servers):
|
|
1190
|
+
key = uri_str
|
|
1191
|
+
if mounted.prefix:
|
|
1192
|
+
if not has_resource_prefix(
|
|
1193
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1194
|
+
):
|
|
1195
|
+
continue
|
|
1196
|
+
key = remove_resource_prefix(
|
|
1197
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
try:
|
|
1201
|
+
# First, get the resource to check if parent's filter allows it
|
|
1202
|
+
resource = await mounted.server._resource_manager.get_resource(key)
|
|
1203
|
+
if not self._should_enable_component(resource):
|
|
1204
|
+
# Parent filter blocks this resource, continue searching
|
|
1205
|
+
continue
|
|
1206
|
+
result = list(await mounted.server._read_resource_middleware(key))
|
|
1207
|
+
return result
|
|
1208
|
+
except NotFoundError:
|
|
1209
|
+
continue
|
|
1210
|
+
|
|
1211
|
+
# Try local resources last (mounted servers override local)
|
|
1212
|
+
try:
|
|
1213
|
+
resource = await self._resource_manager.get_resource(uri_str)
|
|
1214
|
+
if self._should_enable_component(resource):
|
|
1215
|
+
content = await self._resource_manager.read_resource(uri_str)
|
|
1216
|
+
return [
|
|
1217
|
+
ReadResourceContents(
|
|
1218
|
+
content=content,
|
|
1219
|
+
mime_type=resource.mime_type,
|
|
1220
|
+
)
|
|
1221
|
+
]
|
|
1222
|
+
except NotFoundError:
|
|
1223
|
+
pass
|
|
806
1224
|
|
|
807
|
-
|
|
1225
|
+
raise NotFoundError(f"Unknown resource: {uri_str!r}")
|
|
1226
|
+
|
|
1227
|
+
async def _get_prompt_mcp(
|
|
808
1228
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
809
1229
|
) -> GetPromptResult:
|
|
810
1230
|
"""
|
|
@@ -820,7 +1240,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
820
1240
|
|
|
821
1241
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
822
1242
|
try:
|
|
823
|
-
return await self.
|
|
1243
|
+
return await self._get_prompt_middleware(name, arguments)
|
|
824
1244
|
except DisabledError:
|
|
825
1245
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
826
1246
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
@@ -828,24 +1248,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
828
1248
|
# standardize NotFound message
|
|
829
1249
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
830
1250
|
|
|
831
|
-
async def
|
|
1251
|
+
async def _get_prompt_middleware(
|
|
832
1252
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
833
1253
|
) -> GetPromptResult:
|
|
834
1254
|
"""
|
|
835
1255
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
836
1256
|
"""
|
|
837
1257
|
|
|
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
1258
|
mw_context = MiddlewareContext(
|
|
850
1259
|
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
851
1260
|
source="client",
|
|
@@ -853,7 +1262,47 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
853
1262
|
method="prompts/get",
|
|
854
1263
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
855
1264
|
)
|
|
856
|
-
return await self._apply_middleware(
|
|
1265
|
+
return await self._apply_middleware(
|
|
1266
|
+
context=mw_context, call_next=self._get_prompt
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
async def _get_prompt(
|
|
1270
|
+
self,
|
|
1271
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
1272
|
+
) -> GetPromptResult:
|
|
1273
|
+
name = context.message.name
|
|
1274
|
+
|
|
1275
|
+
# Try mounted servers in reverse order (later wins)
|
|
1276
|
+
for mounted in reversed(self._mounted_servers):
|
|
1277
|
+
try_name = name
|
|
1278
|
+
if mounted.prefix:
|
|
1279
|
+
if not name.startswith(f"{mounted.prefix}_"):
|
|
1280
|
+
continue
|
|
1281
|
+
try_name = name[len(mounted.prefix) + 1 :]
|
|
1282
|
+
|
|
1283
|
+
try:
|
|
1284
|
+
# First, get the prompt to check if parent's filter allows it
|
|
1285
|
+
prompt = await mounted.server._prompt_manager.get_prompt(try_name)
|
|
1286
|
+
if not self._should_enable_component(prompt):
|
|
1287
|
+
# Parent filter blocks this prompt, continue searching
|
|
1288
|
+
continue
|
|
1289
|
+
return await mounted.server._get_prompt_middleware(
|
|
1290
|
+
try_name, context.message.arguments
|
|
1291
|
+
)
|
|
1292
|
+
except NotFoundError:
|
|
1293
|
+
continue
|
|
1294
|
+
|
|
1295
|
+
# Try local prompts last (mounted servers override local)
|
|
1296
|
+
try:
|
|
1297
|
+
prompt = await self._prompt_manager.get_prompt(name)
|
|
1298
|
+
if self._should_enable_component(prompt):
|
|
1299
|
+
return await self._prompt_manager.render_prompt(
|
|
1300
|
+
name=name, arguments=context.message.arguments
|
|
1301
|
+
)
|
|
1302
|
+
except NotFoundError:
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
1305
|
+
raise NotFoundError(f"Unknown prompt: {name!r}")
|
|
857
1306
|
|
|
858
1307
|
def add_tool(self, tool: Tool) -> Tool:
|
|
859
1308
|
"""Add a tool to the server.
|
|
@@ -918,6 +1367,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
918
1367
|
name: str | None = None,
|
|
919
1368
|
title: str | None = None,
|
|
920
1369
|
description: str | None = None,
|
|
1370
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
921
1371
|
tags: set[str] | None = None,
|
|
922
1372
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
923
1373
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -934,6 +1384,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
934
1384
|
name: str | None = None,
|
|
935
1385
|
title: str | None = None,
|
|
936
1386
|
description: str | None = None,
|
|
1387
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
937
1388
|
tags: set[str] | None = None,
|
|
938
1389
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
939
1390
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -949,6 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
1400
|
name: str | None = None,
|
|
950
1401
|
title: str | None = None,
|
|
951
1402
|
description: str | None = None,
|
|
1403
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
952
1404
|
tags: set[str] | None = None,
|
|
953
1405
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
954
1406
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -1032,6 +1484,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1032
1484
|
name=tool_name,
|
|
1033
1485
|
title=title,
|
|
1034
1486
|
description=description,
|
|
1487
|
+
icons=icons,
|
|
1035
1488
|
tags=tags,
|
|
1036
1489
|
output_schema=output_schema,
|
|
1037
1490
|
annotations=cast(ToolAnnotations | None, annotations),
|
|
@@ -1065,6 +1518,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1065
1518
|
name=tool_name,
|
|
1066
1519
|
title=title,
|
|
1067
1520
|
description=description,
|
|
1521
|
+
icons=icons,
|
|
1068
1522
|
tags=tags,
|
|
1069
1523
|
output_schema=output_schema,
|
|
1070
1524
|
annotations=annotations,
|
|
@@ -1162,6 +1616,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1162
1616
|
name: str | None = None,
|
|
1163
1617
|
title: str | None = None,
|
|
1164
1618
|
description: str | None = None,
|
|
1619
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1165
1620
|
mime_type: str | None = None,
|
|
1166
1621
|
tags: set[str] | None = None,
|
|
1167
1622
|
enabled: bool | None = None,
|
|
@@ -1261,6 +1716,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1261
1716
|
name=name,
|
|
1262
1717
|
title=title,
|
|
1263
1718
|
description=description,
|
|
1719
|
+
icons=icons,
|
|
1264
1720
|
mime_type=mime_type,
|
|
1265
1721
|
tags=tags,
|
|
1266
1722
|
enabled=enabled,
|
|
@@ -1276,6 +1732,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1276
1732
|
name=name,
|
|
1277
1733
|
title=title,
|
|
1278
1734
|
description=description,
|
|
1735
|
+
icons=icons,
|
|
1279
1736
|
mime_type=mime_type,
|
|
1280
1737
|
tags=tags,
|
|
1281
1738
|
enabled=enabled,
|
|
@@ -1322,6 +1779,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1322
1779
|
name: str | None = None,
|
|
1323
1780
|
title: str | None = None,
|
|
1324
1781
|
description: str | None = None,
|
|
1782
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1325
1783
|
tags: set[str] | None = None,
|
|
1326
1784
|
enabled: bool | None = None,
|
|
1327
1785
|
meta: dict[str, Any] | None = None,
|
|
@@ -1335,6 +1793,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1335
1793
|
name: str | None = None,
|
|
1336
1794
|
title: str | None = None,
|
|
1337
1795
|
description: str | None = None,
|
|
1796
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1338
1797
|
tags: set[str] | None = None,
|
|
1339
1798
|
enabled: bool | None = None,
|
|
1340
1799
|
meta: dict[str, Any] | None = None,
|
|
@@ -1347,6 +1806,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1347
1806
|
name: str | None = None,
|
|
1348
1807
|
title: str | None = None,
|
|
1349
1808
|
description: str | None = None,
|
|
1809
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1350
1810
|
tags: set[str] | None = None,
|
|
1351
1811
|
enabled: bool | None = None,
|
|
1352
1812
|
meta: dict[str, Any] | None = None,
|
|
@@ -1446,6 +1906,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1446
1906
|
name=prompt_name,
|
|
1447
1907
|
title=title,
|
|
1448
1908
|
description=description,
|
|
1909
|
+
icons=icons,
|
|
1449
1910
|
tags=tags,
|
|
1450
1911
|
enabled=enabled,
|
|
1451
1912
|
meta=meta,
|
|
@@ -1476,6 +1937,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1476
1937
|
name=prompt_name,
|
|
1477
1938
|
title=title,
|
|
1478
1939
|
description=description,
|
|
1940
|
+
icons=icons,
|
|
1479
1941
|
tags=tags,
|
|
1480
1942
|
enabled=enabled,
|
|
1481
1943
|
meta=meta,
|
|
@@ -1498,15 +1960,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1498
1960
|
)
|
|
1499
1961
|
|
|
1500
1962
|
with temporary_log_level(log_level):
|
|
1501
|
-
async with
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
self._mcp_server.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1963
|
+
async with self._lifespan_manager():
|
|
1964
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
1965
|
+
logger.info(
|
|
1966
|
+
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
1967
|
+
)
|
|
1968
|
+
await self._mcp_server.run(
|
|
1969
|
+
read_stream,
|
|
1970
|
+
write_stream,
|
|
1971
|
+
self._mcp_server.create_initialization_options(
|
|
1972
|
+
NotificationOptions(tools_changed=True)
|
|
1973
|
+
),
|
|
1974
|
+
)
|
|
1510
1975
|
|
|
1511
1976
|
async def run_http_async(
|
|
1512
1977
|
self,
|
|
@@ -1518,6 +1983,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1518
1983
|
path: str | None = None,
|
|
1519
1984
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1520
1985
|
middleware: list[ASGIMiddleware] | None = None,
|
|
1986
|
+
json_response: bool | None = None,
|
|
1521
1987
|
stateless_http: bool | None = None,
|
|
1522
1988
|
) -> None:
|
|
1523
1989
|
"""Run the server using HTTP transport.
|
|
@@ -1530,6 +1996,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1530
1996
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1531
1997
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1532
1998
|
middleware: A list of middleware to apply to the app
|
|
1999
|
+
json_response: Whether to use JSON response format (defaults to settings.json_response)
|
|
1533
2000
|
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
1534
2001
|
"""
|
|
1535
2002
|
host = host or self._deprecated_settings.host
|
|
@@ -1542,6 +2009,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1542
2009
|
path=path,
|
|
1543
2010
|
transport=transport,
|
|
1544
2011
|
middleware=middleware,
|
|
2012
|
+
json_response=json_response,
|
|
1545
2013
|
stateless_http=stateless_http,
|
|
1546
2014
|
)
|
|
1547
2015
|
|
|
@@ -1566,6 +2034,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1566
2034
|
config_kwargs: dict[str, Any] = {
|
|
1567
2035
|
"timeout_graceful_shutdown": 0,
|
|
1568
2036
|
"lifespan": "on",
|
|
2037
|
+
"ws": "websockets-sansio",
|
|
1569
2038
|
}
|
|
1570
2039
|
config_kwargs.update(_uvicorn_config_from_user)
|
|
1571
2040
|
|
|
@@ -1573,14 +2042,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1573
2042
|
config_kwargs["log_level"] = default_log_level_to_use
|
|
1574
2043
|
|
|
1575
2044
|
with temporary_log_level(log_level):
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2045
|
+
async with self._lifespan_manager():
|
|
2046
|
+
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
2047
|
+
server = uvicorn.Server(config)
|
|
2048
|
+
path = app.state.path.lstrip("/") # type: ignore
|
|
2049
|
+
logger.info(
|
|
2050
|
+
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
2051
|
+
)
|
|
1582
2052
|
|
|
1583
|
-
|
|
2053
|
+
await server.serve()
|
|
1584
2054
|
|
|
1585
2055
|
async def run_sse_async(
|
|
1586
2056
|
self,
|
|
@@ -1842,7 +2312,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1842
2312
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1843
2313
|
# we should treat it as a proxy
|
|
1844
2314
|
if as_proxy is None:
|
|
1845
|
-
as_proxy = server.
|
|
2315
|
+
as_proxy = server._lifespan != default_lifespan
|
|
1846
2316
|
|
|
1847
2317
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1848
2318
|
server = FastMCP.as_proxy(server)
|
|
@@ -1854,9 +2324,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1854
2324
|
resource_prefix_format=self.resource_prefix_format,
|
|
1855
2325
|
)
|
|
1856
2326
|
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
2327
|
|
|
1861
2328
|
async def import_server(
|
|
1862
2329
|
self,
|
|
@@ -1979,6 +2446,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1979
2446
|
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1980
2447
|
self._prompt_manager.add_prompt(prompt)
|
|
1981
2448
|
|
|
2449
|
+
if server._lifespan != default_lifespan:
|
|
2450
|
+
from warnings import warn
|
|
2451
|
+
|
|
2452
|
+
warn(
|
|
2453
|
+
message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
|
|
2454
|
+
category=RuntimeWarning,
|
|
2455
|
+
stacklevel=2,
|
|
2456
|
+
)
|
|
2457
|
+
|
|
1982
2458
|
if prefix:
|
|
1983
2459
|
logger.debug(
|
|
1984
2460
|
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|