Jeremiah Lowin commited on
Commit
0fdb55c
·
1 Parent(s): 7c08231

Add key to component

Browse files
src/fastmcp/resources/resource.py CHANGED
@@ -101,6 +101,16 @@ class Resource(FastMCPComponent, abc.ABC):
101
  def __repr__(self) -> str:
102
  return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
103
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  class FunctionResource(Resource):
106
  """A resource that defers data loading by wrapping a function.
 
101
  def __repr__(self) -> str:
102
  return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
103
 
104
+ @property
105
+ def key(self) -> str:
106
+ """
107
+ The key of the component. This is used for internal bookkeeping
108
+ and may reflect e.g. prefixes or other identifiers. You should not depend on
109
+ keys having a certain value, as the same tool loaded from different
110
+ hierarchies of servers may have different keys.
111
+ """
112
+ return self._key or str(self.uri)
113
+
114
 
115
  class FunctionResource(Resource):
116
  """A resource that defers data loading by wrapping a function.
src/fastmcp/resources/template.py CHANGED
@@ -128,6 +128,16 @@ class ResourceTemplate(FastMCPComponent):
128
  }
129
  return MCPResourceTemplate(**kwargs | overrides)
130
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  class FunctionResourceTemplate(ResourceTemplate):
133
  """A template for dynamically creating resources."""
 
128
  }
129
  return MCPResourceTemplate(**kwargs | overrides)
130
 
131
+ @property
132
+ def key(self) -> str:
133
+ """
134
+ The key of the component. This is used for internal bookkeeping
135
+ and may reflect e.g. prefixes or other identifiers. You should not depend on
136
+ keys having a certain value, as the same tool loaded from different
137
+ hierarchies of servers may have different keys.
138
+ """
139
+ return self._key or self.uri_template
140
+
141
 
142
  class FunctionResourceTemplate(ResourceTemplate):
143
  """A template for dynamically creating resources."""
src/fastmcp/server/server.py CHANGED
@@ -330,11 +330,11 @@ class FastMCP(Generic[LifespanResultT]):
330
  server_tools = await mounted_server.server.get_tools()
331
  # Apply prefix to each tool key if prefix exists and is not empty
332
  if mounted_server.prefix:
333
- server_tools = {
334
- f"{mounted_server.prefix}_{key}": tool
335
- for key, tool in server_tools.items()
336
- }
337
- tools.update(server_tools)
338
  except Exception as e:
