Fix Proxy provider to return all resource contents (#2742)

This commit is contained in:
Jeremiah Lowin 2025-12-25 22:33:45 -05:00 committed by GitHub
commit 1ed1032ca5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 37 deletions

View file

@ -218,28 +218,30 @@ class ProxyResource(Resource):
raise ResourceError(
f"Remote server returned empty content for {backend_uri}"
)
if isinstance(result[0], TextResourceContents):
return ResourceResult(
contents=[
# Process all items in the result list, not just the first one
contents: list[ResourceContent] = []
for item in result:
if isinstance(item, TextResourceContents):
contents.append(
ResourceContent(
content=result[0].text,
mime_type=result[0].mimeType,
meta=result[0].meta,
content=item.text,
mime_type=item.mimeType,
meta=item.meta,
)
]
)
elif isinstance(result[0], BlobResourceContents):
return ResourceResult(
contents=[
)
elif isinstance(item, BlobResourceContents):
contents.append(
ResourceContent(
content=base64.b64decode(result[0].blob),
mime_type=result[0].mimeType,
meta=result[0].meta,
content=base64.b64decode(item.blob),
mime_type=item.mimeType,
meta=item.meta,
)
]
)
else:
raise ResourceError(f"Unsupported content type: {type(result[0])}")
)
else:
raise ResourceError(f"Unsupported content type: {type(item)}")
return ResourceResult(contents=contents)
class ProxyTemplate(ResourceTemplate):
@ -310,28 +312,30 @@ class ProxyTemplate(ResourceTemplate):
raise ResourceError(
f"Remote server returned empty content for {parameterized_uri}"
)
if isinstance(result[0], TextResourceContents):
cached_content = ResourceResult(
contents=[
# Process all items in the result list, not just the first one
contents: list[ResourceContent] = []
for item in result:
if isinstance(item, TextResourceContents):
contents.append(
ResourceContent(
content=result[0].text,
mime_type=result[0].mimeType,
meta=result[0].meta,
content=item.text,
mime_type=item.mimeType,
meta=item.meta,
)
]
)
elif isinstance(result[0], BlobResourceContents):
cached_content = ResourceResult(
contents=[
)
elif isinstance(item, BlobResourceContents):
contents.append(
ResourceContent(
content=base64.b64decode(result[0].blob),
mime_type=result[0].mimeType,
meta=result[0].meta,
content=base64.b64decode(item.blob),
mime_type=item.mimeType,
meta=item.meta,
)
]
)
else:
raise ResourceError(f"Unsupported content type: {type(result[0])}")
)
else:
raise ResourceError(f"Unsupported content type: {type(item)}")
cached_content = ResourceResult(contents=contents)
return ProxyResource(
client_factory=self._client_factory,
@ -339,7 +343,9 @@ class ProxyTemplate(ResourceTemplate):
name=self.name,
title=self.title,
description=self.description,
mime_type=result[0].mimeType,
mime_type=result[
0
].mimeType, # Use first item's mimeType for backward compatibility
icons=self.icons,
meta=self.meta,
tags=(self.meta or {}).get("_fastmcp", {}).get("tags", []),

View file

@ -13,6 +13,7 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
from fastmcp.exceptions import ToolError
from fastmcp.resources import ResourceContent, ResourceResult
from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
from fastmcp.tools.tool import ToolResult
from fastmcp.tools.tool_transform import (
@ -84,6 +85,36 @@ def fastmcp_server():
user = next((user for user in USERS if user["id"] == user_id), None)
return json.dumps(user, separators=(",", ":")) if user else "null"
@server.resource(uri="data://multi")
def get_multi_content() -> ResourceResult:
"""Resource that returns multiple content items."""
return ResourceResult(
contents=[
ResourceContent(content="First item", mime_type="text/plain"),
ResourceContent(
content='{"key": "value"}', mime_type="application/json"
),
ResourceContent(
content="# Markdown\nContent", mime_type="text/markdown"
),
],
meta={"count": 3},
)
@server.resource(uri="data://multi/{id}")
def get_multi_template(id: str) -> ResourceResult:
"""Resource template that returns multiple content items."""
return ResourceResult(
contents=[
ResourceContent(content=f"Item {id} - First", mime_type="text/plain"),
ResourceContent(
content=f'{{"id": "{id}", "status": "active"}}',
mime_type="application/json",
),
],
meta={"id": id},
)
# --- Prompts ---
@server.prompt(
@ -313,6 +344,40 @@ class TestResources:
users = json.loads(result[0].text)
assert users == USERS
async def test_proxy_returns_all_resource_contents(
self, fastmcp_server, proxy_server
):
"""Test that proxy correctly returns all resource contents, not just the first one."""
# Read from original server
async with Client(fastmcp_server) as client:
original_result = await client.read_resource("data://multi")
# Read from proxy server
async with Client(proxy_server) as client:
proxy_result = await client.read_resource("data://multi")
# Both should return the same number of contents
assert len(original_result) == len(proxy_result)
assert len(original_result) == 3
# Verify all contents match
for i, (original, proxied) in enumerate(zip(original_result, proxy_result)):
assert isinstance(original, TextResourceContents)
assert isinstance(proxied, TextResourceContents)
assert original.text == proxied.text, f"Content {i} text mismatch"
assert original.mimeType == proxied.mimeType, (
f"Content {i} mimeType mismatch"
)
assert original.meta == proxied.meta, f"Content {i} meta mismatch"
# Verify the contents are what we expect
assert original_result[0].text == "First item"
assert original_result[0].mimeType == "text/plain"
assert original_result[1].text == '{"key": "value"}'
assert original_result[1].mimeType == "application/json"
assert original_result[2].text == "# Markdown\nContent"
assert original_result[2].mimeType == "text/markdown"
async def test_read_resource_returns_none_if_not_found(self, proxy_server):
with pytest.raises(
McpError, match="Unknown resource: 'resource://nonexistent'"
@ -390,6 +455,37 @@ class TestResourceTemplates:
proxy_result = await client.read_resource("data://user/1")
assert proxy_result == result
async def test_proxy_template_returns_all_resource_contents(
self, fastmcp_server, proxy_server
):
"""Test that proxy template correctly returns all resource contents."""
# Read from original server
async with Client(fastmcp_server) as client:
original_result = await client.read_resource("data://multi/test123")
# Read from proxy server
async with Client(proxy_server) as client:
proxy_result = await client.read_resource("data://multi/test123")
# Both should return the same number of contents
assert len(original_result) == len(proxy_result)
assert len(original_result) == 2
# Verify all contents match
for i, (original, proxied) in enumerate(zip(original_result, proxy_result)):
assert isinstance(original, TextResourceContents)
assert isinstance(proxied, TextResourceContents)
assert original.text == proxied.text, f"Content {i} text mismatch"
assert original.mimeType == proxied.mimeType, (
f"Content {i} mimeType mismatch"
)
# Verify the contents are what we expect
assert original_result[0].text == "Item test123 - First"
assert original_result[0].mimeType == "text/plain"
assert original_result[1].text == '{"id": "test123", "status": "active"}'
assert original_result[1].mimeType == "application/json"
async def test_proxy_can_overwrite_proxied_resource_template(self, proxy_server):
"""
Test that a resource template defined on the proxy can overwrite the proxied template with the same URI template.