fastmcp 2.2.5__py3-none-any.whl → 2.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +1 -0
- fastmcp/client/base.py +0 -1
- fastmcp/client/client.py +255 -49
- fastmcp/client/logging.py +13 -0
- fastmcp/client/sampling.py +2 -0
- fastmcp/client/transports.py +37 -4
- fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +1 -3
- fastmcp/prompts/prompt.py +8 -4
- fastmcp/resources/template.py +5 -2
- fastmcp/resources/types.py +4 -7
- fastmcp/server/context.py +27 -14
- fastmcp/server/openapi.py +94 -32
- fastmcp/server/proxy.py +4 -3
- fastmcp/server/server.py +261 -30
- fastmcp/settings.py +7 -0
- fastmcp/tools/tool.py +24 -22
- fastmcp/tools/tool_manager.py +16 -3
- fastmcp/utilities/http.py +44 -0
- fastmcp/utilities/openapi.py +147 -36
- fastmcp/utilities/types.py +29 -1
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/METADATA +4 -4
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/RECORD +25 -23
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import datetime
|
|
4
6
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
5
7
|
from contextlib import (
|
|
@@ -12,8 +14,13 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
|
12
14
|
|
|
13
15
|
import anyio
|
|
14
16
|
import httpx
|
|
15
|
-
import pydantic_core
|
|
16
17
|
import uvicorn
|
|
18
|
+
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
|
19
|
+
from mcp.server.auth.middleware.bearer_auth import (
|
|
20
|
+
BearerAuthBackend,
|
|
21
|
+
RequireAuthMiddleware,
|
|
22
|
+
)
|
|
23
|
+
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
|
17
24
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
18
25
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
19
26
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
@@ -25,7 +32,9 @@ from mcp.types import (
|
|
|
25
32
|
EmbeddedResource,
|
|
26
33
|
GetPromptResult,
|
|
27
34
|
ImageContent,
|
|
35
|
+
PromptMessage,
|
|
28
36
|
TextContent,
|
|
37
|
+
ToolAnnotations,
|
|
29
38
|
)
|
|
30
39
|
from mcp.types import Prompt as MCPPrompt
|
|
31
40
|
from mcp.types import Resource as MCPResource
|
|
@@ -33,8 +42,12 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
|
33
42
|
from mcp.types import Tool as MCPTool
|
|
34
43
|
from pydantic.networks import AnyUrl
|
|
35
44
|
from starlette.applications import Starlette
|
|
45
|
+
from starlette.middleware import Middleware
|
|
46
|
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
36
47
|
from starlette.requests import Request
|
|
48
|
+
from starlette.responses import Response
|
|
37
49
|
from starlette.routing import Mount, Route
|
|
50
|
+
from starlette.types import Receive, Scope, Send
|
|
38
51
|
|
|
39
52
|
import fastmcp
|
|
40
53
|
import fastmcp.settings
|
|
@@ -46,6 +59,7 @@ from fastmcp.resources.template import ResourceTemplate
|
|
|
46
59
|
from fastmcp.tools import ToolManager
|
|
47
60
|
from fastmcp.tools.tool import Tool
|
|
48
61
|
from fastmcp.utilities.decorators import DecoratedFunction
|
|
62
|
+
from fastmcp.utilities.http import RequestMiddleware
|
|
49
63
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
50
64
|
|
|
51
65
|
if TYPE_CHECKING:
|
|
@@ -63,7 +77,7 @@ class MountedServer:
|
|
|
63
77
|
def __init__(
|
|
64
78
|
self,
|
|
65
79
|
prefix: str,
|
|
66
|
-
server:
|
|
80
|
+
server: FastMCP,
|
|
67
81
|
tool_separator: str | None = None,
|
|
68
82
|
resource_separator: str | None = None,
|
|
69
83
|
prompt_separator: str | None = None,
|
|
@@ -149,7 +163,7 @@ class TimedCache:
|
|
|
149
163
|
|
|
150
164
|
|
|
151
165
|
@asynccontextmanager
|
|
152
|
-
async def default_lifespan(server:
|
|
166
|
+
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
153
167
|
"""Default lifespan context manager that does nothing.
|
|
154
168
|
|
|
155
169
|
Args:
|
|
@@ -162,8 +176,8 @@ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
|
|
|
162
176
|
|
|
163
177
|
|
|
164
178
|
def _lifespan_wrapper(
|
|
165
|
-
app:
|
|
166
|
-
lifespan: Callable[[
|
|
179
|
+
app: FastMCP,
|
|
180
|
+
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
|
|
167
181
|
) -> Callable[
|
|
168
182
|
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
169
183
|
]:
|
|
@@ -181,10 +195,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
181
195
|
self,
|
|
182
196
|
name: str | None = None,
|
|
183
197
|
instructions: str | None = None,
|
|
198
|
+
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any]
|
|
199
|
+
| None = None,
|
|
184
200
|
lifespan: (
|
|
185
|
-
Callable[
|
|
201
|
+
Callable[
|
|
202
|
+
[FastMCP[LifespanResultT]],
|
|
203
|
+
AbstractAsyncContextManager[LifespanResultT],
|
|
204
|
+
]
|
|
205
|
+
| None
|
|
186
206
|
) = None,
|
|
187
207
|
tags: set[str] | None = None,
|
|
208
|
+
tool_serializer: Callable[[Any], str] | None = None,
|
|
188
209
|
**settings: Any,
|
|
189
210
|
):
|
|
190
211
|
self.tags: set[str] = tags or set()
|
|
@@ -198,7 +219,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
198
219
|
self._mounted_servers: dict[str, MountedServer] = {}
|
|
199
220
|
|
|
200
221
|
if lifespan is None:
|
|
222
|
+
self._has_lifespan = False
|
|
201
223
|
lifespan = default_lifespan
|
|
224
|
+
else:
|
|
225
|
+
self._has_lifespan = True
|
|
202
226
|
|
|
203
227
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
204
228
|
name=name or "FastMCP",
|
|
@@ -206,7 +230,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
206
230
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
207
231
|
)
|
|
208
232
|
self._tool_manager = ToolManager(
|
|
209
|
-
duplicate_behavior=self.settings.on_duplicate_tools
|
|
233
|
+
duplicate_behavior=self.settings.on_duplicate_tools,
|
|
234
|
+
serializer=tool_serializer,
|
|
210
235
|
)
|
|
211
236
|
self._resource_manager = ResourceManager(
|
|
212
237
|
duplicate_behavior=self.settings.on_duplicate_resources
|
|
@@ -214,6 +239,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
214
239
|
self._prompt_manager = PromptManager(
|
|
215
240
|
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
216
241
|
)
|
|
242
|
+
|
|
243
|
+
if (self.settings.auth is not None) != (auth_server_provider is not None):
|
|
244
|
+
# TODO: after we support separate authorization servers (see
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"settings.auth must be specified if and only if auth_server_provider "
|
|
247
|
+
"is specified"
|
|
248
|
+
)
|
|
249
|
+
self._auth_server_provider = auth_server_provider
|
|
250
|
+
self._custom_starlette_routes: list[Route] = []
|
|
217
251
|
self.dependencies = self.settings.dependencies
|
|
218
252
|
|
|
219
253
|
# Set up MCP protocol handlers
|
|
@@ -273,7 +307,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
273
307
|
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
274
308
|
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
275
309
|
|
|
276
|
-
def get_context(self) ->
|
|
310
|
+
def get_context(self) -> Context[ServerSession, LifespanResultT]:
|
|
277
311
|
"""
|
|
278
312
|
Returns a Context object. Note that the context will only be valid
|
|
279
313
|
during a request; outside a request, most methods will error.
|
|
@@ -333,6 +367,50 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
333
367
|
self._cache.set("prompts", prompts)
|
|
334
368
|
return prompts
|
|
335
369
|
|
|
370
|
+
def custom_route(
|
|
371
|
+
self,
|
|
372
|
+
path: str,
|
|
373
|
+
methods: list[str],
|
|
374
|
+
name: str | None = None,
|
|
375
|
+
include_in_schema: bool = True,
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Decorator to register a custom HTTP route on the FastMCP server.
|
|
379
|
+
|
|
380
|
+
Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
|
|
381
|
+
which can be useful for OAuth callbacks, health checks, or admin APIs.
|
|
382
|
+
The handler function must be an async function that accepts a Starlette
|
|
383
|
+
Request and returns a Response.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
path: URL path for the route (e.g., "/oauth/callback")
|
|
387
|
+
methods: List of HTTP methods to support (e.g., ["GET", "POST"])
|
|
388
|
+
name: Optional name for the route (to reference this route with
|
|
389
|
+
Starlette's reverse URL lookup feature)
|
|
390
|
+
include_in_schema: Whether to include in OpenAPI schema, defaults to True
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
@server.custom_route("/health", methods=["GET"])
|
|
394
|
+
async def health_check(request: Request) -> Response:
|
|
395
|
+
return JSONResponse({"status": "ok"})
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def decorator(
|
|
399
|
+
func: Callable[[Request], Awaitable[Response]],
|
|
400
|
+
) -> Callable[[Request], Awaitable[Response]]:
|
|
401
|
+
self._custom_starlette_routes.append(
|
|
402
|
+
Route(
|
|
403
|
+
path,
|
|
404
|
+
endpoint=func,
|
|
405
|
+
methods=methods,
|
|
406
|
+
name=name,
|
|
407
|
+
include_in_schema=include_in_schema,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
return func
|
|
411
|
+
|
|
412
|
+
return decorator
|
|
413
|
+
|
|
336
414
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
337
415
|
"""
|
|
338
416
|
List all available tools, in the format expected by the low-level MCP
|
|
@@ -429,7 +507,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
429
507
|
messages = await self._prompt_manager.render_prompt(
|
|
430
508
|
name, arguments=arguments or {}, context=context
|
|
431
509
|
)
|
|
432
|
-
|
|
510
|
+
|
|
511
|
+
return GetPromptResult(
|
|
512
|
+
messages=[
|
|
513
|
+
PromptMessage(role=m.role, content=m.content) for m in messages
|
|
514
|
+
]
|
|
515
|
+
)
|
|
433
516
|
else:
|
|
434
517
|
for server in self._mounted_servers.values():
|
|
435
518
|
if server.match_prompt(name):
|
|
@@ -444,6 +527,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
444
527
|
name: str | None = None,
|
|
445
528
|
description: str | None = None,
|
|
446
529
|
tags: set[str] | None = None,
|
|
530
|
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
447
531
|
) -> None:
|
|
448
532
|
"""Add a tool to the server.
|
|
449
533
|
|
|
@@ -455,9 +539,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
455
539
|
name: Optional name for the tool (defaults to function name)
|
|
456
540
|
description: Optional description of what the tool does
|
|
457
541
|
tags: Optional set of tags for categorizing the tool
|
|
542
|
+
annotations: Optional annotations about the tool's behavior
|
|
458
543
|
"""
|
|
544
|
+
if isinstance(annotations, dict):
|
|
545
|
+
annotations = ToolAnnotations(**annotations)
|
|
546
|
+
|
|
459
547
|
self._tool_manager.add_tool_from_fn(
|
|
460
|
-
fn,
|
|
548
|
+
fn,
|
|
549
|
+
name=name,
|
|
550
|
+
description=description,
|
|
551
|
+
tags=tags,
|
|
552
|
+
annotations=annotations,
|
|
461
553
|
)
|
|
462
554
|
self._cache.clear()
|
|
463
555
|
|
|
@@ -466,6 +558,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
466
558
|
name: str | None = None,
|
|
467
559
|
description: str | None = None,
|
|
468
560
|
tags: set[str] | None = None,
|
|
561
|
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
469
562
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
470
563
|
"""Decorator to register a tool.
|
|
471
564
|
|
|
@@ -477,6 +570,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
477
570
|
name: Optional name for the tool (defaults to function name)
|
|
478
571
|
description: Optional description of what the tool does
|
|
479
572
|
tags: Optional set of tags for categorizing the tool
|
|
573
|
+
annotations: Optional annotations about the tool's behavior
|
|
480
574
|
|
|
481
575
|
Example:
|
|
482
576
|
@server.tool()
|
|
@@ -502,7 +596,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
502
596
|
)
|
|
503
597
|
|
|
504
598
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
505
|
-
self.add_tool(
|
|
599
|
+
self.add_tool(
|
|
600
|
+
fn,
|
|
601
|
+
name=name,
|
|
602
|
+
description=description,
|
|
603
|
+
tags=tags,
|
|
604
|
+
annotations=annotations,
|
|
605
|
+
)
|
|
506
606
|
return fn
|
|
507
607
|
|
|
508
608
|
return decorator
|
|
@@ -728,10 +828,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
728
828
|
log_level: str | None = None,
|
|
729
829
|
) -> None:
|
|
730
830
|
"""Run the server using SSE transport."""
|
|
731
|
-
|
|
831
|
+
app = self.sse_app()
|
|
832
|
+
app = RequestMiddleware(app)
|
|
732
833
|
|
|
733
834
|
config = uvicorn.Config(
|
|
734
|
-
|
|
835
|
+
app,
|
|
735
836
|
host=host or self.settings.host,
|
|
736
837
|
port=port or self.settings.port,
|
|
737
838
|
log_level=log_level or self.settings.log_level.lower(),
|
|
@@ -741,39 +842,169 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
741
842
|
|
|
742
843
|
def sse_app(self) -> Starlette:
|
|
743
844
|
"""Return an instance of the SSE server app."""
|
|
845
|
+
from starlette.middleware import Middleware
|
|
846
|
+
from starlette.routing import Mount, Route
|
|
847
|
+
|
|
848
|
+
# Set up auth context and dependencies
|
|
849
|
+
|
|
744
850
|
sse = SseServerTransport(self.settings.message_path)
|
|
745
851
|
|
|
746
|
-
async def handle_sse(
|
|
852
|
+
async def handle_sse(scope: Scope, receive: Receive, send: Send):
|
|
853
|
+
# Add client ID from auth context into request context if available
|
|
854
|
+
|
|
747
855
|
async with sse.connect_sse(
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
856
|
+
scope,
|
|
857
|
+
receive,
|
|
858
|
+
send,
|
|
751
859
|
) as streams:
|
|
752
860
|
await self._mcp_server.run(
|
|
753
861
|
streams[0],
|
|
754
862
|
streams[1],
|
|
755
863
|
self._mcp_server.create_initialization_options(),
|
|
756
864
|
)
|
|
865
|
+
return Response()
|
|
866
|
+
|
|
867
|
+
# Create routes
|
|
868
|
+
routes: list[Route | Mount] = []
|
|
869
|
+
middleware: list[Middleware] = []
|
|
870
|
+
required_scopes = []
|
|
871
|
+
|
|
872
|
+
# Add auth endpoints if auth provider is configured
|
|
873
|
+
if self._auth_server_provider:
|
|
874
|
+
assert self.settings.auth
|
|
875
|
+
from mcp.server.auth.routes import create_auth_routes
|
|
876
|
+
|
|
877
|
+
required_scopes = self.settings.auth.required_scopes or []
|
|
878
|
+
|
|
879
|
+
middleware = [
|
|
880
|
+
# extract auth info from request (but do not require it)
|
|
881
|
+
Middleware(
|
|
882
|
+
AuthenticationMiddleware,
|
|
883
|
+
backend=BearerAuthBackend(
|
|
884
|
+
provider=self._auth_server_provider,
|
|
885
|
+
),
|
|
886
|
+
),
|
|
887
|
+
# Add the auth context middleware to store
|
|
888
|
+
# authenticated user in a contextvar
|
|
889
|
+
Middleware(AuthContextMiddleware),
|
|
890
|
+
]
|
|
891
|
+
routes.extend(
|
|
892
|
+
create_auth_routes(
|
|
893
|
+
provider=self._auth_server_provider,
|
|
894
|
+
issuer_url=self.settings.auth.issuer_url,
|
|
895
|
+
service_documentation_url=self.settings.auth.service_documentation_url,
|
|
896
|
+
client_registration_options=self.settings.auth.client_registration_options,
|
|
897
|
+
revocation_options=self.settings.auth.revocation_options,
|
|
898
|
+
)
|
|
899
|
+
)
|
|
757
900
|
|
|
901
|
+
# When auth is not configured, we shouldn't require auth
|
|
902
|
+
if self._auth_server_provider:
|
|
903
|
+
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
|
|
904
|
+
routes.append(
|
|
905
|
+
Route(
|
|
906
|
+
self.settings.sse_path,
|
|
907
|
+
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
|
|
908
|
+
methods=["GET"],
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
routes.append(
|
|
912
|
+
Mount(
|
|
913
|
+
self.settings.message_path,
|
|
914
|
+
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
else:
|
|
918
|
+
# Auth is disabled, no need for RequireAuthMiddleware
|
|
919
|
+
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
|
920
|
+
async def sse_endpoint(request: Request) -> None:
|
|
921
|
+
# Convert the Starlette request to ASGI parameters
|
|
922
|
+
await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
|
923
|
+
|
|
924
|
+
routes.append(
|
|
925
|
+
Route(
|
|
926
|
+
self.settings.sse_path,
|
|
927
|
+
endpoint=sse_endpoint,
|
|
928
|
+
methods=["GET"],
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
routes.append(
|
|
932
|
+
Mount(
|
|
933
|
+
self.settings.message_path,
|
|
934
|
+
app=sse.handle_post_message,
|
|
935
|
+
)
|
|
936
|
+
)
|
|
937
|
+
# mount these routes last, so they have the lowest route matching precedence
|
|
938
|
+
routes.extend(self._custom_starlette_routes)
|
|
939
|
+
|
|
940
|
+
# Create Starlette app with routes and middleware
|
|
758
941
|
return Starlette(
|
|
759
|
-
debug=self.settings.debug,
|
|
760
|
-
routes=[
|
|
761
|
-
Route(self.settings.sse_path, endpoint=handle_sse),
|
|
762
|
-
Mount(self.settings.message_path, app=sse.handle_post_message),
|
|
763
|
-
],
|
|
942
|
+
debug=self.settings.debug, routes=routes, middleware=middleware
|
|
764
943
|
)
|
|
765
944
|
|
|
766
945
|
def mount(
|
|
767
946
|
self,
|
|
768
947
|
prefix: str,
|
|
769
|
-
server:
|
|
948
|
+
server: FastMCP[LifespanResultT],
|
|
770
949
|
tool_separator: str | None = None,
|
|
771
950
|
resource_separator: str | None = None,
|
|
772
951
|
prompt_separator: str | None = None,
|
|
952
|
+
as_proxy: bool | None = None,
|
|
773
953
|
) -> None:
|
|
954
|
+
"""Mount another FastMCP server on this server with the given prefix.
|
|
955
|
+
|
|
956
|
+
Unlike importing (with import_server), mounting establishes a dynamic connection
|
|
957
|
+
between servers. When a client interacts with a mounted server's objects through
|
|
958
|
+
the parent server, requests are forwarded to the mounted server in real-time.
|
|
959
|
+
This means changes to the mounted server are immediately reflected when accessed
|
|
960
|
+
through the parent.
|
|
961
|
+
|
|
962
|
+
When a server is mounted:
|
|
963
|
+
- Tools from the mounted server are accessible with prefixed names using the tool_separator.
|
|
964
|
+
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
|
965
|
+
- Resources are accessible with prefixed URIs using the resource_separator.
|
|
966
|
+
Example: If server has a resource with URI "weather://forecast", it will be available as
|
|
967
|
+
"prefix+weather://forecast".
|
|
968
|
+
- Templates are accessible with prefixed URI templates using the resource_separator.
|
|
969
|
+
Example: If server has a template with URI "weather://location/{id}", it will be available
|
|
970
|
+
as "prefix+weather://location/{id}".
|
|
971
|
+
- Prompts are accessible with prefixed names using the prompt_separator.
|
|
972
|
+
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
973
|
+
"prefix_weather_prompt".
|
|
974
|
+
|
|
975
|
+
There are two modes for mounting servers:
|
|
976
|
+
1. Direct mounting (default when server has no custom lifespan): The parent server
|
|
977
|
+
directly accesses the mounted server's objects in-memory for better performance.
|
|
978
|
+
In this mode, no client lifecycle events occur on the mounted server, including
|
|
979
|
+
lifespan execution.
|
|
980
|
+
|
|
981
|
+
2. Proxy mounting (default when server has a custom lifespan): The parent server
|
|
982
|
+
treats the mounted server as a separate entity and communicates with it via a
|
|
983
|
+
Client transport. This preserves all client-facing behaviors, including lifespan
|
|
984
|
+
execution, but with slightly higher overhead.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
prefix: Prefix to use for the mounted server's objects.
|
|
988
|
+
server: The FastMCP server to mount.
|
|
989
|
+
tool_separator: Separator character for tool names (defaults to "_").
|
|
990
|
+
resource_separator: Separator character for resource URIs (defaults to "+").
|
|
991
|
+
prompt_separator: Separator character for prompt names (defaults to "_").
|
|
992
|
+
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
993
|
+
automatically determined based on whether the server has a custom lifespan
|
|
994
|
+
(True if it has a custom lifespan, False otherwise).
|
|
774
995
|
"""
|
|
775
|
-
|
|
776
|
-
|
|
996
|
+
from fastmcp import Client
|
|
997
|
+
from fastmcp.client.transports import FastMCPTransport
|
|
998
|
+
from fastmcp.server.proxy import FastMCPProxy
|
|
999
|
+
|
|
1000
|
+
# if as_proxy is not specified and the server has a custom lifespan,
|
|
1001
|
+
# we should treat it as a proxy
|
|
1002
|
+
if as_proxy is None:
|
|
1003
|
+
as_proxy = server._has_lifespan
|
|
1004
|
+
|
|
1005
|
+
if as_proxy and not isinstance(server, FastMCPProxy):
|
|
1006
|
+
server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
|
|
1007
|
+
|
|
777
1008
|
mounted_server = MountedServer(
|
|
778
1009
|
server=server,
|
|
779
1010
|
prefix=prefix,
|
|
@@ -791,7 +1022,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
791
1022
|
async def import_server(
|
|
792
1023
|
self,
|
|
793
1024
|
prefix: str,
|
|
794
|
-
server:
|
|
1025
|
+
server: FastMCP[LifespanResultT],
|
|
795
1026
|
tool_separator: str | None = None,
|
|
796
1027
|
resource_separator: str | None = None,
|
|
797
1028
|
prompt_separator: str | None = None,
|
|
@@ -865,7 +1096,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
865
1096
|
@classmethod
|
|
866
1097
|
def from_openapi(
|
|
867
1098
|
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
|
868
|
-
) ->
|
|
1099
|
+
) -> FastMCPOpenAPI:
|
|
869
1100
|
"""
|
|
870
1101
|
Create a FastMCP server from an OpenAPI specification.
|
|
871
1102
|
"""
|
|
@@ -875,8 +1106,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
875
1106
|
|
|
876
1107
|
@classmethod
|
|
877
1108
|
def from_fastapi(
|
|
878
|
-
cls, app:
|
|
879
|
-
) ->
|
|
1109
|
+
cls, app: Any, name: str | None = None, **settings: Any
|
|
1110
|
+
) -> FastMCPOpenAPI:
|
|
880
1111
|
"""
|
|
881
1112
|
Create a FastMCP server from a FastAPI application.
|
|
882
1113
|
"""
|
|
@@ -894,7 +1125,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
894
1125
|
)
|
|
895
1126
|
|
|
896
1127
|
@classmethod
|
|
897
|
-
def from_client(cls, client:
|
|
1128
|
+
def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
|
|
898
1129
|
"""
|
|
899
1130
|
Create a FastMCP proxy server from a FastMCP client.
|
|
900
1131
|
"""
|
fastmcp/settings.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Literal
|
|
4
4
|
|
|
5
|
+
from mcp.server.auth.settings import AuthSettings
|
|
5
6
|
from pydantic import Field
|
|
6
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
8
|
|
|
@@ -20,6 +21,8 @@ class Settings(BaseSettings):
|
|
|
20
21
|
env_prefix="FASTMCP_",
|
|
21
22
|
env_file=".env",
|
|
22
23
|
extra="ignore",
|
|
24
|
+
env_nested_delimiter="__",
|
|
25
|
+
nested_model_default_partial_update=True,
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
test_mode: bool = False
|
|
@@ -37,6 +40,8 @@ class ServerSettings(BaseSettings):
|
|
|
37
40
|
env_prefix="FASTMCP_SERVER_",
|
|
38
41
|
env_file=".env",
|
|
39
42
|
extra="ignore",
|
|
43
|
+
env_nested_delimiter="__",
|
|
44
|
+
nested_model_default_partial_update=True,
|
|
40
45
|
)
|
|
41
46
|
|
|
42
47
|
log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
|
|
@@ -65,6 +70,8 @@ class ServerSettings(BaseSettings):
|
|
|
65
70
|
# cache settings (for checking mounted servers)
|
|
66
71
|
cache_expiration_seconds: float = 0
|
|
67
72
|
|
|
73
|
+
auth: AuthSettings | None = None
|
|
74
|
+
|
|
68
75
|
|
|
69
76
|
class ClientSettings(BaseSettings):
|
|
70
77
|
"""FastMCP client settings."""
|
fastmcp/tools/tool.py
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
import json
|
|
5
4
|
from collections.abc import Callable
|
|
6
5
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
7
6
|
|
|
8
7
|
import pydantic_core
|
|
9
|
-
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
8
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
|
10
9
|
from mcp.types import Tool as MCPTool
|
|
11
10
|
from pydantic import BaseModel, BeforeValidator, Field
|
|
12
11
|
|
|
13
12
|
from fastmcp.exceptions import ToolError
|
|
14
13
|
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
|
15
|
-
from fastmcp.utilities.types import
|
|
14
|
+
from fastmcp.utilities.types import (
|
|
15
|
+
Image,
|
|
16
|
+
_convert_set_defaults,
|
|
17
|
+
is_class_member_of_type,
|
|
18
|
+
)
|
|
16
19
|
|
|
17
20
|
if TYPE_CHECKING:
|
|
18
21
|
from mcp.server.session import ServerSessionT
|
|
@@ -39,6 +42,12 @@ class Tool(BaseModel):
|
|
|
39
42
|
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
40
43
|
default_factory=set, description="Tags for the tool"
|
|
41
44
|
)
|
|
45
|
+
annotations: ToolAnnotations | None = Field(
|
|
46
|
+
None, description="Additional annotations about the tool"
|
|
47
|
+
)
|
|
48
|
+
serializer: Callable[[Any], str] | None = Field(
|
|
49
|
+
None, description="Optional custom serializer for tool results"
|
|
50
|
+
)
|
|
42
51
|
|
|
43
52
|
@classmethod
|
|
44
53
|
def from_function(
|
|
@@ -48,6 +57,8 @@ class Tool(BaseModel):
|
|
|
48
57
|
description: str | None = None,
|
|
49
58
|
context_kwarg: str | None = None,
|
|
50
59
|
tags: set[str] | None = None,
|
|
60
|
+
annotations: ToolAnnotations | None = None,
|
|
61
|
+
serializer: Callable[[Any], str] | None = None,
|
|
51
62
|
) -> Tool:
|
|
52
63
|
"""Create a Tool from a function."""
|
|
53
64
|
from fastmcp import Context
|
|
@@ -66,7 +77,7 @@ class Tool(BaseModel):
|
|
|
66
77
|
else:
|
|
67
78
|
sig = inspect.signature(fn)
|
|
68
79
|
for param_name, param in sig.parameters.items():
|
|
69
|
-
if param.annotation
|
|
80
|
+
if is_class_member_of_type(param.annotation, Context):
|
|
70
81
|
context_kwarg = param_name
|
|
71
82
|
break
|
|
72
83
|
|
|
@@ -92,6 +103,8 @@ class Tool(BaseModel):
|
|
|
92
103
|
is_async=is_async,
|
|
93
104
|
context_kwarg=context_kwarg,
|
|
94
105
|
tags=tags or set(),
|
|
106
|
+
annotations=annotations,
|
|
107
|
+
serializer=serializer,
|
|
95
108
|
)
|
|
96
109
|
|
|
97
110
|
async def run(
|
|
@@ -112,7 +125,7 @@ class Tool(BaseModel):
|
|
|
112
125
|
arguments_to_validate=arguments,
|
|
113
126
|
arguments_to_pass_directly=pass_args,
|
|
114
127
|
)
|
|
115
|
-
return _convert_to_content(result)
|
|
128
|
+
return _convert_to_content(result, serializer=self.serializer)
|
|
116
129
|
except Exception as e:
|
|
117
130
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
118
131
|
|
|
@@ -121,6 +134,7 @@ class Tool(BaseModel):
|
|
|
121
134
|
"name": self.name,
|
|
122
135
|
"description": self.description,
|
|
123
136
|
"inputSchema": self.parameters,
|
|
137
|
+
"annotations": self.annotations,
|
|
124
138
|
}
|
|
125
139
|
return MCPTool(**kwargs | overrides)
|
|
126
140
|
|
|
@@ -132,6 +146,7 @@ class Tool(BaseModel):
|
|
|
132
146
|
|
|
133
147
|
def _convert_to_content(
|
|
134
148
|
result: Any,
|
|
149
|
+
serializer: Callable[[Any], str] | None = None,
|
|
135
150
|
_process_as_single_item: bool = False,
|
|
136
151
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
137
152
|
"""Convert a result to a sequence of content objects."""
|
|
@@ -166,23 +181,10 @@ def _convert_to_content(
|
|
|
166
181
|
|
|
167
182
|
return other_content + mcp_types
|
|
168
183
|
|
|
169
|
-
# if the result is a bytes object, convert it to a text content object
|
|
170
184
|
if not isinstance(result, str):
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
elif isinstance(jsonable_result, bool):
|
|
176
|
-
return [
|
|
177
|
-
TextContent(
|
|
178
|
-
type="text", text="true" if jsonable_result else "false"
|
|
179
|
-
)
|
|
180
|
-
]
|
|
181
|
-
elif isinstance(jsonable_result, str | int | float):
|
|
182
|
-
return [TextContent(type="text", text=str(jsonable_result))]
|
|
183
|
-
else:
|
|
184
|
-
return [TextContent(type="text", text=json.dumps(jsonable_result))]
|
|
185
|
-
except Exception:
|
|
186
|
-
result = str(result)
|
|
185
|
+
if serializer is not None:
|
|
186
|
+
result = serializer(result)
|
|
187
|
+
else:
|
|
188
|
+
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
187
189
|
|
|
188
190
|
return [TextContent(type="text", text=result)]
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from mcp.shared.context import LifespanContextT
|
|
7
|
-
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
7
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
|
8
8
|
|
|
9
9
|
from fastmcp.exceptions import NotFoundError
|
|
10
10
|
from fastmcp.settings import DuplicateBehavior
|
|
@@ -22,8 +22,13 @@ logger = get_logger(__name__)
|
|
|
22
22
|
class ToolManager:
|
|
23
23
|
"""Manages FastMCP tools."""
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
duplicate_behavior: DuplicateBehavior | None = None,
|
|
28
|
+
serializer: Callable[[Any], str] | None = None,
|
|
29
|
+
):
|
|
26
30
|
self._tools: dict[str, Tool] = {}
|
|
31
|
+
self._serializer = serializer
|
|
27
32
|
|
|
28
33
|
# Default to "warn" if None is provided
|
|
29
34
|
if duplicate_behavior is None:
|
|
@@ -61,9 +66,17 @@ class ToolManager:
|
|
|
61
66
|
name: str | None = None,
|
|
62
67
|
description: str | None = None,
|
|
63
68
|
tags: set[str] | None = None,
|
|
69
|
+
annotations: ToolAnnotations | None = None,
|
|
64
70
|
) -> Tool:
|
|
65
71
|
"""Add a tool to the server."""
|
|
66
|
-
tool = Tool.from_function(
|
|
72
|
+
tool = Tool.from_function(
|
|
73
|
+
fn,
|
|
74
|
+
name=name,
|
|
75
|
+
description=description,
|
|
76
|
+
tags=tags,
|
|
77
|
+
annotations=annotations,
|
|
78
|
+
serializer=self._serializer,
|
|
79
|
+
)
|
|
67
80
|
return self.add_tool(tool)
|
|
68
81
|
|
|
69
82
|
def add_tool(self, tool: Tool, key: str | None = None) -> Tool:
|