Jeremiah Lowin commited on
Commit
9924ac9
·
unverified ·
2 Parent(s): de817a5239377d

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
- <Warning>
185
- Some MCP clients (like Claude Desktop) might have restrictions on characters allowed in tool names. FastMCP uses standard naming conventions: tools and prompts are prefixed with `{prefix}_` (e.g., "weather_forecast"), and resources use the format `protocol://{prefix}/path` (e.g., "data://weather/forecast").
186
- </Warning>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(key, self.prefix): resource
 
 
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(key, self.prefix): template
 
 
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(key, self.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(uri: str, prefix: str) -> str:
 
 
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
- # Split the URI into protocol and path
1320
- match = URI_PATTERN.match(uri)
1321
- if not match:
1322
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
1323
 
1324
- protocol, path = match.groups()
 
1325
 
1326
- # Add the prefix to the path
1327
- return f"{protocol}{prefix}/{path}"
 
 
 
 
 
 
 
 
 
 
 
1328
 
 
 
 
 
1329
 
1330
- def remove_resource_prefix(uri: str, prefix: str) -> str:
 
 
 
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
- # Split the URI into protocol and path
1353
- match = URI_PATTERN.match(uri)
1354
- if not match:
1355
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
1356
 
1357
- protocol, path = match.groups()
1358
-
1359
- # Check if the path starts with the prefix followed by a /
1360
- prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
1361
- path_match = re.match(prefix_pattern, path)
1362
- if not path_match:
1363
  return uri
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1364
 
1365
- # Return the URI without the prefix
1366
- return f"{protocol}{path_match.group(1)}"
 
 
1367
 
1368
 
1369
- def has_resource_prefix(uri: str, prefix: str) -> bool:
 
 
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
- # Split the URI into protocol and path
1392
- match = URI_PATTERN.match(uri)
1393
- if not match:
1394
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
 
 
 
 
 
 
 
 
 
 
 
 
 
1395
 
1396
- _, path = match.groups()
1397
 
1398
- # Check if the path starts with the prefix followed by a /
1399
- prefix_pattern = f"^{re.escape(prefix)}/"
1400
- return bool(re.match(prefix_pattern, path))
 
 
 
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