mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Merge pull request #869 from jlowin/key
This commit is contained in:
commit
bed24d20cd
6 changed files with 81 additions and 85 deletions
|
|
@ -101,6 +101,16 @@ class Resource(FastMCPComponent, abc.ABC):
|
|||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""
|
||||
The key of the component. This is used for internal bookkeeping
|
||||
and may reflect e.g. prefixes or other identifiers. You should not depend on
|
||||
keys having a certain value, as the same tool loaded from different
|
||||
hierarchies of servers may have different keys.
|
||||
"""
|
||||
return self._key or str(self.uri)
|
||||
|
||||
|
||||
class FunctionResource(Resource):
|
||||
"""A resource that defers data loading by wrapping a function.
|
||||
|
|
|
|||
|
|
@ -128,6 +128,16 @@ class ResourceTemplate(FastMCPComponent):
|
|||
}
|
||||
return MCPResourceTemplate(**kwargs | overrides)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""
|
||||
The key of the component. This is used for internal bookkeeping
|
||||
and may reflect e.g. prefixes or other identifiers. You should not depend on
|
||||
keys having a certain value, as the same tool loaded from different
|
||||
hierarchies of servers may have different keys.
|
||||
"""
|
||||
return self._key or self.uri_template
|
||||
|
||||
|
||||
class FunctionResourceTemplate(ResourceTemplate):
|
||||
"""A template for dynamically creating resources."""
|
||||
|
|
|
|||
|
|
@ -330,11 +330,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
server_tools = await mounted_server.server.get_tools()
|
||||
# Apply prefix to each tool key if prefix exists and is not empty
|
||||
if mounted_server.prefix:
|
||||
server_tools = {
|
||||
f"{mounted_server.prefix}_{key}": tool
|
||||
for key, tool in server_tools.items()
|
||||
}
|
||||
tools.update(server_tools)
|
||||
for tool in server_tools.values():
|
||||
tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
|
||||
tools[tool.key] = tool
|
||||
else:
|
||||
tools.update(server_tools)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
|
||||
|
|
@ -361,15 +361,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
server_resources = await mounted_server.server.get_resources()
|
||||
# Apply prefix to each resource key if prefix exists
|
||||
if mounted_server.prefix:
|
||||
server_resources = {
|
||||
add_resource_prefix(
|
||||
key,
|
||||
mounted_server.prefix,
|
||||
mounted_server.server.resource_prefix_format,
|
||||
): resource
|
||||
for key, resource in server_resources.items()
|
||||
}
|
||||
resources.update(server_resources)
|
||||
for resource in server_resources.values():
|
||||
resource = resource.with_key(
|
||||
add_resource_prefix(
|
||||
resource.key,
|
||||
mounted_server.prefix,
|
||||
self.resource_prefix_format,
|
||||
)
|
||||
)
|
||||
resources[resource.key] = resource
|
||||
else:
|
||||
resources.update(server_resources)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
|
||||
|
|
@ -400,15 +402,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
# Apply prefix to each template key if prefix exists
|
||||
if mounted_server.prefix:
|
||||
server_templates = {
|
||||
add_resource_prefix(
|
||||
key,
|
||||
mounted_server.prefix,
|
||||
mounted_server.server.resource_prefix_format,
|
||||
): template
|
||||
for key, template in server_templates.items()
|
||||
}
|
||||
templates.update(server_templates)
|
||||
for template in server_templates.values():
|
||||
template = template.with_key(
|
||||
add_resource_prefix(
|
||||
template.key,
|
||||
mounted_server.prefix,
|
||||
self.resource_prefix_format,
|
||||
)
|
||||
)
|
||||
templates[template.key] = template
|
||||
else:
|
||||
templates.update(server_templates)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to get resource templates from mounted server "
|
||||
|
|
@ -429,6 +433,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
List all available prompts.
|
||||
"""
|
||||
|
||||
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
||||
prompts: dict[str, Prompt] = {}
|
||||
|
||||
|
|
@ -438,11 +443,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
server_prompts = await mounted_server.server.get_prompts()
|
||||
# Apply prefix to each prompt key if prefix exists
|
||||
if mounted_server.prefix:
|
||||
server_prompts = {
|
||||
f"{mounted_server.prefix}_{key}": prompt
|
||||
for key, prompt in server_prompts.items()
|
||||
}
|
||||
prompts.update(server_prompts)
|
||||
for prompt in server_prompts.values():
|
||||
prompt = prompt.with_key(
|
||||
f"{mounted_server.prefix}_{prompt.key}"
|
||||
)
|
||||
prompts[prompt.key] = prompt
|
||||
else:
|
||||
prompts.update(server_prompts)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
|
||||
|
|
@ -524,6 +531,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
"""
|
||||
resources = await self.get_resources()
|
||||
|
||||
mcp_resources: list[MCPResource] = []
|
||||
for key, resource in resources.items():
|
||||
if self._should_enable_component(resource):
|
||||
|
|
@ -666,12 +674,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
if has_resource_prefix(
|
||||
str(resource_uri),
|
||||
mounted_server.prefix,
|
||||
mounted_server.server.resource_prefix_format,
|
||||
self.resource_prefix_format,
|
||||
):
|
||||
resource_uri = remove_resource_prefix(
|
||||
str(resource_uri),
|
||||
mounted_server.prefix,
|
||||
mounted_server.server.resource_prefix_format,
|
||||
self.resource_prefix_format,
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Annotated, TypeVar
|
||||
from typing import Annotated, Any, TypeVar
|
||||
|
||||
from pydantic import BeforeValidator, Field
|
||||
from pydantic import BeforeValidator, Field, PrivateAttr
|
||||
from typing_extensions import Self
|
||||
|
||||
from fastmcp.utilities.types import FastMCPBaseModel
|
||||
|
||||
|
|
@ -37,6 +38,25 @@ class FastMCPComponent(FastMCPBaseModel):
|
|||
description="Whether the component is enabled.",
|
||||
)
|
||||
|
||||
_key: str | None = PrivateAttr()
|
||||
|
||||
def __init__(self, *, key: str | None = None, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._key = key
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""
|
||||
The key of the component. This is used for internal bookkeeping
|
||||
and may reflect e.g. prefixes or other identifiers. You should not depend on
|
||||
keys having a certain value, as the same tool loaded from different
|
||||
hierarchies of servers may have different keys.
|
||||
"""
|
||||
return self._key or self.name
|
||||
|
||||
def with_key(self, key: str) -> Self:
|
||||
return self.model_copy(update={"_key": key})
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Quick test to verify the revert worked correctly."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
|
||||
|
||||
async def test_empty_prefix_behavior():
|
||||
"""Test that empty prefix correctly adds underscore."""
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
@sub_app.tool
|
||||
def sub_tool() -> str:
|
||||
return "This is from the sub app"
|
||||
|
||||
@sub_app.resource("data://test")
|
||||
def sub_resource():
|
||||
return "Resource data"
|
||||
|
||||
# Mount with empty prefix
|
||||
main_app.mount("", sub_app)
|
||||
|
||||
# Check that tools have underscore prefix
|
||||
tools = await main_app.get_tools()
|
||||
print(f"Tools: {list(tools.keys())}")
|
||||
assert "_sub_tool" in tools, f"Expected '_sub_tool' in {list(tools.keys())}"
|
||||
|
||||
# Check that resources work correctly
|
||||
resources = await main_app.get_resources()
|
||||
print(f"Resources: {list(resources.keys())}")
|
||||
# Empty prefix for resources should result in no prefix change
|
||||
assert "data://test" in resources, (
|
||||
f"Expected 'data://test' in {list(resources.keys())}"
|
||||
)
|
||||
|
||||
# Test calling the tool
|
||||
async with Client(main_app) as client:
|
||||
result = await client.call_tool("_sub_tool", {})
|
||||
print(f"Tool result: {result[0].text}")
|
||||
assert "This is from the sub app" in result[0].text
|
||||
|
||||
print("✅ Empty prefix correctly adds underscore for tools!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_empty_prefix_behavior())
|
||||
|
|
@ -272,9 +272,7 @@ class TestMultipleServerMount:
|
|||
main_app.mount(working_app, "working")
|
||||
|
||||
# Use an unreachable port
|
||||
unreachable_client = Client(
|
||||
transport=SSETransport("http://127.0.0.1:99999/sse")
|
||||
)
|
||||
unreachable_client = Client(transport=SSETransport("http://127.0.0.1:9999/sse"))
|
||||
|
||||
# Create a proxy server that will fail to connect
|
||||
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue