Spaces:
Running
Running
Merge pull request #534 from jlowin/backwards-compat-resources
Browse files
docs/servers/composition.mdx
CHANGED
|
@@ -181,6 +181,57 @@ remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp"))
|
|
| 181 |
main_server.mount("remote", remote_proxy)
|
| 182 |
```
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
main_server.mount("remote", remote_proxy)
|
| 182 |
```
|
| 183 |
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
## Resource Prefix Formats
|
| 187 |
+
|
| 188 |
+
When mounting or importing servers, resource URIs are usually prefixed to avoid naming conflicts. FastMCP supports two different formats for resource prefixes:
|
| 189 |
+
|
| 190 |
+
### Path Format (Default)
|
| 191 |
+
|
| 192 |
+
In path format, prefixes are added to the path component of the URI:
|
| 193 |
+
|
| 194 |
+
```
|
| 195 |
+
resource://prefix/path/to/resource
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
This is the default format since FastMCP 2.4. This format is recommended because it avoids issues with URI protocol restrictions (like underscores not being allowed in protocol names).
|
| 199 |
+
|
| 200 |
+
### Protocol Format (Legacy)
|
| 201 |
+
|
| 202 |
+
In protocol format, prefixes are added as part of the protocol:
|
| 203 |
+
|
| 204 |
+
```
|
| 205 |
+
prefix+resource://path/to/resource
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
This was the default format in FastMCP before 2.4. While still supported, it's not recommended for new code as it can cause problems with prefix names that aren't valid in URI protocols.
|
| 209 |
+
|
| 210 |
+
### Configuring the Prefix Format
|
| 211 |
+
|
| 212 |
+
You can configure the prefix format globally in code:
|
| 213 |
+
|
| 214 |
+
```python
|
| 215 |
+
import fastmcp
|
| 216 |
+
fastmcp.settings.settings.resource_prefix_format = "protocol"
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
Or via environment variable:
|
| 220 |
+
|
| 221 |
+
```bash
|
| 222 |
+
FASTMCP_RESOURCE_PREFIX_FORMAT=protocol
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
Or per-server:
|
| 226 |
+
|
| 227 |
+
```python
|
| 228 |
+
from fastmcp import FastMCP
|
| 229 |
+
|
| 230 |
+
# Create a server that uses legacy protocol format
|
| 231 |
+
server = FastMCP("LegacyServer", resource_prefix_format="protocol")
|
| 232 |
+
|
| 233 |
+
# Create a server that uses new path format
|
| 234 |
+
server = FastMCP("NewServer", resource_prefix_format="path")
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
When mounting or importing servers, the prefix format of the parent server is used.
|
src/fastmcp/server/server.py
CHANGED
|
@@ -124,6 +124,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 124 |
on_duplicate_tools: DuplicateBehavior | None = None,
|
| 125 |
on_duplicate_resources: DuplicateBehavior | None = None,
|
| 126 |
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
|
|
| 127 |
**settings: Any,
|
| 128 |
):
|
| 129 |
if settings:
|
|
@@ -138,6 +139,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 138 |
)
|
| 139 |
self.settings = fastmcp.settings.ServerSettings(**settings)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
self.tags: set[str] = tags or set()
|
| 142 |
self.dependencies = dependencies
|
| 143 |
self._cache = TimedCache(
|
|
@@ -1110,11 +1119,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1110 |
|
| 1111 |
# Import resources and templates from the mounted server
|
| 1112 |
for key, resource in (await server.get_resources()).items():
|
| 1113 |
-
prefixed_key = add_resource_prefix(key, prefix)
|
| 1114 |
self._resource_manager.add_resource(resource, key=prefixed_key)
|
| 1115 |
|
| 1116 |
for key, template in (await server.get_resource_templates()).items():
|
| 1117 |
-
prefixed_key = add_resource_prefix(key, prefix)
|
| 1118 |
self._resource_manager.add_template(template, key=prefixed_key)
|
| 1119 |
|
| 1120 |
# Import prompts from the mounted server
|
|
@@ -1260,14 +1269,18 @@ class MountedServer:
|
|
| 1260 |
async def get_resources(self) -> dict[str, Resource]:
|
| 1261 |
resources = await self.server.get_resources()
|
| 1262 |
return {
|
| 1263 |
-
add_resource_prefix(
|
|
|
|
|
|
|
| 1264 |
for key, resource in resources.items()
|
| 1265 |
}
|
| 1266 |
|
| 1267 |
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
| 1268 |
templates = await self.server.get_resource_templates()
|
| 1269 |
return {
|
| 1270 |
-
add_resource_prefix(
|
|
|
|
|
|
|
| 1271 |
for key, template in templates.items()
|
| 1272 |
}
|
| 1273 |
|
|
@@ -1282,10 +1295,12 @@ class MountedServer:
|
|
| 1282 |
return key.removeprefix(f"{self.prefix}_")
|
| 1283 |
|
| 1284 |
def match_resource(self, key: str) -> bool:
|
| 1285 |
-
return has_resource_prefix(key, self.prefix)
|
| 1286 |
|
| 1287 |
def strip_resource_prefix(self, key: str) -> str:
|
| 1288 |
-
return remove_resource_prefix(
|
|
|
|
|
|
|
| 1289 |
|
| 1290 |
def match_prompt(self, key: str) -> bool:
|
| 1291 |
return key.startswith(f"{self.prefix}_")
|
|
@@ -1294,7 +1309,9 @@ class MountedServer:
|
|
| 1294 |
return key.removeprefix(f"{self.prefix}_")
|
| 1295 |
|
| 1296 |
|
| 1297 |
-
def add_resource_prefix(
|
|
|
|
|
|
|
| 1298 |
"""Add a prefix to a resource URI.
|
| 1299 |
|
| 1300 |
Args:
|
|
@@ -1306,9 +1323,11 @@ def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
| 1306 |
|
| 1307 |
Examples:
|
| 1308 |
>>> add_resource_prefix("resource://path/to/resource", "prefix")
|
| 1309 |
-
"resource://prefix/path/to/resource"
|
|
|
|
|
|
|
| 1310 |
>>> add_resource_prefix("resource:///absolute/path", "prefix")
|
| 1311 |
-
"resource://prefix//absolute/path"
|
| 1312 |
|
| 1313 |
Raises:
|
| 1314 |
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1316,32 +1335,50 @@ def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
| 1316 |
if not prefix:
|
| 1317 |
return uri
|
| 1318 |
|
| 1319 |
-
#
|
| 1320 |
-
match = URI_PATTERN.match(uri)
|
| 1321 |
-
if not match:
|
| 1322 |
-
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
| 1323 |
|
| 1324 |
-
|
|
|
|
| 1325 |
|
| 1326 |
-
|
| 1327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
|
| 1330 |
-
|
|
|
|
|
|
|
|
|
|
| 1331 |
"""Remove a prefix from a resource URI.
|
| 1332 |
|
| 1333 |
Args:
|
| 1334 |
uri: The resource URI with a prefix
|
| 1335 |
prefix: The prefix to remove
|
| 1336 |
-
|
| 1337 |
Returns:
|
| 1338 |
The resource URI with the prefix removed
|
| 1339 |
|
| 1340 |
Examples:
|
| 1341 |
>>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
| 1342 |
-
"resource://path/to/resource"
|
|
|
|
|
|
|
| 1343 |
>>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
| 1344 |
-
"resource:///absolute/path"
|
| 1345 |
|
| 1346 |
Raises:
|
| 1347 |
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1349,24 +1386,41 @@ def remove_resource_prefix(uri: str, prefix: str) -> str:
|
|
| 1349 |
if not prefix:
|
| 1350 |
return uri
|
| 1351 |
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
if not match:
|
| 1355 |
-
raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
|
| 1356 |
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
if not path_match:
|
| 1363 |
return uri
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1364 |
|
| 1365 |
-
|
| 1366 |
-
|
|
|
|
|
|
|
| 1367 |
|
| 1368 |
|
| 1369 |
-
def has_resource_prefix(
|
|
|
|
|
|
|
| 1370 |
"""Check if a resource URI has a specific prefix.
|
| 1371 |
|
| 1372 |
Args:
|
|
@@ -1378,7 +1432,9 @@ def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
| 1378 |
|
| 1379 |
Examples:
|
| 1380 |
>>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
| 1381 |
-
True
|
|
|
|
|
|
|
| 1382 |
>>> has_resource_prefix("resource://other/path/to/resource", "prefix")
|
| 1383 |
False
|
| 1384 |
|
|
@@ -1388,13 +1444,28 @@ def has_resource_prefix(uri: str, prefix: str) -> bool:
|
|
| 1388 |
if not prefix:
|
| 1389 |
return False
|
| 1390 |
|
| 1391 |
-
#
|
| 1392 |
-
|
| 1393 |
-
if
|
| 1394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1395 |
|
| 1396 |
-
|
| 1397 |
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
|
|
|
|
|
|
|
|
| 124 |
on_duplicate_tools: DuplicateBehavior | None = None,
|
| 125 |
on_duplicate_resources: DuplicateBehavior | None = None,
|
| 126 |
on_duplicate_prompts: DuplicateBehavior | None = None,
|
| 127 |
+
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
| 128 |
**settings: Any,
|
| 129 |
):
|
| 130 |
if settings:
|
|
|
|
| 139 |
)
|
| 140 |
self.settings = fastmcp.settings.ServerSettings(**settings)
|
| 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 |
+
|
| 150 |
self.tags: set[str] = tags or set()
|
| 151 |
self.dependencies = dependencies
|
| 152 |
self._cache = TimedCache(
|
|
|
|
| 1119 |
|
| 1120 |
# Import resources and templates from the mounted server
|
| 1121 |
for key, resource in (await server.get_resources()).items():
|
| 1122 |
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
| 1123 |
self._resource_manager.add_resource(resource, key=prefixed_key)
|
| 1124 |
|
| 1125 |
for key, template in (await server.get_resource_templates()).items():
|
| 1126 |
+
prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
| 1127 |
self._resource_manager.add_template(template, key=prefixed_key)
|
| 1128 |
|
| 1129 |
# Import prompts from the mounted server
|
|
|
|
| 1269 |
async def get_resources(self) -> dict[str, Resource]:
|
| 1270 |
resources = await self.server.get_resources()
|
| 1271 |
return {
|
| 1272 |
+
add_resource_prefix(
|
| 1273 |
+
key, self.prefix, self.server.resource_prefix_format
|
| 1274 |
+
): resource
|
| 1275 |
for key, resource in resources.items()
|
| 1276 |
}
|
| 1277 |
|
| 1278 |
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
| 1279 |
templates = await self.server.get_resource_templates()
|
| 1280 |
return {
|
| 1281 |
+
add_resource_prefix(
|
| 1282 |
+
key, self.prefix, self.server.resource_prefix_format
|
| 1283 |
+
): template
|
| 1284 |
for key, template in templates.items()
|
| 1285 |
}
|
| 1286 |
|
|
|
|
| 1295 |
return key.removeprefix(f"{self.prefix}_")
|
| 1296 |
|
| 1297 |
def match_resource(self, key: str) -> bool:
|
| 1298 |
+
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
| 1299 |
|
| 1300 |
def strip_resource_prefix(self, key: str) -> str:
|
| 1301 |
+
return remove_resource_prefix(
|
| 1302 |
+
key, self.prefix, self.server.resource_prefix_format
|
| 1303 |
+
)
|
| 1304 |
|
| 1305 |
def match_prompt(self, key: str) -> bool:
|
| 1306 |
return key.startswith(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:
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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:
|
|
|
|
| 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 |
|
|
|
|
| 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}")
|
src/fastmcp/settings.py
CHANGED
|
@@ -29,6 +29,7 @@ class Settings(BaseSettings):
|
|
| 29 |
|
| 30 |
test_mode: bool = False
|
| 31 |
log_level: LOG_LEVEL = "INFO"
|
|
|
|
| 32 |
client_raise_first_exceptiongroup_error: Annotated[
|
| 33 |
bool,
|
| 34 |
Field(
|
|
@@ -44,6 +45,21 @@ class Settings(BaseSettings):
|
|
| 44 |
),
|
| 45 |
),
|
| 46 |
] = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
tool_attempt_parse_json_args: Annotated[
|
| 48 |
bool,
|
| 49 |
Field(
|
|
|
|
| 29 |
|
| 30 |
test_mode: bool = False
|
| 31 |
log_level: LOG_LEVEL = "INFO"
|
| 32 |
+
|
| 33 |
client_raise_first_exceptiongroup_error: Annotated[
|
| 34 |
bool,
|
| 35 |
Field(
|
|
|
|
| 45 |
),
|
| 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 |
+
|
| 63 |
tool_attempt_parse_json_args: Annotated[
|
| 64 |
bool,
|
| 65 |
Field(
|
tests/deprecated/test_resource_prefixes.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for legacy resource prefix behavior."""
|
| 2 |
+
|
| 3 |
+
from fastmcp import Client, FastMCP
|
| 4 |
+
from fastmcp.server.server import (
|
| 5 |
+
add_resource_prefix,
|
| 6 |
+
has_resource_prefix,
|
| 7 |
+
remove_resource_prefix,
|
| 8 |
+
)
|
| 9 |
+
from fastmcp.utilities.tests import temporary_settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestLegacyResourcePrefixes:
|
| 13 |
+
"""Test the legacy resource prefix behavior."""
|
| 14 |
+
|
| 15 |
+
def test_add_resource_prefix_legacy(self):
|
| 16 |
+
"""Test that add_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'."""
|
| 17 |
+
with temporary_settings(resource_prefix_format="protocol"):
|
| 18 |
+
result = add_resource_prefix("resource://path/to/resource", "prefix")
|
| 19 |
+
assert result == "prefix+resource://path/to/resource"
|
| 20 |
+
|
| 21 |
+
# Empty prefix should return the original URI
|
| 22 |
+
result = add_resource_prefix("resource://path/to/resource", "")
|
| 23 |
+
assert result == "resource://path/to/resource"
|
| 24 |
+
|
| 25 |
+
def test_remove_resource_prefix_legacy(self):
|
| 26 |
+
"""Test that remove_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'."""
|
| 27 |
+
with temporary_settings(resource_prefix_format="protocol"):
|
| 28 |
+
result = remove_resource_prefix(
|
| 29 |
+
"prefix+resource://path/to/resource", "prefix"
|
| 30 |
+
)
|
| 31 |
+
assert result == "resource://path/to/resource"
|
| 32 |
+
|
| 33 |
+
# URI without the prefix should be returned as is
|
| 34 |
+
result = remove_resource_prefix("resource://path/to/resource", "prefix")
|
| 35 |
+
assert result == "resource://path/to/resource"
|
| 36 |
+
|
| 37 |
+
# Empty prefix should return the original URI
|
| 38 |
+
result = remove_resource_prefix("resource://path/to/resource", "")
|
| 39 |
+
assert result == "resource://path/to/resource"
|
| 40 |
+
|
| 41 |
+
def test_has_resource_prefix_legacy(self):
|
| 42 |
+
"""Test that has_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'."""
|
| 43 |
+
with temporary_settings(resource_prefix_format="protocol"):
|
| 44 |
+
result = has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
| 45 |
+
assert result is True
|
| 46 |
+
|
| 47 |
+
result = has_resource_prefix("resource://path/to/resource", "prefix")
|
| 48 |
+
assert result is False
|
| 49 |
+
|
| 50 |
+
# Empty prefix should always return False
|
| 51 |
+
result = has_resource_prefix("resource://path/to/resource", "")
|
| 52 |
+
assert result is False
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
async def test_mount_with_legacy_prefixes():
|
| 56 |
+
"""Test mounting a server with legacy resource prefixes."""
|
| 57 |
+
with temporary_settings(resource_prefix_format="protocol"):
|
| 58 |
+
main_server = FastMCP("MainServer")
|
| 59 |
+
sub_server = FastMCP("SubServer")
|
| 60 |
+
|
| 61 |
+
@sub_server.resource("resource://test")
|
| 62 |
+
def get_test():
|
| 63 |
+
return "test content"
|
| 64 |
+
|
| 65 |
+
# Mount the server with a prefix
|
| 66 |
+
main_server.mount("sub", sub_server)
|
| 67 |
+
|
| 68 |
+
# Check that the resource is prefixed using the legacy format
|
| 69 |
+
resources = await main_server.get_resources()
|
| 70 |
+
|
| 71 |
+
# In legacy format, the key would be "sub+resource://test"
|
| 72 |
+
assert "sub+resource://test" in resources
|
| 73 |
+
|
| 74 |
+
# Test accessing the resource through client
|
| 75 |
+
async with Client(main_server) as client:
|
| 76 |
+
result = await client.read_resource("sub+resource://test")
|
| 77 |
+
# Different content types might be returned, but we just want to verify we got something
|
| 78 |
+
assert len(result) > 0
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
async def test_import_server_with_legacy_prefixes():
|
| 82 |
+
"""Test importing a server with legacy resource prefixes."""
|
| 83 |
+
with temporary_settings(resource_prefix_format="protocol"):
|
| 84 |
+
main_server = FastMCP("MainServer")
|
| 85 |
+
sub_server = FastMCP("SubServer")
|
| 86 |
+
|
| 87 |
+
@sub_server.resource("resource://test")
|
| 88 |
+
def get_test():
|
| 89 |
+
return "test content"
|
| 90 |
+
|
| 91 |
+
# Import the server with a prefix
|
| 92 |
+
await main_server.import_server("sub", sub_server)
|
| 93 |
+
|
| 94 |
+
# Check that the resource is prefixed using the legacy format
|
| 95 |
+
resources = main_server._resource_manager.get_resources()
|
| 96 |
+
|
| 97 |
+
# In legacy format, the key would be "sub+resource://test"
|
| 98 |
+
assert "sub+resource://test" in resources
|
tests/server/test_resource_prefix_formats.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for different resource prefix formats in server mounting and importing."""
|
| 2 |
+
|
| 3 |
+
from fastmcp import FastMCP
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
async def test_resource_prefix_format_in_constructor():
|
| 7 |
+
"""Test that the resource_prefix_format parameter is respected in the constructor."""
|
| 8 |
+
server_path = FastMCP("PathFormat", resource_prefix_format="path")
|
| 9 |
+
server_protocol = FastMCP("ProtocolFormat", resource_prefix_format="protocol")
|
| 10 |
+
|
| 11 |
+
# Check that the format is stored correctly
|
| 12 |
+
assert server_path.resource_prefix_format == "path"
|
| 13 |
+
assert server_protocol.resource_prefix_format == "protocol"
|
| 14 |
+
|
| 15 |
+
# Register resources
|
| 16 |
+
@server_path.resource("resource://test")
|
| 17 |
+
def get_test_path():
|
| 18 |
+
return "test content"
|
| 19 |
+
|
| 20 |
+
@server_protocol.resource("resource://test")
|
| 21 |
+
def get_test_protocol():
|
| 22 |
+
return "test content"
|
| 23 |
+
|
| 24 |
+
# Create mount servers
|
| 25 |
+
main_server_path = FastMCP("MainPath", resource_prefix_format="path")
|
| 26 |
+
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 27 |
+
|
| 28 |
+
# Mount the servers
|
| 29 |
+
main_server_path.mount("sub", server_path)
|
| 30 |
+
main_server_protocol.mount("sub", server_protocol)
|
| 31 |
+
|
| 32 |
+
# Check that the resources are prefixed correctly
|
| 33 |
+
path_resources = await main_server_path.get_resources()
|
| 34 |
+
protocol_resources = await main_server_protocol.get_resources()
|
| 35 |
+
|
| 36 |
+
# Path format should be resource://sub/test
|
| 37 |
+
assert "resource://sub/test" in path_resources
|
| 38 |
+
# Protocol format should be sub+resource://test
|
| 39 |
+
assert "sub+resource://test" in protocol_resources
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def test_resource_prefix_format_in_import_server():
|
| 43 |
+
"""Test that the resource_prefix_format parameter is respected in import_server."""
|
| 44 |
+
server = FastMCP("TestServer")
|
| 45 |
+
|
| 46 |
+
@server.resource("resource://test")
|
| 47 |
+
def get_test():
|
| 48 |
+
return "test content"
|
| 49 |
+
|
| 50 |
+
# Import with path format
|
| 51 |
+
main_server_path = FastMCP("MainPath", resource_prefix_format="path")
|
| 52 |
+
await main_server_path.import_server("sub", server)
|
| 53 |
+
|
| 54 |
+
# Import with protocol format
|
| 55 |
+
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 56 |
+
await main_server_protocol.import_server("sub", server)
|
| 57 |
+
|
| 58 |
+
# Check that the resources are prefixed correctly
|
| 59 |
+
path_resources = main_server_path._resource_manager.get_resources()
|
| 60 |
+
protocol_resources = main_server_protocol._resource_manager.get_resources()
|
| 61 |
+
|
| 62 |
+
# Path format should be resource://sub/test
|
| 63 |
+
assert "resource://sub/test" in path_resources
|
| 64 |
+
# Protocol format should be sub+resource://test
|
| 65 |
+
assert "sub+resource://test" in protocol_resources
|