fastmcp 2.3.5__py3-none-any.whl → 2.4.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 +21 -5
- fastmcp/client/logging.py +14 -8
- fastmcp/client/transports.py +137 -42
- fastmcp/server/http.py +23 -1
- fastmcp/server/openapi.py +113 -10
- fastmcp/server/server.py +329 -96
- fastmcp/settings.py +16 -0
- fastmcp/utilities/mcp_config.py +76 -0
- fastmcp/utilities/openapi.py +233 -602
- fastmcp/utilities/tests.py +8 -4
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/METADATA +24 -1
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/RECORD +15 -14
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.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,20 @@ 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 FastMCPOpenAPI
|
|
66
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
66
67
|
from fastmcp.server.proxy import FastMCPProxy
|
|
67
68
|
logger = get_logger(__name__)
|
|
68
69
|
|
|
69
70
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
70
71
|
|
|
72
|
+
# Compiled URI parsing regex to split a URI into protocol and path components
|
|
73
|
+
URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
74
|
+
|
|
71
75
|
|
|
72
76
|
@asynccontextmanager
|
|
73
77
|
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
@@ -120,6 +124,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
120
124
|
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
121
125
|
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
122
126
|
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
127
|
+
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
|
123
128
|
**settings: Any,
|
|
124
129
|
):
|
|
125
130
|
if settings:
|
|
@@ -134,6 +139,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
134
139
|
)
|
|
135
140
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
136
141
|
|
|
142
|
+
self.resource_prefix_format: Literal["protocol", "path"]
|
|
143
|
+
if resource_prefix_format is None:
|
|
144
|
+
self.resource_prefix_format = (
|
|
145
|
+
fastmcp.settings.settings.resource_prefix_format
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
self.resource_prefix_format = resource_prefix_format
|
|
149
|
+
|
|
137
150
|
self.tags: set[str] = tags or set()
|
|
138
151
|
self.dependencies = dependencies
|
|
139
152
|
self._cache = TimedCache(
|
|
@@ -935,10 +948,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
935
948
|
self,
|
|
936
949
|
prefix: str,
|
|
937
950
|
server: FastMCP[LifespanResultT],
|
|
951
|
+
as_proxy: bool | None = None,
|
|
952
|
+
*,
|
|
938
953
|
tool_separator: str | None = None,
|
|
939
954
|
resource_separator: str | None = None,
|
|
940
955
|
prompt_separator: str | None = None,
|
|
941
|
-
as_proxy: bool | None = None,
|
|
942
956
|
) -> None:
|
|
943
957
|
"""Mount another FastMCP server on this server with the given prefix.
|
|
944
958
|
|
|
@@ -949,15 +963,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
949
963
|
through the parent.
|
|
950
964
|
|
|
951
965
|
When a server is mounted:
|
|
952
|
-
- Tools from the mounted server are accessible with prefixed names
|
|
966
|
+
- Tools from the mounted server are accessible with prefixed names.
|
|
953
967
|
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
|
954
|
-
- Resources are accessible with prefixed URIs
|
|
968
|
+
- Resources are accessible with prefixed URIs.
|
|
955
969
|
Example: If server has a resource with URI "weather://forecast", it will be available as
|
|
956
|
-
"
|
|
957
|
-
- Templates are accessible with prefixed URI templates
|
|
970
|
+
"weather://prefix/forecast".
|
|
971
|
+
- Templates are accessible with prefixed URI templates.
|
|
958
972
|
Example: If server has a template with URI "weather://location/{id}", it will be available
|
|
959
|
-
as "
|
|
960
|
-
- Prompts are accessible with prefixed names
|
|
973
|
+
as "weather://prefix/location/{id}".
|
|
974
|
+
- Prompts are accessible with prefixed names.
|
|
961
975
|
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
962
976
|
"prefix_weather_prompt".
|
|
963
977
|
|
|
@@ -975,17 +989,44 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
975
989
|
Args:
|
|
976
990
|
prefix: Prefix to use for the mounted server's objects.
|
|
977
991
|
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
992
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
982
993
|
automatically determined based on whether the server has a custom lifespan
|
|
983
994
|
(True if it has a custom lifespan, False otherwise).
|
|
995
|
+
tool_separator: Deprecated. Separator character for tool names.
|
|
996
|
+
resource_separator: Deprecated. Separator character for resource URIs.
|
|
997
|
+
prompt_separator: Deprecated. Separator character for prompt names.
|
|
984
998
|
"""
|
|
985
999
|
from fastmcp import Client
|
|
986
1000
|
from fastmcp.client.transports import FastMCPTransport
|
|
987
1001
|
from fastmcp.server.proxy import FastMCPProxy
|
|
988
1002
|
|
|
1003
|
+
if tool_separator is not None:
|
|
1004
|
+
# Deprecated since 2.3.6
|
|
1005
|
+
warnings.warn(
|
|
1006
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1007
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1008
|
+
DeprecationWarning,
|
|
1009
|
+
stacklevel=2,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
if resource_separator is not None:
|
|
1013
|
+
# Deprecated since 2.3.6
|
|
1014
|
+
warnings.warn(
|
|
1015
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1016
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1017
|
+
DeprecationWarning,
|
|
1018
|
+
stacklevel=2,
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
if prompt_separator is not None:
|
|
1022
|
+
# Deprecated since 2.3.6
|
|
1023
|
+
warnings.warn(
|
|
1024
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1025
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1026
|
+
DeprecationWarning,
|
|
1027
|
+
stacklevel=2,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
989
1030
|
# if as_proxy is not specified and the server has a custom lifespan,
|
|
990
1031
|
# we should treat it as a proxy
|
|
991
1032
|
if as_proxy is None:
|
|
@@ -997,9 +1038,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
997
1038
|
mounted_server = MountedServer(
|
|
998
1039
|
server=server,
|
|
999
1040
|
prefix=prefix,
|
|
1000
|
-
tool_separator=tool_separator,
|
|
1001
|
-
resource_separator=resource_separator,
|
|
1002
|
-
prompt_separator=prompt_separator,
|
|
1003
1041
|
)
|
|
1004
1042
|
self._mounted_servers[prefix] = mounted_server
|
|
1005
1043
|
self._cache.clear()
|
|
@@ -1025,81 +1063,136 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1025
1063
|
future changes to the imported server will not be reflected in the
|
|
1026
1064
|
importing server. Server-level configurations and lifespans are not imported.
|
|
1027
1065
|
|
|
1028
|
-
When a server is
|
|
1029
|
-
|
|
1066
|
+
When a server is imported:
|
|
1067
|
+
- The tools are imported with prefixed names
|
|
1030
1068
|
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"
|
|
1069
|
+
available as "prefix_get_weather"
|
|
1070
|
+
- The resources are imported with prefixed URIs using the new format
|
|
1071
|
+
Example: If server has a resource with URI "weather://forecast", it will
|
|
1072
|
+
be available as "weather://prefix/forecast"
|
|
1073
|
+
- The templates are imported with prefixed URI templates using the new format
|
|
1074
|
+
Example: If server has a template with URI "weather://location/{id}", it will
|
|
1075
|
+
be available as "weather://prefix/location/{id}"
|
|
1076
|
+
- The prompts are imported with prefixed names
|
|
1077
|
+
Example: If server has a prompt named "weather_prompt", it will be available as
|
|
1078
|
+
"prefix_weather_prompt"
|
|
1043
1079
|
|
|
1044
1080
|
Args:
|
|
1045
|
-
prefix: The prefix to use for the
|
|
1046
|
-
server
|
|
1047
|
-
|
|
1048
|
-
|
|
1081
|
+
prefix: The prefix to use for the imported server
|
|
1082
|
+
server: The FastMCP server to import
|
|
1083
|
+
tool_separator: Deprecated. Separator for tool names.
|
|
1084
|
+
resource_separator: Deprecated and ignored. Prefix is now
|
|
1085
|
+
applied using the protocol://prefix/path format
|
|
1086
|
+
prompt_separator: Deprecated. Separator for prompt names.
|
|
1049
1087
|
"""
|
|
1050
|
-
if tool_separator is None:
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1088
|
+
if tool_separator is not None:
|
|
1089
|
+
# Deprecated since 2.3.6
|
|
1090
|
+
warnings.warn(
|
|
1091
|
+
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
|
1092
|
+
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
1093
|
+
DeprecationWarning,
|
|
1094
|
+
stacklevel=2,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if resource_separator is not None:
|
|
1098
|
+
# Deprecated since 2.3.6
|
|
1099
|
+
warnings.warn(
|
|
1100
|
+
"The resource_separator parameter is deprecated and ignored. "
|
|
1101
|
+
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
1102
|
+
DeprecationWarning,
|
|
1103
|
+
stacklevel=2,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
if prompt_separator is not None:
|
|
1107
|
+
# Deprecated since 2.3.6
|
|
1108
|
+
warnings.warn(
|
|
1109
|
+
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
|
1110
|
+
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
1111
|
+
DeprecationWarning,
|
|
1112
|
+
stacklevel=2,
|
|
1113
|
+
)
|
|
1056
1114
|
|
|
1057
1115
|
# Import tools from the mounted server
|
|
1058
|
-
tool_prefix = f"{prefix}
|
|
1116
|
+
tool_prefix = f"{prefix}_"
|
|
1059
1117
|
for key, tool in (await server.get_tools()).items():
|
|
1060
1118
|
self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
|
|
1061
1119
|
|
|
1062
1120
|
# Import resources and templates from the mounted server
|
|
1063
|
-
resource_prefix = f"{prefix}{resource_separator}"
|
|
1064
|
-
_validate_resource_prefix(resource_prefix)
|
|
1065
1121
|
for key, resource in (await server.get_resources()).items():
|
|
1066
|
-
|
|
1122
|
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
|
1123
|
+
self._resource_manager.add_resource(resource, key=prefixed_key)
|
|
1124
|
+
|
|
1067
1125
|
for key, template in (await server.get_resource_templates()).items():
|
|
1068
|
-
|
|
1126
|
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
|
1127
|
+
self._resource_manager.add_template(template, key=prefixed_key)
|
|
1069
1128
|
|
|
1070
1129
|
# Import prompts from the mounted server
|
|
1071
|
-
prompt_prefix = f"{prefix}
|
|
1130
|
+
prompt_prefix = f"{prefix}_"
|
|
1072
1131
|
for key, prompt in (await server.get_prompts()).items():
|
|
1073
1132
|
self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
|
|
1074
1133
|
|
|
1075
1134
|
logger.info(f"Imported server {server.name} with prefix '{prefix}'")
|
|
1076
1135
|
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}'")
|
|
1136
|
+
logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
|
|
1079
1137
|
logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
|
|
1080
1138
|
|
|
1081
1139
|
self._cache.clear()
|
|
1082
1140
|
|
|
1083
1141
|
@classmethod
|
|
1084
1142
|
def from_openapi(
|
|
1085
|
-
cls,
|
|
1143
|
+
cls,
|
|
1144
|
+
openapi_spec: dict[str, Any],
|
|
1145
|
+
client: httpx.AsyncClient,
|
|
1146
|
+
route_maps: list[RouteMap] | None = None,
|
|
1147
|
+
all_routes_as_tools: bool = False,
|
|
1148
|
+
**settings: Any,
|
|
1086
1149
|
) -> FastMCPOpenAPI:
|
|
1087
1150
|
"""
|
|
1088
1151
|
Create a FastMCP server from an OpenAPI specification.
|
|
1089
1152
|
"""
|
|
1090
|
-
from .openapi import FastMCPOpenAPI
|
|
1153
|
+
from .openapi import FastMCPOpenAPI, RouteMap, RouteType
|
|
1154
|
+
|
|
1155
|
+
if all_routes_as_tools and route_maps:
|
|
1156
|
+
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1157
|
+
|
|
1158
|
+
elif all_routes_as_tools:
|
|
1159
|
+
route_maps = [
|
|
1160
|
+
RouteMap(
|
|
1161
|
+
methods="*",
|
|
1162
|
+
pattern=r".*",
|
|
1163
|
+
route_type=RouteType.TOOL,
|
|
1164
|
+
)
|
|
1165
|
+
]
|
|
1091
1166
|
|
|
1092
|
-
return FastMCPOpenAPI(
|
|
1167
|
+
return FastMCPOpenAPI(
|
|
1168
|
+
openapi_spec=openapi_spec,
|
|
1169
|
+
client=client,
|
|
1170
|
+
route_maps=route_maps,
|
|
1171
|
+
**settings,
|
|
1172
|
+
)
|
|
1093
1173
|
|
|
1094
1174
|
@classmethod
|
|
1095
1175
|
def from_fastapi(
|
|
1096
|
-
cls,
|
|
1176
|
+
cls,
|
|
1177
|
+
app: Any,
|
|
1178
|
+
name: str | None = None,
|
|
1179
|
+
route_maps: list[RouteMap] | None = None,
|
|
1180
|
+
all_routes_as_tools: bool = False,
|
|
1181
|
+
**settings: Any,
|
|
1097
1182
|
) -> FastMCPOpenAPI:
|
|
1098
1183
|
"""
|
|
1099
1184
|
Create a FastMCP server from a FastAPI application.
|
|
1100
1185
|
"""
|
|
1101
1186
|
|
|
1102
|
-
from .openapi import FastMCPOpenAPI
|
|
1187
|
+
from .openapi import FastMCPOpenAPI, RouteMap, RouteType
|
|
1188
|
+
|
|
1189
|
+
if all_routes_as_tools and route_maps:
|
|
1190
|
+
raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
|
|
1191
|
+
|
|
1192
|
+
elif all_routes_as_tools:
|
|
1193
|
+
route_maps = [
|
|
1194
|
+
RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
|
|
1195
|
+
]
|
|
1103
1196
|
|
|
1104
1197
|
client = httpx.AsyncClient(
|
|
1105
1198
|
transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
|
|
@@ -1108,7 +1201,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1108
1201
|
name = name or app.title
|
|
1109
1202
|
|
|
1110
1203
|
return FastMCPOpenAPI(
|
|
1111
|
-
openapi_spec=app.openapi(),
|
|
1204
|
+
openapi_spec=app.openapi(),
|
|
1205
|
+
client=client,
|
|
1206
|
+
name=name,
|
|
1207
|
+
route_maps=route_maps,
|
|
1208
|
+
**settings,
|
|
1112
1209
|
)
|
|
1113
1210
|
|
|
1114
1211
|
@classmethod
|
|
@@ -1119,6 +1216,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1119
1216
|
| FastMCP[Any]
|
|
1120
1217
|
| AnyUrl
|
|
1121
1218
|
| Path
|
|
1219
|
+
| MCPConfig
|
|
1122
1220
|
| dict[str, Any]
|
|
1123
1221
|
| str,
|
|
1124
1222
|
**settings: Any,
|
|
@@ -1155,84 +1253,219 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1155
1253
|
return cls.as_proxy(client, **settings)
|
|
1156
1254
|
|
|
1157
1255
|
|
|
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
1256
|
class MountedServer:
|
|
1171
1257
|
def __init__(
|
|
1172
1258
|
self,
|
|
1173
1259
|
prefix: str,
|
|
1174
1260
|
server: FastMCP[LifespanResultT],
|
|
1175
|
-
tool_separator: str | None = None,
|
|
1176
|
-
resource_separator: str | None = None,
|
|
1177
|
-
prompt_separator: str | None = None,
|
|
1178
1261
|
):
|
|
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
1262
|
self.server = server
|
|
1189
1263
|
self.prefix = prefix
|
|
1190
|
-
self.tool_separator = tool_separator
|
|
1191
|
-
self.resource_separator = resource_separator
|
|
1192
|
-
self.prompt_separator = prompt_separator
|
|
1193
1264
|
|
|
1194
1265
|
async def get_tools(self) -> dict[str, Tool]:
|
|
1195
1266
|
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
|
-
}
|
|
1267
|
+
return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
|
|
1200
1268
|
|
|
1201
1269
|
async def get_resources(self) -> dict[str, Resource]:
|
|
1202
1270
|
resources = await self.server.get_resources()
|
|
1203
1271
|
return {
|
|
1204
|
-
|
|
1272
|
+
add_resource_prefix(
|
|
1273
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1274
|
+
): resource
|
|
1205
1275
|
for key, resource in resources.items()
|
|
1206
1276
|
}
|
|
1207
1277
|
|
|
1208
1278
|
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
|
1209
1279
|
templates = await self.server.get_resource_templates()
|
|
1210
1280
|
return {
|
|
1211
|
-
|
|
1281
|
+
add_resource_prefix(
|
|
1282
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1283
|
+
): template
|
|
1212
1284
|
for key, template in templates.items()
|
|
1213
1285
|
}
|
|
1214
1286
|
|
|
1215
1287
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
1216
1288
|
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
|
-
}
|
|
1289
|
+
return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
|
|
1221
1290
|
|
|
1222
1291
|
def match_tool(self, key: str) -> bool:
|
|
1223
|
-
return key.startswith(f"{self.prefix}
|
|
1292
|
+
return key.startswith(f"{self.prefix}_")
|
|
1224
1293
|
|
|
1225
1294
|
def strip_tool_prefix(self, key: str) -> str:
|
|
1226
|
-
return key.removeprefix(f"{self.prefix}
|
|
1295
|
+
return key.removeprefix(f"{self.prefix}_")
|
|
1227
1296
|
|
|
1228
1297
|
def match_resource(self, key: str) -> bool:
|
|
1229
|
-
return key
|
|
1298
|
+
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
|
1230
1299
|
|
|
1231
1300
|
def strip_resource_prefix(self, key: str) -> str:
|
|
1232
|
-
return
|
|
1301
|
+
return remove_resource_prefix(
|
|
1302
|
+
key, self.prefix, self.server.resource_prefix_format
|
|
1303
|
+
)
|
|
1233
1304
|
|
|
1234
1305
|
def match_prompt(self, key: str) -> bool:
|
|
1235
|
-
return key.startswith(f"{self.prefix}
|
|
1306
|
+
return key.startswith(f"{self.prefix}_")
|
|
1236
1307
|
|
|
1237
1308
|
def strip_prompt_prefix(self, key: str) -> str:
|
|
1238
|
-
return key.removeprefix(f"{self.prefix}
|
|
1309
|
+
return key.removeprefix(f"{self.prefix}_")
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def add_resource_prefix(
|
|
1313
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1314
|
+
) -> str:
|
|
1315
|
+
"""Add a prefix to a resource URI.
|
|
1316
|
+
|
|
1317
|
+
Args:
|
|
1318
|
+
uri: The original resource URI
|
|
1319
|
+
prefix: The prefix to add
|
|
1320
|
+
|
|
1321
|
+
Returns:
|
|
1322
|
+
The resource URI with the prefix added
|
|
1323
|
+
|
|
1324
|
+
Examples:
|
|
1325
|
+
>>> add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1326
|
+
"resource://prefix/path/to/resource" # with new style
|
|
1327
|
+
>>> add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1328
|
+
"prefix+resource://path/to/resource" # with legacy style
|
|
1329
|
+
>>> add_resource_prefix("resource:///absolute/path", "prefix")
|
|
1330
|
+
"resource://prefix//absolute/path" # with new style
|
|
1331
|
+
|
|
1332
|
+
Raises:
|
|
1333
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1334
|
+
"""
|
|
1335
|
+
if not prefix:
|
|
1336
|
+
return uri
|
|
1337
|
+
|
|
1338
|
+
# Get the server settings to check for legacy format preference
|
|
1339
|
+
|
|
1340
|
+
if prefix_format is None:
|
|
1341
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1342
|
+
|
|
1343
|
+
if prefix_format == "protocol":
|
|
1344
|
+
# Legacy style: prefix+protocol://path
|
|
1345
|
+
return f"{prefix}+{uri}"
|
|
1346
|
+
elif prefix_format == "path":
|
|
1347
|
+
# New style: protocol://prefix/path
|
|
1348
|
+
# Split the URI into protocol and path
|
|
1349
|
+
match = URI_PATTERN.match(uri)
|
|
1350
|
+
if not match:
|
|
1351
|
+
raise ValueError(
|
|
1352
|
+
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
protocol, path = match.groups()
|
|
1356
|
+
|
|
1357
|
+
# Add the prefix to the path
|
|
1358
|
+
return f"{protocol}{prefix}/{path}"
|
|
1359
|
+
else:
|
|
1360
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
def remove_resource_prefix(
|
|
1364
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1365
|
+
) -> str:
|
|
1366
|
+
"""Remove a prefix from a resource URI.
|
|
1367
|
+
|
|
1368
|
+
Args:
|
|
1369
|
+
uri: The resource URI with a prefix
|
|
1370
|
+
prefix: The prefix to remove
|
|
1371
|
+
prefix_format: The format of the prefix to remove
|
|
1372
|
+
Returns:
|
|
1373
|
+
The resource URI with the prefix removed
|
|
1374
|
+
|
|
1375
|
+
Examples:
|
|
1376
|
+
>>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
1377
|
+
"resource://path/to/resource" # with new style
|
|
1378
|
+
>>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
1379
|
+
"resource://path/to/resource" # with legacy style
|
|
1380
|
+
>>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
1381
|
+
"resource:///absolute/path" # with new style
|
|
1382
|
+
|
|
1383
|
+
Raises:
|
|
1384
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1385
|
+
"""
|
|
1386
|
+
if not prefix:
|
|
1387
|
+
return uri
|
|
1388
|
+
|
|
1389
|
+
if prefix_format is None:
|
|
1390
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1391
|
+
|
|
1392
|
+
if prefix_format == "protocol":
|
|
1393
|
+
# Legacy style: prefix+protocol://path
|
|
1394
|
+
legacy_prefix = f"{prefix}+"
|
|
1395
|
+
if uri.startswith(legacy_prefix):
|
|
1396
|
+
return uri[len(legacy_prefix) :]
|
|
1397
|
+
return 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
|
+
# Check if the path starts with the prefix followed by a /
|
|
1410
|
+
prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
|
|
1411
|
+
path_match = re.match(prefix_pattern, path)
|
|
1412
|
+
if not path_match:
|
|
1413
|
+
return uri
|
|
1414
|
+
|
|
1415
|
+
# Return the URI without the prefix
|
|
1416
|
+
return f"{protocol}{path_match.group(1)}"
|
|
1417
|
+
else:
|
|
1418
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def has_resource_prefix(
|
|
1422
|
+
uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
|
|
1423
|
+
) -> bool:
|
|
1424
|
+
"""Check if a resource URI has a specific prefix.
|
|
1425
|
+
|
|
1426
|
+
Args:
|
|
1427
|
+
uri: The resource URI to check
|
|
1428
|
+
prefix: The prefix to look for
|
|
1429
|
+
|
|
1430
|
+
Returns:
|
|
1431
|
+
True if the URI has the specified prefix, False otherwise
|
|
1432
|
+
|
|
1433
|
+
Examples:
|
|
1434
|
+
>>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
1435
|
+
True # with new style
|
|
1436
|
+
>>> has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
1437
|
+
True # with legacy style
|
|
1438
|
+
>>> has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
1439
|
+
False
|
|
1440
|
+
|
|
1441
|
+
Raises:
|
|
1442
|
+
ValueError: If the URI doesn't match the expected protocol://path format
|
|
1443
|
+
"""
|
|
1444
|
+
if not prefix:
|
|
1445
|
+
return False
|
|
1446
|
+
|
|
1447
|
+
# Get the server settings to check for legacy format preference
|
|
1448
|
+
|
|
1449
|
+
if prefix_format is None:
|
|
1450
|
+
prefix_format = fastmcp.settings.settings.resource_prefix_format
|
|
1451
|
+
|
|
1452
|
+
if prefix_format == "protocol":
|
|
1453
|
+
# Legacy style: prefix+protocol://path
|
|
1454
|
+
legacy_prefix = f"{prefix}+"
|
|
1455
|
+
return uri.startswith(legacy_prefix)
|
|
1456
|
+
elif prefix_format == "path":
|
|
1457
|
+
# New style: protocol://prefix/path
|
|
1458
|
+
# Split the URI into protocol and path
|
|
1459
|
+
match = URI_PATTERN.match(uri)
|
|
1460
|
+
if not match:
|
|
1461
|
+
raise ValueError(
|
|
1462
|
+
f"Invalid URI format: {uri}. Expected protocol://path format."
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
_, path = match.groups()
|
|
1466
|
+
|
|
1467
|
+
# Check if the path starts with the prefix followed by a /
|
|
1468
|
+
prefix_pattern = f"^{re.escape(prefix)}/"
|
|
1469
|
+
return bool(re.match(prefix_pattern, path))
|
|
1470
|
+
else:
|
|
1471
|
+
raise ValueError(f"Invalid prefix format: {prefix_format}")
|
fastmcp/settings.py
CHANGED
|
@@ -29,6 +29,7 @@ class Settings(BaseSettings):
|
|
|
29
29
|
|
|
30
30
|
test_mode: bool = False
|
|
31
31
|
log_level: LOG_LEVEL = "INFO"
|
|
32
|
+
|
|
32
33
|
client_raise_first_exceptiongroup_error: Annotated[
|
|
33
34
|
bool,
|
|
34
35
|
Field(
|
|
@@ -44,6 +45,21 @@ class Settings(BaseSettings):
|
|
|
44
45
|
),
|
|
45
46
|
),
|
|
46
47
|
] = True
|
|
48
|
+
|
|
49
|
+
resource_prefix_format: Annotated[
|
|
50
|
+
Literal["protocol", "path"],
|
|
51
|
+
Field(
|
|
52
|
+
default="path",
|
|
53
|
+
description=inspect.cleandoc(
|
|
54
|
+
"""
|
|
55
|
+
When perfixing a resource URI, either use path formatting (resource://prefix/path)
|
|
56
|
+
or protocol formatting (prefix+resource://path). Protocol formatting was the default in FastMCP < 2.4;
|
|
57
|
+
path formatting is current default.
|
|
58
|
+
"""
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
] = "path"
|
|
62
|
+
|
|
47
63
|
tool_attempt_parse_json_args: Annotated[
|
|
48
64
|
bool,
|
|
49
65
|
Field(
|