Skip to content

Commit 03b89e9

Browse files
fix: send params as empty object for list methods without cursor
Some external MCP servers implement strict JSON-RPC validation requiring the params field to always be present. Previously, the Python SDK omitted params entirely when cursor=None, causing validation errors. Now sends params: {} when no cursor is provided, matching TypeScript SDK behavior and fixing compatibility with strict servers. Reported-by: justin-yi-wang
1 parent 61399b3 commit 03b89e9

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

src/mcp/client/session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ async def list_resources(self, cursor: str | None = None) -> types.ListResources
217217
return await self.send_request(
218218
types.ClientRequest(
219219
types.ListResourcesRequest(
220-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
220+
params=types.PaginatedRequestParams(cursor=cursor),
221221
)
222222
),
223223
types.ListResourcesResult,
@@ -228,7 +228,7 @@ async def list_resource_templates(self, cursor: str | None = None) -> types.List
228228
return await self.send_request(
229229
types.ClientRequest(
230230
types.ListResourceTemplatesRequest(
231-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
231+
params=types.PaginatedRequestParams(cursor=cursor),
232232
)
233233
),
234234
types.ListResourceTemplatesResult,
@@ -322,7 +322,7 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu
322322
return await self.send_request(
323323
types.ClientRequest(
324324
types.ListPromptsRequest(
325-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
325+
params=types.PaginatedRequestParams(cursor=cursor),
326326
)
327327
),
328328
types.ListPromptsResult,
@@ -368,7 +368,7 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
368368
result = await self.send_request(
369369
types.ClientRequest(
370370
types.ListToolsRequest(
371-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
371+
params=types.PaginatedRequestParams(cursor=cursor),
372372
)
373373
),
374374
types.ListToolsResult,

tests/client/test_list_methods_cursor.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import pytest
44

5+
from mcp.server import Server
56
from mcp.server.fastmcp import FastMCP
67
from mcp.shared.memory import create_connected_server_and_client_session as create_session
8+
from mcp.types import ListToolsRequest, ListToolsResult
79

810
from .conftest import StreamSpyCollection
911

@@ -36,15 +38,15 @@ async def test_tool_2() -> str:
3638
_ = await client_session.list_tools()
3739
list_tools_requests = spies.get_client_requests(method="tools/list")
3840
assert len(list_tools_requests) == 1
39-
assert list_tools_requests[0].params is None
41+
assert list_tools_requests[0].params == {}
4042

4143
spies.clear()
4244

4345
# Test with cursor=None
4446
_ = await client_session.list_tools(cursor=None)
4547
list_tools_requests = spies.get_client_requests(method="tools/list")
4648
assert len(list_tools_requests) == 1
47-
assert list_tools_requests[0].params is None
49+
assert list_tools_requests[0].params == {}
4850

4951
spies.clear()
5052

@@ -86,15 +88,15 @@ async def test_resource() -> str:
8688
_ = await client_session.list_resources()
8789
list_resources_requests = spies.get_client_requests(method="resources/list")
8890
assert len(list_resources_requests) == 1
89-
assert list_resources_requests[0].params is None
91+
assert list_resources_requests[0].params == {}
9092

9193
spies.clear()
9294

9395
# Test with cursor=None
9496
_ = await client_session.list_resources(cursor=None)
9597
list_resources_requests = spies.get_client_requests(method="resources/list")
9698
assert len(list_resources_requests) == 1
97-
assert list_resources_requests[0].params is None
99+
assert list_resources_requests[0].params == {}
98100

99101
spies.clear()
100102

@@ -135,15 +137,15 @@ async def test_prompt(name: str) -> str:
135137
_ = await client_session.list_prompts()
136138
list_prompts_requests = spies.get_client_requests(method="prompts/list")
137139
assert len(list_prompts_requests) == 1
138-
assert list_prompts_requests[0].params is None
140+
assert list_prompts_requests[0].params == {}
139141

140142
spies.clear()
141143

142144
# Test with cursor=None
143145
_ = await client_session.list_prompts(cursor=None)
144146
list_prompts_requests = spies.get_client_requests(method="prompts/list")
145147
assert len(list_prompts_requests) == 1
146-
assert list_prompts_requests[0].params is None
148+
assert list_prompts_requests[0].params == {}
147149

148150
spies.clear()
149151

@@ -185,15 +187,15 @@ async def test_template(name: str) -> str:
185187
_ = await client_session.list_resource_templates()
186188
list_templates_requests = spies.get_client_requests(method="resources/templates/list")
187189
assert len(list_templates_requests) == 1
188-
assert list_templates_requests[0].params is None
190+
assert list_templates_requests[0].params == {}
189191

190192
spies.clear()
191193

192194
# Test with cursor=None
193195
_ = await client_session.list_resource_templates(cursor=None)
194196
list_templates_requests = spies.get_client_requests(method="resources/templates/list")
195197
assert len(list_templates_requests) == 1
196-
assert list_templates_requests[0].params is None
198+
assert list_templates_requests[0].params == {}
197199

198200
spies.clear()
199201

@@ -212,3 +214,35 @@ async def test_template(name: str) -> str:
212214
assert len(list_templates_requests) == 1
213215
assert list_templates_requests[0].params is not None
214216
assert list_templates_requests[0].params["cursor"] == ""
217+
218+
219+
async def test_list_tools_with_strict_server_validation():
220+
"""Test that list_tools works with strict servers require a params field,
221+
even if it is empty.
222+
223+
Some MCP servers may implement strict JSON-RPC validation that requires
224+
the params field to always be present in requests, even if empty {}.
225+
226+
This test ensures such servers are supported by the client SDK for list_resources
227+
requests without a cursor.
228+
"""
229+
230+
server = Server("strict_server")
231+
232+
@server.list_tools()
233+
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
234+
"""Strict handler that validates params field exists"""
235+
236+
# Simulate strict server validation
237+
if request.params is None:
238+
raise ValueError(
239+
"Strict server validation failed: params field must be present. "
240+
"Expected params: {} for requests without cursor."
241+
)
242+
243+
# Return empty tools list
244+
return ListToolsResult(tools=[])
245+
246+
async with create_session(server) as client_session:
247+
result = await client_session.list_tools()
248+
assert result is not None

0 commit comments

Comments
 (0)