mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Fix Proxy provider to return all resource contents (#2742)
This commit is contained in:
parent
61ebc64730
commit
1ed1032ca5
2 changed files with 139 additions and 37 deletions
|
|
@ -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", []),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue