From c59c740b230e24b0616fd5c22e0fe439b75aa3ce Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 19 Mar 2026 17:41:08 +0000 Subject: [PATCH 01/10] feat(mcp): expose list_resources, list_resource_templates, and read_resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP specification defines a Resources primitive alongside Tools and Prompts, and the underlying ClientSession already implements list_resources(), list_resource_templates(), and read_resource(), but none of these were surfaced through MCPServer or its concrete implementations. This adds them in three places: - Abstract methods on MCPServer (parallel to list_prompts/get_prompt) - Concrete implementations on _MCPServerWithClientSession, delegating to session.list_resources() / .list_resource_templates() / .read_resource(uri) via _maybe_serialize_request - Stub implementations in all test helpers that subclass MCPServer Six tests cover the new surface: - Raises UserError when not connected (x3) - Delegates to the underlying session and returns its result (x3) Closes: (new feature — no existing issue) --- src/agents/mcp/server.py | 54 +++++++++++++- tests/mcp/helpers.py | 15 ++++ tests/mcp/test_mcp_resources.py | 102 +++++++++++++++++++++++++++ tests/mcp/test_mcp_server_manager.py | 46 +++++++++++- tests/mcp/test_prompt_server.py | 10 +++ 5 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 tests/mcp/test_mcp_resources.py diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index edc4695e2e..8bf0e6413c 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -22,7 +22,15 @@ from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client from mcp.shared.exceptions import McpError from mcp.shared.message import SessionMessage -from mcp.types import CallToolResult, GetPromptResult, InitializeResult, ListPromptsResult +from mcp.types import ( + CallToolResult, + GetPromptResult, + InitializeResult, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ReadResourceResult, +) from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -192,6 +200,21 @@ async def get_prompt( """Get a specific prompt from the server.""" pass + @abc.abstractmethod + async def list_resources(self) -> ListResourcesResult: + """List the resources available on the server.""" + pass + + @abc.abstractmethod + async def list_resource_templates(self) -> ListResourceTemplatesResult: + """List the resource templates available on the server.""" + pass + + @abc.abstractmethod + async def read_resource(self, uri: str) -> ReadResourceResult: + """Read the contents of a specific resource by URI.""" + pass + @staticmethod def _normalize_needs_approval( *, @@ -708,6 +731,35 @@ async def get_prompt( assert session is not None return await self._maybe_serialize_request(lambda: session.get_prompt(name, arguments)) + async def list_resources(self) -> ListResourcesResult: + """List the resources available on the server.""" + if not self.session: + raise UserError("Server not initialized. Make sure you call `connect()` first.") + session = self.session + assert session is not None + return await self._maybe_serialize_request(lambda: session.list_resources()) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + """List the resource templates available on the server.""" + if not self.session: + raise UserError("Server not initialized. Make sure you call `connect()` first.") + session = self.session + assert session is not None + return await self._maybe_serialize_request(lambda: session.list_resource_templates()) + + async def read_resource(self, uri: str) -> ReadResourceResult: + """Read the contents of a specific resource by URI. + + Args: + uri: The URI of the resource to read (e.g. ``file:///path/to/file.txt`` or + ``postgres://db/table/row``). + """ + if not self.session: + raise UserError("Server not initialized. Make sure you call `connect()` first.") + session = self.session + assert session is not None + return await self._maybe_serialize_request(lambda: session.read_resource(uri)) + async def cleanup(self): """Cleanup the server.""" async with self._cleanup_lock: diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index d85622f0a8..3d4c7cee77 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -11,7 +11,10 @@ Content, GetPromptResult, ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, PromptMessage, + ReadResourceResult, TextContent, ) @@ -138,6 +141,18 @@ async def get_prompt( message = PromptMessage(role="user", content=TextContent(type="text", text=content)) return GetPromptResult(description=f"Fake prompt: {name}", messages=[message]) + async def list_resources(self) -> ListResourcesResult: + """Return empty list of resources for fake server.""" + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + """Return empty list of resource templates for fake server.""" + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + """Return empty resource contents for fake server.""" + return ReadResourceResult(contents=[]) + @property def name(self) -> str: return self._server_name diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py new file mode 100644 index 0000000000..1e450fc9b1 --- /dev/null +++ b/tests/mcp/test_mcp_resources.py @@ -0,0 +1,102 @@ +"""Tests for MCP server list_resources, list_resource_templates, and read_resource.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest +from mcp.types import ( + ListResourcesResult, + ListResourceTemplatesResult, + ReadResourceResult, + Resource, + ResourceTemplate, + TextResourceContents, +) + +from agents.mcp import MCPServerStreamableHttp + + +@pytest.fixture +def server(): + return MCPServerStreamableHttp(params={"url": "http://localhost:8000/mcp"}) + + +@pytest.mark.asyncio +async def test_list_resources_raises_when_not_connected(server: MCPServerStreamableHttp): + """list_resources raises UserError when server has not been connected.""" + from agents.exceptions import UserError + + with pytest.raises(UserError, match="Server not initialized"): + await server.list_resources() + + +@pytest.mark.asyncio +async def test_list_resource_templates_raises_when_not_connected(server: MCPServerStreamableHttp): + """list_resource_templates raises UserError when server has not been connected.""" + from agents.exceptions import UserError + + with pytest.raises(UserError, match="Server not initialized"): + await server.list_resource_templates() + + +@pytest.mark.asyncio +async def test_read_resource_raises_when_not_connected(server: MCPServerStreamableHttp): + """read_resource raises UserError when server has not been connected.""" + from agents.exceptions import UserError + + with pytest.raises(UserError, match="Server not initialized"): + await server.read_resource("file:///etc/hosts") + + +@pytest.mark.asyncio +async def test_list_resources_returns_result(server: MCPServerStreamableHttp): + """list_resources delegates to the underlying MCP session.""" + mock_session = MagicMock() + expected = ListResourcesResult( + resources=[ + Resource(uri="file:///readme.md", name="readme.md", mimeType="text/markdown"), + ] + ) + mock_session.list_resources = AsyncMock(return_value=expected) + server.session = mock_session + + result = await server.list_resources() + + assert result is expected + mock_session.list_resources.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_list_resource_templates_returns_result(server: MCPServerStreamableHttp): + """list_resource_templates delegates to the underlying MCP session.""" + mock_session = MagicMock() + expected = ListResourceTemplatesResult( + resourceTemplates=[ + ResourceTemplate(uriTemplate="file:///{path}", name="file"), + ] + ) + mock_session.list_resource_templates = AsyncMock(return_value=expected) + server.session = mock_session + + result = await server.list_resource_templates() + + assert result is expected + mock_session.list_resource_templates.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_read_resource_returns_result(server: MCPServerStreamableHttp): + """read_resource delegates to the underlying MCP session with the given URI.""" + mock_session = MagicMock() + uri = "file:///readme.md" + expected = ReadResourceResult( + contents=[ + TextResourceContents(uri=uri, text="# Hello", mimeType="text/markdown"), + ] + ) + mock_session.read_resource = AsyncMock(return_value=expected) + server.session = mock_session + + result = await server.read_resource(uri) + + assert result is expected + mock_session.read_resource.assert_awaited_once_with(uri) diff --git a/tests/mcp/test_mcp_server_manager.py b/tests/mcp/test_mcp_server_manager.py index becb45eaf2..12089c589a 100644 --- a/tests/mcp/test_mcp_server_manager.py +++ b/tests/mcp/test_mcp_server_manager.py @@ -2,7 +2,15 @@ from typing import Any, cast import pytest -from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult, Tool as MCPTool +from mcp.types import ( + CallToolResult, + GetPromptResult, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ReadResourceResult, + Tool as MCPTool, +) from agents.mcp import MCPServer, MCPServerManager from agents.run_context import RunContextWrapper @@ -49,6 +57,15 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError + async def list_resources(self) -> ListResourcesResult: + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult(contents=[]) + class FlakyServer(MCPServer): def __init__(self, failures: int) -> None: @@ -90,6 +107,15 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError + async def list_resources(self) -> ListResourcesResult: + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult(contents=[]) + class CleanupAwareServer(MCPServer): def __init__(self) -> None: @@ -130,6 +156,15 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError + async def list_resources(self) -> ListResourcesResult: + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult(contents=[]) + class CancelledServer(MCPServer): @property @@ -163,6 +198,15 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError + async def list_resources(self) -> ListResourcesResult: + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult(contents=[]) + class FailingTaskBoundServer(TaskBoundServer): @property diff --git a/tests/mcp/test_prompt_server.py b/tests/mcp/test_prompt_server.py index 13bd3bddd5..7d7e531ddd 100644 --- a/tests/mcp/test_prompt_server.py +++ b/tests/mcp/test_prompt_server.py @@ -1,6 +1,7 @@ from typing import Any import pytest +from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult from agents import Agent, Runner from agents.mcp import MCPServer, MCPToolMetaResolver @@ -76,6 +77,15 @@ async def call_tool( ): raise NotImplementedError("This fake server doesn't support tools") + async def list_resources(self) -> ListResourcesResult: + return ListResourcesResult(resources=[]) + + async def list_resource_templates(self) -> ListResourceTemplatesResult: + return ListResourceTemplatesResult(resourceTemplates=[]) + + async def read_resource(self, uri: str) -> ReadResourceResult: + return ReadResourceResult(contents=[]) + @property def name(self) -> str: return self._server_name From de257c280097d234743a974eb2e6225dd870f5e5 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 19 Mar 2026 18:05:17 +0000 Subject: [PATCH 02/10] fix: make resource methods non-abstract to avoid breaking existing subclasses Per Codex P1 review: adding abstract methods to the public MCPServer base class would break any existing user-defined subclass that only implements the previously required methods. Changed list_resources, list_resource_templates, and read_resource to concrete methods with a descriptive NotImplementedError default. Existing subclasses continue to instantiate; they only raise at call time if the user actually invokes a resource method they haven't implemented. Also adds a regression test verifying the NotImplementedError path. --- src/agents/mcp/server.py | 42 ++++++++++++++++++++++------- tests/mcp/test_mcp_resources.py | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 8bf0e6413c..15774a9042 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -200,20 +200,44 @@ async def get_prompt( """Get a specific prompt from the server.""" pass - @abc.abstractmethod async def list_resources(self) -> ListResourcesResult: - """List the resources available on the server.""" - pass + """List the resources available on the server. + + Returns a :class:`~mcp.types.ListResourcesResult` containing all resources + exposed by the server. Subclasses that do not support resources may leave + this unimplemented; it will raise :exc:`NotImplementedError` at call time. + """ + raise NotImplementedError( + f"MCP server '{self.name}' does not support list_resources. " + "Override this method in your server implementation." + ) - @abc.abstractmethod async def list_resource_templates(self) -> ListResourceTemplatesResult: - """List the resource templates available on the server.""" - pass + """List the resource templates available on the server. + + Returns a :class:`~mcp.types.ListResourceTemplatesResult`. Subclasses that + do not support resource templates may leave this unimplemented; it will raise + :exc:`NotImplementedError` at call time. + """ + raise NotImplementedError( + f"MCP server '{self.name}' does not support list_resource_templates. " + "Override this method in your server implementation." + ) - @abc.abstractmethod async def read_resource(self, uri: str) -> ReadResourceResult: - """Read the contents of a specific resource by URI.""" - pass + """Read the contents of a specific resource by URI. + + Args: + uri: The URI of the resource to read (e.g. ``file:///path/to/file.txt``). + + Returns a :class:`~mcp.types.ReadResourceResult`. Subclasses that do not + support resources may leave this unimplemented; it will raise + :exc:`NotImplementedError` at call time. + """ + raise NotImplementedError( + f"MCP server '{self.name}' does not support read_resource. " + "Override this method in your server implementation." + ) @staticmethod def _normalize_needs_approval( diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index 1e450fc9b1..9f315381e2 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -100,3 +100,51 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): assert result is expected mock_session.read_resource.assert_awaited_once_with(uri) + + +@pytest.mark.asyncio +async def test_base_methods_raise_not_implemented(): + """Bare MCPServer subclasses that don't override resource methods get NotImplementedError.""" + from mcp.types import CallToolResult + + from agents.mcp import MCPServer + + class MinimalServer(MCPServer): + """Minimal subclass implementing only the truly abstract methods.""" + + @property + def name(self) -> str: + return "minimal" + + async def connect(self) -> None: + pass + + async def cleanup(self) -> None: + pass + + async def list_tools(self, run_context=None, agent=None): + return [] + + async def call_tool(self, tool_name, tool_arguments, run_context=None, agent=None): + return CallToolResult(content=[]) + + async def list_prompts(self): + from mcp.types import ListPromptsResult + + return ListPromptsResult(prompts=[]) + + async def get_prompt(self, name, arguments=None): + from mcp.types import GetPromptResult + + return GetPromptResult(messages=[]) + + s = MinimalServer() + + with pytest.raises(NotImplementedError, match="list_resources"): + await s.list_resources() + + with pytest.raises(NotImplementedError, match="list_resource_templates"): + await s.list_resource_templates() + + with pytest.raises(NotImplementedError, match="read_resource"): + await s.read_resource("file:///test.txt") From df7336e94dc6714624ccd57517305c0c44eda05c Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 19 Mar 2026 23:43:34 +0000 Subject: [PATCH 03/10] fix(typecheck): pass AnyUrl to ClientSession.read_resource; fix test types mypy requires AnyUrl (not str) for: - ClientSession.read_resource(uri) argument - Resource.uri and TextResourceContents.uri fields Convert str -> AnyUrl at the call site in _MCPServerWithClientSession so the public API stays str (ergonomic) while satisfying the type checker. --- src/agents/mcp/server.py | 4 +++- tests/mcp/test_mcp_resources.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 15774a9042..3843b4b9d3 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -782,7 +782,9 @@ async def read_resource(self, uri: str) -> ReadResourceResult: raise UserError("Server not initialized. Make sure you call `connect()` first.") session = self.session assert session is not None - return await self._maybe_serialize_request(lambda: session.read_resource(uri)) + from mcp.types import AnyUrl + + return await self._maybe_serialize_request(lambda: session.read_resource(AnyUrl(uri))) async def cleanup(self): """Cleanup the server.""" diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index 9f315381e2..99183aea8e 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -4,6 +4,7 @@ import pytest from mcp.types import ( + AnyUrl, ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, @@ -53,7 +54,7 @@ async def test_list_resources_returns_result(server: MCPServerStreamableHttp): mock_session = MagicMock() expected = ListResourcesResult( resources=[ - Resource(uri="file:///readme.md", name="readme.md", mimeType="text/markdown"), + Resource(uri=AnyUrl("file:///readme.md"), name="readme.md", mimeType="text/markdown"), ] ) mock_session.list_resources = AsyncMock(return_value=expected) @@ -90,7 +91,7 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): uri = "file:///readme.md" expected = ReadResourceResult( contents=[ - TextResourceContents(uri=uri, text="# Hello", mimeType="text/markdown"), + TextResourceContents(uri=AnyUrl(uri), text="# Hello", mimeType="text/markdown"), ] ) mock_session.read_resource = AsyncMock(return_value=expected) @@ -99,7 +100,7 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): result = await server.read_resource(uri) assert result is expected - mock_session.read_resource.assert_awaited_once_with(uri) + mock_session.read_resource.assert_awaited_once_with(AnyUrl(uri)) @pytest.mark.asyncio From bfa45e22bef55b1bac76e0a3a65b6beb129c612f Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 19 Mar 2026 23:44:11 +0000 Subject: [PATCH 04/10] fix(typecheck): import AnyUrl from pydantic, not mcp.types mcp.types re-exports AnyUrl at runtime but mypy reports it as not explicitly exported ([attr-defined]). Import directly from pydantic where AnyUrl originates. --- tests/mcp/test_mcp_resources.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index 99183aea8e..c7fef07a91 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -1,3 +1,5 @@ +from pydantic import AnyUrl + """Tests for MCP server list_resources, list_resource_templates, and read_resource.""" from unittest.mock import AsyncMock, MagicMock From bfee07d736172bfe9e52f8b853c7c0c771cd1963 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 19 Mar 2026 23:44:48 +0000 Subject: [PATCH 05/10] =?UTF-8?q?fix(style):=20clean=20up=20test=20imports?= =?UTF-8?q?=20=E2=80=94=20move=20pydantic=20AnyUrl=20to=20top,=20remove=20?= =?UTF-8?q?duplicate=20mcp.types=20AnyUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/mcp/test_mcp_resources.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index c7fef07a91..a36e954463 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -1,12 +1,9 @@ -from pydantic import AnyUrl - """Tests for MCP server list_resources, list_resource_templates, and read_resource.""" from unittest.mock import AsyncMock, MagicMock import pytest from mcp.types import ( - AnyUrl, ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, @@ -14,6 +11,7 @@ ResourceTemplate, TextResourceContents, ) +from pydantic import AnyUrl from agents.mcp import MCPServerStreamableHttp @@ -108,7 +106,7 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): @pytest.mark.asyncio async def test_base_methods_raise_not_implemented(): """Bare MCPServer subclasses that don't override resource methods get NotImplementedError.""" - from mcp.types import CallToolResult + from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult from agents.mcp import MCPServer @@ -132,13 +130,9 @@ async def call_tool(self, tool_name, tool_arguments, run_context=None, agent=Non return CallToolResult(content=[]) async def list_prompts(self): - from mcp.types import ListPromptsResult - return ListPromptsResult(prompts=[]) async def get_prompt(self, name, arguments=None): - from mcp.types import GetPromptResult - return GetPromptResult(messages=[]) s = MinimalServer() From 4fbb7efc069985a2958e8cb9f59aacb2d4908196 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 20 Mar 2026 00:24:18 +0000 Subject: [PATCH 06/10] fix(typecheck): use pydantic.AnyUrl in server.py (mcp.types re-export not recognized by mypy) --- src/agents/mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 3843b4b9d3..d0f3940772 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -782,7 +782,7 @@ async def read_resource(self, uri: str) -> ReadResourceResult: raise UserError("Server not initialized. Make sure you call `connect()` first.") session = self.session assert session is not None - from mcp.types import AnyUrl + from pydantic import AnyUrl return await self._maybe_serialize_request(lambda: session.read_resource(AnyUrl(uri))) From 9e4bd5224c0553d5220e535b08ed17c852616473 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 20 Mar 2026 04:08:05 +0000 Subject: [PATCH 07/10] feat(mcp): add cursor pagination to list_resources and list_resource_templates seratch's review correctly noted that callers had no way to page through results beyond the first page on servers that paginate these endpoints. Changes: - MCPServer.list_resources(cursor=None) / list_resource_templates(cursor=None) base methods now accept an optional opaque cursor string - _MCPServerWithClientSession concrete implementations forward the cursor to session.list_resources(cursor) / session.list_resource_templates(cursor) - Docstrings document the nextCursor / cursor round-trip pattern - Test helpers updated to match the new signature - Two new tests verify cursor is forwarded correctly for both methods --- src/agents/mcp/server.py | 43 ++++++++++++++++++++-------- tests/mcp/test_mcp_resources.py | 32 +++++++++++++++++++-- tests/mcp/test_mcp_server_manager.py | 24 ++++++++++------ tests/mcp/test_prompt_server.py | 6 ++-- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index d0f3940772..d53fa34c0c 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -200,24 +200,41 @@ async def get_prompt( """Get a specific prompt from the server.""" pass - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server. - Returns a :class:`~mcp.types.ListResourcesResult` containing all resources - exposed by the server. Subclasses that do not support resources may leave - this unimplemented; it will raise :exc:`NotImplementedError` at call time. + Args: + cursor: An opaque pagination cursor returned in a previous + :class:`~mcp.types.ListResourcesResult` as ``nextCursor``. Pass it + here to fetch the next page of results. ``None`` fetches the first + page. + + Returns a :class:`~mcp.types.ListResourcesResult`. When the result contains + a ``nextCursor`` field, call this method again with that cursor to retrieve + the next page. Subclasses that do not support resources may leave this + unimplemented; it will raise :exc:`NotImplementedError` at call time. """ raise NotImplementedError( f"MCP server '{self.name}' does not support list_resources. " "Override this method in your server implementation." ) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: """List the resource templates available on the server. - Returns a :class:`~mcp.types.ListResourceTemplatesResult`. Subclasses that - do not support resource templates may leave this unimplemented; it will raise - :exc:`NotImplementedError` at call time. + Args: + cursor: An opaque pagination cursor returned in a previous + :class:`~mcp.types.ListResourceTemplatesResult` as ``nextCursor``. + Pass it here to fetch the next page of results. ``None`` fetches + the first page. + + Returns a :class:`~mcp.types.ListResourceTemplatesResult`. When the result + contains a ``nextCursor`` field, call this method again with that cursor to + retrieve the next page. Subclasses that do not support resource templates + may leave this unimplemented; it will raise :exc:`NotImplementedError` at + call time. """ raise NotImplementedError( f"MCP server '{self.name}' does not support list_resource_templates. " @@ -755,21 +772,23 @@ async def get_prompt( assert session is not None return await self._maybe_serialize_request(lambda: session.get_prompt(name, arguments)) - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """List the resources available on the server.""" if not self.session: raise UserError("Server not initialized. Make sure you call `connect()` first.") session = self.session assert session is not None - return await self._maybe_serialize_request(lambda: session.list_resources()) + return await self._maybe_serialize_request(lambda: session.list_resources(cursor)) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: """List the resource templates available on the server.""" if not self.session: raise UserError("Server not initialized. Make sure you call `connect()` first.") session = self.session assert session is not None - return await self._maybe_serialize_request(lambda: session.list_resource_templates()) + return await self._maybe_serialize_request(lambda: session.list_resource_templates(cursor)) async def read_resource(self, uri: str) -> ReadResourceResult: """Read the contents of a specific resource by URI. diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index a36e954463..75bacc99f7 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -63,7 +63,21 @@ async def test_list_resources_returns_result(server: MCPServerStreamableHttp): result = await server.list_resources() assert result is expected - mock_session.list_resources.assert_awaited_once() + mock_session.list_resources.assert_awaited_once_with(None) + + +@pytest.mark.asyncio +async def test_list_resources_forwards_cursor(server: MCPServerStreamableHttp): + """list_resources forwards the cursor argument for pagination.""" + mock_session = MagicMock() + page2 = ListResourcesResult(resources=[]) + mock_session.list_resources = AsyncMock(return_value=page2) + server.session = mock_session + + result = await server.list_resources(cursor="tok_abc") + + assert result is page2 + mock_session.list_resources.assert_awaited_once_with("tok_abc") @pytest.mark.asyncio @@ -81,7 +95,21 @@ async def test_list_resource_templates_returns_result(server: MCPServerStreamabl result = await server.list_resource_templates() assert result is expected - mock_session.list_resource_templates.assert_awaited_once() + mock_session.list_resource_templates.assert_awaited_once_with(None) + + +@pytest.mark.asyncio +async def test_list_resource_templates_forwards_cursor(server: MCPServerStreamableHttp): + """list_resource_templates forwards the cursor argument for pagination.""" + mock_session = MagicMock() + page2 = ListResourceTemplatesResult(resourceTemplates=[]) + mock_session.list_resource_templates = AsyncMock(return_value=page2) + server.session = mock_session + + result = await server.list_resource_templates(cursor="tok_xyz") + + assert result is page2 + mock_session.list_resource_templates.assert_awaited_once_with("tok_xyz") @pytest.mark.asyncio diff --git a/tests/mcp/test_mcp_server_manager.py b/tests/mcp/test_mcp_server_manager.py index 12089c589a..3ed2f35a86 100644 --- a/tests/mcp/test_mcp_server_manager.py +++ b/tests/mcp/test_mcp_server_manager.py @@ -57,10 +57,12 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: return ListResourceTemplatesResult(resourceTemplates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: @@ -107,10 +109,12 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: return ListResourceTemplatesResult(resourceTemplates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: @@ -156,10 +160,12 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: return ListResourceTemplatesResult(resourceTemplates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: @@ -198,10 +204,12 @@ async def get_prompt( ) -> GetPromptResult: raise NotImplementedError - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: return ListResourceTemplatesResult(resourceTemplates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: diff --git a/tests/mcp/test_prompt_server.py b/tests/mcp/test_prompt_server.py index 7d7e531ddd..cf6254e5dd 100644 --- a/tests/mcp/test_prompt_server.py +++ b/tests/mcp/test_prompt_server.py @@ -77,10 +77,12 @@ async def call_tool( ): raise NotImplementedError("This fake server doesn't support tools") - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: return ListResourceTemplatesResult(resourceTemplates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: From 1fbb55333561ab8661e002f60313fffc42b095b8 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 20 Mar 2026 04:17:08 +0000 Subject: [PATCH 08/10] fix(typecheck): add cursor param to FakeMCPServer stubs to match base class signature --- tests/mcp/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 3d4c7cee77..93e9491cc7 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -141,11 +141,11 @@ async def get_prompt( message = PromptMessage(role="user", content=TextContent(type="text", text=content)) return GetPromptResult(description=f"Fake prompt: {name}", messages=[message]) - async def list_resources(self) -> ListResourcesResult: + async def list_resources(self, cursor: str | None = None) -> ListResourcesResult: """Return empty list of resources for fake server.""" return ListResourcesResult(resources=[]) - async def list_resource_templates(self) -> ListResourceTemplatesResult: + async def list_resource_templates(self, cursor: str | None = None) -> ListResourceTemplatesResult: """Return empty list of resource templates for fake server.""" return ListResourceTemplatesResult(resourceTemplates=[]) From af3b9046c1f35d7efe08678780e3ffaff7e0d6fb Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 20 Mar 2026 04:32:41 +0000 Subject: [PATCH 09/10] fix(lint): ruff format helpers.py --- tests/mcp/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 93e9491cc7..ef820fad99 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -145,7 +145,9 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult """Return empty list of resources for fake server.""" return ListResourcesResult(resources=[]) - async def list_resource_templates(self, cursor: str | None = None) -> ListResourceTemplatesResult: + async def list_resource_templates( + self, cursor: str | None = None + ) -> ListResourceTemplatesResult: """Return empty list of resource templates for fake server.""" return ListResourceTemplatesResult(resourceTemplates=[]) From 6c50eb3cb8965a2bd74bbe3ca6bd4e24afbf4f43 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 20 Mar 2026 16:32:26 +0000 Subject: [PATCH 10/10] docs: replace URI examples with AnyUrl type reference in docstrings Per seratch's review: avoid hardcoding specific URI examples that can become outdated. Reference pydantic.networks.AnyUrl instead. --- src/agents/mcp/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index d53fa34c0c..363d6995b4 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -245,7 +245,8 @@ async def read_resource(self, uri: str) -> ReadResourceResult: """Read the contents of a specific resource by URI. Args: - uri: The URI of the resource to read (e.g. ``file:///path/to/file.txt``). + uri: The URI of the resource to read. See :class:`~pydantic.networks.AnyUrl` + for the supported URI formats. Returns a :class:`~mcp.types.ReadResourceResult`. Subclasses that do not support resources may leave this unimplemented; it will raise @@ -794,8 +795,8 @@ async def read_resource(self, uri: str) -> ReadResourceResult: """Read the contents of a specific resource by URI. Args: - uri: The URI of the resource to read (e.g. ``file:///path/to/file.txt`` or - ``postgres://db/table/row``). + uri: The URI of the resource to read. See :class:`~pydantic.networks.AnyUrl` + for the supported URI formats. """ if not self.session: raise UserError("Server not initialized. Make sure you call `connect()` first.")