fastmcp 2.2.6__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/client/client.py +243 -41
- 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 +12 -1
- fastmcp/server/openapi.py +28 -5
- fastmcp/server/proxy.py +3 -2
- fastmcp/server/server.py +243 -18
- 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.6.dist-info → fastmcp-2.2.7.dist-info}/METADATA +3 -3
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.7.dist-info}/RECORD +20 -19
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.7.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.7.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.6.dist-info → fastmcp-2.2.7.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -14,8 +14,13 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
|
14
14
|
|
|
15
15
|
import anyio
|
|
16
16
|
import httpx
|
|
17
|
-
import pydantic_core
|
|
18
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
|
|
19
24
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
20
25
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
21
26
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
@@ -27,7 +32,9 @@ from mcp.types import (
|
|
|
27
32
|
EmbeddedResource,
|
|
28
33
|
GetPromptResult,
|
|
29
34
|
ImageContent,
|
|
35
|
+
PromptMessage,
|
|
30
36
|
TextContent,
|
|
37
|
+
ToolAnnotations,
|
|
31
38
|
)
|
|
32
39
|
from mcp.types import Prompt as MCPPrompt
|
|
33
40
|
from mcp.types import Resource as MCPResource
|
|
@@ -35,8 +42,12 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
|
35
42
|
from mcp.types import Tool as MCPTool
|
|
36
43
|
from pydantic.networks import AnyUrl
|
|
37
44
|
from starlette.applications import Starlette
|
|
45
|
+
from starlette.middleware import Middleware
|
|
46
|
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
38
47
|
from starlette.requests import Request
|
|
48
|
+
from starlette.responses import Response
|
|
39
49
|
from starlette.routing import Mount, Route
|
|
50
|
+
from starlette.types import Receive, Scope, Send
|
|
40
51
|
|
|
41
52
|
import fastmcp
|
|
42
53
|
import fastmcp.settings
|
|
@@ -48,6 +59,7 @@ from fastmcp.resources.template import ResourceTemplate
|
|
|
48
59
|
from fastmcp.tools import ToolManager
|
|
49
60
|
from fastmcp.tools.tool import Tool
|
|
50
61
|
from fastmcp.utilities.decorators import DecoratedFunction
|
|
62
|
+
from fastmcp.utilities.http import RequestMiddleware
|
|
51
63
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
52
64
|
|
|
53
65
|
if TYPE_CHECKING:
|
|
@@ -183,6 +195,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
183
195
|
self,
|
|
184
196
|
name: str | None = None,
|
|
185
197
|
instructions: str | None = None,
|
|
198
|
+
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any]
|
|
199
|
+
| None = None,
|
|
186
200
|
lifespan: (
|
|
187
201
|
Callable[
|
|
188
202
|
[FastMCP[LifespanResultT]],
|
|
@@ -191,6 +205,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
191
205
|
| None
|
|
192
206
|
) = None,
|
|
193
207
|
tags: set[str] | None = None,
|
|
208
|
+
tool_serializer: Callable[[Any], str] | None = None,
|
|
194
209
|
**settings: Any,
|
|
195
210
|
):
|
|
196
211
|
self.tags: set[str] = tags or set()
|
|
@@ -204,7 +219,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
204
219
|
self._mounted_servers: dict[str, MountedServer] = {}
|
|
205
220
|
|
|
206
221
|
if lifespan is None:
|
|
222
|
+
self._has_lifespan = False
|
|
207
223
|
lifespan = default_lifespan
|
|
224
|
+
else:
|
|
225
|
+
self._has_lifespan = True
|
|
208
226
|
|
|
209
227
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
210
228
|
name=name or "FastMCP",
|
|
@@ -212,7 +230,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
212
230
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
213
231
|
)
|
|
214
232
|
self._tool_manager = ToolManager(
|
|
215
|
-
duplicate_behavior=self.settings.on_duplicate_tools
|
|
233
|
+
duplicate_behavior=self.settings.on_duplicate_tools,
|
|
234
|
+
serializer=tool_serializer,
|
|
216
235
|
)
|
|
217
236
|
self._resource_manager = ResourceManager(
|
|
218
237
|
duplicate_behavior=self.settings.on_duplicate_resources
|
|
@@ -220,6 +239,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
220
239
|
self._prompt_manager = PromptManager(
|
|
221
240
|
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
222
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] = []
|
|
223
251
|
self.dependencies = self.settings.dependencies
|
|
224
252
|
|
|
225
253
|
# Set up MCP protocol handlers
|
|
@@ -339,6 +367,50 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
339
367
|
self._cache.set("prompts", prompts)
|
|
340
368
|
return prompts
|
|
341
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
|
+
|
|
342
414
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
343
415
|
"""
|
|
344
416
|
List all available tools, in the format expected by the low-level MCP
|
|
@@ -435,7 +507,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
435
507
|
messages = await self._prompt_manager.render_prompt(
|
|
436
508
|
name, arguments=arguments or {}, context=context
|
|
437
509
|
)
|
|
438
|
-
|
|
510
|
+
|
|
511
|
+
return GetPromptResult(
|
|
512
|
+
messages=[
|
|
513
|
+
PromptMessage(role=m.role, content=m.content) for m in messages
|
|
514
|
+
]
|
|
515
|
+
)
|
|
439
516
|
else:
|
|
440
517
|
for server in self._mounted_servers.values():
|
|
441
518
|
if server.match_prompt(name):
|
|
@@ -450,6 +527,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
450
527
|
name: str | None = None,
|
|
451
528
|
description: str | None = None,
|
|
452
529
|
tags: set[str] | None = None,
|
|
530
|
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
453
531
|
) -> None:
|
|
454
532
|
"""Add a tool to the server.
|
|
455
533
|
|
|
@@ -461,9 +539,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
461
539
|
name: Optional name for the tool (defaults to function name)
|
|
462
540
|
description: Optional description of what the tool does
|
|
463
541
|
tags: Optional set of tags for categorizing the tool
|
|
542
|
+
annotations: Optional annotations about the tool's behavior
|
|
464
543
|
"""
|
|
544
|
+
if isinstance(annotations, dict):
|
|
545
|
+
annotations = ToolAnnotations(**annotations)
|
|
546
|
+
|
|
465
547
|
self._tool_manager.add_tool_from_fn(
|
|
466
|
-
fn,
|
|
548
|
+
fn,
|
|
549
|
+
name=name,
|
|
550
|
+
description=description,
|
|
551
|
+
tags=tags,
|
|
552
|
+
annotations=annotations,
|
|
467
553
|
)
|
|
468
554
|
self._cache.clear()
|
|
469
555
|
|
|
@@ -472,6 +558,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
472
558
|
name: str | None = None,
|
|
473
559
|
description: str | None = None,
|
|
474
560
|
tags: set[str] | None = None,
|
|
561
|
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
475
562
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
476
563
|
"""Decorator to register a tool.
|
|
477
564
|
|
|
@@ -483,6 +570,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
483
570
|
name: Optional name for the tool (defaults to function name)
|
|
484
571
|
description: Optional description of what the tool does
|
|
485
572
|
tags: Optional set of tags for categorizing the tool
|
|
573
|
+
annotations: Optional annotations about the tool's behavior
|
|
486
574
|
|
|
487
575
|
Example:
|
|
488
576
|
@server.tool()
|
|
@@ -508,7 +596,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
508
596
|
)
|
|
509
597
|
|
|
510
598
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
511
|
-
self.add_tool(
|
|
599
|
+
self.add_tool(
|
|
600
|
+
fn,
|
|
601
|
+
name=name,
|
|
602
|
+
description=description,
|
|
603
|
+
tags=tags,
|
|
604
|
+
annotations=annotations,
|
|
605
|
+
)
|
|
512
606
|
return fn
|
|
513
607
|
|
|
514
608
|
return decorator
|
|
@@ -734,10 +828,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
734
828
|
log_level: str | None = None,
|
|
735
829
|
) -> None:
|
|
736
830
|
"""Run the server using SSE transport."""
|
|
737
|
-
|
|
831
|
+
app = self.sse_app()
|
|
832
|
+
app = RequestMiddleware(app)
|
|
738
833
|
|
|
739
834
|
config = uvicorn.Config(
|
|
740
|
-
|
|
835
|
+
app,
|
|
741
836
|
host=host or self.settings.host,
|
|
742
837
|
port=port or self.settings.port,
|
|
743
838
|
log_level=log_level or self.settings.log_level.lower(),
|
|
@@ -747,26 +842,104 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
747
842
|
|
|
748
843
|
def sse_app(self) -> Starlette:
|
|
749
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
|
+
|
|
750
850
|
sse = SseServerTransport(self.settings.message_path)
|
|
751
851
|
|
|
752
|
-
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
|
+
|
|
753
855
|
async with sse.connect_sse(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
856
|
+
scope,
|
|
857
|
+
receive,
|
|
858
|
+
send,
|
|
757
859
|
) as streams:
|
|
758
860
|
await self._mcp_server.run(
|
|
759
861
|
streams[0],
|
|
760
862
|
streams[1],
|
|
761
863
|
self._mcp_server.create_initialization_options(),
|
|
762
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
|
+
)
|
|
763
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
|
|
764
941
|
return Starlette(
|
|
765
|
-
debug=self.settings.debug,
|
|
766
|
-
routes=[
|
|
767
|
-
Route(self.settings.sse_path, endpoint=handle_sse),
|
|
768
|
-
Mount(self.settings.message_path, app=sse.handle_post_message),
|
|
769
|
-
],
|
|
942
|
+
debug=self.settings.debug, routes=routes, middleware=middleware
|
|
770
943
|
)
|
|
771
944
|
|
|
772
945
|
def mount(
|
|
@@ -776,10 +949,62 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
776
949
|
tool_separator: str | None = None,
|
|
777
950
|
resource_separator: str | None = None,
|
|
778
951
|
prompt_separator: str | None = None,
|
|
952
|
+
as_proxy: bool | None = None,
|
|
779
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).
|
|
780
995
|
"""
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
|
|
783
1008
|
mounted_server = MountedServer(
|
|
784
1009
|
server=server,
|
|
785
1010
|
prefix=prefix,
|
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:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import (
|
|
4
|
+
asynccontextmanager,
|
|
5
|
+
)
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
from fastmcp.utilities.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_current_starlette_request: ContextVar[Request | None] = ContextVar(
|
|
16
|
+
"starlette_request",
|
|
17
|
+
default=None,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@asynccontextmanager
|
|
22
|
+
async def starlette_request_context(request: Request):
|
|
23
|
+
token = _current_starlette_request.set(request)
|
|
24
|
+
try:
|
|
25
|
+
yield
|
|
26
|
+
finally:
|
|
27
|
+
_current_starlette_request.reset(token)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_current_starlette_request() -> Request | None:
|
|
31
|
+
return _current_starlette_request.get()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RequestMiddleware:
|
|
35
|
+
"""
|
|
36
|
+
Middleware that stores each request in a ContextVar
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, app):
|
|
40
|
+
self.app = app
|
|
41
|
+
|
|
42
|
+
async def __call__(self, scope, receive, send):
|
|
43
|
+
async with starlette_request_context(Request(scope)):
|
|
44
|
+
await self.app(scope, receive, send)
|