fastmcp 2.3.5__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/client/client.py +44 -6
- fastmcp/client/logging.py +14 -8
- fastmcp/client/transports.py +202 -57
- fastmcp/prompts/prompt.py +11 -4
- fastmcp/prompts/prompt_manager.py +25 -5
- fastmcp/resources/resource_manager.py +31 -5
- fastmcp/resources/template.py +10 -5
- fastmcp/server/context.py +46 -0
- fastmcp/server/http.py +25 -1
- fastmcp/server/openapi.py +436 -73
- fastmcp/server/server.py +412 -127
- fastmcp/settings.py +46 -1
- fastmcp/tools/tool.py +5 -1
- fastmcp/tools/tool_manager.py +9 -2
- fastmcp/utilities/logging.py +6 -1
- fastmcp/utilities/mcp_config.py +77 -0
- fastmcp/utilities/openapi.py +233 -602
- fastmcp/utilities/tests.py +8 -4
- {fastmcp-2.3.5.dist-info → fastmcp-2.5.0.dist-info}/METADATA +27 -4
- {fastmcp-2.3.5.dist-info → fastmcp-2.5.0.dist-info}/RECORD +23 -22
- {fastmcp-2.3.5.dist-info → fastmcp-2.5.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.5.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.5.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
+
import re
|
|
6
7
|
import warnings
|
|
7
8
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
9
|
from contextlib import (
|
|
@@ -16,7 +17,6 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
|
16
17
|
|
|
17
18
|
import anyio
|
|
18
19
|
import httpx
|
|
19
|
-
import pydantic
|
|
20
20
|
import uvicorn
|
|
21
21
|
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
|
22
22
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
@@ -58,16 +58,22 @@ from fastmcp.tools.tool import Tool
|
|
|
58
58
|
from fastmcp.utilities.cache import TimedCache
|
|
59
59
|
from fastmcp.utilities.decorators import DecoratedFunction
|
|
60
60
|
from fastmcp.utilities.logging import get_logger
|
|
61
|
+
from fastmcp.utilities.mcp_config import MCPConfig
|
|
61
62
|
|
|
62
63
|
if TYPE_CHECKING:
|
|
63
64
|
from fastmcp.client import Client
|
|
64
65
|
from fastmcp.client.transports import ClientTransport
|
|
65
|
-
from fastmcp.server.openapi import
|
|
66
|
+
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
67
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
68
|
+
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
66
69
|
from fastmcp.server.proxy import FastMCPProxy
|
|
67
70
|
logger = get_logger(__name__)
|
|
68
71
|
|
|
69
72
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
70
73
|
|
|
74
|
+
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
75
|
+
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
76
|
+
|
|
71
77
|
|
|
72
78
|
@asynccontextmanager
|
|
73
79
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -120,6 +126,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
120
126
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
121
127
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
122
128
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
129
|
+
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
130
|
+
mask_error_details: bool | None = None,
|
|
123
131
|
**settings: Any,
|
|
124
132
|
):
|
|
125
133
|
if settings:
|
|
@@ -134,6 +142,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
134
142
|
)
|
|
135
143
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
136
144
|
|
|
145
|
+
# If mask_error_details is provided, override the settings value
|
|
146
|
+
if mask_error_details is not None:
|
|
147
|
+
self.settings.mask_error_details = mask_error_details
|
|
148
|
+
|
|
149
|
+
self.resource_prefix_format: Literal["protocol", "path"]
|
|
150
|
+
if resource_prefix_format is None:
|
|
151
|
+
self.resource_prefix_format = (
|
|
152
|
+
fastmcp.settings.settings.resource_prefix_format
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
self.resource_prefix_format = resource_prefix_format
|
|
156
|
+
|
|
137
157
|
self.tags: set[str] = tags or set()
|
|
138
158
|
self.dependencies = dependencies
|
|
139
159
|
self._cache = TimedCache(
|
|
@@ -144,11 +164,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
144
164
|
self._tool_manager = ToolManager(
|
|
145
165
|
duplicate_behavior=on_duplicate_tools,
|
|
146
166
|
serializer=tool_serializer,
|
|
167
|
+
mask_error_details=self.settings.mask_error_details,
|
|
147
168
|
)
|
|
148
169
|
self._resource_manager = ResourceManager(
|
|
149
|
-
duplicate_behavior=on_duplicate_resources
|
|
170
|
+
duplicate_behavior=on_duplicate_resources,
|
|
171
|
+
mask_error_details=self.settings.mask_error_details,
|
|
172
|
+
)
|
|
173
|
+
self._prompt_manager = PromptManager(
|
|
174
|
+
duplicate_behavior=on_duplicate_prompts,
|
|
175
|
+
mask_error_details=self.settings.mask_error_details,
|
|
150
176
|
)
|
|
151
|
-
self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
|
|
152
177
|
|
|
153
178
|
if lifespan is None:
|
|
154
179
|
self._has_lifespan = False
|
|
@@ -364,21 +389,30 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
364
389
|
async def _mcp_call_tool(
|
|
365
390
|
self, key: str, arguments: dict[str, Any]
|
|
366
391
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
367
|
-
"""
|
|
392
|
+
"""Handle MCP 'callTool' requests.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
key: The name of the tool to call
|
|
396
|
+
arguments: Arguments to pass to the tool
|
|
368
397
|
|
|
398
|
+
Returns:
|
|
399
|
+
List of MCP Content objects containing the tool results
|
|
400
|
+
"""
|
|
401
|
+
logger.debug("Call tool: %s with %s", key, arguments)
|
|
402
|
+
|
|
403
|
+
# Create and use context for the entire call
|
|
369
404
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
405
|
+
# Get tool, checking first from our tools, then from the mounted servers
|
|
370
406
|
if self._tool_manager.has_tool(key):
|
|
371
|
-
|
|
407
|
+
return await self._tool_manager.call_tool(key, arguments)
|
|
372
408
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
raise NotFoundError(f"Unknown tool: {key}")
|
|
381
|
-
return result
|
|
409
|
+
# Check mounted servers to see if they have the tool
|
|
410
|
+
for server in self._mounted_servers.values():
|
|
411
|
+
if server.match_tool(key):
|
|
412
|
+
tool_key = server.strip_tool_prefix(key)
|
|
413
|
+
return await server.server._mcp_call_tool(tool_key, arguments)
|
|
414
|
+
|
|
415
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
382
416
|
|
|
383
417
|
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
384
418
|
"""
|
|
@@ -406,24 +440,30 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
406
440
|
async def _mcp_get_prompt(
|
|
407
441
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
408
442
|
) -> GetPromptResult:
|
|
409
|
-
"""
|
|
410
|
-
|
|
411
|
-
|
|
443
|
+
"""Handle MCP 'getPrompt' requests.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
name: The name of the prompt to render
|
|
447
|
+
arguments: Arguments to pass to the prompt
|
|
412
448
|
|
|
449
|
+
Returns:
|
|
450
|
+
GetPromptResult containing the rendered prompt messages
|
|
413
451
|
"""
|
|
452
|
+
logger.debug("Get prompt: %s with %s", name, arguments)
|
|
453
|
+
|
|
454
|
+
# Create and use context for the entire call
|
|
414
455
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
456
|
+
# Get prompt, checking first from our prompts, then from the mounted servers
|
|
415
457
|
if self._prompt_manager.has_prompt(name):
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
else:
|
|
426
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
458
|
+
return await self._prompt_manager.render_prompt(name, arguments)
|
|
459
|
+
|
|
460
|
+
# Check mounted servers to see if they have the prompt
|
|
461
|
+
for server in self._mounted_servers.values():
|
|
462
|
+
if server.match_prompt(name):
|
|
463
|
+
prompt_name = server.strip_prompt_prefix(name)
|
|
464
|
+
return await server.server._mcp_get_prompt(prompt_name, arguments)
|
|
465
|
+
|
|
466
|
+
raise NotFoundError(f"Unknown prompt: {name}")
|
|
427
467
|
|
|
428
468
|
def add_tool(
|
|
429
469
|
self,
|
|
@@ -841,7 +881,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
841
881
|
auth_server_provider=self._auth_server_provider,
|
|
842
882
|
auth_settings=self.settings.auth,
|
|
843
883
|
debug=self.settings.debug,
|
|
844
|
-
routes=self._additional_http_routes,
|
|
845
884
|
middleware=middleware,
|
|
846
885
|
)
|
|
847
886
|
|
|
@@ -892,7 +931,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
892
931
|
json_response=self.settings.json_response,
|
|
893
932
|
stateless_http=self.settings.stateless_http,
|
|
894
933
|
debug=self.settings.debug,
|
|
895
|
-
routes=self._additional_http_routes,
|
|
896
934
|
middleware=middleware,
|
|
897
935
|
)
|
|
898
936
|
elif transport == "sse":
|
|
@@ -903,7 +941,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
903
941
|
auth_server_provider=self._auth_server_provider,
|
|
904
942
|
auth_settings=self.settings.auth,
|
|
905
943
|
debug=self.settings.debug,
|
|
906
|
-
routes=self._additional_http_routes,
|
|
907
944
|
middleware=middleware,
|
|
908
945
|
)
|
|
909
946
|
|
|
@@ -935,10 +972,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
935
972
|
self,
|
|
936
973
|
prefix: str,
|
|
937
974
|
server: FastMCP[LifespanResultT],
|
|
975
|
+
as_proxy: bool | None = None,
|
|
976
|
+
*,
|
|
938
977
|
tool_separator: str | None = None,
|
|
939
978
|
resource_separator: str | None = None,
|
|
940
979
|
prompt_separator: str | None = None,
|
|
941
|
-
as_proxy: bool | None = None,
|
|
942
980
|
) -> None:
|
|
943
981
|
"""Mount another FastMCP server on this server with the given prefix.
|
|
944
982
|
|
|
@@ -949,15 +987,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
987
|
through the parent.
|
|
950
988
|
|
|
951
989
|
When a server is mounted:
|
|
952
|
-
- Tools from the mounted server are accessible with prefixed names
|
|
990
|
+
- Tools from the mounted server are accessible with prefixed names.
|
|
953
991
|
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
|
954
|
-
- Resources are accessible with prefixed URIs
|
|
992
|
+
- Resources are accessible with prefixed URIs.
|
|
955
993
|
Example: If server has a resource with URI "weather://forecast", it will be available as
|
|
956
|
-
"
|
|
957
|
-
- Templates are accessible with prefixed URI templates
|
|
994
|
+
"weather://prefix/forecast".
|
|
995
|
+
- Templates are accessible with prefixed URI templates.
|
|
958
996
|
Example: If server has a template with URI "weather://location/{id}", it will be available
|
|
959
|
-
as "
|
|
960
|
-
- Prompts are accessible with prefixed names
|
|
997
|
+
as "weather://prefix/location/{id}".
|
|
998
|
+
- Prompts are accessible with prefixed names.
|
|
961
999
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
962
1000
|
"prefix_weather_prompt".
|
|
963
1001
|
|
|
@@ -975,17 +1013,44 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
975
1013
|
Args:
|
|
976
1014
|
prefix: Prefix to use for the mounted server's objects.
|
|
977
1015
|
server: The FastMCP server to mount.
|
|
978
|
-
tool_separator: Separator character for tool names (defaults to "_").
|
|
979
|
-
resource_separator: Separator character for resource URIs (defaults to "+").
|
|
980
|
-
prompt_separator: Separator character for prompt names (defaults to "_").
|
|
981
1016
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
982
1017
|
automatically determined based on whether the server has a custom lifespan
|
|
983
1018
|
(True if it has a custom lifespan, False otherwise).
|
|
1019
|
+
tool_separator: Deprecated. Separator character for tool names.
|
|
1020
|
+
resource_separator: Deprecated. Separator character for resource URIs.
|
|
1021
|
+
prompt_separator: Deprecated. Separator character for prompt names.
|
|
984
1022
|
"""
|
|
985
1023
|
from fastmcp import Client
|
|
986
1024
|
from fastmcp.client.transports import FastMCPTransport
|
|
987
1025
|
from fastmcp.server.proxy import FastMCPProxy
|
|
988
1026
|
|
|
1027
|
+
if tool_separator is not None:
|
|
1028
|
+
# Deprecated since 2.4.0
|
|
1029
|
+
warnings.warn(
|
|
1030
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1031
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1032
|
+
DeprecationWarning,
|
|
1033
|
+
stacklevel=2,
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
if resource_separator is not None:
|
|
1037
|
+
# Deprecated since 2.4.0
|
|
1038
|
+
warnings.warn(
|
|
1039
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1040
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1041
|
+
DeprecationWarning,
|
|
1042
|
+
stacklevel=2,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
if prompt_separator is not None:
|
|
1046
|
+
# Deprecated since 2.4.0
|
|
1047
|
+
warnings.warn(
|
|
1048
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1049
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1050
|
+
DeprecationWarning,
|
|
1051
|
+
stacklevel=2,
|
|
1052
|
+
)
|
|
1053
|
+
|
|
989
1054
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
990
1055
|
# we should treat it as a proxy
|
|
991
1056
|
if as_proxy is None:
|
|
@@ -997,9 +1062,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
997
1062
|
mounted_server = MountedServer(
|
|
998
1063
|
server=server,
|
|
999
1064
|
prefix=prefix,
|
|
1000
|
-
tool_separator=tool_separator,
|
|
1001
|
-
resource_separator=resource_separator,
|
|
1002
|
-
prompt_separator=prompt_separator,
|
|
1003
1065
|
)
|
|
1004
1066
|
self._mounted_servers[prefix] = mounted_server
|
|
1005
1067
|
self._cache.clear()
|
|
@@ -1025,90 +1087,177 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1025
1087
|
future changes to the imported server will not be reflected in the
|
|
1026
1088
|
importing server. Server-level configurations and lifespans are not imported.
|
|
1027
1089
|
|
|
1028
|
-
When a server is
|
|
1029
|
-
|
|
1090
|
+
When a server is imported:
|
|
1091
|
+
- The tools are imported with prefixed names
|
|
1030
1092
|
Example: If server has a tool named "get_weather", it will be
|
|
1031
|
-
available as "
|
|
1032
|
-
- The resources are imported with prefixed URIs using the
|
|
1033
|
-
|
|
1034
|
-
"weather://forecast"
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
"
|
|
1040
|
-
|
|
1041
|
-
prompt_separator Example: If server has a prompt named
|
|
1042
|
-
"weather_prompt", it will be available as "weather_weather_prompt"
|
|
1093
|
+
available as "prefix_get_weather"
|
|
1094
|
+
- The resources are imported with prefixed URIs using the new format
|
|
1095
|
+
Example: If server has a resource with URI "weather://forecast", it will
|
|
1096
|
+
be available as "weather://prefix/forecast"
|
|
1097
|
+
- The templates are imported with prefixed URI templates using the new format
|
|
1098
|
+
Example: If server has a template with URI "weather://location/{id}", it will
|
|
1099
|
+
be available as "weather://prefix/location/{id}"
|
|
1100
|
+
- The prompts are imported with prefixed names
|
|
1101
|
+
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1102
|
+
"prefix_weather_prompt"
|
|
1043
1103
|
|
|
1044
1104
|
Args:
|
|
1045
|
-
prefix: The prefix to use for the
|
|
1046
|
-
server
|
|
1047
|
-
|
|
1048
|
-
|
|
1105
|
+
prefix: The prefix to use for the imported server
|
|
1106
|
+
server: The FastMCP server to import
|
|
1107
|
+
tool_separator: Deprecated. Separator for tool names.
|
|
1108
|
+
resource_separator: Deprecated and ignored. Prefix is now
|
|
1109
|
+
applied using the protocol://prefix/path format
|
|
1110
|
+
prompt_separator: Deprecated. Separator for prompt names.
|
|
1049
1111
|
"""
|
|
1050
|
-
if tool_separator is None:
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1112
|
+
if tool_separator is not None:
|
|
1113
|
+
# Deprecated since 2.4.0
|
|
1114
|
+
warnings.warn(
|
|
1115
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1116
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1117
|
+
DeprecationWarning,
|
|
1118
|
+
stacklevel=2,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
if resource_separator is not None:
|
|
1122
|
+
# Deprecated since 2.4.0
|
|
1123
|
+
warnings.warn(
|
|
1124
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1125
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1126
|
+
DeprecationWarning,
|
|
1127
|
+
stacklevel=2,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
if prompt_separator is not None:
|
|
1131
|
+
# Deprecated since 2.4.0
|
|
1132
|
+
warnings.warn(
|
|
1133
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1134
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1135
|
+
DeprecationWarning,
|
|
1136
|
+
stacklevel=2,
|
|
1137
|
+
)
|
|
1056
1138
|
|
|
1057
1139
|
# Import tools from the mounted server
|
|
1058
|
-
tool_prefix = f"{prefix}
|
|
1140
|
+
tool_prefix = f"{prefix}_"
|
|
1059
1141
|
for key, tool in (await server.get_tools()).items():
|
|
1060
1142
|
self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
|
|
1061
1143
|
|
|
1062
1144
|
# Import resources and templates from the mounted server
|
|
1063
|
-
resource_prefix = f"{prefix}{resource_separator}"
|
|
1064
|
-
_validate_resource_prefix(resource_prefix)
|
|
1065
1145
|
for key, resource in (await server.get_resources()).items():
|
|
1066
|
-
|
|
1146
|
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
|
1147
|
+
self._resource_manager.add_resource(resource, key=prefixed_key)
|
|
1148
|
+
|
|
1067
1149
|
for key, template in (await server.get_resource_templates()).items():
|
|
1068
|
-
|
|
1150
|
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
|
1151
|
+
self._resource_manager.add_template(template, key=prefixed_key)
|
|
1069
1152
|
|
|
1070
1153
|
# Import prompts from the mounted server
|
|
1071
|
-
prompt_prefix = f"{prefix}
|
|
1154
|
+
prompt_prefix = f"{prefix}_"
|
|
1072
1155
|
for key, prompt in (await server.get_prompts()).items():
|
|
1073
1156
|
self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
|
|
1074
1157
|
|
|
1075
1158
|
logger.info(f"Imported server {server.name} with prefix '{prefix}'")
|
|
1076
1159
|
logger.debug(f"Imported tools with prefix '{tool_prefix}'")
|
|
1077
|
-
logger.debug(f"Imported resources with prefix '{
|
|
1078
|
-
logger.debug(f"Imported templates with prefix '{resource_prefix}'")
|
|
1160
|
+
logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
|
|
1079
1161
|
logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
|
|
1080
1162
|
|
|
1081
1163
|
self._cache.clear()
|
|
1082
1164
|
|
|
1083
1165
|
@classmethod
|
|
1084
1166
|
def from_openapi(
|
|
1085
|
-
cls,
|
|
1167
|
+
cls,
|
|
1168
|
+
openapi_spec: dict[str, Any],
|
|
1169
|
+
client: httpx.AsyncClient,
|
|
1170
|
+
route_maps: list[RouteMap] | None = None,
|
|
1171
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
1172
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
1173
|
+
mcp_names: dict[str, str] | None = None,
|
|
1174
|
+
all_routes_as_tools: bool = False,
|
|
1175
|
+
**settings: Any,
|
|
1086
1176
|
) -> FastMCPOpenAPI:
|
|
1087
1177
|
"""
|
|
1088
1178
|
Create a FastMCP server from an OpenAPI specification.
|
|
1089
1179
|
"""
|
|
1090
|
-
from .openapi import FastMCPOpenAPI
|
|
1180
|
+
from .openapi import FastMCPOpenAPI, MCPType, RouteMap
|
|
1181
|
+
|
|
1182
|
+
# Deprecated since 2.5.0
|
|
1183
|
+
if all_routes_as_tools:
|
|
1184
|
+
warnings.warn(
|
|
1185
|
+
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
|
1186
|
+
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
|
1187
|
+
DeprecationWarning,
|
|
1188
|
+
stacklevel=2,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
if all_routes_as_tools and route_maps:
|
|
1192
|
+
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1091
1193
|
|
|
1092
|
-
|
|
1194
|
+
elif all_routes_as_tools:
|
|
1195
|
+
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
|
1196
|
+
|
|
1197
|
+
return FastMCPOpenAPI(
|
|
1198
|
+
openapi_spec=openapi_spec,
|
|
1199
|
+
client=client,
|
|
1200
|
+
route_maps=route_maps,
|
|
1201
|
+
route_map_fn=route_map_fn,
|
|
1202
|
+
mcp_component_fn=mcp_component_fn,
|
|
1203
|
+
mcp_names=mcp_names,
|
|
1204
|
+
**settings,
|
|
1205
|
+
)
|
|
1093
1206
|
|
|
1094
1207
|
@classmethod
|
|
1095
1208
|
def from_fastapi(
|
|
1096
|
-
cls,
|
|
1209
|
+
cls,
|
|
1210
|
+
app: Any,
|
|
1211
|
+
name: str | None = None,
|
|
1212
|
+
route_maps: list[RouteMap] | None = None,
|
|
1213
|
+
route_map_fn: OpenAPIRouteMapFn | None = None,
|
|
1214
|
+
mcp_component_fn: OpenAPIComponentFn | None = None,
|
|
1215
|
+
mcp_names: dict[str, str] | None = None,
|
|
1216
|
+
all_routes_as_tools: bool = False,
|
|
1217
|
+
httpx_client_kwargs: dict[str, Any] | None = None,
|
|
1218
|
+
**settings: Any,
|
|
1097
1219
|
) -> FastMCPOpenAPI:
|
|
1098
1220
|
"""
|
|
1099
1221
|
Create a FastMCP server from a FastAPI application.
|
|
1100
1222
|
"""
|
|
1101
1223
|
|
|
1102
|
-
from .openapi import FastMCPOpenAPI
|
|
1224
|
+
from .openapi import FastMCPOpenAPI, MCPType, RouteMap
|
|
1225
|
+
|
|
1226
|
+
# Deprecated since 2.5.0
|
|
1227
|
+
if all_routes_as_tools:
|
|
1228
|
+
warnings.warn(
|
|
1229
|
+
"The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
|
|
1230
|
+
'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
|
|
1231
|
+
DeprecationWarning,
|
|
1232
|
+
stacklevel=2,
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
if all_routes_as_tools and route_maps:
|
|
1236
|
+
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1237
|
+
|
|
1238
|
+
elif all_routes_as_tools:
|
|
1239
|
+
route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
|
|
1240
|
+
|
|
1241
|
+
if httpx_client_kwargs is None:
|
|
1242
|
+
httpx_client_kwargs = {}
|
|
1243
|
+
httpx_client_kwargs.setdefault("base_url", "http://fastapi")
|
|
1103
1244
|
|
|
1104
1245
|
client = httpx.AsyncClient(
|
|
1105
|
-
transport=httpx.ASGITransport(app=app),
|
|
1246
|
+
transport=httpx.ASGITransport(app=app),
|
|
1247
|
+
**httpx_client_kwargs,
|
|
1106
1248
|
)
|
|
1107
1249
|
|
|
1108
1250
|
name = name or app.title
|
|
1109
1251
|
|
|
1110
1252
|
return FastMCPOpenAPI(
|
|
1111
|
-
openapi_spec=app.openapi(),
|
|
1253
|
+
openapi_spec=app.openapi(),
|
|
1254
|
+
client=client,
|
|
1255
|
+
name=name,
|
|
1256
|
+
route_maps=route_maps,
|
|
1257
|
+
route_map_fn=route_map_fn,
|
|
1258
|
+
mcp_component_fn=mcp_component_fn,
|
|
1259
|
+
mcp_names=mcp_names,
|
|
1260
|
+
**settings,
|
|
1112
1261
|
)
|
|
1113
1262
|
|
|
1114
1263
|
@classmethod
|
|
@@ -1119,6 +1268,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1119
1268
|
| FastMCP[Any]
|
|
1120
1269
|
| AnyUrl
|
|
1121
1270
|
| Path
|
|
1271
|
+
| MCPConfig
|
|
1122
1272
|
| dict[str, Any]
|
|
1123
1273
|
| str,
|
|
1124
1274
|
**settings: Any,
|
|
@@ -1155,84 +1305,219 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1155
1305
|
return cls.as_proxy(client, **settings)
|
|
1156
1306
|
|
|
1157
1307
|
|
|
1158
|
-
def _validate_resource_prefix(prefix: str) -> None:
|
|
1159
|
-
valid_resource = "resource://path/to/resource"
|
|
1160
|
-
test_case = f"{prefix}{valid_resource}"
|
|
1161
|
-
try:
|
|
1162
|
-
AnyUrl(test_case)
|
|
1163
|
-
except pydantic.ValidationError as e:
|
|
1164
|
-
raise ValueError(
|
|
1165
|
-
"Resource prefix or separator would result in an "
|
|
1166
|
-
f"invalid resource URI (test case was {test_case!r}): {e}"
|
|
1167
|
-
)
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
1308
|
class MountedServer:
|
|
1171
1309
|
def __init__(
|
|
1172
1310
|
self,
|
|
1173
1311
|
prefix: str,
|
|
1174
1312
|
server: FastMCP[LifespanResultT],
|
|
1175
|
-
tool_separator: str | None = None,
|
|
1176
|
-
resource_separator: str | None = None,
|
|
1177
|
-
prompt_separator: str | None = None,
|
|
1178
1313
|
):
|
|
1179
|
-
if tool_separator is None:
|
|
1180
|
-
tool_separator = "_"
|
|
1181
|
-
if resource_separator is None:
|
|
1182
|
-
resource_separator = "+"
|
|
1183
|
-
if prompt_separator is None:
|
|
1184
|
-
prompt_separator = "_"
|
|
1185
|
-
|
|
1186
|
-
_validate_resource_prefix(f"{prefix}{resource_separator}")
|
|
1187
|
-
|
|
1188
1314
|
self.server = server
|
|
1189
1315
|
self.prefix = prefix
|
|
1190
|
-
self.tool_separator = tool_separator
|
|
1191
|
-
self.resource_separator = resource_separator
|
|
1192
|
-
self.prompt_separator = prompt_separator
|
|
1193
1316
|
|
|
1194
1317
|
async def get_tools(self) -> dict[str, Tool]:
|
|
1195
1318
|
tools = await self.server.get_tools()
|
|
1196
|
-
return {
|
|
1197
|
-
f"{self.prefix}{self.tool_separator}{key}": tool
|
|
1198
|
-
for key, tool in tools.items()
|
|
1199
|
-
}
|
|
1319
|
+
return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
|
|
1200
1320
|
|
|
1201
1321
|
async def get_resources(self) -> dict[str, Resource]:
|
|
1202
1322
|
resources = await self.server.get_resources()
|
|
1203
1323
|
return {
|
|
1204
|
-
|
|
1324
|
+
add_resource_prefix(
|
|
1325
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1326
|
+
): resource
|
|
1205
1327
|
for key, resource in resources.items()
|
|
1206
1328
|
}
|
|
1207
1329
|
|
|
1208
1330
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
1209
1331
|
templates = await self.server.get_resource_templates()
|
|
1210
1332
|
return {
|
|
1211
|
-
|
|
1333
|
+
add_resource_prefix(
|
|
1334
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1335
|
+
): template
|
|
1212
1336
|
for key, template in templates.items()
|
|
1213
1337
|
}
|
|
1214
1338
|
|
|
1215
1339
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
1216
1340
|
prompts = await self.server.get_prompts()
|
|
1217
|
-
return {
|
|
1218
|
-
f"{self.prefix}{self.prompt_separator}{key}": prompt
|
|
1219
|
-
for key, prompt in prompts.items()
|
|
1220
|
-
}
|
|
1341
|
+
return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
|
|
1221
1342
|
|
|
1222
1343
|
def match_tool(self, key: str) -> bool:
|
|
1223
|
-
return key.startswith(f"{self.prefix}
|
|
1344
|
+
return key.startswith(f"{self.prefix}_")
|
|
1224
1345
|
|
|
1225
1346
|
def strip_tool_prefix(self, key: str) -> str:
|
|
1226
|
-
return key.removeprefix(f"{self.prefix}
|
|
1347
|
+
return key.removeprefix(f"{self.prefix}_")
|
|
1227
1348
|
|
|
1228
1349
|
def match_resource(self, key: str) -> bool:
|
|
1229
|
-
return key
|
|
1350
|
+
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
|
1230
1351
|
|
|
1231
1352
|
def strip_resource_prefix(self, key: str) -> str:
|
|
1232
|
-
return
|
|
1353
|
+
return remove_resource_prefix(
|
|
1354
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1355
|
+
)
|
|
1233
1356
|
|
|
1234
1357
|
def match_prompt(self, key: str) -> bool:
|
|
1235
|
-
return key.startswith(f"{self.prefix}
|
|
1358
|
+
return key.startswith(f"{self.prefix}_")
|
|
1236
1359
|
|
|
1237
1360
|
def strip_prompt_prefix(self, key: str) -> str:
|
|
1238
|
-
return key.removeprefix(f"{self.prefix}
|
|
1361
|
+
return key.removeprefix(f"{self.prefix}_")
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def add_resource_prefix(
|
|
1365
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1366
|
+
) -> str:
|
|
1367
|
+
"""Add a prefix to a resource URI.
|
|
1368
|
+
|
|
1369
|
+
Args:
|
|
1370
|
+
uri: The original resource URI
|
|
1371
|
+
prefix: The prefix to add
|
|
1372
|
+
|
|
1373
|
+
Returns:
|
|
1374
|
+
The resource URI with the prefix added
|
|
1375
|
+
|
|
1376
|
+
Examples:
|
|
1377
|
+
>>> add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1378
|
+
"resource://prefix/path/to/resource" # with new style
|
|
1379
|
+
>>> add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1380
|
+
"prefix+resource://path/to/resource" # with legacy style
|
|
1381
|
+
>>> add_resource_prefix("resource:///absolute/path", "prefix")
|
|
1382
|
+
"resource://prefix//absolute/path" # with new style
|
|
1383
|
+
|
|
1384
|
+
Raises:
|
|
1385
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1386
|
+
"""
|
|
1387
|
+
if not prefix:
|
|
1388
|
+
return uri
|
|
1389
|
+
|
|
1390
|
+
# Get the server settings to check for legacy format preference
|
|
1391
|
+
|
|
1392
|
+
if prefix_format is None:
|
|
1393
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1394
|
+
|
|
1395
|
+
if prefix_format == "protocol":
|
|
1396
|
+
# Legacy style: prefix+protocol://path
|
|
1397
|
+
return f"{prefix}+{uri}"
|
|
1398
|
+
elif prefix_format == "path":
|
|
1399
|
+
# New style: protocol://prefix/path
|
|
1400
|
+
# Split the URI into protocol and path
|
|
1401
|
+
match = URI_PATTERN.match(uri)
|
|
1402
|
+
if not match:
|
|
1403
|
+
raise ValueError(
|
|
1404
|
+
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
protocol, path = match.groups()
|
|
1408
|
+
|
|
1409
|
+
# Add the prefix to the path
|
|
1410
|
+
return f"{protocol}{prefix}/{path}"
|
|
1411
|
+
else:
|
|
1412
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def remove_resource_prefix(
|
|
1416
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1417
|
+
) -> str:
|
|
1418
|
+
"""Remove a prefix from a resource URI.
|
|
1419
|
+
|
|
1420
|
+
Args:
|
|
1421
|
+
uri: The resource URI with a prefix
|
|
1422
|
+
prefix: The prefix to remove
|
|
1423
|
+
prefix_format: The format of the prefix to remove
|
|
1424
|
+
Returns:
|
|
1425
|
+
The resource URI with the prefix removed
|
|
1426
|
+
|
|
1427
|
+
Examples:
|
|
1428
|
+
>>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
1429
|
+
"resource://path/to/resource" # with new style
|
|
1430
|
+
>>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
1431
|
+
"resource://path/to/resource" # with legacy style
|
|
1432
|
+
>>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
1433
|
+
"resource:///absolute/path" # with new style
|
|
1434
|
+
|
|
1435
|
+
Raises:
|
|
1436
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1437
|
+
"""
|
|
1438
|
+
if not prefix:
|
|
1439
|
+
return uri
|
|
1440
|
+
|
|
1441
|
+
if prefix_format is None:
|
|
1442
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1443
|
+
|
|
1444
|
+
if prefix_format == "protocol":
|
|
1445
|
+
# Legacy style: prefix+protocol://path
|
|
1446
|
+
legacy_prefix = f"{prefix}+"
|
|
1447
|
+
if uri.startswith(legacy_prefix):
|
|
1448
|
+
return uri[len(legacy_prefix) :]
|
|
1449
|
+
return uri
|
|
1450
|
+
elif prefix_format == "path":
|
|
1451
|
+
# New style: protocol://prefix/path
|
|
1452
|
+
# Split the URI into protocol and path
|
|
1453
|
+
match = URI_PATTERN.match(uri)
|
|
1454
|
+
if not match:
|
|
1455
|
+
raise ValueError(
|
|
1456
|
+
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
protocol, path = match.groups()
|
|
1460
|
+
|
|
1461
|
+
# Check if the path starts with the prefix followed by a /
|
|
1462
|
+
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
1463
|
+
path_match = re.match(prefix_pattern, path)
|
|
1464
|
+
if not path_match:
|
|
1465
|
+
return uri
|
|
1466
|
+
|
|
1467
|
+
# Return the URI without the prefix
|
|
1468
|
+
return f"{protocol}{path_match.group(1)}"
|
|
1469
|
+
else:
|
|
1470
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def has_resource_prefix(
|
|
1474
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1475
|
+
) -> bool:
|
|
1476
|
+
"""Check if a resource URI has a specific prefix.
|
|
1477
|
+
|
|
1478
|
+
Args:
|
|
1479
|
+
uri: The resource URI to check
|
|
1480
|
+
prefix: The prefix to look for
|
|
1481
|
+
|
|
1482
|
+
Returns:
|
|
1483
|
+
True if the URI has the specified prefix, False otherwise
|
|
1484
|
+
|
|
1485
|
+
Examples:
|
|
1486
|
+
>>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
1487
|
+
True # with new style
|
|
1488
|
+
>>> has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
1489
|
+
True # with legacy style
|
|
1490
|
+
>>> has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
1491
|
+
False
|
|
1492
|
+
|
|
1493
|
+
Raises:
|
|
1494
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1495
|
+
"""
|
|
1496
|
+
if not prefix:
|
|
1497
|
+
return False
|
|
1498
|
+
|
|
1499
|
+
# Get the server settings to check for legacy format preference
|
|
1500
|
+
|
|
1501
|
+
if prefix_format is None:
|
|
1502
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1503
|
+
|
|
1504
|
+
if prefix_format == "protocol":
|
|
1505
|
+
# Legacy style: prefix+protocol://path
|
|
1506
|
+
legacy_prefix = f"{prefix}+"
|
|
1507
|
+
return uri.startswith(legacy_prefix)
|
|
1508
|
+
elif prefix_format == "path":
|
|
1509
|
+
# New style: protocol://prefix/path
|
|
1510
|
+
# Split the URI into protocol and path
|
|
1511
|
+
match = URI_PATTERN.match(uri)
|
|
1512
|
+
if not match:
|
|
1513
|
+
raise ValueError(
|
|
1514
|
+
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1517
|
+
_, path = match.groups()
|
|
1518
|
+
|
|
1519
|
+
# Check if the path starts with the prefix followed by a /
|
|
1520
|
+
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
1521
|
+
return bool(re.match(prefix_pattern, path))
|
|
1522
|
+
else:
|
|
1523
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|