339
  logger.warning(
340
  f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
@@ -361,15 +361,17 @@ class FastMCP(Generic[LifespanResultT]):
361
  server_resources = await mounted_server.server.get_resources()
362
  # Apply prefix to each resource key if prefix exists
363
  if mounted_server.prefix:
364
- server_resources = {
365
- add_resource_prefix(
366
- key,
367
- mounted_server.prefix,
368
- mounted_server.server.resource_prefix_format,
369
- ): resource
370
- for key, resource in server_resources.items()
371
- }
372
- resources.update(server_resources)
 
 
373
  except Exception as e:
374
  logger.warning(
375
  f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
@@ -400,15 +402,17 @@ class FastMCP(Generic[LifespanResultT]):
400
  )
401
  # Apply prefix to each template key if prefix exists
402
  if mounted_server.prefix:
403
- server_templates = {
404
- add_resource_prefix(
405
- key,
406
- mounted_server.prefix,
407
- mounted_server.server.resource_prefix_format,
408
- ): template
409
- for key, template in server_templates.items()
410
- }
411
- templates.update(server_templates)
 
 
412
  except Exception as e:
413
  logger.warning(
414
  "Failed to get resource templates from mounted server "
@@ -429,6 +433,7 @@ class FastMCP(Generic[LifespanResultT]):
429
  """
430
  List all available prompts.
431
  """
 
432
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
433
  prompts: dict[str, Prompt] = {}
434
 
@@ -438,11 +443,13 @@ class FastMCP(Generic[LifespanResultT]):
438
  server_prompts = await mounted_server.server.get_prompts()
439
  # Apply prefix to each prompt key if prefix exists
440
  if mounted_server.prefix:
441
- server_prompts = {
442
- f"{mounted_server.prefix}_{key}": prompt
443
- for key, prompt in server_prompts.items()
444
- }
445
- prompts.update(server_prompts)
 
 
446
  except Exception as e:
447
  logger.warning(
448
  f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
@@ -524,6 +531,7 @@ class FastMCP(Generic[LifespanResultT]):
524
 
525
  """
526
  resources = await self.get_resources()
 
527
  mcp_resources: list[MCPResource] = []
528
  for key, resource in resources.items():
529
  if self._should_enable_component(resource):
@@ -666,12 +674,12 @@ class FastMCP(Generic[LifespanResultT]):
666
  if has_resource_prefix(
667
  str(resource_uri),
668
  mounted_server.prefix,
669
- mounted_server.server.resource_prefix_format,
670
  ):
671
  resource_uri = remove_resource_prefix(
672
  str(resource_uri),
673
  mounted_server.prefix,
674
- mounted_server.server.resource_prefix_format,
675
  )
676
  else:
677
  continue
 
330
  server_tools = await mounted_server.server.get_tools()
331
  # Apply prefix to each tool key if prefix exists and is not empty
332
  if mounted_server.prefix:
333
+ for tool in server_tools.values():
334
+ tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
335
+ tools[tool.key] = tool
336
+ else:
337
+ tools.update(server_tools)
338
  except Exception as e:
339
  logger.warning(
340
  f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
 
361
  server_resources = await mounted_server.server.get_resources()
362
  # Apply prefix to each resource key if prefix exists
363
  if mounted_server.prefix:
364
+ for resource in server_resources.values():
365
+ resource = resource.with_key(
366
+ add_resource_prefix(
367
+ resource.key,
368
+ mounted_server.prefix,
369
+ self.resource_prefix_format,
370
+ )
371
+ )
372
+ resources[resource.key] = resource
373
+ else:
374
+ resources.update(server_resources)
375
  except Exception as e:
376
  logger.warning(
377
  f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
 
402
  )
403
  # Apply prefix to each template key if prefix exists
404
  if mounted_server.prefix:
405
+ for template in server_templates.values():
406
+ template = template.with_key(
407
+ add_resource_prefix(
408
+ template.key,
409
+ mounted_server.prefix,
410
+ self.resource_prefix_format,
411
+ )
412
+ )
413
+ templates[template.key] = template
414
+ else:
415
+ templates.update(server_templates)
416
  except Exception as e:
417
  logger.warning(
418
  "Failed to get resource templates from mounted server "
 
433
  """
434
  List all available prompts.
435
  """
436
+
437
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
438
  prompts: dict[str, Prompt] = {}
439
 
 
443
  server_prompts = await mounted_server.server.get_prompts()
444
  # Apply prefix to each prompt key if prefix exists
445
  if mounted_server.prefix:
446
+ for prompt in server_prompts.values():
447
+ prompt = prompt.with_key(
448
+ f"{mounted_server.prefix}_{prompt.key}"
449
+ )
450
+ prompts[prompt.key] = prompt
451
+ else:
452
+ prompts.update(server_prompts)
453
  except Exception as e:
454
  logger.warning(
455
  f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
 
531
 
532
  """
533
  resources = await self.get_resources()
534
+
535
  mcp_resources: list[MCPResource] = []
536
  for key, resource in resources.items():
537
  if self._should_enable_component(resource):
 
674
  if has_resource_prefix(
675
  str(resource_uri),
676
  mounted_server.prefix,
677
+ self.resource_prefix_format,
678
  ):
679
  resource_uri = remove_resource_prefix(
680
  str(resource_uri),
681
  mounted_server.prefix,
682
+ self.resource_prefix_format,
683
  )
684
  else:
685
  continue
src/fastmcp/utilities/components.py CHANGED
@@ -1,7 +1,8 @@
1
  from collections.abc import Sequence
2
- from typing import Annotated, TypeVar
3
 
4
- from pydantic import BeforeValidator, Field
 
5
 
6
  from fastmcp.utilities.types import FastMCPBaseModel
7
 
@@ -37,6 +38,25 @@ class FastMCPComponent(FastMCPBaseModel):
37
  description="Whether the component is enabled.",
38
  )
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def __eq__(self, other: object) -> bool:
41
  if type(self) is not type(other):
42
  return False
 
1
  from collections.abc import Sequence
2
+ from typing import Annotated, Any, TypeVar
3
 
4
+ from pydantic import BeforeValidator, Field, PrivateAttr
5
+ from typing_extensions import Self
6
 
7
  from fastmcp.utilities.types import FastMCPBaseModel
8
 
 
38
  description="Whether the component is enabled.",
39
  )
40
 
41
+ _key: str | None = PrivateAttr()
42
+
43
+ def __init__(self, *, key: str | None = None, **kwargs: Any) -> None:
44
+ super().__init__(**kwargs)
45
+ self._key = key
46
+
47
+ @property
48
+ def key(self) -> str:
49
+ """
50
+ The key of the component. This is used for internal bookkeeping
51
+ and may reflect e.g. prefixes or other identifiers. You should not depend on
52
+ keys having a certain value, as the same tool loaded from different
53
+ hierarchies of servers may have different keys.
54
+ """
55
+ return self._key or self.name
56
+
57
+ def with_key(self, key: str) -> Self:
58
+ return self.model_copy(update={"_key": key})
59
+
60
  def __eq__(self, other: object) -> bool:
61
  if type(self) is not type(other):
62
  return False
test_revert_check.py DELETED
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Quick test to verify the revert worked correctly."""
3
-
4
- import asyncio
5
-
6
- from fastmcp import FastMCP
7
- from fastmcp.client import Client
8
-
9
-
10
- async def test_empty_prefix_behavior():
11
- """Test that empty prefix correctly adds underscore."""
12
-
13
- main_app = FastMCP("MainApp")
14
- sub_app = FastMCP("SubApp")
15
-
16
- @sub_app.tool
17
- def sub_tool() -> str:
18
- return "This is from the sub app"
19
-
20
- @sub_app.resource("data://test")
21
- def sub_resource():
22
- return "Resource data"
23
-
24
- # Mount with empty prefix
25
- main_app.mount("", sub_app)
26
-
27
- # Check that tools have underscore prefix
28
- tools = await main_app.get_tools()
29
- print(f"Tools: {list(tools.keys())}")
30
- assert "_sub_tool" in tools, f"Expected '_sub_tool' in {list(tools.keys())}"
31
-
32
- # Check that resources work correctly
33
- resources = await main_app.get_resources()
34
- print(f"Resources: {list(resources.keys())}")
35
- # Empty prefix for resources should result in no prefix change
36
- assert "data://test" in resources, (
37
- f"Expected 'data://test' in {list(resources.keys())}"
38
- )
39
-
40
- # Test calling the tool
41
- async with Client(main_app) as client:
42
- result = await client.call_tool("_sub_tool", {})
43
- print(f"Tool result: {result[0].text}")
44
- assert "This is from the sub app" in result[0].text
45
-
46
- print("✅ Empty prefix correctly adds underscore for tools!")
47
-
48
-
49
- if __name__ == "__main__":
50
- asyncio.run(test_empty_prefix_behavior())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/server/test_mount.py CHANGED
@@ -272,9 +272,7 @@ class TestMultipleServerMount:
272
  main_app.mount(working_app, "working")
273
 
274
  # Use an unreachable port
275
- unreachable_client = Client(
276
- transport=SSETransport("http://127.0.0.1:99999/sse")
277
- )
278
 
279
  # Create a proxy server that will fail to connect
280
  unreachable_proxy = FastMCP.as_proxy(unreachable_client)
 
272
  main_app.mount(working_app, "working")
273
 
274
  # Use an unreachable port
275
+ unreachable_client = Client(transport=SSETransport("http://127.0.0.1:9999/sse"))
 
 
276
 
277
  # Create a proxy server that will fail to connect
278
  unreachable_proxy = FastMCP.as_proxy(unreachable_client)