mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Forward backend capabilities in ProxyProvider (#3956)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e3f845f558
commit
98f69bdba0
4 changed files with 258 additions and 11 deletions
|
|
@ -915,3 +915,110 @@ class TestProxyProviderCache:
|
|||
result = await proxy.call_tool("greet", {"name": "Alice"})
|
||||
mock_list.assert_not_called()
|
||||
assert result.content[0].text == "Hello, Alice!" # type: ignore[union-attr] # ty:ignore[unresolved-attribute]
|
||||
|
||||
|
||||
def _make_tools_only_backend() -> FastMCP:
|
||||
"""Create a FastMCP backend that only supports tools (no resources/prompts)."""
|
||||
backend = FastMCP("ToolsOnlyBackend")
|
||||
|
||||
@backend.tool
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
|
||||
# Remove resource and prompt handlers to simulate a tools-only MCP server
|
||||
backend._mcp_server.request_handlers.pop(mcp_types.ListResourcesRequest, None)
|
||||
backend._mcp_server.request_handlers.pop(
|
||||
mcp_types.ListResourceTemplatesRequest, None
|
||||
)
|
||||
backend._mcp_server.request_handlers.pop(mcp_types.ReadResourceRequest, None)
|
||||
backend._mcp_server.request_handlers.pop(mcp_types.ListPromptsRequest, None)
|
||||
backend._mcp_server.request_handlers.pop(mcp_types.GetPromptRequest, None)
|
||||
return backend
|
||||
|
||||
|
||||
class TestProxyCapabilityForwarding:
|
||||
"""Proxy advertises the backend's actual capabilities (issue #3948)."""
|
||||
|
||||
async def test_proxy_forwards_tools_only_capabilities(self):
|
||||
"""When backend only supports tools, proxy initialize response should not
|
||||
include resources or prompts capabilities."""
|
||||
backend = _make_tools_only_backend()
|
||||
proxy = create_proxy(FastMCPTransport(backend))
|
||||
|
||||
async with Client(proxy) as client:
|
||||
init_result = client.initialize_result
|
||||
assert init_result is not None
|
||||
caps = init_result.capabilities
|
||||
assert caps.tools is not None
|
||||
assert caps.resources is None
|
||||
assert caps.prompts is None
|
||||
|
||||
async def test_proxy_full_backend_advertises_all_capabilities(
|
||||
self, fastmcp_server: FastMCP
|
||||
):
|
||||
"""When backend supports tools, resources and prompts, proxy should
|
||||
advertise all three."""
|
||||
proxy = create_proxy(FastMCPTransport(fastmcp_server))
|
||||
|
||||
async with Client(proxy) as client:
|
||||
init_result = client.initialize_result
|
||||
assert init_result is not None
|
||||
caps = init_result.capabilities
|
||||
assert caps.tools is not None
|
||||
assert caps.resources is not None
|
||||
assert caps.prompts is not None
|
||||
|
||||
async def test_proxy_resources_list_returns_method_not_found(self):
|
||||
"""When backend does not support resources, resources/list on the proxy
|
||||
must return a METHOD_NOT_FOUND error, not an empty list."""
|
||||
backend = _make_tools_only_backend()
|
||||
proxy = create_proxy(FastMCPTransport(backend))
|
||||
|
||||
async with Client(proxy) as client:
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await client.list_resources()
|
||||
assert exc_info.value.error.code == mcp_types.METHOD_NOT_FOUND
|
||||
|
||||
async def test_proxy_prompts_list_returns_method_not_found(self):
|
||||
"""When backend does not support prompts, prompts/list on the proxy
|
||||
must return a METHOD_NOT_FOUND error, not an empty list."""
|
||||
backend = _make_tools_only_backend()
|
||||
proxy = create_proxy(FastMCPTransport(backend))
|
||||
|
||||
async with Client(proxy) as client:
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await client.list_prompts()
|
||||
assert exc_info.value.error.code == mcp_types.METHOD_NOT_FOUND
|
||||
|
||||
async def test_proxy_preserves_local_resources_even_without_backend_support(self):
|
||||
"""When proxy has local resources but backend doesn't, resources capability
|
||||
should still be advertised and local resources should be accessible."""
|
||||
backend = _make_tools_only_backend()
|
||||
proxy = create_proxy(FastMCPTransport(backend))
|
||||
|
||||
@proxy.resource("local://data")
|
||||
def local_data() -> str:
|
||||
return "local"
|
||||
|
||||
async with Client(proxy) as client:
|
||||
init_result = client.initialize_result
|
||||
assert init_result is not None
|
||||
caps = init_result.capabilities
|
||||
assert caps.tools is not None
|
||||
assert caps.resources is not None # local resource present
|
||||
assert caps.prompts is None # still no prompts
|
||||
|
||||
resources = await client.list_resources()
|
||||
assert any(str(r.uri) == "local://data" for r in resources)
|
||||
|
||||
async def test_proxy_backend_capabilities_preloaded_in_lifespan(self):
|
||||
"""ProxyProvider._backend_capabilities should be set after lifespan runs."""
|
||||
backend = _make_tools_only_backend()
|
||||
provider = ProxyProvider(lambda: ProxyClient(FastMCPTransport(backend)))
|
||||
|
||||
async with provider.lifespan():
|
||||
caps = provider._backend_capabilities
|
||||
assert caps is not None
|
||||
assert caps.tools is not None
|
||||
assert caps.resources is None
|
||||
assert caps.prompts is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue