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/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 using the tool_separator.
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 using the resource_separator.
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
- "prefix+weather://forecast".
957
- - Templates are accessible with prefixed URI templates using the resource_separator.
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 "prefix+weather://location/{id}".
960
- - Prompts are accessible with prefixed names using the prompt_separator.
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 mounted: - The tools are imported with prefixed names
1029
- using the tool_separator
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 "weatherget_weather"
1032
- - The resources are imported with prefixed URIs using the
1033
- resource_separator Example: If server has a resource with URI
1034
- "weather://forecast", it will be available as
1035
- "weather+weather://forecast"
1036
- - The templates are imported with prefixed URI templates using the
1037
- resource_separator Example: If server has a template with URI
1038
- "weather://location/{id}", it will be available as
1039
- "weather+weather://location/{id}"
1040
- - The prompts are imported with prefixed names using the
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 mounted server server: The FastMCP
1046
- server to mount tool_separator: Separator for tool names (defaults
1047
- to "_") resource_separator: Separator for resource URIs (defaults to
1048
- "+") prompt_separator: Separator for prompt names (defaults to "_")
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
- tool_separator = "_"
1052
- if resource_separator is None:
1053
- resource_separator = "+"
1054
- if prompt_separator is None:
1055
- prompt_separator = "_"
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}{tool_separator}"
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
- self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
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
- self._resource_manager.add_template(template, key=f"{resource_prefix}{key}")
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}{prompt_separator}"
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 '{resource_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, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
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(openapi_spec=openapi_spec, client=client, **settings)
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, app: Any, name: str | None = None, **settings: Any
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(), client=client, name=name, **settings
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
- f"{self.prefix}{self.resource_separator}{key}": resource
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
- f"{self.prefix}{self.resource_separator}{key}": template
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}{self.tool_separator}")
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}{self.tool_separator}")
1295
+ return key.removeprefix(f"{self.prefix}_")
1227
1296
 
1228
1297
  def match_resource(self, key: str) -> bool:
1229
- return key.startswith(f"{self.prefix}{self.resource_separator}")
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 key.removeprefix(f"{self.prefix}{self.resource_separator}")
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}{self.prompt_separator}")
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}{self.prompt_separator}")
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(