Jeremiah Lowin commited on
Commit
b45adb7
·
1 Parent(s): 10ef66b

Make resource prefix format configurable

Browse files
docs/servers/composition.mdx CHANGED
@@ -177,6 +177,49 @@ remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp"))
177
  main_server.mount("remote", remote_proxy)
178
  ```
179
 
180
- <Warning>
181
- 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").
182
- </Warning>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  main_server.mount("remote", remote_proxy)
178
  ```
179
 
180
+
181
+
182
+ ## Resource Prefix Formats
183
+
184
+ When mounting or importing servers, resource URIs are usually prefixed to avoid naming conflicts. FastMCP supports two different formats for resource prefixes:
185
+
186
+ ### Path Format (Default)
187
+
188
+ In path format, prefixes are added to the path component of the URI:
189
+
190
+ ```
191
+ resource://prefix/path/to/resource
192
+ ```
193
+
194
+ 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).
195
+
196
+ ### Protocol Format (Legacy)
197
+
198
+ In protocol format, prefixes are added as part of the protocol:
199
+
200
+ ```
201
+ prefix+resource://path/to/resource
202
+ ```
203
+
204
+ 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.
205
+
206
+ ### Configuring the Prefix Format
207
+
208
+ You can configure the prefix format globally:
209
+
210
+ ```python
211
+ from fastmcp import settings
212
+ settings.settings.resource_prefix_format = "protocol" # Switch to legacy format
213
+ ```
214
+
215
+ Or per-server:
216
+
217
+ ```python
218
+ # Create a server that uses legacy protocol format
219
+ server = FastMCP("LegacyServer", resource_prefix_format="protocol")
220
+
221
+ # Create a server that uses new path format
222
+ server = FastMCP("NewServer", resource_prefix_format="path")
223
+ ```
224
+
225
+ When mounting or importing servers, the prefix format of the parent server is used.
src/fastmcp/server/server.py CHANGED
@@ -123,6 +123,7 @@ class FastMCP(Generic[LifespanResultT]):
123
  on_duplicate_tools: DuplicateBehavior | None = None,
124
  on_duplicate_resources: DuplicateBehavior | None = None,
125
  on_duplicate_prompts: DuplicateBehavior | None = None,
 
126
  **settings: Any,
127
  ):
128
  if settings:
@@ -137,6 +138,14 @@ class FastMCP(Generic[LifespanResultT]):
137
  )
138
  self.settings = fastmcp.settings.ServerSettings(**settings)
139
 
 
 
 
 
 
 
 
 
140
  self.tags: set[str] = tags or set()
141
  self.dependencies = dependencies
