Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def list_resources(self, cursor: str | None = None) -> types.ListResources
return await self.send_request(
types.ClientRequest(
types.ListResourcesRequest(
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
params=types.PaginatedRequestParams(cursor=cursor),
)
),
types.ListResourcesResult,
Expand All @@ -228,7 +228,7 @@ async def list_resource_templates(self, cursor: str | None = None) -> types.List
return await self.send_request(
types.ClientRequest(
types.ListResourceTemplatesRequest(
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
params=types.PaginatedRequestParams(cursor=cursor),
)
),
types.ListResourceTemplatesResult,
Expand Down Expand Up @@ -322,7 +322,7 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu
return await self.send_request(
types.ClientRequest(
types.ListPromptsRequest(
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
params=types.PaginatedRequestParams(cursor=cursor),
)
),
types.ListPromptsResult,
Expand Down Expand Up @@ -368,7 +368,7 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
result = await self.send_request(
types.ClientRequest(
types.ListToolsRequest(
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
params=types.PaginatedRequestParams(cursor=cursor),
)
),
types.ListToolsResult,
Expand Down
50 changes: 42 additions & 8 deletions tests/client/test_list_methods_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import pytest

from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import create_connected_server_and_client_session as create_session
from mcp.types import ListToolsRequest, ListToolsResult

from .conftest import StreamSpyCollection

Expand Down Expand Up @@ -36,15 +38,15 @@ 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
assert list_tools_requests[0].params == {}

spies.clear()

# Test with cursor=None
_ = 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
assert list_tools_requests[0].params == {}

spies.clear()

Expand Down Expand Up @@ -86,15 +88,15 @@ 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
assert list_resources_requests[0].params == {}

spies.clear()

# Test with cursor=None
_ = 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
assert list_resources_requests[0].params == {}

spies.clear()

Expand Down Expand Up @@ -135,15 +137,15 @@ 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
assert list_prompts_requests[0].params == {}

spies.clear()

# Test with cursor=None
_ = 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
assert list_prompts_requests[0].params == {}

spies.clear()

Expand Down Expand Up @@ -185,15 +187,15 @@ async def test_template(name: str) -> str:
_ = 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
assert list_templates_requests[0].params == {}

spies.clear()

# Test with cursor=None
_ = 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
assert list_templates_requests[0].params == {}

spies.clear()

Expand All @@ -212,3 +214,35 @@ async def test_template(name: str) -> str:
assert len(list_templates_requests) == 1
assert list_templates_requests[0].params is not None
assert list_templates_requests[0].params["cursor"] == ""


async def test_list_tools_with_strict_server_validation():
"""Test that list_tools works with strict servers require a params field,
even if it is empty.

Some MCP servers may implement strict JSON-RPC validation that requires
the params field to always be present in requests, even if empty {}.

This test ensures such servers are supported by the client SDK for list_resources
requests without a cursor.
"""

server = Server("strict_server")

@server.list_tools()
async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
"""Strict handler that validates params field exists"""

# Simulate strict server validation
if request.params is None:
raise ValueError(
"Strict server validation failed: params field must be present. "
"Expected params: {} for requests without cursor."
)

# Return empty tools list
return ListToolsResult(tools=[])

async with create_session(server) as client_session:
result = await client_session.list_tools()
assert result is not None
Loading