Skip to content

Commit 906f53c

Browse files
fix(shared): strip envelope fields from request_data before re-wrapping
BaseSession.send_request constructs a JSONRPCRequest as: JSONRPCRequest(jsonrpc="2.0", id=request_id, **request_data) When request_data came from model_dump() on an existing JSONRPCRequest (forwarding/proxy scenarios — e.g. a ServerSession receiving a request and re-emitting it through a ClientSession), the dump preserves the envelope fields jsonrpc and id, which then collide with the explicit kwargs above and raise: TypeError: got multiple values for keyword argument 'jsonrpc' Fix: pop jsonrpc and id from request_data before the unpack. Added a regression test that exercises the forwarding pattern via create_client_server_memory_streams. Fixes #2548
1 parent 161834d commit 906f53c

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

src/mcp/shared/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ async def send_request(
282282
meta: dict[str, Any] = request_data.setdefault("params", {}).setdefault("_meta", {})
283283
inject_trace_context(meta)
284284

285+
# Strip envelope fields that would collide with the explicit kwargs below.
286+
# Happens when request_data comes from model_dump() on a JSONRPCRequest
287+
# (e.g. forwarding/proxy scenarios). See issue #2548.
288+
request_data.pop("jsonrpc", None)
289+
request_data.pop("id", None)
290+
285291
jsonrpc_request = JSONRPCRequest(jsonrpc="2.0", id=request_id, **request_data)
286292
await self._write_stream.send(SessionMessage(message=jsonrpc_request, metadata=metadata))
287293

tests/shared/test_session.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,56 @@ async def make_request(client_session: ClientSession):
416416
# Pending request completed successfully
417417
assert len(result_holder) == 1
418418
assert isinstance(result_holder[0], EmptyResult)
419+
420+
421+
@pytest.mark.anyio
422+
async def test_send_request_strips_envelope_fields_when_forwarding():
423+
"""Test that send_request does not raise TypeError when request_data contains
424+
jsonrpc and id fields (e.g. from model_dump() on a JSONRPCRequest).
425+
426+
This is the forwarding/proxy scenario: a session receives an incoming
427+
JSONRPCRequest and re-emits it via another session's send_request.
428+
model_dump() on a JSONRPCRequest preserves the envelope fields jsonrpc and
429+
id, which used to collide with the explicit kwargs at the construction site.
430+
Regression test for issue #2548.
431+
"""
432+
ev_response_received = anyio.Event()
433+
result_holder: list[EmptyResult] = []
434+
435+
async with create_client_server_memory_streams() as (client_streams, server_streams):
436+
client_read, client_write = client_streams
437+
server_read, server_write = server_streams
438+
439+
async def mock_server() -> None:
440+
"""Receive the forwarded request and send back an empty response."""
441+
message = await server_read.receive()
442+
assert isinstance(message, SessionMessage)
443+
assert isinstance(message.message, JSONRPCRequest)
444+
request_id = message.message.id
445+
await server_write.send(SessionMessage(message=JSONRPCResponse(jsonrpc="2.0", id=request_id, result={})))
446+
447+
async def forward_request(client_session: ClientSession) -> None:
448+
"""Simulate a proxy forwarding an incoming JSONRPCRequest."""
449+
# Build a JSONRPCRequest as a proxy would receive it from an upstream
450+
# server. model_dump() on this object includes jsonrpc and id, which
451+
# previously caused TypeError: got multiple values for keyword argument.
452+
incoming = JSONRPCRequest(jsonrpc="2.0", id=99, method="ping", params=None)
453+
result = await client_session.send_request(
454+
incoming, # type: ignore[arg-type]
455+
EmptyResult,
456+
)
457+
result_holder.append(result)
458+
ev_response_received.set()
459+
460+
async with (
461+
anyio.create_task_group() as tg,
462+
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
463+
):
464+
tg.start_soon(mock_server)
465+
tg.start_soon(forward_request, client_session)
466+
467+
with anyio.fail_after(2): # pragma: no branch
468+
await ev_response_received.wait()
469+
470+
assert len(result_holder) == 1
471+
assert isinstance(result_holder[0], EmptyResult)

0 commit comments

Comments
 (0)