Forward backend capabilities in ProxyProvider (#3956)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jeremiah Lowin 2026-04-17 15:22:08 -04:00 committed by GitHub
commit 98f69bdba0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 258 additions and 11 deletions

View file

@ -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