fastmcp 2.8.1__py3-none-any.whl → 2.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/cli/cli.py +99 -1
- fastmcp/cli/run.py +1 -3
- fastmcp/client/auth/oauth.py +1 -2
- fastmcp/client/client.py +23 -7
- fastmcp/client/logging.py +1 -2
- fastmcp/client/messages.py +126 -0
- 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 +109 -13
- fastmcp/prompts/prompt_manager.py +119 -43
- fastmcp/resources/resource.py +27 -1
- fastmcp/resources/resource_manager.py +249 -76
- fastmcp/resources/template.py +44 -2
- fastmcp/server/auth/providers/bearer.py +62 -13
- fastmcp/server/context.py +113 -10
- fastmcp/server/http.py +8 -0
- fastmcp/server/low_level.py +35 -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 +446 -280
- fastmcp/settings.py +2 -2
- fastmcp/tools/tool.py +22 -2
- fastmcp/tools/tool_manager.py +114 -45
- fastmcp/tools/tool_transform.py +42 -16
- 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 +75 -5
- fastmcp/utilities/tests.py +1 -1
- fastmcp/utilities/types.py +90 -1
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/METADATA +2 -2
- fastmcp-2.9.1.dist-info/RECORD +78 -0
- fastmcp-2.8.1.dist-info/RECORD +0 -69
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -12,16 +12,17 @@ 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
|
|
24
|
-
from mcp.server.lowlevel.server import Server as MCPServer
|
|
25
26
|
from mcp.server.stdio import stdio_server
|
|
26
27
|
from mcp.types import (
|
|
27
28
|
AnyFunction,
|
|
@@ -33,7 +34,7 @@ from mcp.types import Resource as MCPResource
|
|
|
33
34
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
34
35
|
from mcp.types import Tool as MCPTool
|
|
35
36
|
from pydantic import AnyUrl
|
|
36
|
-
from starlette.middleware import Middleware
|
|
37
|
+
from starlette.middleware import Middleware as ASGIMiddleware
|
|
37
38
|
from starlette.requests import Request
|
|
38
39
|
from starlette.responses import Response
|
|
39
40
|
from starlette.routing import BaseRoute, Route
|
|
@@ -52,6 +53,8 @@ from fastmcp.server.http import (
|
|
|
52
53
|
create_sse_app,
|
|
53
54
|
create_streamable_http_app,
|
|
54
55
|
)
|
|
56
|
+
from fastmcp.server.low_level import LowLevelServer
|
|
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"^([^:]+://)(.*?)$")
|
|
@@ -95,10 +99,12 @@ def _lifespan_wrapper(
|
|
|
95
99
|
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
96
100
|
],
|
|
97
101
|
) -> Callable[
|
|
98
|
-
[
|
|
102
|
+
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
99
103
|
]:
|
|
100
104
|
@asynccontextmanager
|
|
101
|
-
async def wrap(
|
|
105
|
+
async def wrap(
|
|
106
|
+
s: LowLevelServer[LifespanResultT],
|
|
107
|
+
) -> AsyncIterator[LifespanResultT]:
|
|
102
108
|
async with AsyncExitStack() as stack:
|
|
103
109
|
context = await stack.enter_async_context(lifespan(app))
|
|
104
110
|
yield context
|
|
@@ -111,7 +117,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
111
117
|
self,
|
|
112
118
|
name: str | None = None,
|
|
113
119
|
instructions: str | None = None,
|
|
120
|
+
*,
|
|
121
|
+
version: str | None = None,
|
|
114
122
|
auth: OAuthProvider | None = None,
|
|
123
|
+
middleware: list[Middleware] | None = None,
|
|
115
124
|
lifespan: (
|
|
116
125
|
Callable[
|
|
117
126
|
[FastMCP[LifespanResultT]],
|
|
@@ -152,7 +161,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
152
161
|
self._cache = TimedCache(
|
|
153
162
|
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
154
163
|
)
|
|
155
|
-
self._mounted_servers: dict[str, MountedServer] = {}
|
|
156
164
|
self._additional_http_routes: list[BaseRoute] = []
|
|
157
165
|
self._tool_manager = ToolManager(
|
|
158
166
|
duplicate_behavior=on_duplicate_tools,
|
|
@@ -173,8 +181,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
173
181
|
lifespan = default_lifespan
|
|
174
182
|
else:
|
|
175
183
|
self._has_lifespan = True
|
|
176
|
-
self._mcp_server =
|
|
184
|
+
self._mcp_server = LowLevelServer[LifespanResultT](
|
|
177
185
|
name=name or "FastMCP",
|
|
186
|
+
version=version,
|
|
178
187
|
instructions=instructions,
|
|
179
188
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
180
189
|
)
|
|
@@ -192,6 +201,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
201
|
self.include_tags = include_tags
|
|
193
202
|
self.exclude_tags = exclude_tags
|
|
194
203
|
|
|
204
|
+
self.middleware = middleware or []
|
|
205
|
+
|
|
195
206
|
# Set up MCP protocol handlers
|
|
196
207
|
self._setup_handlers()
|
|
197
208
|
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
|
@@ -272,7 +283,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
272
283
|
|
|
273
284
|
async def run_async(
|
|
274
285
|
self,
|
|
275
|
-
transport:
|
|
286
|
+
transport: Transport | None = None,
|
|
276
287
|
**transport_kwargs: Any,
|
|
277
288
|
) -> None:
|
|
278
289
|
"""Run the FastMCP server asynchronously.
|
|
@@ -282,19 +293,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
282
293
|
"""
|
|
283
294
|
if transport is None:
|
|
284
295
|
transport = "stdio"
|
|
285
|
-
if transport not in {"stdio", "
|
|
296
|
+
if transport not in {"stdio", "http", "sse", "streamable-http"}:
|
|
286
297
|
raise ValueError(f"Unknown transport: {transport}")
|
|
287
298
|
|
|
288
299
|
if transport == "stdio":
|
|
289
300
|
await self.run_stdio_async(**transport_kwargs)
|
|
290
|
-
elif transport in {"
|
|
301
|
+
elif transport in {"http", "sse", "streamable-http"}:
|
|
291
302
|
await self.run_http_async(transport=transport, **transport_kwargs)
|
|
292
303
|
else:
|
|
293
304
|
raise ValueError(f"Unknown transport: {transport}")
|
|
294
305
|
|
|
295
306
|
def run(
|
|
296
307
|
self,
|
|
297
|
-
transport:
|
|
308
|
+
transport: Transport | None = None,
|
|
298
309
|
**transport_kwargs: Any,
|
|
299
310
|
) -> None:
|
|
300
311
|
"""Run the FastMCP server. Note this is a synchronous function.
|
|
@@ -315,22 +326,23 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
315
326
|
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
316
327
|
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
317
328
|
|
|
329
|
+
async def _apply_middleware(
|
|
330
|
+
self,
|
|
331
|
+
context: MiddlewareContext[Any],
|
|
332
|
+
call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
|
|
333
|
+
) -> Any:
|
|
334
|
+
"""Builds and executes the middleware chain."""
|
|
335
|
+
chain = call_next
|
|
336
|
+
for mw in reversed(self.middleware):
|
|
337
|
+
chain = partial(mw, call_next=chain)
|
|
338
|
+
return await chain(context)
|
|
339
|
+
|
|
340
|
+
def add_middleware(self, middleware: Middleware) -> None:
|
|
341
|
+
self.middleware.append(middleware)
|
|
342
|
+
|
|
318
343
|
async def get_tools(self) -> dict[str, Tool]:
|
|
319
344
|
"""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
|
|
345
|
+
return await self._tool_manager.get_tools()
|
|
334
346
|
|
|
335
347
|
async def get_tool(self, key: str) -> Tool:
|
|
336
348
|
tools = await self.get_tools()
|
|
@@ -340,20 +352,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
340
352
|
|
|
341
353
|
async def get_resources(self) -> dict[str, Resource]:
|
|
342
354
|
"""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
|
|
355
|
+
return await self._resource_manager.get_resources()
|
|
357
356
|
|
|
358
357
|
async def get_resource(self, key: str) -> Resource:
|
|
359
358
|
resources = await self.get_resources()
|
|
@@ -363,25 +362,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
363
362
|
|
|
364
363
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
365
364
|
"""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
|
|
365
|
+
return await self._resource_manager.get_resource_templates()
|
|
383
366
|
|
|
384
367
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
368
|
+
"""Get a registered resource template by key."""
|
|
385
369
|
templates = await self.get_resource_templates()
|
|
386
370
|
if key not in templates:
|
|
387
371
|
raise NotFoundError(f"Unknown resource template: {key}")
|
|
@@ -391,20 +375,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
391
375
|
"""
|
|
392
376
|
List all available prompts.
|
|
393
377
|
"""
|
|
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
|
|
378
|
+
return await self._prompt_manager.get_prompts()
|
|
408
379
|
|
|
409
380
|
async def get_prompt(self, key: str) -> Prompt:
|
|
410
381
|
prompts = await self.get_prompts()
|
|
@@ -435,9 +406,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
435
406
|
include_in_schema: Whether to include in OpenAPI schema, defaults to True
|
|
436
407
|
|
|
437
408
|
Example:
|
|
409
|
+
Register a custom HTTP route for a health check endpoint:
|
|
410
|
+
```python
|
|
438
411
|
@server.custom_route("/health", methods=["GET"])
|
|
439
412
|
async def health_check(request: Request) -> Response:
|
|
440
413
|
return JSONResponse({"status": "ok"})
|
|
414
|
+
```
|
|
441
415
|
"""
|
|
442
416
|
|
|
443
417
|
def decorator(
|
|
@@ -457,58 +431,165 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
457
431
|
return decorator
|
|
458
432
|
|
|
459
433
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
434
|
+
logger.debug("Handler called: list_tools")
|
|
435
|
+
|
|
436
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
437
|
+
tools = await self._list_tools()
|
|
438
|
+
return [tool.to_mcp_tool(name=tool.key) for tool in tools]
|
|
439
|
+
|
|
440
|
+
async def _list_tools(self) -> list[Tool]:
|
|
460
441
|
"""
|
|
461
442
|
List all available tools, in the format expected by the low-level MCP
|
|
462
443
|
server.
|
|
463
444
|
|
|
464
445
|
"""
|
|
465
|
-
tools = await self.get_tools()
|
|
466
446
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
447
|
+
async def _handler(
|
|
448
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
449
|
+
) -> list[Tool]:
|
|
450
|
+
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
|
451
|
+
|
|
452
|
+
mcp_tools: list[Tool] = []
|
|
453
|
+
for tool in tools:
|
|
454
|
+
if self._should_enable_component(tool):
|
|
455
|
+
mcp_tools.append(tool)
|
|
456
|
+
|
|
457
|
+
return mcp_tools
|
|
458
|
+
|
|
459
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
460
|
+
# Create the middleware context.
|
|
461
|
+
mw_context = MiddlewareContext(
|
|
462
|
+
message=mcp.types.ListToolsRequest(method="tools/list"),
|
|
463
|
+
source="client",
|
|
464
|
+
type="request",
|
|
465
|
+
method="tools/list",
|
|
466
|
+
fastmcp_context=fastmcp_ctx,
|
|
467
|
+
)
|
|
471
468
|
|
|
472
|
-
|
|
469
|
+
# Apply the middleware chain.
|
|
470
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
473
471
|
|
|
474
472
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
473
|
+
logger.debug("Handler called: list_resources")
|
|
474
|
+
|
|
475
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
476
|
+
resources = await self._list_resources()
|
|
477
|
+
return [
|
|
478
|
+
resource.to_mcp_resource(uri=resource.key) for resource in resources
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
async def _list_resources(self) -> list[Resource]:
|
|
475
482
|
"""
|
|
476
483
|
List all available resources, in the format expected by the low-level MCP
|
|
477
484
|
server.
|
|
478
485
|
|
|
479
486
|
"""
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
487
|
+
|
|
488
|
+
async def _handler(
|
|
489
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
490
|
+
) -> list[Resource]:
|
|
491
|
+
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
|
492
|
+
|
|
493
|
+
mcp_resources: list[Resource] = []
|
|
494
|
+
for resource in resources:
|
|
495
|
+
if self._should_enable_component(resource):
|
|
496
|
+
mcp_resources.append(resource)
|
|
497
|
+
|
|
498
|
+
return mcp_resources
|
|
499
|
+
|
|
500
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
501
|
+
# Create the middleware context.
|
|
502
|
+
mw_context = MiddlewareContext(
|
|
503
|
+
message={}, # List resources doesn't have parameters
|
|
504
|
+
source="client",
|
|
505
|
+
type="request",
|
|
506
|
+
method="resources/list",
|
|
507
|
+
fastmcp_context=fastmcp_ctx,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Apply the middleware chain.
|
|
511
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
486
512
|
|
|
487
513
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
514
|
+
logger.debug("Handler called: list_resource_templates")
|
|
515
|
+
|
|
516
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
517
|
+
templates = await self._list_resource_templates()
|
|
518
|
+
return [
|
|
519
|
+
template.to_mcp_template(uriTemplate=template.key)
|
|
520
|
+
for template in templates
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
async def _list_resource_templates(self) -> list[ResourceTemplate]:
|
|
488
524
|
"""
|
|
489
|
-
List all available resource templates, in the format expected by the low-level
|
|
490
|
-
|
|
525
|
+
List all available resource templates, in the format expected by the low-level MCP
|
|
526
|
+
server.
|
|
491
527
|
|
|
492
528
|
"""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
529
|
+
|
|
530
|
+
async def _handler(
|
|
531
|
+
context: MiddlewareContext[dict[str, Any]],
|
|
532
|
+
) -> list[ResourceTemplate]:
|
|
533
|
+
templates = await self._resource_manager.list_resource_templates()
|
|
534
|
+
|
|
535
|
+
mcp_templates: list[ResourceTemplate] = []
|
|
536
|
+
for template in templates:
|
|
537
|
+
if self._should_enable_component(template):
|
|
538
|
+
mcp_templates.append(template)
|
|
539
|
+
|
|
540
|
+
return mcp_templates
|
|
541
|
+
|
|
542
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
543
|
+
# Create the middleware context.
|
|
544
|
+
mw_context = MiddlewareContext(
|
|
545
|
+
message={}, # List resource templates doesn't have parameters
|
|
546
|
+
source="client",
|
|
547
|
+
type="request",
|
|
548
|
+
method="resources/templates/list",
|
|
549
|
+
fastmcp_context=fastmcp_ctx,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Apply the middleware chain.
|
|
553
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
499
554
|
|
|
500
555
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
556
|
+
logger.debug("Handler called: list_prompts")
|
|
557
|
+
|
|
558
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
559
|
+
prompts = await self._list_prompts()
|
|
560
|
+
return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
|
|
561
|
+
|
|
562
|
+
async def _list_prompts(self) -> list[Prompt]:
|
|
501
563
|
"""
|
|
502
564
|
List all available prompts, in the format expected by the low-level MCP
|
|
503
565
|
server.
|
|
504
566
|
|
|
505
567
|
"""
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
568
|
+
|
|
569
|
+
async def _handler(
|
|
570
|
+
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
|
571
|
+
) -> list[Prompt]:
|
|
572
|
+
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
|
573
|
+
|
|
574
|
+
mcp_prompts: list[Prompt] = []
|
|
575
|
+
for prompt in prompts:
|
|
576
|
+
if self._should_enable_component(prompt):
|
|
577
|
+
mcp_prompts.append(prompt)
|
|
578
|
+
|
|
579
|
+
return mcp_prompts
|
|
580
|
+
|
|
581
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
582
|
+
# Create the middleware context.
|
|
583
|
+
mw_context = MiddlewareContext(
|
|
584
|
+
message=mcp.types.ListPromptsRequest(method="prompts/list"),
|
|
585
|
+
source="client",
|
|
586
|
+
type="request",
|
|
587
|
+
method="prompts/list",
|
|
588
|
+
fastmcp_context=fastmcp_ctx,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Apply the middleware chain.
|
|
592
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
512
593
|
|
|
513
594
|
async def _mcp_call_tool(
|
|
514
595
|
self, key: str, arguments: dict[str, Any]
|
|
@@ -525,46 +606,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
525
606
|
Returns:
|
|
526
607
|
List of MCP Content objects containing the tool results
|
|
527
608
|
"""
|
|
528
|
-
logger.debug("
|
|
609
|
+
logger.debug("Handler called: call_tool %s with %s", key, arguments)
|
|
529
610
|
|
|
530
|
-
|
|
531
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
611
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
532
612
|
try:
|
|
533
613
|
return await self._call_tool(key, arguments)
|
|
534
614
|
except DisabledError:
|
|
535
|
-
# convert to NotFoundError to avoid leaking tool presence
|
|
536
615
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
537
616
|
except NotFoundError:
|
|
538
|
-
# standardize NotFound message
|
|
539
617
|
raise NotFoundError(f"Unknown tool: {key}")
|
|
540
618
|
|
|
541
619
|
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
542
620
|
"""
|
|
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
|
|
621
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
552
622
|
"""
|
|
553
623
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
624
|
+
async def _handler(
|
|
625
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
626
|
+
) -> list[MCPContent]:
|
|
627
|
+
tool = await self._tool_manager.get_tool(context.message.name)
|
|
557
628
|
if not self._should_enable_component(tool):
|
|
558
|
-
raise
|
|
559
|
-
return await self._tool_manager.call_tool(key, arguments)
|
|
629
|
+
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
|
560
630
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
tool_key = server.strip_tool_prefix(key)
|
|
565
|
-
return await server.server._call_tool(tool_key, arguments)
|
|
631
|
+
return await self._tool_manager.call_tool(
|
|
632
|
+
key=context.message.name, arguments=context.message.arguments or {}
|
|
633
|
+
)
|
|
566
634
|
|
|
567
|
-
|
|
635
|
+
mw_context = MiddlewareContext(
|
|
636
|
+
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
|
637
|
+
source="client",
|
|
638
|
+
type="request",
|
|
639
|
+
method="tools/call",
|
|
640
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
641
|
+
)
|
|
642
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
568
643
|
|
|
569
644
|
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
570
645
|
"""
|
|
@@ -572,9 +647,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
572
647
|
|
|
573
648
|
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
|
|
574
649
|
"""
|
|
575
|
-
logger.debug("
|
|
650
|
+
logger.debug("Handler called: read_resource %s", uri)
|
|
576
651
|
|
|
577
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
652
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
578
653
|
try:
|
|
579
654
|
return await self._read_resource(uri)
|
|
580
655
|
except DisabledError:
|
|
@@ -586,27 +661,38 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
586
661
|
|
|
587
662
|
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
588
663
|
"""
|
|
589
|
-
|
|
590
|
-
server.
|
|
664
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
591
665
|
"""
|
|
592
|
-
|
|
593
|
-
|
|
666
|
+
|
|
667
|
+
async def _handler(
|
|
668
|
+
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
|
669
|
+
) -> list[ReadResourceContents]:
|
|
670
|
+
resource = await self._resource_manager.get_resource(context.message.uri)
|
|
594
671
|
if not self._should_enable_component(resource):
|
|
595
|
-
raise
|
|
596
|
-
|
|
672
|
+
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
|
673
|
+
|
|
674
|
+
content = await self._resource_manager.read_resource(context.message.uri)
|
|
597
675
|
return [
|
|
598
676
|
ReadResourceContents(
|
|
599
677
|
content=content,
|
|
600
678
|
mime_type=resource.mime_type,
|
|
601
679
|
)
|
|
602
680
|
]
|
|
681
|
+
|
|
682
|
+
# Convert string URI to AnyUrl if needed
|
|
683
|
+
if isinstance(uri, str):
|
|
684
|
+
uri_param = AnyUrl(uri)
|
|
603
685
|
else:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
686
|
+
uri_param = uri
|
|
687
|
+
|
|
688
|
+
mw_context = MiddlewareContext(
|
|
689
|
+
message=mcp.types.ReadResourceRequestParams(uri=uri_param),
|
|
690
|
+
source="client",
|
|
691
|
+
type="request",
|
|
692
|
+
method="resources/read",
|
|
693
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
694
|
+
)
|
|
695
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
610
696
|
|
|
611
697
|
async def _mcp_get_prompt(
|
|
612
698
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
@@ -616,9 +702,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
616
702
|
|
|
617
703
|
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
618
704
|
"""
|
|
619
|
-
logger.debug("
|
|
705
|
+
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
|
|
620
706
|
|
|
621
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
707
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
622
708
|
try:
|
|
623
709
|
return await self._get_prompt(name, arguments)
|
|
624
710
|
except DisabledError:
|
|
@@ -631,31 +717,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
631
717
|
async def _get_prompt(
|
|
632
718
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
633
719
|
) -> 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
720
|
"""
|
|
643
|
-
|
|
721
|
+
Applies this server's middleware and delegates the filtered call to the manager.
|
|
722
|
+
"""
|
|
644
723
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
724
|
+
async def _handler(
|
|
725
|
+
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
|
726
|
+
) -> GetPromptResult:
|
|
727
|
+
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
|
648
728
|
if not self._should_enable_component(prompt):
|
|
649
|
-
raise
|
|
650
|
-
return await self._prompt_manager.render_prompt(name, arguments)
|
|
729
|
+
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
|
651
730
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
prompt_name = server.strip_prompt_prefix(name)
|
|
656
|
-
return await server.server._mcp_get_prompt(prompt_name, arguments)
|
|
731
|
+
return await self._prompt_manager.render_prompt(
|
|
732
|
+
name=context.message.name, arguments=context.message.arguments
|
|
733
|
+
)
|
|
657
734
|
|
|
658
|
-
|
|
735
|
+
mw_context = MiddlewareContext(
|
|
736
|
+
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
|
737
|
+
source="client",
|
|
738
|
+
type="request",
|
|
739
|
+
method="prompts/get",
|
|
740
|
+
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
|
741
|
+
)
|
|
742
|
+
return await self._apply_middleware(mw_context, _handler)
|
|
659
743
|
|
|
660
744
|
def add_tool(self, tool: Tool) -> None:
|
|
661
745
|
"""Add a tool to the server.
|
|
@@ -669,6 +753,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
669
753
|
self._tool_manager.add_tool(tool)
|
|
670
754
|
self._cache.clear()
|
|
671
755
|
|
|
756
|
+
# Send notification if we're in a request context
|
|
757
|
+
try:
|
|
758
|
+
from fastmcp.server.dependencies import get_context
|
|
759
|
+
|
|
760
|
+
context = get_context()
|
|
761
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
762
|
+
except RuntimeError:
|
|
763
|
+
pass # No context available
|
|
764
|
+
|
|
672
765
|
def remove_tool(self, name: str) -> None:
|
|
673
766
|
"""Remove a tool from the server.
|
|
674
767
|
|
|
@@ -681,6 +774,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
681
774
|
self._tool_manager.remove_tool(name)
|
|
682
775
|
self._cache.clear()
|
|
683
776
|
|
|
777
|
+
# Send notification if we're in a request context
|
|
778
|
+
try:
|
|
779
|
+
from fastmcp.server.dependencies import get_context
|
|
780
|
+
|
|
781
|
+
context = get_context()
|
|
782
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
783
|
+
except RuntimeError:
|
|
784
|
+
pass # No context available
|
|
785
|
+
|
|
684
786
|
@overload
|
|
685
787
|
def tool(
|
|
686
788
|
self,
|
|
@@ -736,15 +838,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
736
838
|
name: Optional name for the tool (keyword-only, alternative to name_or_fn)
|
|
737
839
|
description: Optional description of what the tool does
|
|
738
840
|
tags: Optional set of tags for categorizing the tool
|
|
739
|
-
annotations: Optional annotations about the tool's behavior
|
|
841
|
+
annotations: Optional annotations about the tool's behavior
|
|
740
842
|
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
741
843
|
enabled: Optional boolean to enable or disable the tool
|
|
742
844
|
|
|
743
|
-
|
|
845
|
+
Examples:
|
|
846
|
+
Register a tool with a custom name:
|
|
847
|
+
```python
|
|
744
848
|
@server.tool
|
|
745
849
|
def my_tool(x: int) -> str:
|
|
746
850
|
return str(x)
|
|
747
851
|
|
|
852
|
+
# Register a tool with a custom name
|
|
748
853
|
@server.tool
|
|
749
854
|
def my_tool(x: int) -> str:
|
|
750
855
|
return str(x)
|
|
@@ -759,6 +864,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
759
864
|
|
|
760
865
|
# Direct function call
|
|
761
866
|
server.tool(my_function, name="custom_name")
|
|
867
|
+
```
|
|
762
868
|
"""
|
|
763
869
|
if isinstance(annotations, dict):
|
|
764
870
|
annotations = ToolAnnotations(**annotations)
|
|
@@ -823,23 +929,41 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
823
929
|
enabled=enabled,
|
|
824
930
|
)
|
|
825
931
|
|
|
826
|
-
def add_resource(self, resource: Resource
|
|
932
|
+
def add_resource(self, resource: Resource) -> None:
|
|
827
933
|
"""Add a resource to the server.
|
|
828
934
|
|
|
829
935
|
Args:
|
|
830
936
|
resource: A Resource instance to add
|
|
831
937
|
"""
|
|
832
938
|
|
|
833
|
-
self._resource_manager.add_resource(resource
|
|
939
|
+
self._resource_manager.add_resource(resource)
|
|
834
940
|
self._cache.clear()
|
|
835
941
|
|
|
836
|
-
|
|
942
|
+
# Send notification if we're in a request context
|
|
943
|
+
try:
|
|
944
|
+
from fastmcp.server.dependencies import get_context
|
|
945
|
+
|
|
946
|
+
context = get_context()
|
|
947
|
+
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
948
|
+
except RuntimeError:
|
|
949
|
+
pass # No context available
|
|
950
|
+
|
|
951
|
+
def add_template(self, template: ResourceTemplate) -> None:
|
|
837
952
|
"""Add a resource template to the server.
|
|
838
953
|
|
|
839
954
|
Args:
|
|
840
955
|
template: A ResourceTemplate instance to add
|
|
841
956
|
"""
|
|
842
|
-
self._resource_manager.add_template(template
|
|
957
|
+
self._resource_manager.add_template(template)
|
|
958
|
+
|
|
959
|
+
# Send notification if we're in a request context
|
|
960
|
+
try:
|
|
961
|
+
from fastmcp.server.dependencies import get_context
|
|
962
|
+
|
|
963
|
+
context = get_context()
|
|
964
|
+
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
965
|
+
except RuntimeError:
|
|
966
|
+
pass # No context available
|
|
843
967
|
|
|
844
968
|
def add_resource_fn(
|
|
845
969
|
self,
|
|
@@ -913,7 +1037,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
913
1037
|
tags: Optional set of tags for categorizing the resource
|
|
914
1038
|
enabled: Optional boolean to enable or disable the resource
|
|
915
1039
|
|
|
916
|
-
|
|
1040
|
+
Examples:
|
|
1041
|
+
Register a resource with a custom name:
|
|
1042
|
+
```python
|
|
917
1043
|
@server.resource("resource://my-resource")
|
|
918
1044
|
def get_data() -> str:
|
|
919
1045
|
return "Hello, world!"
|
|
@@ -936,6 +1062,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
936
1062
|
async def get_weather(city: str) -> str:
|
|
937
1063
|
data = await fetch_weather(city)
|
|
938
1064
|
return f"Weather for {city}: {data}"
|
|
1065
|
+
```
|
|
939
1066
|
"""
|
|
940
1067
|
# Check if user passed function directly instead of calling decorator
|
|
941
1068
|
if inspect.isroutine(uri):
|
|
@@ -1009,6 +1136,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1009
1136
|
self._prompt_manager.add_prompt(prompt)
|
|
1010
1137
|
self._cache.clear()
|
|
1011
1138
|
|
|
1139
|
+
# Send notification if we're in a request context
|
|
1140
|
+
try:
|
|
1141
|
+
from fastmcp.server.dependencies import get_context
|
|
1142
|
+
|
|
1143
|
+
context = get_context()
|
|
1144
|
+
context._queue_prompt_list_changed() # type: ignore[private-use]
|
|
1145
|
+
except RuntimeError:
|
|
1146
|
+
pass # No context available
|
|
1147
|
+
|
|
1012
1148
|
@overload
|
|
1013
1149
|
def prompt(
|
|
1014
1150
|
self,
|
|
@@ -1060,7 +1196,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1060
1196
|
tags: Optional set of tags for categorizing the prompt
|
|
1061
1197
|
enabled: Optional boolean to enable or disable the prompt
|
|
1062
1198
|
|
|
1063
|
-
|
|
1199
|
+
Examples:
|
|
1200
|
+
|
|
1201
|
+
```python
|
|
1064
1202
|
@server.prompt
|
|
1065
1203
|
def analyze_table(table_name: str) -> list[Message]:
|
|
1066
1204
|
schema = read_table_schema(table_name)
|
|
@@ -1104,6 +1242,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1104
1242
|
|
|
1105
1243
|
# Direct function call
|
|
1106
1244
|
server.prompt(my_function, name="custom_name")
|
|
1245
|
+
```
|
|
1107
1246
|
"""
|
|
1108
1247
|
|
|
1109
1248
|
if isinstance(name_or_fn, classmethod):
|
|
@@ -1176,13 +1315,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1176
1315
|
|
|
1177
1316
|
async def run_http_async(
|
|
1178
1317
|
self,
|
|
1179
|
-
transport: Literal["streamable-http", "sse"] = "
|
|
1318
|
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
1180
1319
|
host: str | None = None,
|
|
1181
1320
|
port: int | None = None,
|
|
1182
1321
|
log_level: str | None = None,
|
|
1183
1322
|
path: str | None = None,
|
|
1184
1323
|
uvicorn_config: dict[str, Any] | None = None,
|
|
1185
|
-
middleware: list[
|
|
1324
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1186
1325
|
) -> None:
|
|
1187
1326
|
"""Run the server using HTTP transport.
|
|
1188
1327
|
|
|
@@ -1253,7 +1392,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1253
1392
|
self,
|
|
1254
1393
|
path: str | None = None,
|
|
1255
1394
|
message_path: str | None = None,
|
|
1256
|
-
middleware: list[
|
|
1395
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1257
1396
|
) -> StarletteWithLifespan:
|
|
1258
1397
|
"""
|
|
1259
1398
|
Create a Starlette app for the SSE server.
|
|
@@ -1283,7 +1422,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1283
1422
|
def streamable_http_app(
|
|
1284
1423
|
self,
|
|
1285
1424
|
path: str | None = None,
|
|
1286
|
-
middleware: list[
|
|
1425
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1287
1426
|
) -> StarletteWithLifespan:
|
|
1288
1427
|
"""
|
|
1289
1428
|
Create a Starlette app for the StreamableHTTP server.
|
|
@@ -1304,10 +1443,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1304
1443
|
def http_app(
|
|
1305
1444
|
self,
|
|
1306
1445
|
path: str | None = None,
|
|
1307
|
-
middleware: list[
|
|
1446
|
+
middleware: list[ASGIMiddleware] | None = None,
|
|
1308
1447
|
json_response: bool | None = None,
|
|
1309
1448
|
stateless_http: bool | None = None,
|
|
1310
|
-
transport: Literal["streamable-http", "sse"] = "
|
|
1449
|
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
1311
1450
|
) -> StarletteWithLifespan:
|
|
1312
1451
|
"""Create a Starlette app using the specified HTTP transport.
|
|
1313
1452
|
|
|
@@ -1320,7 +1459,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1320
1459
|
A Starlette application configured with the specified transport
|
|
1321
1460
|
"""
|
|
1322
1461
|
|
|
1323
|
-
if transport
|
|
1462
|
+
if transport in ("streamable-http", "http"):
|
|
1324
1463
|
return create_streamable_http_app(
|
|
1325
1464
|
server=self,
|
|
1326
1465
|
streamable_http_path=path
|
|
@@ -1367,7 +1506,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1367
1506
|
stacklevel=2,
|
|
1368
1507
|
)
|
|
1369
1508
|
await self.run_http_async(
|
|
1370
|
-
transport="
|
|
1509
|
+
transport="http",
|
|
1371
1510
|
host=host,
|
|
1372
1511
|
port=port,
|
|
1373
1512
|
log_level=log_level,
|
|
@@ -1377,15 +1516,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1377
1516
|
|
|
1378
1517
|
def mount(
|
|
1379
1518
|
self,
|
|
1380
|
-
prefix: str,
|
|
1381
1519
|
server: FastMCP[LifespanResultT],
|
|
1520
|
+
prefix: str | None = None,
|
|
1382
1521
|
as_proxy: bool | None = None,
|
|
1383
1522
|
*,
|
|
1384
1523
|
tool_separator: str | None = None,
|
|
1385
1524
|
resource_separator: str | None = None,
|
|
1386
1525
|
prompt_separator: str | None = None,
|
|
1387
1526
|
) -> None:
|
|
1388
|
-
"""Mount another FastMCP server on this server with
|
|
1527
|
+
"""Mount another FastMCP server on this server with an optional prefix.
|
|
1389
1528
|
|
|
1390
1529
|
Unlike importing (with import_server), mounting establishes a dynamic connection
|
|
1391
1530
|
between servers. When a client interacts with a mounted server's objects through
|
|
@@ -1393,7 +1532,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1393
1532
|
This means changes to the mounted server are immediately reflected when accessed
|
|
1394
1533
|
through the parent.
|
|
1395
1534
|
|
|
1396
|
-
When a server is mounted:
|
|
1535
|
+
When a server is mounted with a prefix:
|
|
1397
1536
|
- Tools from the mounted server are accessible with prefixed names.
|
|
1398
1537
|
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
|
1399
1538
|
- Resources are accessible with prefixed URIs.
|
|
@@ -1406,6 +1545,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1406
1545
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1407
1546
|
"prefix_weather_prompt".
|
|
1408
1547
|
|
|
1548
|
+
When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
|
|
1549
|
+
and prompts are accessible with their original names. Multiple servers can be mounted
|
|
1550
|
+
without prefixes, and they will be tried in order until a match is found.
|
|
1551
|
+
|
|
1409
1552
|
There are two modes for mounting servers:
|
|
1410
1553
|
1. Direct mounting (default when server has no custom lifespan): The parent server
|
|
1411
1554
|
directly accesses the mounted server's objects in-memory for better performance.
|
|
@@ -1418,8 +1561,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1418
1561
|
execution, but with slightly higher overhead.
|
|
1419
1562
|
|
|
1420
1563
|
Args:
|
|
1421
|
-
prefix: Prefix to use for the mounted server's objects.
|
|
1422
1564
|
server: The FastMCP server to mount.
|
|
1565
|
+
prefix: Optional prefix to use for the mounted server's objects. If None,
|
|
1566
|
+
the server's objects are accessible with their original names.
|
|
1423
1567
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
1424
1568
|
automatically determined based on whether the server has a custom lifespan
|
|
1425
1569
|
(True if it has a custom lifespan, False otherwise).
|
|
@@ -1431,6 +1575,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1431
1575
|
from fastmcp.client.transports import FastMCPTransport
|
|
1432
1576
|
from fastmcp.server.proxy import FastMCPProxy
|
|
1433
1577
|
|
|
1578
|
+
# Deprecated since 2.9.0
|
|
1579
|
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1580
|
+
# second was the server. Here we swap them if needed now that the prefix
|
|
1581
|
+
# is optional.
|
|
1582
|
+
if isinstance(server, str):
|
|
1583
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1584
|
+
warnings.warn(
|
|
1585
|
+
"Mount prefixes are now optional and the first positional argument "
|
|
1586
|
+
"should be the server you want to mount.",
|
|
1587
|
+
DeprecationWarning,
|
|
1588
|
+
stacklevel=2,
|
|
1589
|
+
)
|
|
1590
|
+
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1591
|
+
|
|
1434
1592
|
if tool_separator is not None:
|
|
1435
1593
|
# Deprecated since 2.4.0
|
|
1436
1594
|
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1469,21 +1627,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1469
1627
|
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1470
1628
|
server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
|
|
1471
1629
|
|
|
1630
|
+
# Delegate mounting to all three managers
|
|
1472
1631
|
mounted_server = MountedServer(
|
|
1473
|
-
server=server,
|
|
1474
1632
|
prefix=prefix,
|
|
1633
|
+
server=server,
|
|
1634
|
+
resource_prefix_format=self.resource_prefix_format,
|
|
1475
1635
|
)
|
|
1476
|
-
self.
|
|
1477
|
-
self.
|
|
1636
|
+
self._tool_manager.mount(mounted_server)
|
|
1637
|
+
self._resource_manager.mount(mounted_server)
|
|
1638
|
+
self._prompt_manager.mount(mounted_server)
|
|
1478
1639
|
|
|
1479
|
-
def unmount(self, prefix: str) -> None:
|
|
1480
|
-
self._mounted_servers.pop(prefix)
|
|
1481
1640
|
self._cache.clear()
|
|
1482
1641
|
|
|
1483
1642
|
async def import_server(
|
|
1484
1643
|
self,
|
|
1485
|
-
prefix: str,
|
|
1486
1644
|
server: FastMCP[LifespanResultT],
|
|
1645
|
+
prefix: str | None = None,
|
|
1487
1646
|
tool_separator: str | None = None,
|
|
1488
1647
|
resource_separator: str | None = None,
|
|
1489
1648
|
prompt_separator: str | None = None,
|
|
@@ -1497,7 +1656,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1497
1656
|
future changes to the imported server will not be reflected in the
|
|
1498
1657
|
importing server. Server-level configurations and lifespans are not imported.
|
|
1499
1658
|
|
|
1500
|
-
When a server is imported:
|
|
1659
|
+
When a server is imported with a prefix:
|
|
1501
1660
|
- The tools are imported with prefixed names
|
|
1502
1661
|
Example: If server has a tool named "get_weather", it will be
|
|
1503
1662
|
available as "prefix_get_weather"
|
|
@@ -1511,14 +1670,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1511
1670
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1512
1671
|
"prefix_weather_prompt"
|
|
1513
1672
|
|
|
1673
|
+
When a server is imported without a prefix (prefix=None), its tools, resources,
|
|
1674
|
+
templates, and prompts are imported with their original names.
|
|
1675
|
+
|
|
1514
1676
|
Args:
|
|
1515
|
-
prefix: The prefix to use for the imported server
|
|
1516
1677
|
server: The FastMCP server to import
|
|
1678
|
+
prefix: Optional prefix to use for the imported server's objects. If None,
|
|
1679
|
+
objects are imported with their original names.
|
|
1517
1680
|
tool_separator: Deprecated. Separator for tool names.
|
|
1518
1681
|
resource_separator: Deprecated and ignored. Prefix is now
|
|
1519
1682
|
applied using the protocol://prefix/path format
|
|
1520
1683
|
prompt_separator: Deprecated. Separator for prompt names.
|
|
1521
1684
|
"""
|
|
1685
|
+
|
|
1686
|
+
# Deprecated since 2.9.0
|
|
1687
|
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
|
1688
|
+
# second was the server. Here we swap them if needed now that the prefix
|
|
1689
|
+
# is optional.
|
|
1690
|
+
if isinstance(server, str):
|
|
1691
|
+
if fastmcp.settings.deprecation_warnings:
|
|
1692
|
+
warnings.warn(
|
|
1693
|
+
"Import prefixes are now optional and the first positional argument "
|
|
1694
|
+
"should be the server you want to import.",
|
|
1695
|
+
DeprecationWarning,
|
|
1696
|
+
stacklevel=2,
|
|
1697
|
+
)
|
|
1698
|
+
server, prefix = cast(FastMCP[Any], prefix), server
|
|
1699
|
+
|
|
1522
1700
|
if tool_separator is not None:
|
|
1523
1701
|
# Deprecated since 2.4.0
|
|
1524
1702
|
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1549,29 +1727,39 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1549
1727
|
stacklevel=2,
|
|
1550
1728
|
)
|
|
1551
1729
|
|
|
1552
|
-
# Import tools from the
|
|
1553
|
-
tool_prefix = f"{prefix}_"
|
|
1730
|
+
# Import tools from the server
|
|
1554
1731
|
for key, tool in (await server.get_tools()).items():
|
|
1555
|
-
|
|
1732
|
+
if prefix:
|
|
1733
|
+
tool = tool.with_key(f"{prefix}_{key}")
|
|
1734
|
+
self._tool_manager.add_tool(tool)
|
|
1556
1735
|
|
|
1557
|
-
# Import resources and templates from the
|
|
1736
|
+
# Import resources and templates from the server
|
|
1558
1737
|
for key, resource in (await server.get_resources()).items():
|
|
1559
|
-
|
|
1560
|
-
|
|
1738
|
+
if prefix:
|
|
1739
|
+
resource_key = add_resource_prefix(
|
|
1740
|
+
key, prefix, self.resource_prefix_format
|
|
1741
|
+
)
|
|
1742
|
+
resource = resource.with_key(resource_key)
|
|
1743
|
+
self._resource_manager.add_resource(resource)
|
|
1561
1744
|
|
|
1562
1745
|
for key, template in (await server.get_resource_templates()).items():
|
|
1563
|
-
|
|
1564
|
-
|
|
1746
|
+
if prefix:
|
|
1747
|
+
template_key = add_resource_prefix(
|
|
1748
|
+
key, prefix, self.resource_prefix_format
|
|
1749
|
+
)
|
|
1750
|
+
template = template.with_key(template_key)
|
|
1751
|
+
self._resource_manager.add_template(template)
|
|
1565
1752
|
|
|
1566
|
-
# Import prompts from the
|
|
1567
|
-
prompt_prefix = f"{prefix}_"
|
|
1753
|
+
# Import prompts from the server
|
|
1568
1754
|
for key, prompt in (await server.get_prompts()).items():
|
|
1569
|
-
|
|
1755
|
+
if prefix:
|
|
1756
|
+
prompt = prompt.with_key(f"{prefix}_{key}")
|
|
1757
|
+
self._prompt_manager.add_prompt(prompt)
|
|
1570
1758
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1759
|
+
if prefix:
|
|
1760
|
+
logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
|
|
1761
|
+
else:
|
|
1762
|
+
logger.debug(f"Imported server {server.name}")
|
|
1575
1763
|
|
|
1576
1764
|
self._cache.clear()
|
|
1577
1765
|
|
|
@@ -1660,10 +1848,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1660
1848
|
) -> FastMCPProxy:
|
|
1661
1849
|
"""Create a FastMCP proxy server for the given backend.
|
|
1662
1850
|
|
|
1663
|
-
The
|
|
1664
|
-
instance or any value accepted as the
|
|
1665
|
-
|
|
1666
|
-
|
|
1851
|
+
The `backend` argument can be either an existing `fastmcp.client.Client`
|
|
1852
|
+
instance or any value accepted as the `transport` argument of
|
|
1853
|
+
`fastmcp.client.Client`. This mirrors the convenience of the
|
|
1854
|
+
`fastmcp.client.Client` constructor.
|
|
1667
1855
|
"""
|
|
1668
1856
|
from fastmcp.client.client import Client
|
|
1669
1857
|
from fastmcp.server.proxy import FastMCPProxy
|
|
@@ -1700,14 +1888,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1700
1888
|
Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
|
|
1701
1889
|
|
|
1702
1890
|
Rules:
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1891
|
+
- If the component's enabled property is False, always return False.
|
|
1892
|
+
- If both include_tags and exclude_tags are None, return True.
|
|
1893
|
+
- If exclude_tags is provided, check each exclude tag:
|
|
1706
1894
|
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
|
1707
|
-
|
|
1895
|
+
- If include_tags is provided, check each include tag:
|
|
1708
1896
|
- If the include tag is a string, it must be present in the input tags to include.
|
|
1709
|
-
|
|
1710
|
-
|
|
1897
|
+
- If include_tags is provided and none of the include tags match, return False.
|
|
1898
|
+
- If include_tags is not provided, return True.
|
|
1711
1899
|
"""
|
|
1712
1900
|
if not component.enabled:
|
|
1713
1901
|
return False
|
|
@@ -1728,60 +1916,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1728
1916
|
return True
|
|
1729
1917
|
|
|
1730
1918
|
|
|
1919
|
+
@dataclass
|
|
1731
1920
|
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}_")
|
|
1921
|
+
prefix: str | None
|
|
1922
|
+
server: FastMCP[Any]
|
|
1923
|
+
resource_prefix_format: Literal["protocol", "path"] | None = None
|
|
1785
1924
|
|
|
1786
1925
|
|
|
1787
1926
|
def add_resource_prefix(
|
|
@@ -1797,12 +1936,21 @@ def add_resource_prefix(
|
|
|
1797
1936
|
The resource URI with the prefix added
|
|
1798
1937
|
|
|
1799
1938
|
Examples:
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
"
|
|
1804
|
-
|
|
1805
|
-
|
|
1939
|
+
With new style:
|
|
1940
|
+
```python
|
|
1941
|
+
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1942
|
+
"resource://prefix/path/to/resource"
|
|
1943
|
+
```
|
|
1944
|
+
With legacy style:
|
|
1945
|
+
```python
|
|
1946
|
+
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1947
|
+
"prefix+resource://path/to/resource"
|
|
1948
|
+
```
|
|
1949
|
+
With absolute path:
|
|
1950
|
+
```python
|
|
1951
|
+
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
1952
|
+
"resource://prefix//absolute/path"
|
|
1953
|
+
```
|
|
1806
1954
|
|
|
1807
1955
|
Raises:
|
|
1808
1956
|
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1848,12 +1996,21 @@ def remove_resource_prefix(
|
|
|
1848
1996
|
The resource URI with the prefix removed
|
|
1849
1997
|
|
|
1850
1998
|
Examples:
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
"resource://path/to/resource"
|
|
1855
|
-
|
|
1856
|
-
|
|
1999
|
+
With new style:
|
|
2000
|
+
```python
|
|
2001
|
+
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2002
|
+
"resource://path/to/resource"
|
|
2003
|
+
```
|
|
2004
|
+
With legacy style:
|
|
2005
|
+
```python
|
|
2006
|
+
remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2007
|
+
"resource://path/to/resource"
|
|
2008
|
+
```
|
|
2009
|
+
With absolute path:
|
|
2010
|
+
```python
|
|
2011
|
+
remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
2012
|
+
"resource:///absolute/path"
|
|
2013
|
+
```
|
|
1857
2014
|
|
|
1858
2015
|
Raises:
|
|
1859
2016
|
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1906,12 +2063,21 @@ def has_resource_prefix(
|
|
|
1906
2063
|
True if the URI has the specified prefix, False otherwise
|
|
1907
2064
|
|
|
1908
2065
|
Examples:
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
True
|
|
1913
|
-
|
|
2066
|
+
With new style:
|
|
2067
|
+
```python
|
|
2068
|
+
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2069
|
+
True
|
|
2070
|
+
```
|
|
2071
|
+
With legacy style:
|
|
2072
|
+
```python
|
|
2073
|
+
has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2074
|
+
True
|
|
2075
|
+
```
|
|
2076
|
+
With other path:
|
|
2077
|
+
```python
|
|
2078
|
+
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
1914
2079
|
False
|
|
2080
|
+
```
|
|
1915
2081
|
|
|
1916
2082
|
Raises:
|
|
1917
2083
|
ValueError: If the URI doesn't match the expected protocol://path format
|