142
  self._cache = TimedCache(
@@ -1109,11 +1118,11 @@ class FastMCP(Generic[LifespanResultT]):
1109
 
1110
  # Import resources and templates from the mounted server
1111
  for key, resource in (await server.get_resources()).items():
1112
- prefixed_key = add_resource_prefix(key, prefix)
1113
  self._resource_manager.add_resource(resource, key=prefixed_key)
1114
 
1115
  for key, template in (await server.get_resource_templates()).items():
1116
- prefixed_key = add_resource_prefix(key, prefix)
1117
  self._resource_manager.add_template(template, key=prefixed_key)
1118
 
1119
  # Import prompts from the mounted server
@@ -1258,14 +1267,18 @@ class MountedServer:
1258
  async def get_resources(self) -> dict[str, Resource]:
1259
  resources = await self.server.get_resources()
1260
  return {
1261
- add_resource_prefix(key, self.prefix): resource
 
 
1262
  for key, resource in resources.items()
1263
  }
1264
 
1265
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1266
  templates = await self.server.get_resource_templates()
1267
  return {
1268
- add_resource_prefix(key, self.prefix): template
 
 
1269
  for key, template in templates.items()
1270
  }
1271
 
@@ -1280,10 +1293,12 @@ class MountedServer:
1280
  return key.removeprefix(f"{self.prefix}_")
1281
 
1282
  def match_resource(self, key: str) -> bool:
1283
- return has_resource_prefix(key, self.prefix)
1284
 
1285
  def strip_resource_prefix(self, key: str) -> str:
1286
- return remove_resource_prefix(key, self.prefix)
 
 
1287
 
1288
  def match_prompt(self, key: str) -> bool:
1289
  return key.startswith(f"{self.prefix}_")
@@ -1292,7 +1307,9 @@ class MountedServer:
1292
  return key.removeprefix(f"{self.prefix}_")
1293
 
1294
 
1295
- def add_resource_prefix(uri: str, prefix: str) -> str:
 
 
1296
  """Add a prefix to a resource URI.
1297
 
1298
  Args:
@@ -1304,9 +1321,11 @@ def add_resource_prefix(uri: str, prefix: str) -> str:
1304
 
1305
  Examples:
1306
  >>> add_resource_prefix("resource://path/to/resource", "prefix")
1307
- "resource://prefix/path/to/resource"
 
 
1308
  >>> add_resource_prefix("resource:///absolute/path", "prefix")
1309
- "resource://prefix//absolute/path"
1310
 
1311
  Raises:
1312
  ValueError: If the URI doesn't match the expected protocol://path format
@@ -1314,32 +1333,50 @@ def add_resource_prefix(uri: str, prefix: str) -> str:
1314
  if not prefix:
1315
  return uri
1316
 
1317
- # Split the URI into protocol and path
1318
- match = URI_PATTERN.match(uri)
1319
- if not match:
1320
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
1321
 
1322
- protocol, path = match.groups()
 
1323
 
1324
- # Add the prefix to the path
1325
- return f"{protocol}{prefix}/{path}"
 
 
 
 
 
 
 
 
 
 
 
1326
 
 
 
 
 
1327
 
1328
- def remove_resource_prefix(uri: str, prefix: str) -> str:
 
 
 
1329
  """Remove a prefix from a resource URI.
1330
 
1331
  Args:
1332
  uri: The resource URI with a prefix
1333
  prefix: The prefix to remove
1334
-
1335
  Returns:
1336
  The resource URI with the prefix removed
1337
 
1338
  Examples:
1339
  >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
1340
- "resource://path/to/resource"
 
 
1341
  >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
1342
- "resource:///absolute/path"
1343
 
1344
  Raises:
1345
  ValueError: If the URI doesn't match the expected protocol://path format
@@ -1347,24 +1384,41 @@ def remove_resource_prefix(uri: str, prefix: str) -> str:
1347
  if not prefix:
1348
  return uri
1349
 
1350
- # Split the URI into protocol and path
1351
- match = URI_PATTERN.match(uri)
1352
- if not match:
1353
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
1354
 
1355
- protocol, path = match.groups()
1356
-
1357
- # Check if the path starts with the prefix followed by a /
1358
- prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
1359
- path_match = re.match(prefix_pattern, path)
1360
- if not path_match:
1361
  return uri
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1362
 
1363
- # Return the URI without the prefix
1364
- return f"{protocol}{path_match.group(1)}"
 
 
1365
 
1366
 
1367
- def has_resource_prefix(uri: str, prefix: str) -> bool:
 
 
1368
  """Check if a resource URI has a specific prefix.
1369
 
1370
  Args:
@@ -1376,7 +1430,9 @@ def has_resource_prefix(uri: str, prefix: str) -> bool:
1376
 
1377
  Examples:
1378
  >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
1379
- True
 
 
1380
  >>> has_resource_prefix("resource://other/path/to/resource", "prefix")
1381
  False
1382
 
@@ -1386,13 +1442,28 @@ def has_resource_prefix(uri: str, prefix: str) -> bool:
1386
  if not prefix:
1387
  return False
1388
 
1389
- # Split the URI into protocol and path
1390
- match = URI_PATTERN.match(uri)
1391
- if not match:
1392
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
 
 
 
 
 
 
 
 
 
 
 
 
 
1393
 
1394
- _, path = match.groups()
1395
 
1396
- # Check if the path starts with the prefix followed by a /
1397
- prefix_pattern = f"^{re.escape(prefix)}/"
1398
- return bool(re.match(prefix_pattern, path))
 
 
 
123
  on_duplicate_tools: DuplicateBehavior | None = None,
124
  on_duplicate_resources: DuplicateBehavior | None = None,
125
  on_duplicate_prompts: DuplicateBehavior | None = None,
126
+ resource_prefix_format: Literal["protocol", "path"] | None = None,
127
  **settings: Any,
128
  ):
129
  if settings:
 
138
  )
139
  self.settings = fastmcp.settings.ServerSettings(**settings)
140
 
141
+ self.resource_prefix_format: Literal["protocol", "path"]
142
+ if resource_prefix_format is None:
143
+ self.resource_prefix_format = (
144
+ fastmcp.settings.settings.resource_prefix_format
145
+ )
146
+ else:
147
+ self.resource_prefix_format = resource_prefix_format
148
+
149
  self.tags: set[str] = tags or set()
150
  self.dependencies = dependencies
151
  self._cache = TimedCache(
 
1118
 
1119
  # Import resources and templates from the mounted server
1120
  for key, resource in (await server.get_resources()).items():
1121
+ prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1122
  self._resource_manager.add_resource(resource, key=prefixed_key)
1123
 
1124
  for key, template in (await server.get_resource_templates()).items():
1125
+ prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1126
  self._resource_manager.add_template(template, key=prefixed_key)
1127
 
1128
  # Import prompts from the mounted server
 
1267
  async def get_resources(self) -> dict[str, Resource]:
1268
  resources = await self.server.get_resources()
1269
  return {
1270
+ add_resource_prefix(
1271
+ key, self.prefix, self.server.resource_prefix_format
1272
+ ): resource
1273
  for key, resource in resources.items()
1274
  }
1275
 
1276
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1277
  templates = await self.server.get_resource_templates()
1278
  return {
1279
+ add_resource_prefix(
1280
+ key, self.prefix, self.server.resource_prefix_format
1281
+ ): template
1282
  for key, template in templates.items()
1283
  }
1284
 
 
1293
  return key.removeprefix(f"{self.prefix}_")
1294
 
1295
  def match_resource(self, key: str) -> bool:
1296
+ return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
1297
 
1298
  def strip_resource_prefix(self, key: str) -> str:
1299
+ return remove_resource_prefix(
1300
+ key, self.prefix, self.server.resource_prefix_format
1301
+ )
1302
 
1303
  def match_prompt(self, key: str) -> bool:
1304
  return key.startswith(f"{self.prefix}_")
 
1307
  return key.removeprefix(f"{self.prefix}_")
1308
 
1309
 
1310
+ def add_resource_prefix(
1311
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1312
+ ) -> str:
1313
  """Add a prefix to a resource URI.
1314
 
1315
  Args:
 
1321
 
1322
  Examples:
1323
  >>> add_resource_prefix("resource://path/to/resource", "prefix")
1324
+ "resource://prefix/path/to/resource" # with new style
1325
+ >>> add_resource_prefix("resource://path/to/resource", "prefix")
1326
+ "prefix+resource://path/to/resource" # with legacy style
1327
  >>> add_resource_prefix("resource:///absolute/path", "prefix")
1328
+ "resource://prefix//absolute/path" # with new style
1329
 
1330
  Raises:
1331
  ValueError: If the URI doesn't match the expected protocol://path format
 
1333
  if not prefix:
1334
  return uri
1335
 
1336
+ # Get the server settings to check for legacy format preference
 
 
 
1337
 
1338
+ if prefix_format is None:
1339
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
1340
 
1341
+ if prefix_format == "protocol":
1342
+ # Legacy style: prefix+protocol://path
1343
+ return f"{prefix}+{uri}"
1344
+ elif prefix_format == "path":
1345
+ # New style: protocol://prefix/path
1346
+ # Split the URI into protocol and path
1347
+ match = URI_PATTERN.match(uri)
1348
+ if not match:
1349
+ raise ValueError(
1350
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1351
+ )
1352
+
1353
+ protocol, path = match.groups()
1354
 
1355
+ # Add the prefix to the path
1356
+ return f"{protocol}{prefix}/{path}"
1357
+ else:
1358
+ raise ValueError(f"Invalid prefix format: {prefix_format}")
1359
 
1360
+
1361
+ def remove_resource_prefix(
1362
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1363
+ ) -> str:
1364
  """Remove a prefix from a resource URI.
1365
 
1366
  Args:
1367
  uri: The resource URI with a prefix
1368
  prefix: The prefix to remove
1369
+ prefix_format: The format of the prefix to remove
1370
  Returns:
1371
  The resource URI with the prefix removed
1372
 
1373
  Examples:
1374
  >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
1375
+ "resource://path/to/resource" # with new style
1376
+ >>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
1377
+ "resource://path/to/resource" # with legacy style
1378
  >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
1379
+ "resource:///absolute/path" # with new style
1380
 
1381
  Raises:
1382
  ValueError: If the URI doesn't match the expected protocol://path format
 
1384
  if not prefix:
1385
  return uri
1386
 
1387
+ if prefix_format is None:
1388
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
 
 
1389
 
1390
+ if prefix_format == "protocol":
1391
+ # Legacy style: prefix+protocol://path
1392
+ legacy_prefix = f"{prefix}+"
1393
+ if uri.startswith(legacy_prefix):
1394
+ return uri[len(legacy_prefix) :]
 
1395
  return uri
1396
+ elif prefix_format == "path":
1397
+ # New style: protocol://prefix/path
1398
+ # Split the URI into protocol and path
1399
+ match = URI_PATTERN.match(uri)
1400
+ if not match:
1401
+ raise ValueError(
1402
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1403
+ )
1404
+
1405
+ protocol, path = match.groups()
1406
+
1407
+ # Check if the path starts with the prefix followed by a /
1408
+ prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
1409
+ path_match = re.match(prefix_pattern, path)
1410
+ if not path_match:
1411
+ return uri
1412
 
1413
+ # Return the URI without the prefix
1414
+ return f"{protocol}{path_match.group(1)}"
1415
+ else:
1416
+ raise ValueError(f"Invalid prefix format: {prefix_format}")
1417
 
1418
 
1419
+ def has_resource_prefix(
1420
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1421
+ ) -> bool:
1422
  """Check if a resource URI has a specific prefix.
1423
 
1424
  Args:
 
1430
 
1431
  Examples:
1432
  >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
1433
+ True # with new style
1434
+ >>> has_resource_prefix("prefix+resource://path/to/resource", "prefix")
1435
+ True # with legacy style
1436
  >>> has_resource_prefix("resource://other/path/to/resource", "prefix")
1437
  False
1438
 
 
1442
  if not prefix:
1443
  return False
1444
 
1445
+ # Get the server settings to check for legacy format preference
1446
+
1447
+ if prefix_format is None:
1448
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
1449
+
1450
+ if prefix_format == "protocol":
1451
+ # Legacy style: prefix+protocol://path
1452
+ legacy_prefix = f"{prefix}+"
1453
+ return uri.startswith(legacy_prefix)
1454
+ elif prefix_format == "path":
1455
+ # New style: protocol://prefix/path
1456
+ # Split the URI into protocol and path
1457
+ match = URI_PATTERN.match(uri)
1458
+ if not match:
1459
+ raise ValueError(
1460
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1461
+ )
1462
 
1463
+ _, path = match.groups()
1464
 
1465
+ # Check if the path starts with the prefix followed by a /
1466
+ prefix_pattern = f"^{re.escape(prefix)}/"
1467
+ return bool(re.match(prefix_pattern, path))
1468
+ else:
1469
+ 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