mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 21:14:17 +02:00
Merge branch 'main' into headers
This commit is contained in:
commit
586072f605
2 changed files with 113 additions and 13 deletions
|
|
@ -257,9 +257,15 @@ 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 server in self._mounted_servers.values():
|
||||
server_tools = await server.get_tools()
|
||||
tools.update(server_tools)
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
try:
|
||||
server_tools = await server.get_tools()
|
||||
tools.update(server_tools)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get tools from mounted server '{prefix}': {e}"
|
||||
)
|
||||
continue
|
||||
tools.update(self._tool_manager.get_tools())
|
||||
self._cache.set("tools", tools)
|
||||
return tools
|
||||
|
|
@ -268,9 +274,15 @@ 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 server in self._mounted_servers.values():
|
||||
server_resources = await server.get_resources()
|
||||
resources.update(server_resources)
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
try:
|
||||
server_resources = await server.get_resources()
|
||||
resources.update(server_resources)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get resources from mounted server '{prefix}': {e}"
|
||||
)
|
||||
continue
|
||||
resources.update(self._resource_manager.get_resources())
|
||||
self._cache.set("resources", resources)
|
||||
return resources
|
||||
|
|
@ -281,9 +293,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
templates := self._cache.get("resource_templates")
|
||||
) is self._cache.NOT_FOUND:
|
||||
templates: dict[str, ResourceTemplate] = {}
|
||||
for server in self._mounted_servers.values():
|
||||
server_templates = await server.get_resource_templates()
|
||||
templates.update(server_templates)
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
try:
|
||||
server_templates = await server.get_resource_templates()
|
||||
templates.update(server_templates)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to get resource templates from mounted server "
|
||||
f"'{prefix}': {e}"
|
||||
)
|
||||
continue
|
||||
templates.update(self._resource_manager.get_templates())
|
||||
self._cache.set("resource_templates", templates)
|
||||
return templates
|
||||
|
|
@ -294,9 +313,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
||||
prompts: dict[str, Prompt] = {}
|
||||
for server in self._mounted_servers.values():
|
||||
server_prompts = await server.get_prompts()
|
||||
prompts.update(server_prompts)
|
||||
for prefix, server in self._mounted_servers.items():
|
||||
try:
|
||||
server_prompts = await server.get_prompts()
|
||||
prompts.update(server_prompts)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get prompts from mounted server '{prefix}': {e}"
|
||||
)
|
||||
continue
|
||||
prompts.update(self._prompt_manager.get_prompts())
|
||||
self._cache.set("prompts", prompts)
|
||||
return prompts
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import pytest
|
||||
|
|
@ -7,7 +8,7 @@ from mcp.types import TextContent, TextResourceContents
|
|||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
from fastmcp.client.transports import FastMCPTransport, SSETransport
|
||||
from fastmcp.exceptions import NotFoundError
|
||||
from fastmcp.server.proxy import FastMCPProxy
|
||||
|
||||
|
|
@ -182,6 +183,80 @@ class TestMultipleServerMount:
|
|||
# Second app's tool should be accessible
|
||||
assert "api_second_tool" in tools
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform == "win32", reason="Windows asyncio networking timeouts."
|
||||
)
|
||||
async def test_mount_with_unreachable_proxy_servers(self, caplog):
|
||||
"""Test graceful handling when multiple mounted servers fail to connect."""
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
working_app = FastMCP("WorkingApp")
|
||||
|
||||
@working_app.tool()
|
||||
def working_tool() -> str:
|
||||
return "Working tool"
|
||||
|
||||
@working_app.resource(uri="working://data")
|
||||
def working_resource():
|
||||
return "Working resource"
|
||||
|
||||
@working_app.prompt()
|
||||
def working_prompt() -> str:
|
||||
return "Working prompt"
|
||||
|
||||
# Mount the working server
|
||||
main_app.mount("working", working_app)
|
||||
|
||||
# Use an unreachable port
|
||||
unreachable_client = Client(
|
||||
transport=SSETransport("http://127.0.0.1:99999/sse")
|
||||
)
|
||||
|
||||
# Create a proxy server that will fail to connect
|
||||
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
||||
|
||||
# Mount the unreachable proxy
|
||||
main_app.mount("unreachable", unreachable_proxy)
|
||||
|
||||
# All object types should work from working server despite unreachable proxy
|
||||
async with Client(main_app) as client:
|
||||
# Test tools
|
||||
tools = await client.list_tools()
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert "working_working_tool" in tool_names
|
||||
|
||||
# Test calling a tool
|
||||
result = await client.call_tool("working_working_tool", {})
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].text == "Working tool"
|
||||
|
||||
# Test resources
|
||||
resources = await client.list_resources()
|
||||
resource_uris = [str(resource.uri) for resource in resources]
|
||||
assert "working://working/data" in resource_uris
|
||||
|
||||
# Test prompts
|
||||
prompts = await client.list_prompts()
|
||||
prompt_names = [prompt.name for prompt in prompts]
|
||||
assert "working_working_prompt" in prompt_names
|
||||
|
||||
# Verify that warnings were logged for the unreachable server
|
||||
warning_messages = [
|
||||
record.message for record in caplog.records if record.levelname == "WARNING"
|
||||
]
|
||||
assert any(
|
||||
"Failed to get tools from mounted server 'unreachable'" in msg
|
||||
for msg in warning_messages
|
||||
)
|
||||
assert any(
|
||||
"Failed to get resources from mounted server 'unreachable'" in msg
|
||||
for msg in warning_messages
|
||||
)
|
||||
assert any(
|
||||
"Failed to get prompts from mounted server 'unreachable'" in msg
|
||||
for msg in warning_messages
|
||||
)
|
||||
|
||||
|
||||
class TestDynamicChanges:
|
||||
"""Test that changes to mounted servers are reflected dynamically."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue