fastmcp 2.8.1__py3-none-any.whl → 2.9.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 +99 -1
- fastmcp/cli/run.py +1 -3
- fastmcp/client/auth/oauth.py +1 -2
- fastmcp/client/client.py +21 -5
- fastmcp/client/transports.py +17 -2
- fastmcp/contrib/mcp_mixin/README.md +79 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
- fastmcp/prompts/prompt.py +91 -11
- fastmcp/prompts/prompt_manager.py +119 -43
- fastmcp/resources/resource.py +11 -1
- fastmcp/resources/resource_manager.py +249 -76
- fastmcp/resources/template.py +27 -1
- fastmcp/server/auth/providers/bearer.py +32 -10
- fastmcp/server/context.py +41 -2
- fastmcp/server/http.py +8 -0
- fastmcp/server/middleware/__init__.py +6 -0
- fastmcp/server/middleware/error_handling.py +206 -0
- fastmcp/server/middleware/logging.py +165 -0
- fastmcp/server/middleware/middleware.py +236 -0
- fastmcp/server/middleware/rate_limiting.py +231 -0
- fastmcp/server/middleware/timing.py +156 -0
- fastmcp/server/proxy.py +250 -140
- fastmcp/server/server.py +320 -242
- fastmcp/settings.py +2 -2
- fastmcp/tools/tool.py +6 -2
- fastmcp/tools/tool_manager.py +114 -45
- fastmcp/utilities/components.py +22 -2
- fastmcp/utilities/inspect.py +326 -0
- fastmcp/utilities/json_schema.py +67 -23
- fastmcp/utilities/mcp_config.py +13 -7
- fastmcp/utilities/openapi.py +5 -3
- fastmcp/utilities/tests.py +1 -1
- fastmcp/utilities/types.py +90 -1
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/METADATA +2 -2
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/RECORD +38 -31
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -12,12 +12,14 @@ from contextlib import (
|
|
|
12
12
|
AsyncExitStack,
|
|
13
13
|
asynccontextmanager,
|
|
14
14
|
)
|
|
15
|
+
from dataclasses import dataclass
|
|
15
16
|
from functools import partial
|
|
16
17
|
from pathlib import Path
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
|
|
18
19
|
|
|
19
20
|
import anyio
|
|
20
21
|
import httpx
|
|
22
|
+
import mcp.types
|
|
21
23
|
import uvicorn
|
|
22
24
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
23
25
|
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
@@ -33,7 +35,7 @@ from mcp.types import Resource as MCPResource
|
|
|
33
35
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
34
36
|
from mcp.types import Tool as MCPTool
|
|
35
37
|
from pydantic import AnyUrl
|
|
36
|
-
from starlette.middleware import Middleware
|
|
38
|
+
from starlette.middleware import Middleware as ASGIMiddleware
|
|
37
39
|
from starlette.requests import Request
|
|
38
40
|
from starlette.responses import Response
|
|
39
41
|
from starlette.routing import BaseRoute, Route
|
|
@@ -52,6 +54,7 @@ from fastmcp.server.http import (
|
|
|
52
54
|
create_sse_app,
|
|
53
55
|
create_streamable_http_app,
|
|
54
56
|
)
|
|
57
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
55
58
|
from fastmcp.settings import Settings
|
|
56
59
|
from fastmcp.tools import ToolManager
|
|
57
60
|
from fastmcp.tools.tool import FunctionTool, Tool
|
|
@@ -71,6 +74,7 @@ if TYPE_CHECKING:
|
|
|
71
74
|
logger = get_logger(__name__)
|
|
72
75
|
|
|
73
76
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
77
|
+
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
74
78
|
|
|
75
79
|
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
76
80
|
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
@@ -111,7 +115,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
111
115
|
self,
|
|
112
116
|
name: str | None = None,
|
|
113
117
|
instructions: str | None = None,
|
|
118
|
+
*,
|
|
119
|
+
version: str | None = None,
|
|
114
120
|
auth: OAuthProvider | None = None,
|
|
121
|
+
middleware: list[Middleware] | None = None,
|
|
115
122
|
lifespan: (
|
|
116
123
|
Callable[
|
|
117
124
|
[FastMCP[LifespanResultT]],
|
|
@@ -152,7 +159,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
152
159
|
self._cache = TimedCache(
|
|
153
160
|
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
154
161
|
)
|
|
155
|
-
self._mounted_servers: dict[str, MountedServer] = {}
|
|
156
162
|
self._additional_http_routes: list[BaseRoute] = []
|
|
157
163
|
self._tool_manager = ToolManager(
|
|
158
164
|
duplicate_behavior=on_duplicate_tools,
|
|
@@ -175,6 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
175
181
|
self._has_lifespan = True
|
|
176
182
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
177
183
|
name=name or "FastMCP",
|
|
184
|
+
version=version,
|
|
178
185
|
instructions=instructions,
|
|
179
186
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
180
187
|
)
|
|
@@ -192,6 +199,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
199
|
self.include_tags = include_tags
|
|
193
200
|
self.exclude_tags = exclude_tags
|
|
194
201
|
|
|
202
|
+
self.middleware = middleware or []
|
|
203
|
+
|
|
195
204
|
# Set up MCP protocol handlers
|
|
196
205
|
self._setup_handlers()
|
|
197
206
|
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
|
@@ -272,7 +281,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
272
281
|
|
|
273
282
|
async def run_async(
|
|
274
283
|
self,
|
|
275
|
-
transport:
|
|
284
|
+
transport: Transport | None = None,
|
|
276
285
|
**transport_kwargs: Any,
|
|
277
286
|
) -> None:
|
|
278
287
|
"""Run the FastMCP server asynchronously.
|
|
@@ -282,19 +291,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
282
291
|
"""
|
|
283
292
|
if transport is None:
|
|
284
293
|
transport = "stdio"
|
|
285
|
-
if transport not in {"stdio", "
|
|
294
|
+
if transport not in {"stdio", "http", "sse", "streamable-http"}:
|
|
286
295
|
raise ValueError(f"Unknown transport: {transport}")
|
|
287
296
|
|
|
288
297
|
if transport == "stdio":
|
|
289
298
|
await self.run_stdio_async(**transport_kwargs)
|
|
290
|
-
elif transport in {"
|
|
299
|
+
elif transport in {"http", "sse", "streamable-http"}:
|
|
291
300
|
await self.run_http_async(transport=transport, **transport_kwargs)
|
|
292
301
|
else:
|
|
293
302
|
raise ValueError(f"Unknown transport: {transport}")
|
|
294
303
|
|
|
295
304
|
def run(
|
|
296
305
|
self,
|
|
297
|
-
transport:
|
|
306
|
+
transport: Transport | None = None,
|
|
298
307
|
**transport_kwargs: Any,
|
|
299
308
|
) -> None:
|
|
300
309
|
"""Run the FastMCP server. Note this is a synchronous function.
|
|
@@ -315,22 +324,23 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
315
324
|
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
316
325
|
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
317
326
|
|
|
327
|
+
async def _apply_middleware(
|
|
328
|
+
self,
|
|
329
|
+
context: MiddlewareContext[Any],
|
|
330
|
+
call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
|
|
331
|
+
) -> Any:
|
|
332
|
+
"""Builds and executes the middleware chain."""
|
|
333
|
+
chain = call_next
|
|
334
|
+
for mw in reversed(self.middleware):
|
|
335
|
+
chain = partial(mw, call_next=chain)
|
|
336
|
+
return await chain(context)
|
|
337
|
+
|
|
338
|
+
def add_middleware(self, middleware: Middleware) -> None:
|
|
339
|
+
self.middleware.append(middleware)
|
|
340
|
+
|
|
318
341
|
async def get_tools(self) -> dict[str, Tool]:
|
|
319
342
|
"""Get all registered tools, indexed by registered key."""
|
|
320
|
-
|
|
321
|
-
tools: dict[str, Tool] = {}
|
|
322
|
-
for prefix, server in self._mounted_servers.items():
|
|
323
|
-
try:
|
|
324
|
-
server_tools = await server.get_tools()
|
|
325
|
-
tools.update(server_tools)
|
|
326
|
-
except Exception as e:
|
|
327
|
-
logger.warning(
|
|
328
|
-
f"Failed to get tools from mounted server '{prefix}': {e}"
|
|
329
|
-
)
|
|
330
|
-
continue
|
|
331
|
-
tools.update(self._tool_manager.get_tools())
|
|
332
|
-
self._cache.set("tools", tools)
|
|
333
|
-
return tools
|
|
343
|
+
return await self._tool_manager.get_tools()
|
|
334
344
|
|
|
335
345
|
async def get_tool(self, key: str) -> Tool:
|
|
336
346
|
tools = await self.get_tools()
|
|
@@ -340,20 +350,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
340
350
|
|
|
341
351
|
async def get_resources(self) -> dict[str, Resource]:
|
|
342
352
|
"""Get all registered resources, indexed by registered key."""
|
|
343
|
-
|
|
344
|
-
resources: dict[str, Resource] = {}
|
|
345
|
-
for prefix, server in self._mounted_servers.items():
|
|
346
|
-
try:
|
|
347
|
-
server_resources = await server.get_resources()
|
|
348
|
-
resources.update(server_resources)
|
|
349
|
-
except Exception as e:
|
|
350
|
-
logger.warning(
|
|
351
|
-
f"Failed to get resources from mounted server '{prefix}': {e}"
|
|
352
|
-
)
|
|
353
|
-
continue
|
|
354
|
-
resources.update(self._resource_manager.get_resources())
|
|
355
|
-
self._cache.set("resources", resources)
|
|
356
|
-
return resources
|
|
353
|
+
return await self._resource_manager.get_resources()
|
|
357
354
|
|
|
358
355
|
async def get_resource(self, key: str) -> Resource:
|
|
359
356
|
resources = await self.get_resources()
|
|
@@ -363,23 +360,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
363
360
|
|
|
364
361
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
365
362
|
"""Get all registered resource templates, indexed by registered key."""
|
|
366
|
-
|
|
367
|
-
templates := self._cache.get("resource_templates")
|
|
368
|
-
) is self._cache.NOT_FOUND:
|
|
369
|
-
templates: dict[str, ResourceTemplate] = {}
|
|
370
|
-
for prefix, server in self._mounted_servers.items():
|
|
371
|
-
try:
|
|
372
|
-
server_templates = await server.get_resource_templates()
|
|
373
|
-
templates.update(server_templates)
|
|
374
|
-
except Exception as e:
|
|
375
|
-
logger.warning(
|
|
376
|
-
"Failed to get resource templates from mounted server "
|
|
377
|
-
f"'{prefix}': {e}"
|
|
378
|
-
)
|
|
379
|
-
continue
|
|
380
|
-
templates.update(self._resource_manager.get_templates())
|
|
381
|
-
self._cache.set("resource_templates", templates)
|
|
382
|
-
return templates
|
|
363
|
+
return await self._resource_manager.get_resource_templates()
|
|
383
364
|
|
|
384
365
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
385
366
|
templates = await self.get_resource_templates()
|
|
@@ -391,20 +372,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
391
372
|
"""
|
|
392
373
|
List all available prompts.
|
|
393
374
|
"""
|
|
394
|
-
|
|
395
|
-
prompts: dict[str, Prompt] = {}
|
|
396
|
-
for prefix, server in self._mounted_servers.items():
|
|
397
|
-
try:
|
|
398
|
-
server_prompts = await server.get_prompts()
|
|
399
|
-
prompts.update(server_prompts)
|
|
400
|
-
except Exception as e:
|
|
401
|
-
logger.warning(
|
|
402
|
-
f"Failed to get prompts from mounted server '{prefix}': {e}"
|
|
403
|
-
)
|
|
404
|
-
continue
|
|
405
|
-
prompts.update(self._prompt_manager.get_prompts())
|
|
406
|
-
self._cache.set("prompts", prompts)
|
|
407
|
-
return prompts
|
|
375
|
+
return await self._prompt_manager.get_prompts()
|
|
408
376
|
|
|
409
377
|
async def get_prompt(self, key: str) -> Prompt:
|
|
410
378
|
prompts = await self.get_prompts()
|
|
@@ -457,58 +425,165 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
457
425
|
return decorator
|
|
458
426
|
|
|
459
427
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
428
|
+
logger.debug("Handler called: list_tools")
|
|
429
|
+
|
|
430
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
431
|
+
tools = await self._list_tools()
|
|
432
|
+
return [tool.to_mcp_tool(name=tool.key) for tool in tools]
|
|
433
|
+
|
|
434
|
+
async def _list_tools(self) -> list[Tool]:
|
|
460
435
|
"""
|
|
461
436
|
List all available tools, in the format expected by the low-level MCP
|
|
462
437
|
server.
|
|
463
438
|
|
|
464
439
|
"""
|
|
465
|
-
tools = await self.get_tools()
|
|
466
440
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
441
|
+
async def _handler(
|
|
442
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
443
|
+
) -> list[Tool]:
|
|
444
|
+
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
|
445
|
+
|
|
446
|
+
mcp_tools: list[Tool] = []
|
|
447
|
+
for tool in tools:
|
|
448
|
+
if self._should_enable_component(tool):
|
|
449
|
+
mcp_tools.append(tool)
|
|
450
|
+
|
|
451
|
+
return mcp_tools
|
|
452
|
+
|
|
453
|
+
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
454
|
+
# Create the middleware context.
|
|
455
|
+
mw_context = MiddlewareContext(
|
|
456
|
+
message=mcp.types.ListToolsRequest(method="tools/list"),
|
|
457
|
+
source="client",
|
|
458
|
+
type="request",
|
|
459
|
+
method="tools/list",
|
|
460
|
+
fastmcp_context=fastmcp_ctx,
|
|
461
|
+
)
|
|
471
462
|
|
|
472
|
-
|
|
463
|
+
# Apply the middleware chain.
|
|
464
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
473
465
|
|
|
474
466
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
467
|
+
logger.debug("Handler called: list_resources")
|
|
468
|
+
|
|
469
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
470
|
+
resources = await self._list_resources()
|
|
471
|
+
return [
|
|
472
|
+
resource.to_mcp_resource(uri=resource.key) for resource in resources
|
|
473
|
+
]
|
|
474
|
+
|
|
475
|
+
async def _list_resources(self) -> list[Resource]:
|
|
475
476
|
"""
|
|
476
477
|
List all available resources, in the format expected by the low-level MCP
|
|
477
478
|
server.
|
|
478
479
|
|
|
479
480
|
"""
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
481
|
+
|
|
482
|
+
async def _handler(
|
|
483
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
484
|
+
) -> list[Resource]:
|
|
485
|
+
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
|
486
|
+
|
|
487
|
+
mcp_resources: list[Resource] = []
|
|
488
|
+
for resource in resources:
|
|
489
|
+
if self._should_enable_component(resource):
|
|
490
|
+
mcp_resources.append(resource)
|
|
491
|
+
|
|
492
|
+
return mcp_resources
|
|
493
|
+
|
|
494
|
+
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
495
|
+
# Create the middleware context.
|
|
496
|
+
mw_context = MiddlewareContext(
|
|
497
|
+
message={}, # List resources doesn't have parameters
|
|
498
|
+
source="client",
|
|
499
|
+
type="request",
|
|
500
|
+
method="resources/list",
|
|
501
|
+
fastmcp_context=fastmcp_ctx,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Apply the middleware chain.
|
|
505
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
486
506
|
|
|
487
507
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
508
|
+
logger.debug("Handler called: list_resource_templates")
|
|
509
|
+
|
|
510
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
511
|
+
templates = await self._list_resource_templates()
|
|
512
|
+
return [
|
|
513
|
+
template.to_mcp_template(uriTemplate=template.key)
|
|
514
|
+
for template in templates
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
async def _list_resource_templates(self) -> list[ResourceTemplate]:
|
|
488
518
|
"""
|
|
489
|
-
List all available resource templates, in the format expected by the low-level
|
|
490
|
-
|
|
519
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
520
|
+
server.
|
|
491
521
|
|
|
492
522
|
"""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
523
|
+
|
|
524
|
+
async def _handler(
|
|
525
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
526
|
+
) -> list[ResourceTemplate]:
|
|
527
|
+
templates = await self._resource_manager.list_resource_templates()
|
|
528
|
+
|
|
529
|
+
mcp_templates: list[ResourceTemplate] = []
|
|
530
|
+
for template in templates:
|
|
531
|
+
if self._should_enable_component(template):
|
|
532
|
+
mcp_templates.append(template)
|
|
533
|
+
|
|
534
|
+
return mcp_templates
|
|
535
|
+
|
|
536
|
+
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
537
|
+
# Create the middleware context.
|
|
538
|
+
mw_context = MiddlewareContext(
|
|
539
|
+
message={}, # List resource templates doesn't have parameters
|
|
540
|
+
source="client",
|
|
541
|
+
type="request",
|
|
542
|
+
method="resources/templates/list",
|
|
543
|
+
fastmcp_context=fastmcp_ctx,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Apply the middleware chain.
|
|
547
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
499
548
|
|
|
500
549
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
550
|
+
logger.debug("Handler called: list_prompts")
|
|
551
|
+
|
|
552
|
+
with fastmcp.server.context.Context(fastmcp=self):
|
|
553
|
+
prompts = await self._list_prompts()
|
|
554
|
+
return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
|
|
555
|
+
|
|
556
|
+
async def _list_prompts(self) -> list[Prompt]:
|
|
501
557
|
"""
|
|
502
558
|
List all available prompts, in the format expected by the low-level MCP
|
|
503
559
|
server.
|
|
504
560
|
|
|
505
561
|
"""
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
562
|
+
|
|
563
|
+
async def _handler(
|
|
564
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
565
|
+
) -> list[Prompt]:
|
|
566
|
+
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
|
567
|
+
|
|
568
|
+
mcp_prompts: list[Prompt] = []
|
|
569
|
+
for prompt in prompts:
|
|
570
|
+
if self._should_enable_component(prompt):
|
|
571
|
+
mcp_prompts.append(prompt)
|
|
572
|
+
|
|
573
|
+
return mcp_prompts
|
|
574
|
+
|
|
575
|
+
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
576
|
+
# Create the middleware context.
|
|
577
|
+
mw_context = MiddlewareContext(
|
|
578
|
+
message=mcp.types.ListPromptsRequest(method="prompts/list"),
|
|
579
|
+
source="client",
|
|
580
|
+
type="request",
|
|
581
|
+
method="prompts/list",
|
|
582
|
+
fastmcp_context=fastmcp_ctx,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Apply the middleware chain.
|
|
586
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
512
587
|
|
|
513
588
|
async def _mcp_call_tool(
|
|
514
589
|
self, key: str, arguments: dict[str, Any]
|
|
@@ -525,46 +600,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
525
600
|
Returns:
|
|
526
601
|
List of MCP Content objects containing the tool results
|
|
527
602
|
"""
|
|
528
|
-
logger.debug("
|
|
603
|
+
logger.debug("Handler called: call_tool %s with %s", key, arguments)
|
|
529
604
|
|
|
530
|
-
# Create and use context for the entire call
|
|
531
605
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
532
606
|
try:
|
|
533
607
|
return await self._call_tool(key, arguments)
|
|
534
608
|
except DisabledError:
|
|
535
|
-
# convert to NotFoundError to avoid leaking tool presence
|
|
536
609
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
537
610
|
except NotFoundError:
|
|
538
|
-
# standardize NotFound message
|
|
539
611
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
540
612
|
|
|
541
613
|
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
542
614
|
"""
|
|
543
|
-
|
|
544
|
-
this method, not _mcp_call_tool.
|
|
545
|
-
|
|
546
|
-
Args:
|
|
547
|
-
key: The name of the tool to call arguments: Arguments to pass to
|
|
548
|
-
the tool
|
|
549
|
-
|
|
550
|
-
Returns:
|
|
551
|
-
List of MCP Content objects containing the tool results
|
|
615
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
552
616
|
"""
|
|
553
617
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
618
|
+
async def _handler(
|
|
619
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
620
|
+
) -> list[MCPContent]:
|
|
621
|
+
tool = await self._tool_manager.get_tool(context.message.name)
|
|
557
622
|
if not self._should_enable_component(tool):
|
|
558
|
-
raise
|
|
559
|
-
return await self._tool_manager.call_tool(key, arguments)
|
|
623
|
+
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
|
560
624
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
tool_key = server.strip_tool_prefix(key)
|
|
565
|
-
return await server.server._call_tool(tool_key, arguments)
|
|
625
|
+
return await self._tool_manager.call_tool(
|
|
626
|
+
key=context.message.name, arguments=context.message.arguments or {}
|
|
627
|
+
)
|
|
566
628
|
|
|
567
|
-
|
|
629
|
+
mw_context = MiddlewareContext(
|
|
630
|
+
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
631
|
+
source="client",
|
|
632
|
+
type="request",
|
|
633
|
+
method="tools/call",
|
|
634
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
635
|
+
)
|
|
636
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
568
637
|
|
|
569
638
|
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
570
639
|
"""
|
|
@@ -572,7 +641,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
572
641
|
|
|
573
642
|
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
|
|
574
643
|
"""
|
|
575
|
-
logger.debug("
|
|
644
|
+
logger.debug("Handler called: read_resource %s", uri)
|
|
576
645
|
|
|
577
646
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
578
647
|
try:
|
|
@@ -586,27 +655,38 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
586
655
|
|
|
587
656
|
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
588
657
|
"""
|
|
589
|
-
|
|
590
|
-
server.
|
|
658
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
591
659
|
"""
|
|
592
|
-
|
|
593
|
-
|
|
660
|
+
|
|
661
|
+
async def _handler(
|
|
662
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
663
|
+
) -> list[ReadResourceContents]:
|
|
664
|
+
resource = await self._resource_manager.get_resource(context.message.uri)
|
|
594
665
|
if not self._should_enable_component(resource):
|
|
595
|
-
raise
|
|
596
|
-
|
|
666
|
+
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
|
667
|
+
|
|
668
|
+
content = await self._resource_manager.read_resource(context.message.uri)
|
|
597
669
|
return [
|
|
598
670
|
ReadResourceContents(
|
|
599
671
|
content=content,
|
|
600
672
|
mime_type=resource.mime_type,
|
|
601
673
|
)
|
|
602
674
|
]
|
|
675
|
+
|
|
676
|
+
# Convert string URI to AnyUrl if needed
|
|
677
|
+
if isinstance(uri, str):
|
|
678
|
+
uri_param = AnyUrl(uri)
|
|
603
679
|
else:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
680
|
+
uri_param = uri
|
|
681
|
+
|
|
682
|
+
mw_context = MiddlewareContext(
|
|
683
|
+
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
|
|
684
|
+
source="client",
|
|
685
|
+
type="request",
|
|
686
|
+
method="resources/read",
|
|
687
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
688
|
+
)
|
|
689
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
610
690
|
|
|
611
691
|
async def _mcp_get_prompt(
|
|
612
692
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
@@ -616,7 +696,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
616
696
|
|
|
617
697
|
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
618
698
|
"""
|
|
619
|
-
logger.debug("
|
|
699
|
+
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
|
|
620
700
|
|
|
621
701
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
622
702
|
try:
|
|
@@ -631,31 +711,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
631
711
|
async def _get_prompt(
|
|
632
712
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
633
713
|
) -> GetPromptResult:
|
|
634
|
-
"""Handle MCP 'getPrompt' requests.
|
|
635
|
-
|
|
636
|
-
Args:
|
|
637
|
-
name: The name of the prompt to render
|
|
638
|
-
arguments: Arguments to pass to the prompt
|
|
639
|
-
|
|
640
|
-
Returns:
|
|
641
|
-
GetPromptResult containing the rendered prompt messages
|
|
642
714
|
"""
|
|
643
|
-
|
|
715
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
716
|
+
"""
|
|
644
717
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
718
|
+
async def _handler(
|
|
719
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
720
|
+
) -> GetPromptResult:
|
|
721
|
+
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
|
648
722
|
if not self._should_enable_component(prompt):
|
|
649
|
-
raise
|
|
650
|
-
return await self._prompt_manager.render_prompt(name, arguments)
|
|
723
|
+
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
|
651
724
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
prompt_name = server.strip_prompt_prefix(name)
|
|
656
|
-
return await server.server._mcp_get_prompt(prompt_name, arguments)
|
|
725
|
+
return await self._prompt_manager.render_prompt(
|
|
726
|
+
name=context.message.name, arguments=context.message.arguments
|
|
727
|
+
)
|
|
657
728
|
|
|
658
|
-
|
|
729
|
+
mw_context = MiddlewareContext(
|
|
730
|
+
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
731
|
+
source="client",
|
|
732
|
+
type="request",
|
|
733
|
+
method="prompts/get",
|
|
734
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
735
|
+
)
|
|
736
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
659
737
|
|
|
660
738
|
def add_tool(self, tool: Tool) -> None:
|
|
661
739
|
"""Add a tool to the server.
|
|
@@ -823,23 +901,23 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
823
901
|
enabled=enabled,
|
|
824
902
|
)
|
|
825
903
|
|
|
826
|
-
def add_resource(self, resource: Resource
|
|
904
|
+
def add_resource(self, resource: Resource) -> None:
|
|
827
905
|
"""Add a resource to the server.
|
|
828
906
|
|
|
829
907
|
Args:
|
|
830
908
|
resource: A Resource instance to add
|
|
831
909
|
"""
|
|
832
910
|
|
|
833
|
-
self._resource_manager.add_resource(resource
|
|
911
|
+
self._resource_manager.add_resource(resource)
|
|
834
912
|
self._cache.clear()
|
|
835
913
|
|
|
836
|
-
def add_template(self, template: ResourceTemplate
|
|
914
|
+
def add_template(self, template: ResourceTemplate) -> None:
|
|
837
915
|
"""Add a resource template to the server.
|
|
838
916
|
|
|
839
917
|
Args:
|
|
840
918
|
template: A ResourceTemplate instance to add
|
|
841
919
|
"""
|
|
842
|
-
self._resource_manager.add_template(template
|
|
920
|
+
self._resource_manager.add_template(template)
|
|
843
921
|
|
|
844
922
|
def add_resource_fn(
|
|
845
923
|
self,
|
|
@@ -1176,13 +1254,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1176
1254
|
|
|
1177
1255
|
async def run_http_async(
|
|
1178
1256
|
self,
|
|
1179
|
-
transport: Literal["streamable-http", "sse"] = "
|
|
1257
|
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
1180
1258
|
host: str | None = None,
|
|
1181
1259
|
port: int | None = None,
|
|
1182
1260
|
log_level: str | None = None,
|
|
1183
1261
|
path: str | None = None,
|
|
1184
1262
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1185
|
-
middleware: list[
|
|
1263
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1186
1264
|
) -> None:
|
|
1187
1265
|
"""Run the server using HTTP transport.
|
|
1188
1266
|
|
|
@@ -1253,7 +1331,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1253
1331
|
self,
|
|
1254
1332
|
path: str | None = None,
|
|
1255
1333
|
message_path: str | None = None,
|
|
1256
|
-
middleware: list[
|
|
1334
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1257
1335
|
) -> StarletteWithLifespan:
|
|
1258
1336
|
"""
|
|
1259
1337
|
Create a Starlette app for the SSE server.
|
|
@@ -1283,7 +1361,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1283
1361
|
def streamable_http_app(
|
|
1284
1362
|
self,
|
|
1285
1363
|
path: str | None = None,
|
|
1286
|
-
middleware: list[
|
|
1364
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1287
1365
|
) -> StarletteWithLifespan:
|
|
1288
1366
|
"""
|
|
1289
1367
|
Create a Starlette app for the StreamableHTTP server.
|
|
@@ -1304,10 +1382,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1304
1382
|
def http_app(
|
|
1305
1383
|
self,
|
|
1306
1384
|
path: str | None = None,
|
|
1307
|
-
middleware: list[
|
|
1385
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1308
1386
|
json_response: bool | None = None,
|
|
1309
1387
|
stateless_http: bool | None = None,
|
|
1310
|
-
transport: Literal["streamable-http", "sse"] = "
|
|
1388
|
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
1311
1389
|
) -> StarletteWithLifespan:
|
|
1312
1390
|
"""Create a Starlette app using the specified HTTP transport.
|
|
1313
1391
|
|
|
@@ -1320,7 +1398,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1320
1398
|
A Starlette application configured with the specified transport
|
|
1321
1399
|
"""
|
|
1322
1400
|
|
|
1323
|
-
if transport
|
|
1401
|
+
if transport in ("streamable-http", "http"):
|
|
1324
1402
|
return create_streamable_http_app(
|
|
1325
1403
|
server=self,
|
|
1326
1404
|
streamable_http_path=path
|
|
@@ -1367,7 +1445,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1367
1445
|
stacklevel=2,
|
|
1368
1446
|
)
|
|
1369
1447
|
await self.run_http_async(
|
|
1370
|
-
transport="
|
|
1448
|
+
transport="http",
|
|
1371
1449
|
host=host,
|
|
1372
1450
|
port=port,
|
|
1373
1451
|
log_level=log_level,
|
|
@@ -1377,15 +1455,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1377
1455
|
|
|
1378
1456
|
def mount(
|
|
1379
1457
|
self,
|
|
1380
|
-
prefix: str,
|
|
1381
1458
|
server: FastMCP[LifespanResultT],
|
|
1459
|
+
prefix: str | None = None,
|
|
1382
1460
|
as_proxy: bool | None = None,
|
|
1383
1461
|
*,
|
|
1384
1462
|
tool_separator: str | None = None,
|
|
1385
1463
|
resource_separator: str | None = None,
|
|
1386
1464
|
prompt_separator: str | None = None,
|
|
1387
1465
|
) -> None:
|
|
1388
|
-
"""Mount another FastMCP server on this server with
|
|
1466
|
+
"""Mount another FastMCP server on this server with an optional prefix.
|
|
1389
1467
|
|
|
1390
1468
|
Unlike importing (with import_server), mounting establishes a dynamic connection
|
|
1391
1469
|
between servers. When a client interacts with a mounted server's objects through
|
|
@@ -1393,7 +1471,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1393
1471
|
This means changes to the mounted server are immediately reflected when accessed
|
|
1394
1472
|
through the parent.
|
|
1395
1473
|
|
|
1396
|
-
When a server is mounted:
|
|
1474
|
+
When a server is mounted with a prefix:
|
|
1397
1475
|
- Tools from the mounted server are accessible with prefixed names.
|
|
1398
1476
|
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
|
1399
1477
|
- Resources are accessible with prefixed URIs.
|
|
@@ -1406,6 +1484,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1406
1484
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1407
1485
|
"prefix_weather_prompt".
|
|
1408
1486
|
|
|
1487
|
+
When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
|
|
1488
|
+
and prompts are accessible with their original names. Multiple servers can be mounted
|
|
1489
|
+
without prefixes, and they will be tried in order until a match is found.
|
|
1490
|
+
|
|
1409
1491
|
There are two modes for mounting servers:
|
|
1410
1492
|
1. Direct mounting (default when server has no custom lifespan): The parent server
|
|
1411
1493
|
directly accesses the mounted server's objects in-memory for better performance.
|
|
@@ -1418,8 +1500,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1418
1500
|
execution, but with slightly higher overhead.
|
|
1419
1501
|
|
|
1420
1502
|
Args:
|
|
1421
|
-
prefix: Prefix to use for the mounted server's objects.
|
|
1422
1503
|
server: The FastMCP server to mount.
|
|
1504
|
+
prefix: Optional prefix to use for the mounted server's objects. If None,
|
|
1505
|
+
the server's objects are accessible with their original names.
|
|
1423
1506
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
1424
1507
|
automatically determined based on whether the server has a custom lifespan
|
|
1425
1508
|
(True if it has a custom lifespan, False otherwise).
|
|
@@ -1431,6 +1514,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1431
1514
|
from fastmcp.client.transports import FastMCPTransport
|
|
1432
1515
|
from fastmcp.server.proxy import FastMCPProxy
|
|
1433
1516
|
|
|
1517
|
+
# Deprecated since 2.9.0
|
|
1518
|
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1519
|
+
# second was the server. Here we swap them if needed now that the prefix
|
|
1520
|
+
# is optional.
|
|
1521
|
+
if isinstance(server, str):
|
|
1522
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1523
|
+
warnings.warn(
|
|
1524
|
+
"Mount prefixes are now optional and the first positional argument "
|
|
1525
|
+
"should be the server you want to mount.",
|
|
1526
|
+
DeprecationWarning,
|
|
1527
|
+
stacklevel=2,
|
|
1528
|
+
)
|
|
1529
|
+
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1530
|
+
|
|
1434
1531
|
if tool_separator is not None:
|
|
1435
1532
|
# Deprecated since 2.4.0
|
|
1436
1533
|
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1469,21 +1566,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1469
1566
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1470
1567
|
server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
|
|
1471
1568
|
|
|
1569
|
+
# Delegate mounting to all three managers
|
|
1472
1570
|
mounted_server = MountedServer(
|
|
1473
|
-
server=server,
|
|
1474
1571
|
prefix=prefix,
|
|
1572
|
+
server=server,
|
|
1573
|
+
resource_prefix_format=self.resource_prefix_format,
|
|
1475
1574
|
)
|
|
1476
|
-
self.
|
|
1477
|
-
self.
|
|
1575
|
+
self._tool_manager.mount(mounted_server)
|
|
1576
|
+
self._resource_manager.mount(mounted_server)
|
|
1577
|
+
self._prompt_manager.mount(mounted_server)
|
|
1478
1578
|
|
|
1479
|
-
def unmount(self, prefix: str) -> None:
|
|
1480
|
-
self._mounted_servers.pop(prefix)
|
|
1481
1579
|
self._cache.clear()
|
|
1482
1580
|
|
|
1483
1581
|
async def import_server(
|
|
1484
1582
|
self,
|
|
1485
|
-
prefix: str,
|
|
1486
1583
|
server: FastMCP[LifespanResultT],
|
|
1584
|
+
prefix: str | None = None,
|
|
1487
1585
|
tool_separator: str | None = None,
|
|
1488
1586
|
resource_separator: str | None = None,
|
|
1489
1587
|
prompt_separator: str | None = None,
|
|
@@ -1497,7 +1595,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1497
1595
|
future changes to the imported server will not be reflected in the
|
|
1498
1596
|
importing server. Server-level configurations and lifespans are not imported.
|
|
1499
1597
|
|
|
1500
|
-
When a server is imported:
|
|
1598
|
+
When a server is imported with a prefix:
|
|
1501
1599
|
- The tools are imported with prefixed names
|
|
1502
1600
|
Example: If server has a tool named "get_weather", it will be
|
|
1503
1601
|
available as "prefix_get_weather"
|
|
@@ -1511,14 +1609,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1511
1609
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1512
1610
|
"prefix_weather_prompt"
|
|
1513
1611
|
|
|
1612
|
+
When a server is imported without a prefix (prefix=None), its tools, resources,
|
|
1613
|
+
templates, and prompts are imported with their original names.
|
|
1614
|
+
|
|
1514
1615
|
Args:
|
|
1515
|
-
prefix: The prefix to use for the imported server
|
|
1516
1616
|
server: The FastMCP server to import
|
|
1617
|
+
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
1618
|
+
objects are imported with their original names.
|
|
1517
1619
|
tool_separator: Deprecated. Separator for tool names.
|
|
1518
1620
|
resource_separator: Deprecated and ignored. Prefix is now
|
|
1519
1621
|
applied using the protocol://prefix/path format
|
|
1520
1622
|
prompt_separator: Deprecated. Separator for prompt names.
|
|
1521
1623
|
"""
|
|
1624
|
+
|
|
1625
|
+
# Deprecated since 2.9.0
|
|
1626
|
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1627
|
+
# second was the server. Here we swap them if needed now that the prefix
|
|
1628
|
+
# is optional.
|
|
1629
|
+
if isinstance(server, str):
|
|
1630
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1631
|
+
warnings.warn(
|
|
1632
|
+
"Import prefixes are now optional and the first positional argument "
|
|
1633
|
+
"should be the server you want to import.",
|
|
1634
|
+
DeprecationWarning,
|
|
1635
|
+
stacklevel=2,
|
|
1636
|
+
)
|
|
1637
|
+
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1638
|
+
|
|
1522
1639
|
if tool_separator is not None:
|
|
1523
1640
|
# Deprecated since 2.4.0
|
|
1524
1641
|
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1549,29 +1666,39 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1549
1666
|
stacklevel=2,
|
|
1550
1667
|
)
|
|
1551
1668
|
|
|
1552
|
-
# Import tools from the
|
|
1553
|
-
tool_prefix = f"{prefix}_"
|
|
1669
|
+
# Import tools from the server
|
|
1554
1670
|
for key, tool in (await server.get_tools()).items():
|
|
1555
|
-
|
|
1671
|
+
if prefix:
|
|
1672
|
+
tool = tool.with_key(f"{prefix}_{key}")
|
|
1673
|
+
self._tool_manager.add_tool(tool)
|
|
1556
1674
|
|
|
1557
|
-
# Import resources and templates from the
|
|
1675
|
+
# Import resources and templates from the server
|
|
1558
1676
|
for key, resource in (await server.get_resources()).items():
|
|
1559
|
-
|
|
1560
|
-
|
|
1677
|
+
if prefix:
|
|
1678
|
+
resource_key = add_resource_prefix(
|
|
1679
|
+
key, prefix, self.resource_prefix_format
|
|
1680
|
+
)
|
|
1681
|
+
resource = resource.with_key(resource_key)
|
|
1682
|
+
self._resource_manager.add_resource(resource)
|
|
1561
1683
|
|
|
1562
1684
|
for key, template in (await server.get_resource_templates()).items():
|
|
1563
|
-
|
|
1564
|
-
|
|
1685
|
+
if prefix:
|
|
1686
|
+
template_key = add_resource_prefix(
|
|
1687
|
+
key, prefix, self.resource_prefix_format
|
|
1688
|
+
)
|
|
1689
|
+
template = template.with_key(template_key)
|
|
1690
|
+
self._resource_manager.add_template(template)
|
|
1565
1691
|
|
|
1566
|
-
# Import prompts from the
|
|
1567
|
-
prompt_prefix = f"{prefix}_"
|
|
1692
|
+
# Import prompts from the server
|
|
1568
1693
|
for key, prompt in (await server.get_prompts()).items():
|
|
1569
|
-
|
|
1694
|
+
if prefix:
|
|
1695
|
+
prompt = prompt.with_key(f"{prefix}_{key}")
|
|
1696
|
+
self._prompt_manager.add_prompt(prompt)
|
|
1570
1697
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1698
|
+
if prefix:
|
|
1699
|
+
logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
|
|
1700
|
+
else:
|
|
1701
|
+
logger.debug(f"Imported server {server.name}")
|
|
1575
1702
|
|
|
1576
1703
|
self._cache.clear()
|
|
1577
1704
|
|
|
@@ -1728,60 +1855,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1728
1855
|
return True
|
|
1729
1856
|
|
|
1730
1857
|
|
|
1858
|
+
@dataclass
|
|
1731
1859
|
class MountedServer:
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
server: FastMCP[LifespanResultT],
|
|
1736
|
-
):
|
|
1737
|
-
self.server = server
|
|
1738
|
-
self.prefix = prefix
|
|
1739
|
-
|
|
1740
|
-
async def get_tools(self) -> dict[str, Tool]:
|
|
1741
|
-
tools = await self.server.get_tools()
|
|
1742
|
-
return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
|
|
1743
|
-
|
|
1744
|
-
async def get_resources(self) -> dict[str, Resource]:
|
|
1745
|
-
resources = await self.server.get_resources()
|
|
1746
|
-
return {
|
|
1747
|
-
add_resource_prefix(
|
|
1748
|
-
key, self.prefix, self.server.resource_prefix_format
|
|
1749
|
-
): resource
|
|
1750
|
-
for key, resource in resources.items()
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
1754
|
-
templates = await self.server.get_resource_templates()
|
|
1755
|
-
return {
|
|
1756
|
-
add_resource_prefix(
|
|
1757
|
-
key, self.prefix, self.server.resource_prefix_format
|
|
1758
|
-
): template
|
|
1759
|
-
for key, template in templates.items()
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
async def get_prompts(self) -> dict[str, Prompt]:
|
|
1763
|
-
prompts = await self.server.get_prompts()
|
|
1764
|
-
return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
|
|
1765
|
-
|
|
1766
|
-
def match_tool(self, key: str) -> bool:
|
|
1767
|
-
return key.startswith(f"{self.prefix}_")
|
|
1768
|
-
|
|
1769
|
-
def strip_tool_prefix(self, key: str) -> str:
|
|
1770
|
-
return key.removeprefix(f"{self.prefix}_")
|
|
1771
|
-
|
|
1772
|
-
def match_resource(self, key: str) -> bool:
|
|
1773
|
-
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
|
1774
|
-
|
|
1775
|
-
def strip_resource_prefix(self, key: str) -> str:
|
|
1776
|
-
return remove_resource_prefix(
|
|
1777
|
-
key, self.prefix, self.server.resource_prefix_format
|
|
1778
|
-
)
|
|
1779
|
-
|
|
1780
|
-
def match_prompt(self, key: str) -> bool:
|
|
1781
|
-
return key.startswith(f"{self.prefix}_")
|
|
1782
|
-
|
|
1783
|
-
def strip_prompt_prefix(self, key: str) -> str:
|
|
1784
|
-
return key.removeprefix(f"{self.prefix}_")
|
|
1860
|
+
prefix: str | None
|
|
1861
|
+
server: FastMCP[Any]
|
|
1862
|
+
resource_prefix_format: Literal["protocol", "path"] | None = None
|
|
1785
1863
|
|
|
1786
1864
|
|
|
1787
1865
|
def add_resource_prefix(
|