Jeremiah Lowin commited on
Commit
ba3b96f
·
2 Parent(s): ac3212f4d747d9

Merge branch 'main' into middleware-2

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
@@ -349,11 +349,11 @@ class FastMCP(Generic[LifespanResultT]):
349
  server_tools = await mounted_server.server.get_tools()
350
  # Apply prefix to each tool key if prefix exists and is not empty
351
  if mounted_server.prefix:
352
- server_tools = {
353
- f"{mounted_server.prefix}_{key}": tool
354
- for key, tool in server_tools.items()
355
- }
356
- tools.update(server_tools)
357
  except Exception as e:
358
  logger.warning(
359
  f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
@@ -380,15 +380,17 @@ class FastMCP(Generic[LifespanResultT]):
380
  server_resources = await mounted_server.server.get_resources()
381
  # Apply prefix to each resource key if prefix exists
382
  if mounted_server.prefix:
383
- server_resources = {
384
- add_resource_prefix(
385
- key,
386
- mounted_server.prefix,
387
- mounted_server.server.resource_prefix_format,
388
- ): resource
389
- for key, resource in server_resources.items()
390
- }
391
- resources.update(server_resources)
 
 
392
  except Exception as e:
393
  logger.warning(
394
  f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
@@ -419,15 +421,17 @@ class FastMCP(Generic[LifespanResultT]):
419
  )
420
  # Apply prefix to each template key if prefix exists
421
  if mounted_server.prefix:
422
- server_templates = {
423
- add_resource_prefix(
424
- key,
425
- mounted_server.prefix,
426
- mounted_server.server.resource_prefix_format,
427
- ): template
428
- for key, template in server_templates.items()
429
- }
430
- templates.update(server_templates)
 
 
431
  except Exception as e:
432
  logger.warning(
433
  "Failed to get resource templates from mounted server "
@@ -448,6 +452,7 @@ class FastMCP(Generic[LifespanResultT]):
448
  """
449
  List all available prompts.
450
  """
 
451
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
452
  prompts: dict[str, Prompt] = {}
453
 
@@ -457,11 +462,13 @@ class FastMCP(Generic[LifespanResultT]):
457
  server_prompts = await mounted_server.server.get_prompts()
458
  # Apply prefix to each prompt key if prefix exists
459
  if mounted_server.prefix:
460
- server_prompts = {
461
- f"{mounted_server.prefix}_{key}": prompt
462
- for key, prompt in server_prompts.items()
463
- }
464
- prompts.update(server_prompts)
 
 
465
  except Exception as e:
466
  logger.warning(
467
  f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
@@ -824,12 +831,12 @@ class FastMCP(Generic[LifespanResultT]):
824
  if has_resource_prefix(
825
  str(resource_uri),
826
  mounted_server.prefix,
827
- mounted_server.server.resource_prefix_format,
828
  ):
829
  resource_uri = remove_resource_prefix(
830
  str(resource_uri),
831
  mounted_server.prefix,
832
- mounted_server.server.resource_prefix_format,
833
  )
834
  else:
835
  continue
 
349
  server_tools = await mounted_server.server.get_tools()
350
  # Apply prefix to each tool key if prefix exists and is not empty
351
  if mounted_server.prefix:
352
+ for tool in server_tools.values():
353
+ tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
354
+ tools[tool.key] = tool
355
+ else:
356
+ tools.update(server_tools)
357
  except Exception as e:
358
  logger.warning(
359
  f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
 
380
  server_resources = await mounted_server.server.get_resources()
381
  # Apply prefix to each resource key if prefix exists
382
  if mounted_server.prefix:
383
+ for resource in server_resources.values():
384
+ resource = resource.with_key(
385
+ add_resource_prefix(
386
+ resource.key,
387
+ mounted_server.prefix,
388
+ self.resource_prefix_format,
389
+ )
390
+ )
391
+ resources[resource.key] = resource
392
+ else:
393
+ resources.update(server_resources)
394
  except Exception as e:
395
  logger.warning(
396
  f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
 
421
  )
422
  # Apply prefix to each template key if prefix exists
423
  if mounted_server.prefix:
424
+ for template in server_templates.values():
425
+ template = template.with_key(
426
+ add_resource_prefix(
427
+ template.key,
428
+ mounted_server.prefix,
429
+ self.resource_prefix_format,
430
+ )
431
+ )
432
+ templates[template.key] = template
433
+ else:
434
+ templates.update(server_templates)
435
  except Exception as e:
436
  logger.warning(
437
  "Failed to get resource templates from mounted server "
 
452
  """
453
  List all available prompts.
454
  """
455
+
456
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
457
  prompts: dict[str, Prompt] = {}
458
 
 
462
  server_prompts = await mounted_server.server.get_prompts()
463
  # Apply prefix to each prompt key if prefix exists
464
  if mounted_server.prefix:
465
+ for prompt in server_prompts.values():
466
+ prompt = prompt.with_key(
467
+ f"{mounted_server.prefix}_{prompt.key}"
468
+ )
469
+ prompts[prompt.key] = prompt
470
+ else:
471
+ prompts.update(server_prompts)
472
  except Exception as e:
473
  logger.warning(
474
  f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
 
831
  if has_resource_prefix(
832
  str(resource_uri),
833
  mounted_server.prefix,
834
+ self.resource_prefix_format,
835
  ):
836
  resource_uri = remove_resource_prefix(
837
  str(resource_uri),
838
  mounted_server.prefix,
839
+ self.resource_prefix_format,
840
  )
841
  else:
842
  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)