mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Consolidate prefix logic
This commit is contained in:
parent
eeedd175a5
commit
084e52badd
5 changed files with 144 additions and 184 deletions
|
|
@ -12,6 +12,7 @@ from contextlib import (
|
|||
AsyncExitStack,
|
||||
asynccontextmanager,
|
||||
)
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
||||
|
|
@ -322,10 +323,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""Get all registered tools, indexed by registered key."""
|
||||
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
||||
tools: dict[str, Tool] = {}
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
try:
|
||||
server_tools = await server.get_tools()
|
||||
tools.update(server_tools)
|
||||
server_tools = await mounted_server.server.get_tools()
|
||||
# Apply prefix to each tool key
|
||||
prefixed_tools = {
|
||||
f"{prefix}_{key}": tool for key, tool in server_tools.items()
|
||||
}
|
||||
tools.update(prefixed_tools)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get tools from mounted server '{prefix}': {e}"
|
||||
|
|
@ -345,10 +350,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""Get all registered resources, indexed by registered key."""
|
||||
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
||||
resources: dict[str, Resource] = {}
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
try:
|
||||
server_resources = await server.get_resources()
|
||||
resources.update(server_resources)
|
||||
server_resources = await mounted_server.server.get_resources()
|
||||
# Apply prefix to each resource key
|
||||
prefixed_resources = {
|
||||
add_resource_prefix(
|
||||
key, prefix, mounted_server.server.resource_prefix_format
|
||||
): resource
|
||||
for key, resource in server_resources.items()
|
||||
}
|
||||
resources.update(prefixed_resources)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get resources from mounted server '{prefix}': {e}"
|
||||
|
|
@ -370,10 +382,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
templates := self._cache.get("resource_templates")
|
||||
) is self._cache.NOT_FOUND:
|
||||
templates: dict[str, ResourceTemplate] = {}
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
try:
|
||||
server_templates = await server.get_resource_templates()
|
||||
templates.update(server_templates)
|
||||
server_templates = (
|
||||
await mounted_server.server.get_resource_templates()
|
||||
)
|
||||
# Apply prefix to each template key
|
||||
prefixed_templates = {
|
||||
add_resource_prefix(
|
||||
key, prefix, mounted_server.server.resource_prefix_format
|
||||
): template
|
||||
for key, template in server_templates.items()
|
||||
}
|
||||
templates.update(prefixed_templates)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to get resource templates from mounted server "
|
||||
|
|
@ -396,10 +417,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
||||
prompts: dict[str, Prompt] = {}
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
try:
|
||||
server_prompts = await server.get_prompts()
|
||||
prompts.update(server_prompts)
|
||||
server_prompts = await mounted_server.server.get_prompts()
|
||||
# Apply prefix to each prompt key
|
||||
prefixed_prompts = {
|
||||
f"{prefix}_{key}": prompt
|
||||
for key, prompt in server_prompts.items()
|
||||
}
|
||||
prompts.update(prefixed_prompts)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get prompts from mounted server '{prefix}': {e}"
|
||||
|
|
@ -562,10 +588,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
return await self._tool_manager.call_tool(key, arguments)
|
||||
|
||||
# Check mounted servers to see if they have the tool
|
||||
for server in self._mounted_servers.values():
|
||||
if server.match_tool(key):
|
||||
tool_key = server.strip_tool_prefix(key)
|
||||
return await server.server._call_tool(tool_key, arguments)
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
if key.startswith(f"{prefix}_"):
|
||||
tool_key = key.removeprefix(f"{prefix}_")
|
||||
return await mounted_server.server._call_tool(tool_key, arguments)
|
||||
|
||||
raise NotFoundError(f"Unknown tool: {key!r}")
|
||||
|
||||
|
|
@ -604,10 +630,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
]
|
||||
else:
|
||||
for server in self._mounted_servers.values():
|
||||
if server.match_resource(str(uri)):
|
||||
new_uri = server.strip_resource_prefix(str(uri))
|
||||
return await server.server._mcp_read_resource(new_uri)
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
if has_resource_prefix(
|
||||
str(uri), prefix, mounted_server.server.resource_prefix_format
|
||||
):
|
||||
new_uri = remove_resource_prefix(
|
||||
str(uri), prefix, mounted_server.server.resource_prefix_format
|
||||
)
|
||||
return await mounted_server.server._mcp_read_resource(new_uri)
|
||||
else:
|
||||
raise NotFoundError(f"Unknown resource: {uri}")
|
||||
|
||||
|
|
@ -653,10 +683,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
return await self._prompt_manager.render_prompt(name, arguments)
|
||||
|
||||
# Check mounted servers to see if they have the prompt
|
||||
for server in self._mounted_servers.values():
|
||||
if server.match_prompt(name):
|
||||
prompt_name = server.strip_prompt_prefix(name)
|
||||
return await server.server._mcp_get_prompt(prompt_name, arguments)
|
||||
for prefix, mounted_server in self._mounted_servers.items():
|
||||
if name.startswith(f"{prefix}_"):
|
||||
prompt_name = name.removeprefix(f"{prefix}_")
|
||||
return await mounted_server.server._mcp_get_prompt(
|
||||
prompt_name, arguments
|
||||
)
|
||||
|
||||
raise NotFoundError(f"Unknown prompt: {name}")
|
||||
|
||||
|
|
@ -1731,60 +1763,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MountedServer:
|
||||
def __init__(
|
||||
self,
|
||||
prefix: str,
|
||||
server: FastMCP[LifespanResultT],
|
||||
):
|
||||
self.server = server
|
||||
self.prefix = prefix
|
||||
|
||||
async def get_tools(self) -> dict[str, Tool]:
|
||||
tools = await self.server.get_tools()
|
||||
return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
|
||||
|
||||
async def get_resources(self) -> dict[str, Resource]:
|
||||
resources = await self.server.get_resources()
|
||||
return {
|
||||
add_resource_prefix(
|
||||
key, self.prefix, self.server.resource_prefix_format
|
||||
): resource
|
||||
for key, resource in resources.items()
|
||||
}
|
||||
|
||||
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
||||
templates = await self.server.get_resource_templates()
|
||||
return {
|
||||
add_resource_prefix(
|
||||
key, self.prefix, self.server.resource_prefix_format
|
||||
): template
|
||||
for key, template in templates.items()
|
||||
}
|
||||
|
||||
async def get_prompts(self) -> dict[str, Prompt]:
|
||||
prompts = await self.server.get_prompts()
|
||||
return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
|
||||
|
||||
def match_tool(self, key: str) -> bool:
|
||||
return key.startswith(f"{self.prefix}_")
|
||||
|
||||
def strip_tool_prefix(self, key: str) -> str:
|
||||
return key.removeprefix(f"{self.prefix}_")
|
||||
|
||||
def match_resource(self, key: str) -> bool:
|
||||
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
||||
|
||||
def strip_resource_prefix(self, key: str) -> str:
|
||||
return remove_resource_prefix(
|
||||
key, self.prefix, self.server.resource_prefix_format
|
||||
)
|
||||
|
||||
def match_prompt(self, key: str) -> bool:
|
||||
return key.startswith(f"{self.prefix}_")
|
||||
|
||||
def strip_prompt_prefix(self, key: str) -> str:
|
||||
return key.removeprefix(f"{self.prefix}_")
|
||||
prefix: str
|
||||
server: FastMCP[Any]
|
||||
|
||||
|
||||
def add_resource_prefix(
|
||||
|
|
|
|||
50
test_revert_check.py
Normal file
50
test_revert_check.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/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())
|
||||
|
|
@ -109,83 +109,3 @@ def test_from_client_deprecation_warning():
|
|||
server = FastMCP("TestServer")
|
||||
with pytest.warns(DeprecationWarning, match="from_client"):
|
||||
FastMCP.from_client(Client(server))
|
||||
|
||||
|
||||
def test_mount_tool_separator_deprecation_warning():
|
||||
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, tool_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.tool
|
||||
def test_tool():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_tool("sub_test_tool")
|
||||
assert not mounted_server.match_tool("sub-test_tool")
|
||||
|
||||
|
||||
def test_mount_resource_separator_deprecation_warning():
|
||||
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
main_app.mount("sub", sub_app, resource_separator="+")
|
||||
|
||||
|
||||
def test_mount_prompt_separator_deprecation_warning():
|
||||
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, prompt_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.prompt
|
||||
def test_prompt():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_prompt("sub_test_prompt")
|
||||
assert not mounted_server.match_prompt("sub-test_prompt")
|
||||
|
||||
|
||||
async def test_import_server_separator_deprecation_warnings():
|
||||
"""Test that using separators in import_server() raises deprecation warnings."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, tool_separator="-")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, resource_separator="+")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, prompt_separator="-")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
"""Tests for the deprecated separator parameters in mount() and import_server() methods."""
|
||||
|
||||
import pytest
|
||||
from mcp import McpError
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp import Client, FastMCP
|
||||
|
||||
# reset deprecation warnings for this module
|
||||
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
|
||||
|
||||
|
||||
def test_mount_tool_separator_deprecation_warning():
|
||||
def test_mount_resource_separator_deprecation_warning():
|
||||
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
main_app.mount("sub", sub_app, resource_separator="+")
|
||||
|
||||
|
||||
async def test_mount_tool_separator_deprecation_warning():
|
||||
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
|
@ -24,24 +37,12 @@ def test_mount_tool_separator_deprecation_warning():
|
|||
def test_tool():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_tool("sub_test_tool")
|
||||
assert not mounted_server.match_tool("sub-test_tool")
|
||||
async with Client(main_app) as client:
|
||||
assert "sub_test_tool" in {t.name for t in await client.list_tools()}
|
||||
assert "sub-test_tool" not in {t.name for t in await client.list_tools()}
|
||||
|
||||
|
||||
def test_mount_resource_separator_deprecation_warning():
|
||||
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
main_app.mount("sub", sub_app, resource_separator="+")
|
||||
|
||||
|
||||
def test_mount_prompt_separator_deprecation_warning():
|
||||
async def test_mount_prompt_separator_deprecation_warning():
|
||||
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
|
@ -57,9 +58,10 @@ def test_mount_prompt_separator_deprecation_warning():
|
|||
def test_prompt():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_prompt("sub_test_prompt")
|
||||
assert not mounted_server.match_prompt("sub-test_prompt")
|
||||
async with Client(main_app) as client:
|
||||
assert await client.get_prompt("sub_test_prompt")
|
||||
with pytest.raises(McpError, match="Unknown prompt"):
|
||||
await client.get_prompt("sub-test_prompt")
|
||||
|
||||
|
||||
async def test_import_server_separator_deprecation_warnings():
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from fastmcp.exceptions import NotFoundError
|
|||
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
|
||||
from fastmcp.resources import Resource, ResourceTemplate
|
||||
from fastmcp.server.server import (
|
||||
MountedServer,
|
||||
add_resource_prefix,
|
||||
has_resource_prefix,
|
||||
remove_resource_prefix,
|
||||
|
|
@ -1183,16 +1182,23 @@ class TestResourcePrefixMounting:
|
|||
async def test_mounted_server_matching_and_stripping(
|
||||
self, uri, prefix, expected_match, expected_strip
|
||||
):
|
||||
"""Test that MountedServer correctly matches and strips resource prefixes."""
|
||||
# Create a basic server to mount
|
||||
"""Test that resource prefix utility functions correctly match and strip resource prefixes."""
|
||||
from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
|
||||
|
||||
# Create a basic server to get the default resource prefix format
|
||||
server = FastMCP()
|
||||
mounted = MountedServer(prefix=prefix, server=server)
|
||||
|
||||
# Test matching
|
||||
assert mounted.match_resource(uri) == expected_match
|
||||
assert (
|
||||
has_resource_prefix(uri, prefix, server.resource_prefix_format)
|
||||
== expected_match
|
||||
)
|
||||
|
||||
# Test stripping
|
||||
assert mounted.strip_resource_prefix(uri) == expected_strip
|
||||
assert (
|
||||
remove_resource_prefix(uri, prefix, server.resource_prefix_format)
|
||||
== expected_strip
|
||||
)
|
||||
|
||||
async def test_import_server_with_new_prefix_format(self):
|
||||
"""Test that import_server correctly uses the new prefix format."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue