From 7e7674eb183def9ed8c4a3f0b3861f432e3f069f Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Sun, 18 May 2025 06:04:29 -0700 Subject: [PATCH 1/5] fix: Pass cursor parameter to server --- src/mcp/client/session.py | 8 +- src/mcp/types.py | 32 ++-- tests/client/conftest.py | 140 +++++++++++++++ tests/client/test_list_methods_cursor.py | 209 +++++++++++++++++------ 4 files changed, 320 insertions(+), 69 deletions(-) create mode 100644 tests/client/conftest.py diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index c714c44bb..f784ecc5d 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -209,7 +209,7 @@ async def list_resources( types.ClientRequest( types.ListResourcesRequest( method="resources/list", - cursor=cursor, + params=types.PaginatedRequestParams(cursor=cursor), ) ), types.ListResourcesResult, @@ -223,7 +223,7 @@ async def list_resource_templates( types.ClientRequest( types.ListResourceTemplatesRequest( method="resources/templates/list", - cursor=cursor, + params=types.PaginatedRequestParams(cursor=cursor), ) ), types.ListResourceTemplatesResult, @@ -295,7 +295,7 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu types.ClientRequest( types.ListPromptsRequest( method="prompts/list", - cursor=cursor, + params=types.PaginatedRequestParams(cursor=cursor), ) ), types.ListPromptsResult, @@ -340,7 +340,7 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult: types.ClientRequest( types.ListToolsRequest( method="tools/list", - cursor=cursor, + params=types.PaginatedRequestParams(cursor=cursor), ) ), types.ListToolsResult, diff --git a/src/mcp/types.py b/src/mcp/types.py index d864b19da..37f01ce4d 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -53,6 +53,14 @@ class Meta(BaseModel): meta: Meta | None = Field(alias="_meta", default=None) +class PaginatedRequestParams(RequestParams): + cursor: Cursor | None = None + """ + An opaque token representing the current pagination position. + If provided, the server should return results starting after this cursor. + """ + + class NotificationParams(BaseModel): class Meta(BaseModel): model_config = ConfigDict(extra="allow") @@ -79,14 +87,6 @@ class Request(BaseModel, Generic[RequestParamsT, MethodT]): model_config = ConfigDict(extra="allow") -class PaginatedRequest(Request[RequestParamsT, MethodT]): - cursor: Cursor | None = None - """ - An opaque token representing the current pagination position. - If provided, the server should return results starting after this cursor. - """ - - class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): """Base class for JSON-RPC notifications.""" @@ -359,12 +359,12 @@ class ProgressNotification( class ListResourcesRequest( - PaginatedRequest[RequestParams | None, Literal["resources/list"]] + Request[PaginatedRequestParams | None, Literal["resources/list"]] ): """Sent from the client to request a list of resources the server has.""" method: Literal["resources/list"] - params: RequestParams | None = None + params: PaginatedRequestParams | None = None class Annotations(BaseModel): @@ -423,12 +423,12 @@ class ListResourcesResult(PaginatedResult): class ListResourceTemplatesRequest( - PaginatedRequest[RequestParams | None, Literal["resources/templates/list"]] + Request[PaginatedRequestParams | None, Literal["resources/templates/list"]] ): """Sent from the client to request a list of resource templates the server has.""" method: Literal["resources/templates/list"] - params: RequestParams | None = None + params: PaginatedRequestParams | None = None class ListResourceTemplatesResult(PaginatedResult): @@ -571,12 +571,12 @@ class ResourceUpdatedNotification( class ListPromptsRequest( - PaginatedRequest[RequestParams | None, Literal["prompts/list"]] + Request[PaginatedRequestParams | None, Literal["prompts/list"]] ): """Sent from the client to request a list of prompts and prompt templates.""" method: Literal["prompts/list"] - params: RequestParams | None = None + params: PaginatedRequestParams | None = None class PromptArgument(BaseModel): @@ -703,11 +703,11 @@ class PromptListChangedNotification( params: NotificationParams | None = None -class ListToolsRequest(PaginatedRequest[RequestParams | None, Literal["tools/list"]]): +class ListToolsRequest(Request[PaginatedRequestParams | None, Literal["tools/list"]]): """Sent from the client to request a list of tools the server has.""" method: Literal["tools/list"] - params: RequestParams | None = None + params: PaginatedRequestParams | None = None class ToolAnnotations(BaseModel): diff --git a/tests/client/conftest.py b/tests/client/conftest.py new file mode 100644 index 000000000..f9df7f765 --- /dev/null +++ b/tests/client/conftest.py @@ -0,0 +1,140 @@ +from contextlib import asynccontextmanager +from unittest.mock import patch + +import pytest + +import mcp.shared.memory +from mcp.shared.message import SessionMessage +from mcp.types import ( + JSONRPCNotification, + JSONRPCRequest, +) + + +class SpyMemoryObjectSendStream: + def __init__(self, original_stream): + self.original_stream = original_stream + self.sent_messages: list[SessionMessage] = [] + + async def send(self, message): + self.sent_messages.append(message) + await self.original_stream.send(message) + + async def aclose(self): + await self.original_stream.aclose() + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + await self.aclose() + + +class StreamSpyCollection: + def __init__( + self, + client_spy: SpyMemoryObjectSendStream, + server_spy: SpyMemoryObjectSendStream, + ): + self.client = client_spy + self.server = server_spy + + def clear(self) -> None: + """Clear all captured messages.""" + self.client.sent_messages.clear() + self.server.sent_messages.clear() + + def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]: + """Get client-sent requests, optionally filtered by method.""" + return [ + req.message.root + for req in self.client.sent_messages + if isinstance(req.message.root, JSONRPCRequest) + and (method is None or req.message.root.method == method) + ] + + def get_server_requests(self, method: str | None = None) -> list[JSONRPCRequest]: + """Get server-sent requests, optionally filtered by method.""" + return [ + req.message.root + for req in self.server.sent_messages + if isinstance(req.message.root, JSONRPCRequest) + and (method is None or req.message.root.method == method) + ] + + def get_client_notifications( + self, method: str | None = None + ) -> list[JSONRPCNotification]: + """Get client-sent notifications, optionally filtered by method.""" + return [ + notif.message.root + for notif in self.client.sent_messages + if isinstance(notif.message.root, JSONRPCNotification) + and (method is None or notif.message.root.method == method) + ] + + def get_server_notifications( + self, method: str | None = None + ) -> list[JSONRPCNotification]: + """Get server-sent notifications, optionally filtered by method.""" + return [ + notif.message.root + for notif in self.server.sent_messages + if isinstance(notif.message.root, JSONRPCNotification) + and (method is None or notif.message.root.method == method) + ] + + +@pytest.fixture +def stream_spy(): + """Fixture that provides spies for both client and server write streams. + + Example usage: + async def test_something(stream_spy): + # ... set up server and client ... + + spies = stream_spy() + + # Run some operation that sends messages + await client.some_operation() + + # Check the messages + requests = spies.get_client_requests(method="some/method") + assert len(requests) == 1 + + # Clear for the next operation + spies.clear() + """ + client_spy = None + server_spy = None + + # Store references to our spy objects + def capture_spies(c_spy, s_spy): + nonlocal client_spy, server_spy + client_spy = c_spy + server_spy = s_spy + + # Create patched version of stream creation + original_create_streams = mcp.shared.memory.create_client_server_memory_streams + + @asynccontextmanager + async def patched_create_streams(): + async with original_create_streams() as (client_streams, server_streams): + client_read, client_write = client_streams + server_read, server_write = server_streams + + # Create spy wrappers + spy_client_write = SpyMemoryObjectSendStream(client_write) + spy_server_write = SpyMemoryObjectSendStream(server_write) + + # Capture references for the test to use + capture_spies(spy_client_write, spy_server_write) + + yield (client_read, spy_client_write), (server_read, spy_server_write) + + # Apply the patch for the duration of the test + with patch( + "mcp.shared.memory.create_client_server_memory_streams", patched_create_streams + ): + # Return a collection with helper methods + yield lambda: StreamSpyCollection(client_spy, server_spy) diff --git a/tests/client/test_list_methods_cursor.py b/tests/client/test_list_methods_cursor.py index b0d6e36b8..7ee11a108 100644 --- a/tests/client/test_list_methods_cursor.py +++ b/tests/client/test_list_methods_cursor.py @@ -9,11 +9,11 @@ pytestmark = pytest.mark.anyio -async def test_list_tools_cursor_parameter(): - """Test that the cursor parameter is accepted for list_tools. +async def test_list_tools_cursor_parameter(stream_spy): + """Test that the cursor parameter is accepted for list_tools + and that it is correctly passed to the server. - Note: FastMCP doesn't currently implement pagination, so this test - only verifies that the cursor parameter is accepted by the client. + See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format """ server = FastMCP("test") @@ -29,28 +29,54 @@ async def test_tool_2() -> str: return "Result 2" async with create_session(server._mcp_server) as client_session: + spies = stream_spy() + # Test without cursor parameter (omitted) - result1 = await client_session.list_tools() - assert len(result1.tools) == 2 + _ = await client_session.list_tools() + list_tools_requests = spies.get_client_requests(method="tools/list") + assert len(list_tools_requests) == 1 + assert ( + list_tools_requests[0].params is None + or "cursor" not in list_tools_requests[0].params + or list_tools_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor=None - result2 = await client_session.list_tools(cursor=None) - assert len(result2.tools) == 2 + _ = await client_session.list_tools(cursor=None) + list_tools_requests = spies.get_client_requests(method="tools/list") + assert len(list_tools_requests) == 1 + assert ( + list_tools_requests[0].params is None + or "cursor" not in list_tools_requests[0].params + or list_tools_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor as string - result3 = await client_session.list_tools(cursor="some_cursor_value") - assert len(result3.tools) == 2 + _ = await client_session.list_tools(cursor="some_cursor_value") + list_tools_requests = spies.get_client_requests(method="tools/list") + assert len(list_tools_requests) == 1 + assert list_tools_requests[0].params is not None + assert list_tools_requests[0].params["cursor"] == "some_cursor_value" + + spies.clear() # Test with empty string cursor - result4 = await client_session.list_tools(cursor="") - assert len(result4.tools) == 2 + _ = await client_session.list_tools(cursor="") + list_tools_requests = spies.get_client_requests(method="tools/list") + assert len(list_tools_requests) == 1 + assert list_tools_requests[0].params is not None + assert list_tools_requests[0].params["cursor"] == "" -async def test_list_resources_cursor_parameter(): - """Test that the cursor parameter is accepted for list_resources. +async def test_list_resources_cursor_parameter(stream_spy): + """Test that the cursor parameter is accepted for list_resources + and that it is correctly passed to the server. - Note: FastMCP doesn't currently implement pagination, so this test - only verifies that the cursor parameter is accepted by the client. + See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format """ server = FastMCP("test") @@ -61,28 +87,53 @@ async def test_resource() -> str: return "Test data" async with create_session(server._mcp_server) as client_session: + spies = stream_spy() + # Test without cursor parameter (omitted) - result1 = await client_session.list_resources() - assert len(result1.resources) >= 1 + _ = await client_session.list_resources() + list_resources_requests = spies.get_client_requests(method="resources/list") + assert len(list_resources_requests) == 1 + assert ( + list_resources_requests[0].params is None + or "cursor" not in list_resources_requests[0].params + or list_resources_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor=None - result2 = await client_session.list_resources(cursor=None) - assert len(result2.resources) >= 1 + _ = await client_session.list_resources(cursor=None) + list_resources_requests = spies.get_client_requests(method="resources/list") + assert len(list_resources_requests) == 1 + assert ( + list_resources_requests[0].params is None + or "cursor" not in list_resources_requests[0].params + or list_resources_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor as string - result3 = await client_session.list_resources(cursor="some_cursor") - assert len(result3.resources) >= 1 + _ = await client_session.list_resources(cursor="some_cursor") + list_resources_requests = spies.get_client_requests(method="resources/list") + assert len(list_resources_requests) == 1 + assert list_resources_requests[0].params is not None + assert list_resources_requests[0].params["cursor"] == "some_cursor" - # Test with empty string cursor - result4 = await client_session.list_resources(cursor="") - assert len(result4.resources) >= 1 + spies.clear() + # Test with empty string cursor + _ = await client_session.list_resources(cursor="") + list_resources_requests = spies.get_client_requests(method="resources/list") + assert len(list_resources_requests) == 1 + assert list_resources_requests[0].params is not None + assert list_resources_requests[0].params["cursor"] == "" -async def test_list_prompts_cursor_parameter(): - """Test that the cursor parameter is accepted for list_prompts. - Note: FastMCP doesn't currently implement pagination, so this test - only verifies that the cursor parameter is accepted by the client. +async def test_list_prompts_cursor_parameter(stream_spy): + """Test that the cursor parameter is accepted for list_prompts + and that it is correctly passed to the server. + See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format """ server = FastMCP("test") @@ -93,28 +144,54 @@ async def test_prompt(name: str) -> str: return f"Hello, {name}!" async with create_session(server._mcp_server) as client_session: + spies = stream_spy() + # Test without cursor parameter (omitted) - result1 = await client_session.list_prompts() - assert len(result1.prompts) >= 1 + _ = await client_session.list_prompts() + list_prompts_requests = spies.get_client_requests(method="prompts/list") + assert len(list_prompts_requests) == 1 + assert ( + list_prompts_requests[0].params is None + or "cursor" not in list_prompts_requests[0].params + or list_prompts_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor=None - result2 = await client_session.list_prompts(cursor=None) - assert len(result2.prompts) >= 1 + _ = await client_session.list_prompts(cursor=None) + list_prompts_requests = spies.get_client_requests(method="prompts/list") + assert len(list_prompts_requests) == 1 + assert ( + list_prompts_requests[0].params is None + or "cursor" not in list_prompts_requests[0].params + or list_prompts_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor as string - result3 = await client_session.list_prompts(cursor="some_cursor") - assert len(result3.prompts) >= 1 + _ = await client_session.list_prompts(cursor="some_cursor") + list_prompts_requests = spies.get_client_requests(method="prompts/list") + assert len(list_prompts_requests) == 1 + assert list_prompts_requests[0].params is not None + assert list_prompts_requests[0].params["cursor"] == "some_cursor" + + spies.clear() # Test with empty string cursor - result4 = await client_session.list_prompts(cursor="") - assert len(result4.prompts) >= 1 + _ = await client_session.list_prompts(cursor="") + list_prompts_requests = spies.get_client_requests(method="prompts/list") + assert len(list_prompts_requests) == 1 + assert list_prompts_requests[0].params is not None + assert list_prompts_requests[0].params["cursor"] == "" -async def test_list_resource_templates_cursor_parameter(): - """Test that the cursor parameter is accepted for list_resource_templates. +async def test_list_resource_templates_cursor_parameter(stream_spy): + """Test that the cursor parameter is accepted for list_resource_templates + and that it is correctly passed to the server. - Note: FastMCP doesn't currently implement pagination, so this test - only verifies that the cursor parameter is accepted by the client. + See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format """ server = FastMCP("test") @@ -125,18 +202,52 @@ async def test_template(name: str) -> str: return f"Data for {name}" async with create_session(server._mcp_server) as client_session: + spies = stream_spy() + # Test without cursor parameter (omitted) - result1 = await client_session.list_resource_templates() - assert len(result1.resourceTemplates) >= 1 + _ = await client_session.list_resource_templates() + list_templates_requests = spies.get_client_requests( + method="resources/templates/list" + ) + assert len(list_templates_requests) == 1 + assert ( + list_templates_requests[0].params is None + or "cursor" not in list_templates_requests[0].params + or list_templates_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor=None - result2 = await client_session.list_resource_templates(cursor=None) - assert len(result2.resourceTemplates) >= 1 + _ = await client_session.list_resource_templates(cursor=None) + list_templates_requests = spies.get_client_requests( + method="resources/templates/list" + ) + assert len(list_templates_requests) == 1 + assert ( + list_templates_requests[0].params is None + or "cursor" not in list_templates_requests[0].params + or list_templates_requests[0].params["cursor"] is None + ) + + spies.clear() # Test with cursor as string - result3 = await client_session.list_resource_templates(cursor="some_cursor") - assert len(result3.resourceTemplates) >= 1 + _ = await client_session.list_resource_templates(cursor="some_cursor") + list_templates_requests = spies.get_client_requests( + method="resources/templates/list" + ) + assert len(list_templates_requests) == 1 + assert list_templates_requests[0].params is not None + assert list_templates_requests[0].params["cursor"] == "some_cursor" + + spies.clear() # Test with empty string cursor - result4 = await client_session.list_resource_templates(cursor="") - assert len(result4.resourceTemplates) >= 1 + _ = await client_session.list_resource_templates(cursor="") + list_templates_requests = spies.get_client_requests( + method="resources/templates/list" + ) + assert len(list_templates_requests) == 1 + assert list_templates_requests[0].params is not None + assert list_templates_requests[0].params["cursor"] == "" From 82f35efc46521539c1c3f4bcc2cc3e50e8f3e2d7 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Sun, 18 May 2025 06:04:32 -0700 Subject: [PATCH 2/5] Fix lints --- tests/client/conftest.py | 7 ++++++- tests/issues/test_129_resource_templates.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/client/conftest.py b/tests/client/conftest.py index f9df7f765..60ccac743 100644 --- a/tests/client/conftest.py +++ b/tests/client/conftest.py @@ -137,4 +137,9 @@ async def patched_create_streams(): "mcp.shared.memory.create_client_server_memory_streams", patched_create_streams ): # Return a collection with helper methods - yield lambda: StreamSpyCollection(client_spy, server_spy) + def get_spy_collection() -> StreamSpyCollection: + assert client_spy is not None, "client_spy was not initialized" + assert server_spy is not None, "server_spy was not initialized" + return StreamSpyCollection(client_spy, server_spy) + + yield get_spy_collection diff --git a/tests/issues/test_129_resource_templates.py b/tests/issues/test_129_resource_templates.py index e6eff3d46..314952303 100644 --- a/tests/issues/test_129_resource_templates.py +++ b/tests/issues/test_129_resource_templates.py @@ -25,7 +25,7 @@ def get_user_profile(user_id: str) -> str: # The handler returns a ServerResult with a ListResourceTemplatesResult inside result = await mcp._mcp_server.request_handlers[types.ListResourceTemplatesRequest]( types.ListResourceTemplatesRequest( - method="resources/templates/list", params=None, cursor=None + method="resources/templates/list", params=None ) ) assert isinstance(result.root, types.ListResourceTemplatesResult) From a529f39ccc51177a2033ab6cea84a323da971a4c Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Tue, 20 May 2025 20:46:32 -0700 Subject: [PATCH 3/5] Fixes from PR review --- src/mcp/client/session.py | 16 ++++++-- src/mcp/types.py | 21 +++++++---- tests/client/test_list_methods_cursor.py | 48 ++++-------------------- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index f784ecc5d..fe90716e2 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -209,7 +209,9 @@ async def list_resources( types.ClientRequest( types.ListResourcesRequest( method="resources/list", - params=types.PaginatedRequestParams(cursor=cursor), + params=types.PaginatedRequestParams(cursor=cursor) + if cursor is not None + else None, ) ), types.ListResourcesResult, @@ -223,7 +225,9 @@ async def list_resource_templates( types.ClientRequest( types.ListResourceTemplatesRequest( method="resources/templates/list", - params=types.PaginatedRequestParams(cursor=cursor), + params=types.PaginatedRequestParams(cursor=cursor) + if cursor is not None + else None, ) ), types.ListResourceTemplatesResult, @@ -295,7 +299,9 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu types.ClientRequest( types.ListPromptsRequest( method="prompts/list", - params=types.PaginatedRequestParams(cursor=cursor), + params=types.PaginatedRequestParams(cursor=cursor) + if cursor is not None + else None, ) ), types.ListPromptsResult, @@ -340,7 +346,9 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult: types.ClientRequest( types.ListToolsRequest( method="tools/list", - params=types.PaginatedRequestParams(cursor=cursor), + params=types.PaginatedRequestParams(cursor=cursor) + if cursor is not None + else None, ) ), types.ListToolsResult, diff --git a/src/mcp/types.py b/src/mcp/types.py index 37f01ce4d..7fd2984f0 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -87,6 +87,15 @@ class Request(BaseModel, Generic[RequestParamsT, MethodT]): model_config = ConfigDict(extra="allow") +class PaginatedRequest( + Request[PaginatedRequestParams | None, MethodT], Generic[MethodT] +): + """Base class for paginated requests, + matching the schema's PaginatedRequest interface.""" + + params: PaginatedRequestParams | None = None + + class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): """Base class for JSON-RPC notifications.""" @@ -358,9 +367,7 @@ class ProgressNotification( params: ProgressNotificationParams -class ListResourcesRequest( - Request[PaginatedRequestParams | None, Literal["resources/list"]] -): +class ListResourcesRequest(PaginatedRequest[Literal["resources/list"]]): """Sent from the client to request a list of resources the server has.""" method: Literal["resources/list"] @@ -423,7 +430,7 @@ class ListResourcesResult(PaginatedResult): class ListResourceTemplatesRequest( - Request[PaginatedRequestParams | None, Literal["resources/templates/list"]] + PaginatedRequest[Literal["resources/templates/list"]] ): """Sent from the client to request a list of resource templates the server has.""" @@ -570,9 +577,7 @@ class ResourceUpdatedNotification( params: ResourceUpdatedNotificationParams -class ListPromptsRequest( - Request[PaginatedRequestParams | None, Literal["prompts/list"]] -): +class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]): """Sent from the client to request a list of prompts and prompt templates.""" method: Literal["prompts/list"] @@ -703,7 +708,7 @@ class PromptListChangedNotification( params: NotificationParams | None = None -class ListToolsRequest(Request[PaginatedRequestParams | None, Literal["tools/list"]]): +class ListToolsRequest(PaginatedRequest[Literal["tools/list"]]): """Sent from the client to request a list of tools the server has.""" method: Literal["tools/list"] diff --git a/tests/client/test_list_methods_cursor.py b/tests/client/test_list_methods_cursor.py index 7ee11a108..a6df7ec7e 100644 --- a/tests/client/test_list_methods_cursor.py +++ b/tests/client/test_list_methods_cursor.py @@ -35,11 +35,7 @@ async def test_tool_2() -> str: _ = await client_session.list_tools() list_tools_requests = spies.get_client_requests(method="tools/list") assert len(list_tools_requests) == 1 - assert ( - list_tools_requests[0].params is None - or "cursor" not in list_tools_requests[0].params - or list_tools_requests[0].params["cursor"] is None - ) + assert list_tools_requests[0].params is None spies.clear() @@ -47,11 +43,7 @@ async def test_tool_2() -> str: _ = await client_session.list_tools(cursor=None) list_tools_requests = spies.get_client_requests(method="tools/list") assert len(list_tools_requests) == 1 - assert ( - list_tools_requests[0].params is None - or "cursor" not in list_tools_requests[0].params - or list_tools_requests[0].params["cursor"] is None - ) + assert list_tools_requests[0].params is None spies.clear() @@ -93,11 +85,7 @@ async def test_resource() -> str: _ = await client_session.list_resources() list_resources_requests = spies.get_client_requests(method="resources/list") assert len(list_resources_requests) == 1 - assert ( - list_resources_requests[0].params is None - or "cursor" not in list_resources_requests[0].params - or list_resources_requests[0].params["cursor"] is None - ) + assert list_resources_requests[0].params is None spies.clear() @@ -105,11 +93,7 @@ async def test_resource() -> str: _ = await client_session.list_resources(cursor=None) list_resources_requests = spies.get_client_requests(method="resources/list") assert len(list_resources_requests) == 1 - assert ( - list_resources_requests[0].params is None - or "cursor" not in list_resources_requests[0].params - or list_resources_requests[0].params["cursor"] is None - ) + assert list_resources_requests[0].params is None spies.clear() @@ -150,11 +134,7 @@ async def test_prompt(name: str) -> str: _ = await client_session.list_prompts() list_prompts_requests = spies.get_client_requests(method="prompts/list") assert len(list_prompts_requests) == 1 - assert ( - list_prompts_requests[0].params is None - or "cursor" not in list_prompts_requests[0].params - or list_prompts_requests[0].params["cursor"] is None - ) + assert list_prompts_requests[0].params is None spies.clear() @@ -162,11 +142,7 @@ async def test_prompt(name: str) -> str: _ = await client_session.list_prompts(cursor=None) list_prompts_requests = spies.get_client_requests(method="prompts/list") assert len(list_prompts_requests) == 1 - assert ( - list_prompts_requests[0].params is None - or "cursor" not in list_prompts_requests[0].params - or list_prompts_requests[0].params["cursor"] is None - ) + assert list_prompts_requests[0].params is None spies.clear() @@ -210,11 +186,7 @@ async def test_template(name: str) -> str: method="resources/templates/list" ) assert len(list_templates_requests) == 1 - assert ( - list_templates_requests[0].params is None - or "cursor" not in list_templates_requests[0].params - or list_templates_requests[0].params["cursor"] is None - ) + assert list_templates_requests[0].params is None spies.clear() @@ -224,11 +196,7 @@ async def test_template(name: str) -> str: method="resources/templates/list" ) assert len(list_templates_requests) == 1 - assert ( - list_templates_requests[0].params is None - or "cursor" not in list_templates_requests[0].params - or list_templates_requests[0].params["cursor"] is None - ) + assert list_templates_requests[0].params is None spies.clear() From ba90839aa5340ee83f5b58e0e1c017db2bb328bf Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 21 May 2025 10:19:08 -0700 Subject: [PATCH 4/5] Remove unnecessary field --- src/mcp/types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mcp/types.py b/src/mcp/types.py index 7fd2984f0..87c010a5e 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -93,8 +93,6 @@ class PaginatedRequest( """Base class for paginated requests, matching the schema's PaginatedRequest interface.""" - params: PaginatedRequestParams | None = None - class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): """Base class for JSON-RPC notifications.""" From bea109387042d49850a9e5ef380d278ff26a98e1 Mon Sep 17 00:00:00 2001 From: Nate Barbettini Date: Wed, 21 May 2025 14:17:48 -0700 Subject: [PATCH 5/5] Final cleanup --- src/mcp/types.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/mcp/types.py b/src/mcp/types.py index 87c010a5e..465fc6ee6 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -93,6 +93,8 @@ class PaginatedRequest( """Base class for paginated requests, matching the schema's PaginatedRequest interface.""" + params: PaginatedRequestParams | None = None + class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): """Base class for JSON-RPC notifications.""" @@ -369,7 +371,6 @@ class ListResourcesRequest(PaginatedRequest[Literal["resources/list"]]): """Sent from the client to request a list of resources the server has.""" method: Literal["resources/list"] - params: PaginatedRequestParams | None = None class Annotations(BaseModel): @@ -433,7 +434,6 @@ class ListResourceTemplatesRequest( """Sent from the client to request a list of resource templates the server has.""" method: Literal["resources/templates/list"] - params: PaginatedRequestParams | None = None class ListResourceTemplatesResult(PaginatedResult): @@ -579,7 +579,6 @@ class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]): """Sent from the client to request a list of prompts and prompt templates.""" method: Literal["prompts/list"] - params: PaginatedRequestParams | None = None class PromptArgument(BaseModel): @@ -710,7 +709,6 @@ class ListToolsRequest(PaginatedRequest[Literal["tools/list"]]): """Sent from the client to request a list of tools the server has.""" method: Literal["tools/list"] - params: PaginatedRequestParams | None = None class ToolAnnotations(BaseModel): @@ -744,7 +742,7 @@ class ToolAnnotations(BaseModel): idempotentHint: bool | None = None """ - If true, calling the tool repeatedly with the same arguments + If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment. (This property is meaningful only when `readOnlyHint == false`) Default: false