diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index e08a161c..122acebb 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -3,7 +3,7 @@ import inspect import json import re -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Iterable from contextlib import ( AbstractAsyncContextManager, asynccontextmanager, @@ -236,7 +236,7 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]: for template in templates ] - async def read_resource(self, uri: AnyUrl | str) -> ReadResourceContents: + async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: """Read a resource by URI.""" resource = await self._resource_manager.get_resource(uri) @@ -245,7 +245,7 @@ async def read_resource(self, uri: AnyUrl | str) -> ReadResourceContents: try: content = await resource.read() - return ReadResourceContents(content=content, mime_type=resource.mime_type) + return [ReadResourceContents(content=content, mime_type=resource.mime_type)] except Exception as e: logger.error(f"Error reading resource {uri}: {e}") raise ResourceError(str(e)) @@ -649,7 +649,7 @@ async def report_progress( progress_token=progress_token, progress=progress, total=total ) - async def read_resource(self, uri: str | AnyUrl) -> ReadResourceContents: + async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]: """Read a resource by URI. Args: diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index c0008b32..25e94365 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -67,9 +67,9 @@ async def main(): import contextvars import logging import warnings -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager -from typing import Any, AsyncIterator, Generic, Sequence, TypeVar +from typing import Any, AsyncIterator, Generic, TypeVar import anyio from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -279,7 +279,9 @@ async def handler(_: Any): def read_resource(self): def decorator( - func: Callable[[AnyUrl], Awaitable[str | bytes | ReadResourceContents]], + func: Callable[ + [AnyUrl], Awaitable[str | bytes | Iterable[ReadResourceContents]] + ], ): logger.debug("Registering handler for ReadResourceRequest") @@ -307,13 +309,22 @@ def create_content(data: str | bytes, mime_type: str | None): case str() | bytes() as data: warnings.warn( "Returning str or bytes from read_resource is deprecated. " - "Use ReadResourceContents instead.", + "Use Iterable[ReadResourceContents] instead.", DeprecationWarning, stacklevel=2, ) content = create_content(data, None) - case ReadResourceContents() as contents: - content = create_content(contents.content, contents.mime_type) + case Iterable() as contents: + contents_list = [ + create_content(content_item.content, content_item.mime_type) + for content_item in contents + if isinstance(content_item, ReadResourceContents) + ] + return types.ServerResult( + types.ReadResourceResult( + contents=contents_list, + ) + ) case _: raise ValueError( f"Unexpected return type from read_resource: {type(result)}" @@ -387,7 +398,7 @@ def decorator( func: Callable[ ..., Awaitable[ - Sequence[ + Iterable[ types.TextContent | types.ImageContent | types.EmbeddedResource ] ], diff --git a/tests/issues/test_141_resource_templates.py b/tests/issues/test_141_resource_templates.py index d6526e9f..3c17cd55 100644 --- a/tests/issues/test_141_resource_templates.py +++ b/tests/issues/test_141_resource_templates.py @@ -51,8 +51,10 @@ def get_user_profile_missing(user_id: str) -> str: # Verify valid template works result = await mcp.read_resource("resource://users/123/posts/456") - assert result.content == "Post 456 by user 123" - assert result.mime_type == "text/plain" + result_list = list(result) + assert len(result_list) == 1 + assert result_list[0].content == "Post 456 by user 123" + assert result_list[0].mime_type == "text/plain" # Verify invalid parameters raise error with pytest.raises(ValueError, match="Unknown resource"): diff --git a/tests/issues/test_152_resource_mime_type.py b/tests/issues/test_152_resource_mime_type.py index 7a1b6606..1143195e 100644 --- a/tests/issues/test_152_resource_mime_type.py +++ b/tests/issues/test_152_resource_mime_type.py @@ -99,11 +99,11 @@ async def handle_list_resources(): @server.read_resource() async def handle_read_resource(uri: AnyUrl): if str(uri) == "test://image": - return ReadResourceContents(content=base64_string, mime_type="image/png") + return [ReadResourceContents(content=base64_string, mime_type="image/png")] elif str(uri) == "test://image_bytes": - return ReadResourceContents( - content=bytes(image_bytes), mime_type="image/png" - ) + return [ + ReadResourceContents(content=bytes(image_bytes), mime_type="image/png") + ] raise Exception(f"Resource not found: {uri}") # Test that resources are listed with correct mime type diff --git a/tests/server/fastmcp/servers/test_file_server.py b/tests/server/fastmcp/servers/test_file_server.py index edaaa159..c51ecb25 100644 --- a/tests/server/fastmcp/servers/test_file_server.py +++ b/tests/server/fastmcp/servers/test_file_server.py @@ -88,7 +88,10 @@ async def test_list_resources(mcp: FastMCP): @pytest.mark.anyio async def test_read_resource_dir(mcp: FastMCP): - res = await mcp.read_resource("dir://test_dir") + res_iter = await mcp.read_resource("dir://test_dir") + res_list = list(res_iter) + assert len(res_list) == 1 + res = res_list[0] assert res.mime_type == "text/plain" files = json.loads(res.content) @@ -102,7 +105,10 @@ async def test_read_resource_dir(mcp: FastMCP): @pytest.mark.anyio async def test_read_resource_file(mcp: FastMCP): - res = await mcp.read_resource("file://test_dir/example.py") + res_iter = await mcp.read_resource("file://test_dir/example.py") + res_list = list(res_iter) + assert len(res_list) == 1 + res = res_list[0] assert res.content == "print('hello world')" @@ -119,5 +125,8 @@ async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path): await mcp.call_tool( "delete_file", arguments=dict(path=str(test_dir / "example.py")) ) - res = await mcp.read_resource("file://test_dir/example.py") + res_iter = await mcp.read_resource("file://test_dir/example.py") + res_list = list(res_iter) + assert len(res_list) == 1 + res = res_list[0] assert res.content == "File not found" diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index d90e9939..5d375ccc 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -581,7 +581,10 @@ def test_resource() -> str: @mcp.tool() async def tool_with_resource(ctx: Context) -> str: - r = await ctx.read_resource("test://data") + r_iter = await ctx.read_resource("test://data") + r_list = list(r_iter) + assert len(r_list) == 1 + r = r_list[0] return f"Read resource: {r.content} with mime type {r.mime_type}" async with client_session(mcp._mcp_server) as client: diff --git a/tests/server/test_read_resource.py b/tests/server/test_read_resource.py index de00bc3d..469eef85 100644 --- a/tests/server/test_read_resource.py +++ b/tests/server/test_read_resource.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable from pathlib import Path from tempfile import NamedTemporaryFile @@ -26,8 +27,8 @@ async def test_read_resource_text(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> ReadResourceContents: - return ReadResourceContents(content="Hello World", mime_type="text/plain") + async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + return [ReadResourceContents(content="Hello World", mime_type="text/plain")] # Get the handler directly from the server handler = server.request_handlers[types.ReadResourceRequest] @@ -54,10 +55,12 @@ async def test_read_resource_binary(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> ReadResourceContents: - return ReadResourceContents( - content=b"Hello World", mime_type="application/octet-stream" - ) + async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + return [ + ReadResourceContents( + content=b"Hello World", mime_type="application/octet-stream" + ) + ] # Get the handler directly from the server handler = server.request_handlers[types.ReadResourceRequest] @@ -83,11 +86,13 @@ async def test_read_resource_default_mime(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> ReadResourceContents: - return ReadResourceContents( - content="Hello World", - # No mime_type specified, should default to text/plain - ) + async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + return [ + ReadResourceContents( + content="Hello World", + # No mime_type specified, should default to text/plain + ) + ] # Get the handler directly from the server handler = server.request_handlers[types.ReadResourceRequest]