Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions src/mcp/shared/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ async def send_request(
meta: dict[str, Any] = request_data.setdefault("params", {}).setdefault("_meta", {})
inject_trace_context(meta)

# Strip envelope fields that would collide with the explicit kwargs below.
# Happens when request_data comes from model_dump() on a JSONRPCRequest
# (e.g. forwarding/proxy scenarios). See issue #2548.
request_data.pop("jsonrpc", None)
request_data.pop("id", None)

jsonrpc_request = JSONRPCRequest(jsonrpc="2.0", id=request_id, **request_data)
await self._write_stream.send(SessionMessage(message=jsonrpc_request, metadata=metadata))

Expand Down
53 changes: 53 additions & 0 deletions tests/shared/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,56 @@ async def make_request(client_session: ClientSession):
# Pending request completed successfully
assert len(result_holder) == 1
assert isinstance(result_holder[0], EmptyResult)


@pytest.mark.anyio
async def test_send_request_strips_envelope_fields_when_forwarding():
"""Test that send_request does not raise TypeError when request_data contains
jsonrpc and id fields (e.g. from model_dump() on a JSONRPCRequest).

This is the forwarding/proxy scenario: a session receives an incoming
JSONRPCRequest and re-emits it via another session's send_request.
model_dump() on a JSONRPCRequest preserves the envelope fields jsonrpc and
id, which used to collide with the explicit kwargs at the construction site.
Regression test for issue #2548.
"""
ev_response_received = anyio.Event()
result_holder: list[EmptyResult] = []

async with create_client_server_memory_streams() as (client_streams, server_streams):
client_read, client_write = client_streams
server_read, server_write = server_streams

async def mock_server() -> None:
"""Receive the forwarded request and send back an empty response."""
message = await server_read.receive()
assert isinstance(message, SessionMessage)
assert isinstance(message.message, JSONRPCRequest)
request_id = message.message.id
await server_write.send(SessionMessage(message=JSONRPCResponse(jsonrpc="2.0", id=request_id, result={})))

async def forward_request(client_session: ClientSession) -> None:
"""Simulate a proxy forwarding an incoming JSONRPCRequest."""
# Build a JSONRPCRequest as a proxy would receive it from an upstream
# server. model_dump() on this object includes jsonrpc and id, which
# previously caused TypeError: got multiple values for keyword argument.
incoming = JSONRPCRequest(jsonrpc="2.0", id=99, method="ping", params=None)
result = await client_session.send_request(
incoming, # type: ignore[arg-type]
EmptyResult,
)
result_holder.append(result)
ev_response_received.set()

async with (
anyio.create_task_group() as tg,
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
):
tg.start_soon(mock_server)
tg.start_soon(forward_request, client_session)

with anyio.fail_after(2): # pragma: no branch
await ev_response_received.wait()

assert len(result_holder) == 1
assert isinstance(result_holder[0], EmptyResult)
Loading