fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc2__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 +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- 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 +81 -171
- fastmcp/client/transports.py +76 -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/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1228 -233
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +13 -7
- fastmcp/server/auth/providers/aws.py +14 -3
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +14 -8
- fastmcp/server/auth/providers/google.py +15 -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 +17 -14
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +57 -17
- 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/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +32 -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 +497 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/METADATA +8 -4
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -89,6 +89,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
|
89
89
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
90
90
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
91
91
|
|
|
92
|
+
LifespanCallable = Callable[
|
|
93
|
+
["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
|
|
94
|
+
]
|
|
95
|
+
|
|
92
96
|
|
|
93
97
|
@asynccontextmanager
|
|
94
98
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -98,26 +102,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
|
|
|
98
102
|
server: The server instance this lifespan is managing
|
|
99
103
|
|
|
100
104
|
Returns:
|
|
101
|
-
An empty
|
|
105
|
+
An empty dictionary as the lifespan result.
|
|
102
106
|
"""
|
|
103
107
|
yield {}
|
|
104
108
|
|
|
105
109
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
lifespan: Callable[
|
|
109
|
-
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
110
|
-
],
|
|
110
|
+
def _lifespan_proxy(
|
|
111
|
+
fastmcp_server: FastMCP[LifespanResultT],
|
|
111
112
|
) -> Callable[
|
|
112
113
|
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
113
114
|
]:
|
|
114
115
|
@asynccontextmanager
|
|
115
116
|
async def wrap(
|
|
116
|
-
|
|
117
|
+
low_level_server: LowLevelServer[LifespanResultT],
|
|
117
118
|
) -> AsyncIterator[LifespanResultT]:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
if fastmcp_server._lifespan is default_lifespan:
|
|
120
|
+
yield {}
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if not fastmcp_server._lifespan_result_set:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
|
|
126
|
+
+ " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
yield fastmcp_server._lifespan_result
|
|
121
130
|
|
|
122
131
|
return wrap
|
|
123
132
|
|
|
@@ -129,15 +138,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
129
138
|
instructions: str | None = None,
|
|
130
139
|
*,
|
|
131
140
|
version: str | None = None,
|
|
141
|
+
website_url: str | None = None,
|
|
142
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
132
143
|
auth: AuthProvider | None | NotSetT = NotSet,
|
|
133
144
|
middleware: list[Middleware] | None = None,
|
|
134
|
-
lifespan:
|
|
135
|
-
Callable[
|
|
136
|
-
[FastMCP[LifespanResultT]],
|
|
137
|
-
AbstractAsyncContextManager[LifespanResultT],
|
|
138
|
-
]
|
|
139
|
-
| None
|
|
140
|
-
) = None,
|
|
145
|
+
lifespan: LifespanCallable | None = None,
|
|
141
146
|
dependencies: list[str] | None = None,
|
|
142
147
|
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
143
148
|
mask_error_details: bool | None = None,
|
|
@@ -150,6 +155,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
150
155
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
151
156
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
152
157
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
158
|
+
strict_input_validation: bool | None = None,
|
|
153
159
|
# ---
|
|
154
160
|
# ---
|
|
155
161
|
# --- The following arguments are DEPRECATED ---
|
|
@@ -188,17 +194,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
188
194
|
)
|
|
189
195
|
self._tool_serializer = tool_serializer
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
self._has_lifespan = True
|
|
197
|
+
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
|
|
198
|
+
self._lifespan_result: LifespanResultT | None = None
|
|
199
|
+
self._lifespan_result_set = False
|
|
200
|
+
|
|
196
201
|
# Generate random ID if no name provided
|
|
197
202
|
self._mcp_server = LowLevelServer[LifespanResultT](
|
|
203
|
+
fastmcp=self,
|
|
198
204
|
name=name or self.generate_name(),
|
|
199
|
-
version=version,
|
|
205
|
+
version=version or fastmcp.__version__,
|
|
200
206
|
instructions=instructions,
|
|
201
|
-
|
|
207
|
+
website_url=website_url,
|
|
208
|
+
icons=icons,
|
|
209
|
+
lifespan=_lifespan_proxy(fastmcp_server=self),
|
|
202
210
|
)
|
|
203
211
|
|
|
204
212
|
# if auth is `NotSet`, try to create a provider from the environment
|
|
@@ -218,6 +226,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
218
226
|
|
|
219
227
|
self.include_tags = include_tags
|
|
220
228
|
self.exclude_tags = exclude_tags
|
|
229
|
+
self.strict_input_validation = (
|
|
230
|
+
strict_input_validation
|
|
231
|
+
if strict_input_validation is not None
|
|
232
|
+
else fastmcp.settings.strict_input_validation
|
|
233
|
+
)
|
|
221
234
|
|
|
222
235
|
self.middleware = middleware or []
|
|
223
236
|
|
|
@@ -333,6 +346,38 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
333
346
|
def version(self) -> str | None:
|
|
334
347
|
return self._mcp_server.version
|
|
335
348
|
|
|
349
|
+
@property
|
|
350
|
+
def website_url(self) -> str | None:
|
|
351
|
+
return self._mcp_server.website_url
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def icons(self) -> list[mcp.types.Icon]:
|
|
355
|
+
if self._mcp_server.icons is None:
|
|
356
|
+
return []
|
|
357
|
+
else:
|
|
358
|
+
return list(self._mcp_server.icons)
|
|
359
|
+
|
|
360
|
+
@asynccontextmanager
|
|
361
|
+
async def _lifespan_manager(self) -> AsyncIterator[None]:
|
|
362
|
+
if self._lifespan_result_set:
|
|
363
|
+
yield
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
async with self._lifespan(self) as lifespan_result:
|
|
367
|
+
self._lifespan_result = lifespan_result
|
|
368
|
+
self._lifespan_result_set = True
|
|
369
|
+
|
|
370
|
+
async with AsyncExitStack[bool | None]() as stack:
|
|
371
|
+
for server in self._mounted_servers:
|
|
372
|
+
await stack.enter_async_context(
|
|
373
|
+
cm=server.server._lifespan_manager()
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
yield
|
|
377
|
+
|
|
378
|
+
self._lifespan_result_set = False
|
|
379
|
+
self._lifespan_result = None
|
|
380
|
+
|
|
336
381
|
async def run_async(
|
|
337
382
|
self,
|
|
338
383
|
transport: Transport | None = None,
|
|
@@ -386,13 +431,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
386
431
|
|
|
387
432
|
def _setup_handlers(self) -> None:
|
|
388
433
|
"""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
|
-
|
|
434
|
+
self._mcp_server.list_tools()(self._list_tools_mcp)
|
|
435
|
+
self._mcp_server.list_resources()(self._list_resources_mcp)
|
|
436
|
+
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
|
437
|
+
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
|
438
|
+
self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
|
|
439
|
+
self._call_tool_mcp
|
|
440
|
+
)
|
|
441
|
+
self._mcp_server.read_resource()(self._read_resource_mcp)
|
|
442
|
+
self._mcp_server.get_prompt()(self._get_prompt_mcp)
|
|
396
443
|
|
|
397
444
|
async def _apply_middleware(
|
|
398
445
|
self,
|
|
@@ -409,8 +456,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
409
456
|
self.middleware.append(middleware)
|
|
410
457
|
|
|
411
458
|
async def get_tools(self) -> dict[str, Tool]:
|
|
412
|
-
"""Get all
|
|
413
|
-
|
|
459
|
+
"""Get all tools (unfiltered), including mounted servers, indexed by key."""
|
|
460
|
+
all_tools = dict(await self._tool_manager.get_tools())
|
|
461
|
+
|
|
462
|
+
for mounted in self._mounted_servers:
|
|
463
|
+
try:
|
|
464
|
+
child_tools = await mounted.server.get_tools()
|
|
465
|
+
for key, tool in child_tools.items():
|
|
466
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
467
|
+
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.warning(
|
|
470
|
+
f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
|
|
471
|
+
)
|
|
472
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
473
|
+
raise
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
return all_tools
|
|
414
477
|
|
|
415
478
|
async def get_tool(self, key: str) -> Tool:
|
|
416
479
|
tools = await self.get_tools()
|
|
@@ -419,8 +482,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
419
482
|
return tools[key]
|
|
420
483
|
|
|
421
484
|
async def get_resources(self) -> dict[str, Resource]:
|
|
422
|
-
"""Get all
|
|
423
|
-
|
|
485
|
+
"""Get all resources (unfiltered), including mounted servers, indexed by key."""
|
|
486
|
+
all_resources = dict(await self._resource_manager.get_resources())
|
|
487
|
+
|
|
488
|
+
for mounted in self._mounted_servers:
|
|
489
|
+
try:
|
|
490
|
+
child_resources = await mounted.server.get_resources()
|
|
491
|
+
for key, resource in child_resources.items():
|
|
492
|
+
new_key = (
|
|
493
|
+
add_resource_prefix(
|
|
494
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
495
|
+
)
|
|
496
|
+
if mounted.prefix
|
|
497
|
+
else key
|
|
498
|
+
)
|
|
499
|
+
update = (
|
|
500
|
+
{"name": f"{mounted.prefix}_{resource.name}"}
|
|
501
|
+
if mounted.prefix and resource.name
|
|
502
|
+
else {}
|
|
503
|
+
)
|
|
504
|
+
all_resources[new_key] = resource.model_copy(
|
|
505
|
+
key=new_key, update=update
|
|
506
|
+
)
|
|
507
|
+
except Exception as e:
|
|
508
|
+
logger.warning(
|
|
509
|
+
f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
|
|
510
|
+
)
|
|
511
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
512
|
+
raise
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
return all_resources
|
|
424
516
|
|
|
425
517
|
async def get_resource(self, key: str) -> Resource:
|
|
426
518
|
resources = await self.get_resources()
|
|
@@ -429,8 +521,37 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
429
521
|
return resources[key]
|
|
430
522
|
|
|
431
523
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
432
|
-
"""Get all
|
|
433
|
-
|
|
524
|
+
"""Get all resource templates (unfiltered), including mounted servers, indexed by key."""
|
|
525
|
+
all_templates = dict(await self._resource_manager.get_resource_templates())
|
|
526
|
+
|
|
527
|
+
for mounted in self._mounted_servers:
|
|
528
|
+
try:
|
|
529
|
+
child_templates = await mounted.server.get_resource_templates()
|
|
530
|
+
for key, template in child_templates.items():
|
|
531
|
+
new_key = (
|
|
532
|
+
add_resource_prefix(
|
|
533
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
534
|
+
)
|
|
535
|
+
if mounted.prefix
|
|
536
|
+
else key
|
|
537
|
+
)
|
|
538
|
+
update = (
|
|
539
|
+
{"name": f"{mounted.prefix}_{template.name}"}
|
|
540
|
+
if mounted.prefix and template.name
|
|
541
|
+
else {}
|
|
542
|
+
)
|
|
543
|
+
all_templates[new_key] = template.model_copy(
|
|
544
|
+
key=new_key, update=update
|
|
545
|
+
)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
logger.warning(
|
|
548
|
+
f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
|
|
549
|
+
)
|
|
550
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
551
|
+
raise
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
return all_templates
|
|
434
555
|
|
|
435
556
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
436
557
|
"""Get a registered resource template by key."""
|
|
@@ -440,10 +561,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
440
561
|
return templates[key]
|
|
441
562
|
|
|
442
563
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
443
|
-
"""
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
564
|
+
"""Get all prompts (unfiltered), including mounted servers, indexed by key."""
|
|
565
|
+
all_prompts = dict(await self._prompt_manager.get_prompts())
|
|
566
|
+
|
|
567
|
+
for mounted in self._mounted_servers:
|
|
568
|
+
try:
|
|
569
|
+
child_prompts = await mounted.server.get_prompts()
|
|
570
|
+
for key, prompt in child_prompts.items():
|
|
571
|
+
new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
|
|
572
|
+
all_prompts[new_key] = prompt.model_copy(key=new_key)
|
|
573
|
+
except Exception as e:
|
|
574
|
+
logger.warning(
|
|
575
|
+
f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
|
|
576
|
+
)
|
|
577
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
578
|
+
raise
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
return all_prompts
|
|
447
582
|
|
|
448
583
|
async def get_prompt(self, key: str) -> Prompt:
|
|
449
584
|
prompts = await self.get_prompts()
|
|
@@ -519,11 +654,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
519
654
|
|
|
520
655
|
return routes
|
|
521
656
|
|
|
522
|
-
async def
|
|
657
|
+
async def _list_tools_mcp(self) -> list[MCPTool]:
|
|
658
|
+
"""
|
|
659
|
+
List all available tools, in the format expected by the low-level MCP
|
|
660
|
+
server.
|
|
661
|
+
"""
|
|
523
662
|
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
524
663
|
|
|
525
664
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
|
-
tools = await self.
|
|
665
|
+
tools = await self._list_tools_middleware()
|
|
527
666
|
return [
|
|
528
667
|
tool.to_mcp_tool(
|
|
529
668
|
name=tool.key,
|
|
@@ -532,24 +671,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
532
671
|
for tool in tools
|
|
533
672
|
]
|
|
534
673
|
|
|
535
|
-
async def
|
|
674
|
+
async def _list_tools_middleware(self) -> list[Tool]:
|
|
536
675
|
"""
|
|
537
|
-
List all available tools,
|
|
538
|
-
server.
|
|
676
|
+
List all available tools, applying MCP middleware.
|
|
539
677
|
"""
|
|
540
678
|
|
|
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
679
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
554
680
|
# Create the middleware context.
|
|
555
681
|
mw_context = MiddlewareContext(
|
|
@@ -561,13 +687,66 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
561
687
|
)
|
|
562
688
|
|
|
563
689
|
# Apply the middleware chain.
|
|
564
|
-
return
|
|
690
|
+
return list(
|
|
691
|
+
await self._apply_middleware(
|
|
692
|
+
context=mw_context, call_next=self._list_tools
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
async def _list_tools(
|
|
697
|
+
self,
|
|
698
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
699
|
+
) -> list[Tool]:
|
|
700
|
+
"""
|
|
701
|
+
List all available tools.
|
|
702
|
+
"""
|
|
703
|
+
# 1. Get local tools and filter them
|
|
704
|
+
local_tools = await self._tool_manager.get_tools()
|
|
705
|
+
filtered_local = [
|
|
706
|
+
tool for tool in local_tools.values() if self._should_enable_component(tool)
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
# 2. Get tools from mounted servers
|
|
710
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
711
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
712
|
+
all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
|
|
713
|
+
|
|
714
|
+
for mounted in self._mounted_servers:
|
|
715
|
+
try:
|
|
716
|
+
child_tools = await mounted.server._list_tools_middleware()
|
|
717
|
+
for tool in child_tools:
|
|
718
|
+
# Apply parent server's filtering to mounted components
|
|
719
|
+
if not self._should_enable_component(tool):
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
key = tool.key
|
|
723
|
+
if mounted.prefix:
|
|
724
|
+
key = f"{mounted.prefix}_{tool.key}"
|
|
725
|
+
tool = tool.model_copy(key=key)
|
|
726
|
+
# Later mounted servers override earlier ones
|
|
727
|
+
all_tools[key] = tool
|
|
728
|
+
except Exception as e:
|
|
729
|
+
server_name = getattr(
|
|
730
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
731
|
+
)
|
|
732
|
+
logger.warning(
|
|
733
|
+
f"Failed to list tools from mounted server {server_name!r}: {e}"
|
|
734
|
+
)
|
|
735
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
736
|
+
raise
|
|
737
|
+
continue
|
|
738
|
+
|
|
739
|
+
return list(all_tools.values())
|
|
565
740
|
|
|
566
|
-
async def
|
|
741
|
+
async def _list_resources_mcp(self) -> list[MCPResource]:
|
|
742
|
+
"""
|
|
743
|
+
List all available resources, in the format expected by the low-level MCP
|
|
744
|
+
server.
|
|
745
|
+
"""
|
|
567
746
|
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
568
747
|
|
|
569
748
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
570
|
-
resources = await self.
|
|
749
|
+
resources = await self._list_resources_middleware()
|
|
571
750
|
return [
|
|
572
751
|
resource.to_mcp_resource(
|
|
573
752
|
uri=resource.key,
|
|
@@ -576,25 +755,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
576
755
|
for resource in resources
|
|
577
756
|
]
|
|
578
757
|
|
|
579
|
-
async def
|
|
758
|
+
async def _list_resources_middleware(self) -> list[Resource]:
|
|
580
759
|
"""
|
|
581
|
-
List all available resources,
|
|
582
|
-
server.
|
|
583
|
-
|
|
760
|
+
List all available resources, applying MCP middleware.
|
|
584
761
|
"""
|
|
585
762
|
|
|
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
763
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
599
764
|
# Create the middleware context.
|
|
600
765
|
mw_context = MiddlewareContext(
|
|
@@ -606,13 +771,75 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
606
771
|
)
|
|
607
772
|
|
|
608
773
|
# Apply the middleware chain.
|
|
609
|
-
return
|
|
774
|
+
return list(
|
|
775
|
+
await self._apply_middleware(
|
|
776
|
+
context=mw_context, call_next=self._list_resources
|
|
777
|
+
)
|
|
778
|
+
)
|
|
610
779
|
|
|
611
|
-
async def
|
|
780
|
+
async def _list_resources(
|
|
781
|
+
self,
|
|
782
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
783
|
+
) -> list[Resource]:
|
|
784
|
+
"""
|
|
785
|
+
List all available resources.
|
|
786
|
+
"""
|
|
787
|
+
# 1. Filter local resources
|
|
788
|
+
local_resources = await self._resource_manager.get_resources()
|
|
789
|
+
filtered_local = [
|
|
790
|
+
resource
|
|
791
|
+
for resource in local_resources.values()
|
|
792
|
+
if self._should_enable_component(resource)
|
|
793
|
+
]
|
|
794
|
+
|
|
795
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
796
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
797
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
798
|
+
all_resources: dict[str, Resource] = {
|
|
799
|
+
resource.key: resource for resource in filtered_local
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for mounted in self._mounted_servers:
|
|
803
|
+
try:
|
|
804
|
+
child_resources = await mounted.server._list_resources_middleware()
|
|
805
|
+
for resource in child_resources:
|
|
806
|
+
# Apply parent server's filtering to mounted components
|
|
807
|
+
if not self._should_enable_component(resource):
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
key = resource.key
|
|
811
|
+
if mounted.prefix:
|
|
812
|
+
key = add_resource_prefix(
|
|
813
|
+
resource.key,
|
|
814
|
+
mounted.prefix,
|
|
815
|
+
mounted.resource_prefix_format,
|
|
816
|
+
)
|
|
817
|
+
resource = resource.model_copy(
|
|
818
|
+
key=key,
|
|
819
|
+
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
820
|
+
)
|
|
821
|
+
# Later mounted servers override earlier ones
|
|
822
|
+
all_resources[key] = resource
|
|
823
|
+
except Exception as e:
|
|
824
|
+
server_name = getattr(
|
|
825
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
826
|
+
)
|
|
827
|
+
logger.warning(f"Failed to list resources from {server_name!r}: {e}")
|
|
828
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
829
|
+
raise
|
|
830
|
+
continue
|
|
831
|
+
|
|
832
|
+
return list(all_resources.values())
|
|
833
|
+
|
|
834
|
+
async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
|
|
835
|
+
"""
|
|
836
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
837
|
+
server.
|
|
838
|
+
"""
|
|
612
839
|
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
613
840
|
|
|
614
841
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
615
|
-
templates = await self.
|
|
842
|
+
templates = await self._list_resource_templates_middleware()
|
|
616
843
|
return [
|
|
617
844
|
template.to_mcp_template(
|
|
618
845
|
uriTemplate=template.key,
|
|
@@ -621,25 +848,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
621
848
|
for template in templates
|
|
622
849
|
]
|
|
623
850
|
|
|
624
|
-
async def
|
|
851
|
+
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
|
625
852
|
"""
|
|
626
|
-
List all available resource templates,
|
|
627
|
-
server.
|
|
853
|
+
List all available resource templates, applying MCP middleware.
|
|
628
854
|
|
|
629
855
|
"""
|
|
630
856
|
|
|
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
857
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
644
858
|
# Create the middleware context.
|
|
645
859
|
mw_context = MiddlewareContext(
|
|
@@ -651,13 +865,79 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
651
865
|
)
|
|
652
866
|
|
|
653
867
|
# Apply the middleware chain.
|
|
654
|
-
return
|
|
868
|
+
return list(
|
|
869
|
+
await self._apply_middleware(
|
|
870
|
+
context=mw_context, call_next=self._list_resource_templates
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
async def _list_resource_templates(
|
|
875
|
+
self,
|
|
876
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
877
|
+
) -> list[ResourceTemplate]:
|
|
878
|
+
"""
|
|
879
|
+
List all available resource templates.
|
|
880
|
+
"""
|
|
881
|
+
# 1. Filter local templates
|
|
882
|
+
local_templates = await self._resource_manager.get_resource_templates()
|
|
883
|
+
filtered_local = [
|
|
884
|
+
template
|
|
885
|
+
for template in local_templates.values()
|
|
886
|
+
if self._should_enable_component(template)
|
|
887
|
+
]
|
|
888
|
+
|
|
889
|
+
# 2. Get from mounted servers with resource prefix handling
|
|
890
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
891
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
892
|
+
all_templates: dict[str, ResourceTemplate] = {
|
|
893
|
+
template.key: template for template in filtered_local
|
|
894
|
+
}
|
|
655
895
|
|
|
656
|
-
|
|
896
|
+
for mounted in self._mounted_servers:
|
|
897
|
+
try:
|
|
898
|
+
child_templates = (
|
|
899
|
+
await mounted.server._list_resource_templates_middleware()
|
|
900
|
+
)
|
|
901
|
+
for template in child_templates:
|
|
902
|
+
# Apply parent server's filtering to mounted components
|
|
903
|
+
if not self._should_enable_component(template):
|
|
904
|
+
continue
|
|
905
|
+
|
|
906
|
+
key = template.key
|
|
907
|
+
if mounted.prefix:
|
|
908
|
+
key = add_resource_prefix(
|
|
909
|
+
template.key,
|
|
910
|
+
mounted.prefix,
|
|
911
|
+
mounted.resource_prefix_format,
|
|
912
|
+
)
|
|
913
|
+
template = template.model_copy(
|
|
914
|
+
key=key,
|
|
915
|
+
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
916
|
+
)
|
|
917
|
+
# Later mounted servers override earlier ones
|
|
918
|
+
all_templates[key] = template
|
|
919
|
+
except Exception as e:
|
|
920
|
+
server_name = getattr(
|
|
921
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
922
|
+
)
|
|
923
|
+
logger.warning(
|
|
924
|
+
f"Failed to list resource templates from {server_name!r}: {e}"
|
|
925
|
+
)
|
|
926
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
927
|
+
raise
|
|
928
|
+
continue
|
|
929
|
+
|
|
930
|
+
return list(all_templates.values())
|
|
931
|
+
|
|
932
|
+
async def _list_prompts_mcp(self) -> list[MCPPrompt]:
|
|
933
|
+
"""
|
|
934
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
935
|
+
server.
|
|
936
|
+
"""
|
|
657
937
|
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
658
938
|
|
|
659
939
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
660
|
-
prompts = await self.
|
|
940
|
+
prompts = await self._list_prompts_middleware()
|
|
661
941
|
return [
|
|
662
942
|
prompt.to_mcp_prompt(
|
|
663
943
|
name=prompt.key,
|
|
@@ -666,25 +946,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
666
946
|
for prompt in prompts
|
|
667
947
|
]
|
|
668
948
|
|
|
669
|
-
async def
|
|
949
|
+
async def _list_prompts_middleware(self) -> list[Prompt]:
|
|
670
950
|
"""
|
|
671
|
-
List all available prompts,
|
|
672
|
-
server.
|
|
951
|
+
List all available prompts, applying MCP middleware.
|
|
673
952
|
|
|
674
953
|
"""
|
|
675
954
|
|
|
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
955
|
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
689
956
|
# Create the middleware context.
|
|
690
957
|
mw_context = MiddlewareContext(
|
|
@@ -696,9 +963,62 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
696
963
|
)
|
|
697
964
|
|
|
698
965
|
# Apply the middleware chain.
|
|
699
|
-
return
|
|
966
|
+
return list(
|
|
967
|
+
await self._apply_middleware(
|
|
968
|
+
context=mw_context, call_next=self._list_prompts
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
async def _list_prompts(
|
|
973
|
+
self,
|
|
974
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
975
|
+
) -> list[Prompt]:
|
|
976
|
+
"""
|
|
977
|
+
List all available prompts.
|
|
978
|
+
"""
|
|
979
|
+
# 1. Filter local prompts
|
|
980
|
+
local_prompts = await self._prompt_manager.get_prompts()
|
|
981
|
+
filtered_local = [
|
|
982
|
+
prompt
|
|
983
|
+
for prompt in local_prompts.values()
|
|
984
|
+
if self._should_enable_component(prompt)
|
|
985
|
+
]
|
|
986
|
+
|
|
987
|
+
# 2. Get from mounted servers
|
|
988
|
+
# Mounted servers apply their own filtering, but we also apply parent's filtering
|
|
989
|
+
# Use a dict to implement "later wins" deduplication by key
|
|
990
|
+
all_prompts: dict[str, Prompt] = {
|
|
991
|
+
prompt.key: prompt for prompt in filtered_local
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
for mounted in self._mounted_servers:
|
|
995
|
+
try:
|
|
996
|
+
child_prompts = await mounted.server._list_prompts_middleware()
|
|
997
|
+
for prompt in child_prompts:
|
|
998
|
+
# Apply parent server's filtering to mounted components
|
|
999
|
+
if not self._should_enable_component(prompt):
|
|
1000
|
+
continue
|
|
1001
|
+
|
|
1002
|
+
key = prompt.key
|
|
1003
|
+
if mounted.prefix:
|
|
1004
|
+
key = f"{mounted.prefix}_{prompt.key}"
|
|
1005
|
+
prompt = prompt.model_copy(key=key)
|
|
1006
|
+
# Later mounted servers override earlier ones
|
|
1007
|
+
all_prompts[key] = prompt
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
server_name = getattr(
|
|
1010
|
+
getattr(mounted, "server", None), "name", repr(mounted)
|
|
1011
|
+
)
|
|
1012
|
+
logger.warning(
|
|
1013
|
+
f"Failed to list prompts from mounted server {server_name!r}: {e}"
|
|
1014
|
+
)
|
|
1015
|
+
if fastmcp.settings.mounted_components_raise_on_load_error:
|
|
1016
|
+
raise
|
|
1017
|
+
continue
|
|
700
1018
|
|
|
701
|
-
|
|
1019
|
+
return list(all_prompts.values())
|
|
1020
|
+
|
|
1021
|
+
async def _call_tool_mcp(
|
|
702
1022
|
self, key: str, arguments: dict[str, Any]
|
|
703
1023
|
) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
|
|
704
1024
|
"""
|
|
@@ -719,29 +1039,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
719
1039
|
|
|
720
1040
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
721
1041
|
try:
|
|
722
|
-
result = await self.
|
|
1042
|
+
result = await self._call_tool_middleware(key, arguments)
|
|
723
1043
|
return result.to_mcp_result()
|
|
724
1044
|
except DisabledError:
|
|
725
1045
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
726
1046
|
except NotFoundError:
|
|
727
1047
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
728
1048
|
|
|
729
|
-
async def
|
|
1049
|
+
async def _call_tool_middleware(
|
|
1050
|
+
self,
|
|
1051
|
+
key: str,
|
|
1052
|
+
arguments: dict[str, Any],
|
|
1053
|
+
) -> ToolResult:
|
|
730
1054
|
"""
|
|
731
1055
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
732
1056
|
"""
|
|
733
1057
|
|
|
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
1058
|
mw_context = MiddlewareContext[CallToolRequestParams](
|
|
746
1059
|
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
747
1060
|
source="client",
|
|
@@ -749,9 +1062,53 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
749
1062
|
method="tools/call",
|
|
750
1063
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
751
1064
|
)
|
|
752
|
-
return await self._apply_middleware(
|
|
1065
|
+
return await self._apply_middleware(
|
|
1066
|
+
context=mw_context, call_next=self._call_tool
|
|
1067
|
+
)
|
|
753
1068
|
|
|
754
|
-
async def
|
|
1069
|
+
async def _call_tool(
|
|
1070
|
+
self,
|
|
1071
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
1072
|
+
) -> ToolResult:
|
|
1073
|
+
"""
|
|
1074
|
+
Call a tool
|
|
1075
|
+
"""
|
|
1076
|
+
tool_name = context.message.name
|
|
1077
|
+
|
|
1078
|
+
# Try mounted servers in reverse order (later wins)
|
|
1079
|
+
for mounted in reversed(self._mounted_servers):
|
|
1080
|
+
try_name = tool_name
|
|
1081
|
+
if mounted.prefix:
|
|
1082
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1083
|
+
continue
|
|
1084
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1085
|
+
|
|
1086
|
+
try:
|
|
1087
|
+
# First, get the tool to check if parent's filter allows it
|
|
1088
|
+
tool = await mounted.server._tool_manager.get_tool(try_name)
|
|
1089
|
+
if not self._should_enable_component(tool):
|
|
1090
|
+
# Parent filter blocks this tool, continue searching
|
|
1091
|
+
continue
|
|
1092
|
+
|
|
1093
|
+
return await mounted.server._call_tool_middleware(
|
|
1094
|
+
try_name, context.message.arguments or {}
|
|
1095
|
+
)
|
|
1096
|
+
except NotFoundError:
|
|
1097
|
+
continue
|
|
1098
|
+
|
|
1099
|
+
# Try local tools last (mounted servers override local)
|
|
1100
|
+
try:
|
|
1101
|
+
tool = await self._tool_manager.get_tool(tool_name)
|
|
1102
|
+
if self._should_enable_component(tool):
|
|
1103
|
+
return await self._tool_manager.call_tool(
|
|
1104
|
+
key=tool_name, arguments=context.message.arguments or {}
|
|
1105
|
+
)
|
|
1106
|
+
except NotFoundError:
|
|
1107
|
+
pass
|
|
1108
|
+
|
|
1109
|
+
raise NotFoundError(f"Unknown tool: {tool_name!r}")
|
|
1110
|
+
|
|
1111
|
+
async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
755
1112
|
"""
|
|
756
1113
|
Handle MCP 'readResource' requests.
|
|
757
1114
|
|
|
@@ -761,7 +1118,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
761
1118
|
|
|
762
1119
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
763
1120
|
try:
|
|
764
|
-
return
|
|
1121
|
+
return list[ReadResourceContents](
|
|
1122
|
+
await self._read_resource_middleware(uri)
|
|
1123
|
+
)
|
|
765
1124
|
except DisabledError:
|
|
766
1125
|
# convert to NotFoundError to avoid leaking resource presence
|
|
767
1126
|
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
@@ -769,26 +1128,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
769
1128
|
# standardize NotFound message
|
|
770
1129
|
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
|
771
1130
|
|
|
772
|
-
async def
|
|
1131
|
+
async def _read_resource_middleware(
|
|
1132
|
+
self,
|
|
1133
|
+
uri: AnyUrl | str,
|
|
1134
|
+
) -> list[ReadResourceContents]:
|
|
773
1135
|
"""
|
|
774
1136
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
775
1137
|
"""
|
|
776
1138
|
|
|
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
1139
|
# Convert string URI to AnyUrl if needed
|
|
793
1140
|
if isinstance(uri, str):
|
|
794
1141
|
uri_param = AnyUrl(uri)
|
|
@@ -802,9 +1149,61 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
802
1149
|
method="resources/read",
|
|
803
1150
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
804
1151
|
)
|
|
805
|
-
return
|
|
1152
|
+
return list(
|
|
1153
|
+
await self._apply_middleware(
|
|
1154
|
+
context=mw_context, call_next=self._read_resource
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
async def _read_resource(
|
|
1159
|
+
self,
|
|
1160
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
1161
|
+
) -> list[ReadResourceContents]:
|
|
1162
|
+
"""
|
|
1163
|
+
Read a resource
|
|
1164
|
+
"""
|
|
1165
|
+
uri_str = str(context.message.uri)
|
|
1166
|
+
|
|
1167
|
+
# Try mounted servers in reverse order (later wins)
|
|
1168
|
+
for mounted in reversed(self._mounted_servers):
|
|
1169
|
+
key = uri_str
|
|
1170
|
+
if mounted.prefix:
|
|
1171
|
+
if not has_resource_prefix(
|
|
1172
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1173
|
+
):
|
|
1174
|
+
continue
|
|
1175
|
+
key = remove_resource_prefix(
|
|
1176
|
+
key, mounted.prefix, mounted.resource_prefix_format
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
try:
|
|
1180
|
+
# First, get the resource to check if parent's filter allows it
|
|
1181
|
+
resource = await mounted.server._resource_manager.get_resource(key)
|
|
1182
|
+
if not self._should_enable_component(resource):
|
|
1183
|
+
# Parent filter blocks this resource, continue searching
|
|
1184
|
+
continue
|
|
1185
|
+
result = list(await mounted.server._read_resource_middleware(key))
|
|
1186
|
+
return result
|
|
1187
|
+
except NotFoundError:
|
|
1188
|
+
continue
|
|
806
1189
|
|
|
807
|
-
|
|
1190
|
+
# Try local resources last (mounted servers override local)
|
|
1191
|
+
try:
|
|
1192
|
+
resource = await self._resource_manager.get_resource(uri_str)
|
|
1193
|
+
if self._should_enable_component(resource):
|
|
1194
|
+
content = await self._resource_manager.read_resource(uri_str)
|
|
1195
|
+
return [
|
|
1196
|
+
ReadResourceContents(
|
|
1197
|
+
content=content,
|
|
1198
|
+
mime_type=resource.mime_type,
|
|
1199
|
+
)
|
|
1200
|
+
]
|
|
1201
|
+
except NotFoundError:
|
|
1202
|
+
pass
|
|
1203
|
+
|
|
1204
|
+
raise NotFoundError(f"Unknown resource: {uri_str!r}")
|
|
1205
|
+
|
|
1206
|
+
async def _get_prompt_mcp(
|
|
808
1207
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
809
1208
|
) -> GetPromptResult:
|
|
810
1209
|
"""
|
|
@@ -820,7 +1219,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
820
1219
|
|
|
821
1220
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
822
1221
|
try:
|
|
823
|
-
return await self.
|
|
1222
|
+
return await self._get_prompt_middleware(name, arguments)
|
|
824
1223
|
except DisabledError:
|
|
825
1224
|
# convert to NotFoundError to avoid leaking prompt presence
|
|
826
1225
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
@@ -828,24 +1227,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
828
1227
|
# standardize NotFound message
|
|
829
1228
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
830
1229
|
|
|
831
|
-
async def
|
|
1230
|
+
async def _get_prompt_middleware(
|
|
832
1231
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
833
1232
|
) -> GetPromptResult:
|
|
834
1233
|
"""
|
|
835
1234
|
Applies this server's middleware and delegates the filtered call to the manager.
|
|
836
1235
|
"""
|
|
837
1236
|
|
|
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
1237
|
mw_context = MiddlewareContext(
|
|
850
1238
|
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
851
1239
|
source="client",
|
|
@@ -853,7 +1241,47 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
853
1241
|
method="prompts/get",
|
|
854
1242
|
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
855
1243
|
)
|
|
856
|
-
return await self._apply_middleware(
|
|
1244
|
+
return await self._apply_middleware(
|
|
1245
|
+
context=mw_context, call_next=self._get_prompt
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
async def _get_prompt(
|
|
1249
|
+
self,
|
|
1250
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
1251
|
+
) -> GetPromptResult:
|
|
1252
|
+
name = context.message.name
|
|
1253
|
+
|
|
1254
|
+
# Try mounted servers in reverse order (later wins)
|
|
1255
|
+
for mounted in reversed(self._mounted_servers):
|
|
1256
|
+
try_name = name
|
|
1257
|
+
if mounted.prefix:
|
|
1258
|
+
if not name.startswith(f"{mounted.prefix}_"):
|
|
1259
|
+
continue
|
|
1260
|
+
try_name = name[len(mounted.prefix) + 1 :]
|
|
1261
|
+
|
|
1262
|
+
try:
|
|
1263
|
+
# First, get the prompt to check if parent's filter allows it
|
|
1264
|
+
prompt = await mounted.server._prompt_manager.get_prompt(try_name)
|
|
1265
|
+
if not self._should_enable_component(prompt):
|
|
1266
|
+
# Parent filter blocks this prompt, continue searching
|
|
1267
|
+
continue
|
|
1268
|
+
return await mounted.server._get_prompt_middleware(
|
|
1269
|
+
try_name, context.message.arguments
|
|
1270
|
+
)
|
|
1271
|
+
except NotFoundError:
|
|
1272
|
+
continue
|
|
1273
|
+
|
|
1274
|
+
# Try local prompts last (mounted servers override local)
|
|
1275
|
+
try:
|
|
1276
|
+
prompt = await self._prompt_manager.get_prompt(name)
|
|
1277
|
+
if self._should_enable_component(prompt):
|
|
1278
|
+
return await self._prompt_manager.render_prompt(
|
|
1279
|
+
name=name, arguments=context.message.arguments
|
|
1280
|
+
)
|
|
1281
|
+
except NotFoundError:
|
|
1282
|
+
pass
|
|
1283
|
+
|
|
1284
|
+
raise NotFoundError(f"Unknown prompt: {name!r}")
|
|
857
1285
|
|
|
858
1286
|
def add_tool(self, tool: Tool) -> Tool:
|
|
859
1287
|
"""Add a tool to the server.
|
|
@@ -918,6 +1346,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
918
1346
|
name: str | None = None,
|
|
919
1347
|
title: str | None = None,
|
|
920
1348
|
description: str | None = None,
|
|
1349
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
921
1350
|
tags: set[str] | None = None,
|
|
922
1351
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
923
1352
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -934,6 +1363,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
934
1363
|
name: str | None = None,
|
|
935
1364
|
title: str | None = None,
|
|
936
1365
|
description: str | None = None,
|
|
1366
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
937
1367
|
tags: set[str] | None = None,
|
|
938
1368
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
939
1369
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -949,6 +1379,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
1379
|
name: str | None = None,
|
|
950
1380
|
title: str | None = None,
|
|
951
1381
|
description: str | None = None,
|
|
1382
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
952
1383
|
tags: set[str] | None = None,
|
|
953
1384
|
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
954
1385
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
@@ -1032,6 +1463,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1032
1463
|
name=tool_name,
|
|
1033
1464
|
title=title,
|
|
1034
1465
|
description=description,
|
|
1466
|
+
icons=icons,
|
|
1035
1467
|
tags=tags,
|
|
1036
1468
|
output_schema=output_schema,
|
|
1037
1469
|
annotations=cast(ToolAnnotations | None, annotations),
|
|
@@ -1065,6 +1497,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1065
1497
|
name=tool_name,
|
|
1066
1498
|
title=title,
|
|
1067
1499
|
description=description,
|
|
1500
|
+
icons=icons,
|
|
1068
1501
|
tags=tags,
|
|
1069
1502
|
output_schema=output_schema,
|
|
1070
1503
|
annotations=annotations,
|
|
@@ -1162,6 +1595,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1162
1595
|
name: str | None = None,
|
|
1163
1596
|
title: str | None = None,
|
|
1164
1597
|
description: str | None = None,
|
|
1598
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1165
1599
|
mime_type: str | None = None,
|
|
1166
1600
|
tags: set[str] | None = None,
|
|
1167
1601
|
enabled: bool | None = None,
|
|
@@ -1261,6 +1695,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1261
1695
|
name=name,
|
|
1262
1696
|
title=title,
|
|
1263
1697
|
description=description,
|
|
1698
|
+
icons=icons,
|
|
1264
1699
|
mime_type=mime_type,
|
|
1265
1700
|
tags=tags,
|
|
1266
1701
|
enabled=enabled,
|
|
@@ -1276,6 +1711,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1276
1711
|
name=name,
|
|
1277
1712
|
title=title,
|
|
1278
1713
|
description=description,
|
|
1714
|
+
icons=icons,
|
|
1279
1715
|
mime_type=mime_type,
|
|
1280
1716
|
tags=tags,
|
|
1281
1717
|
enabled=enabled,
|
|
@@ -1322,6 +1758,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1322
1758
|
name: str | None = None,
|
|
1323
1759
|
title: str | None = None,
|
|
1324
1760
|
description: str | None = None,
|
|
1761
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1325
1762
|
tags: set[str] | None = None,
|
|
1326
1763
|
enabled: bool | None = None,
|
|
1327
1764
|
meta: dict[str, Any] | None = None,
|
|
@@ -1335,6 +1772,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1335
1772
|
name: str | None = None,
|
|
1336
1773
|
title: str | None = None,
|
|
1337
1774
|
description: str | None = None,
|
|
1775
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1338
1776
|
tags: set[str] | None = None,
|
|
1339
1777
|
enabled: bool | None = None,
|
|
1340
1778
|
meta: dict[str, Any] | None = None,
|
|
@@ -1347,6 +1785,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1347
1785
|
name: str | None = None,
|
|
1348
1786
|
title: str | None = None,
|
|
1349
1787
|
description: str | None = None,
|
|
1788
|
+
icons: list[mcp.types.Icon] | None = None,
|
|
1350
1789
|
tags: set[str] | None = None,
|
|
1351
1790
|
enabled: bool | None = None,
|
|
1352
1791
|
meta: dict[str, Any] | None = None,
|
|
@@ -1446,6 +1885,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1446
1885
|
name=prompt_name,
|
|
1447
1886
|
title=title,
|
|
1448
1887
|
description=description,
|
|
1888
|
+
icons=icons,
|
|
1449
1889
|
tags=tags,
|
|
1450
1890
|
enabled=enabled,
|
|
1451
1891
|
meta=meta,
|
|
@@ -1476,6 +1916,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1476
1916
|
name=prompt_name,
|
|
1477
1917
|
title=title,
|
|
1478
1918
|
description=description,
|
|
1919
|
+
icons=icons,
|
|
1479
1920
|
tags=tags,
|
|
1480
1921
|
enabled=enabled,
|
|
1481
1922
|
meta=meta,
|
|
@@ -1498,15 +1939,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1498
1939
|
)
|
|
1499
1940
|
|
|
1500
1941
|
with temporary_log_level(log_level):
|
|
1501
|
-
async with
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
self._mcp_server.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1942
|
+
async with self._lifespan_manager():
|
|
1943
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
1944
|
+
logger.info(
|
|
1945
|
+
f"Starting MCP server {self.name!r} with transport 'stdio'"
|
|
1946
|
+
)
|
|
1947
|
+
await self._mcp_server.run(
|
|
1948
|
+
read_stream,
|
|
1949
|
+
write_stream,
|
|
1950
|
+
self._mcp_server.create_initialization_options(
|
|
1951
|
+
NotificationOptions(tools_changed=True)
|
|
1952
|
+
),
|
|
1953
|
+
)
|
|
1510
1954
|
|
|
1511
1955
|
async def run_http_async(
|
|
1512
1956
|
self,
|
|
@@ -1518,6 +1962,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1518
1962
|
path: str | None = None,
|
|
1519
1963
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1520
1964
|
middleware: list[ASGIMiddleware] | None = None,
|
|
1965
|
+
json_response: bool | None = None,
|
|
1521
1966
|
stateless_http: bool | None = None,
|
|
1522
1967
|
) -> None:
|
|
1523
1968
|
"""Run the server using HTTP transport.
|
|
@@ -1530,6 +1975,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1530
1975
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
1531
1976
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
1532
1977
|
middleware: A list of middleware to apply to the app
|
|
1978
|
+
json_response: Whether to use JSON response format (defaults to settings.json_response)
|
|
1533
1979
|
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
|
1534
1980
|
"""
|
|
1535
1981
|
host = host or self._deprecated_settings.host
|
|
@@ -1542,6 +1988,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1542
1988
|
path=path,
|
|
1543
1989
|
transport=transport,
|
|
1544
1990
|
middleware=middleware,
|
|
1991
|
+
json_response=json_response,
|
|
1545
1992
|
stateless_http=stateless_http,
|
|
1546
1993
|
)
|
|
1547
1994
|
|
|
@@ -1566,6 +2013,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1566
2013
|
config_kwargs: dict[str, Any] = {
|
|
1567
2014
|
"timeout_graceful_shutdown": 0,
|
|
1568
2015
|
"lifespan": "on",
|
|
2016
|
+
"ws": "websockets-sansio",
|
|
1569
2017
|
}
|
|
1570
2018
|
config_kwargs.update(_uvicorn_config_from_user)
|
|
1571
2019
|
|
|
@@ -1573,14 +2021,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1573
2021
|
config_kwargs["log_level"] = default_log_level_to_use
|
|
1574
2022
|
|
|
1575
2023
|
with temporary_log_level(log_level):
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2024
|
+
async with self._lifespan_manager():
|
|
2025
|
+
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
2026
|
+
server = uvicorn.Server(config)
|
|
2027
|
+
path = app.state.path.lstrip("/") # type: ignore
|
|
2028
|
+
logger.info(
|
|
2029
|
+
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
2030
|
+
)
|
|
1582
2031
|
|
|
1583
|
-
|
|
2032
|
+
await server.serve()
|
|
1584
2033
|
|
|
1585
2034
|
async def run_sse_async(
|
|
1586
2035
|
self,
|
|
@@ -1842,7 +2291,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1842
2291
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1843
2292
|
# we should treat it as a proxy
|
|
1844
2293
|
if as_proxy is None:
|
|
1845
|
-
as_proxy = server.
|
|
2294
|
+
as_proxy = server._lifespan != default_lifespan
|
|
1846
2295
|
|
|
1847
2296
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1848
2297
|
server = FastMCP.as_proxy(server)
|
|
@@ -1854,9 +2303,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1854
2303
|
resource_prefix_format=self.resource_prefix_format,
|
|
1855
2304
|
)
|
|
1856
2305
|
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
2306
|
|
|
1861
2307
|
async def import_server(
|
|
1862
2308
|
self,
|
|
@@ -1979,6 +2425,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1979
2425
|
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1980
2426
|
self._prompt_manager.add_prompt(prompt)
|
|
1981
2427
|
|
|
2428
|
+
if server._lifespan != default_lifespan:
|
|
2429
|
+
from warnings import warn
|
|
2430
|
+
|
|
2431
|
+
warn(
|
|
2432
|
+
message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
|
|
2433
|
+
category=RuntimeWarning,
|
|
2434
|
+
stacklevel=2,
|
|
2435
|
+
)
|
|
2436
|
+
|
|
1982
2437
|
if prefix:
|
|
1983
2438
|
logger.debug(
|
|
1984
2439
|
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|