Summary
On adcp==6.6.0, ADCPClient/ADCPMultiAgentClient constructed with signing=SigningConfig(...) never actually sign outbound requests over the MCP transport, even though the signing hook fires and no error is raised. Reproduced end-to-end against a local FastMCP server with a header spy: the signing hook fired on every call, current_operation read None every time, no get_adcp_capabilities fetch occurred, and no Signature header was ever sent on the wire.
Root cause
adcp/protocols/mcp.py:560 sets the current_operation ContextVar in the caller's asyncio task, wrapped around the session.call_tool(...) invocation.
mcp/client/streamable_http.py:566 (the mcp package's own transport) dispatches the actual signed POST via tg.start_soon(handle_request_async) from post_writer's task. Since contextvars.Context is captured at task-spawn time (i.e., when post_writer's task group started, at connect time) and NOT inherited live from the calling task at request time, the spawned task's copy of current_operation is whatever it was when the connection was established — effectively always None for the first (and typically only) request.
ADCPClient._sign_outgoing_request (adcp/client.py:972) reads current_operation from the ContextVar, sees None, and returns early without signing — silently, no exception, no log.
The SSE transport branch (adcp/protocols/mcp.py:383-388) is aware of this class of problem and explicitly warns-and-skips rather than attempting to sign. The streamable-HTTP transport (the default) has no such guard and just silently no-ops.
Note: the A2A transport is unaffected — its signing hook runs in the caller's own task (adcp/protocols/a2a.py:262), so current_operation propagates correctly there. This appears to be specific to how the mcp package's streamable-HTTP client schedules its send.
Suggested fix direction
Capture the operation at message-send time (inside the task that actually performs session.call_tool/the HTTP POST) rather than via a ContextVar set in a different task, OR thread the operation through ClientMessageMetadata (or an equivalent explicit parameter) rather than relying on ambient context propagated across an anyio task-group boundary.
Impact
Any caller relying on ADCPClient(signing=...) over MCP transport gets zero signing with no error surfaced — a silent security gap for anyone who believes they've turned on RFC 9421 request signing for MCP-transport outbound calls.
Repro sketch
- Stand up a minimal FastMCP server that echoes the received headers.
- Construct
ADCPClient(agent_url=..., protocol="mcp", signing=SigningConfig(...)).
- Call any tool via the client 5 times.
- Observe: signing hook invoked 5/5 times,
current_operation is None on all 5, zero Signature header on the wire, zero get_adcp_capabilities fetch (which _sign_outgoing_request would trigger if it proceeded past the operation is None check).
Happy to share the exact repro script if useful — surfaced this while implementing outbound request signing for a downstream seller agent (prebid/salesagent, RFC 9421 adoption epic).
Summary
On adcp==6.6.0,
ADCPClient/ADCPMultiAgentClientconstructed withsigning=SigningConfig(...)never actually sign outbound requests over the MCP transport, even though the signing hook fires and no error is raised. Reproduced end-to-end against a local FastMCP server with a header spy: the signing hook fired on every call,current_operationreadNoneevery time, noget_adcp_capabilitiesfetch occurred, and noSignatureheader was ever sent on the wire.Root cause
adcp/protocols/mcp.py:560sets thecurrent_operationContextVar in the caller's asyncio task, wrapped around thesession.call_tool(...)invocation.mcp/client/streamable_http.py:566(themcppackage's own transport) dispatches the actual signed POST viatg.start_soon(handle_request_async)frompost_writer's task. Sincecontextvars.Contextis captured at task-spawn time (i.e., whenpost_writer's task group started, at connect time) and NOT inherited live from the calling task at request time, the spawned task's copy ofcurrent_operationis whatever it was when the connection was established — effectively alwaysNonefor the first (and typically only) request.ADCPClient._sign_outgoing_request(adcp/client.py:972) readscurrent_operationfrom the ContextVar, seesNone, and returns early without signing — silently, no exception, no log.The SSE transport branch (
adcp/protocols/mcp.py:383-388) is aware of this class of problem and explicitly warns-and-skips rather than attempting to sign. The streamable-HTTP transport (the default) has no such guard and just silently no-ops.Note: the A2A transport is unaffected — its signing hook runs in the caller's own task (
adcp/protocols/a2a.py:262), socurrent_operationpropagates correctly there. This appears to be specific to how themcppackage's streamable-HTTP client schedules its send.Suggested fix direction
Capture the operation at message-send time (inside the task that actually performs
session.call_tool/the HTTP POST) rather than via a ContextVar set in a different task, OR thread the operation throughClientMessageMetadata(or an equivalent explicit parameter) rather than relying on ambient context propagated across ananyiotask-group boundary.Impact
Any caller relying on
ADCPClient(signing=...)over MCP transport gets zero signing with no error surfaced — a silent security gap for anyone who believes they've turned on RFC 9421 request signing for MCP-transport outbound calls.Repro sketch
ADCPClient(agent_url=..., protocol="mcp", signing=SigningConfig(...)).current_operationisNoneon all 5, zeroSignatureheader on the wire, zeroget_adcp_capabilitiesfetch (which_sign_outgoing_requestwould trigger if it proceeded past theoperation is Nonecheck).Happy to share the exact repro script if useful — surfaced this while implementing outbound request signing for a downstream seller agent (prebid/salesagent, RFC 9421 adoption epic).