diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py index ba5819679..65c954f78 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -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", []), diff --git a/tests/server/proxy/test_proxy_server.py b/tests/server/proxy/test_proxy_server.py index da0e074e3..ce6d2046f 100644 --- a/tests/server/proxy/test_proxy_server.py +++ b/tests/server/proxy/test_proxy_server.py @@ -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.