contextId Isn't a Trace ID: The Gap in A2A and MCP's Own Correlation Fields
7 min read Fullmakt Team
- observability
- traceability
- a2a
- mcp
- agents
- governance
Open the A2A spec and you’ll
find a contextId and a taskId on every task object. Open the
MCP spec and, for the
Streamable HTTP transport, you’ll find an Mcp-Session-Id header the server
assigns and the client must echo back on every request. Both look, at a
glance, like the correlation ID multi-agent traceability
needs. Both were built to
solve a narrower problem, and the gap between what they actually cover and
what a full trace needs is exactly where incident reviews stall.
What contextId actually scopes: one conversation, not one chain
contextId groups the tasks and messages inside a single A2A relationship —
it’s how one agent keeps track of “these three tasks are part of the same
back-and-forth with this other agent,” the way a chat session ID groups
turns in a conversation. taskId identifies one unit of work within that
relationship. Both are real, spec-defined, and useful for exactly what
they’re for: letting two agents refer back to shared state without
re-establishing context on every message.
Neither field is defined to travel past the A2A call itself. If the agent
receiving a task then calls an MCP tool, queries an internal database, or
kicks off a second A2A call to a third agent, nothing in the spec requires
contextId to ride along into any of that. It’s scoped to the relationship
between the two agents that negotiated it — not to whatever either of them
does next.
What Mcp-Session-Id actually scopes: one connection, not one tool call’s blast radius
Mcp-Session-Id does a similarly narrow job on the MCP side. It’s assigned
by an MCP server at initialization and echoed by the client on every
subsequent request over that transport, so the server can bind a sequence of
JSON-RPC calls to one stateful session instead of treating each as a cold
start. If the server terminates the session, the client is required to
re-initialize and gets a new ID — by design, a session ID that identifies
“this MCP connection,” not “this end-to-end action.”
That’s the wrong shape for tracing what a tool call actually does. When a tool invocation causes the MCP server to call a third-party API, the session ID doesn’t travel into that outbound call — there’s no field in the spec for it to travel in. And a session ID is, definitionally, local to one client-server pair: it means nothing to the next MCP server the same workflow touches, or to the A2A agent that triggered the tool call in the first place.
Two IDs, two different scopes, and a chain that crosses both
Put a realistic chain next to both definitions and the gap is exact. A user
request reaches Agent A. Agent A delegates to Agent B over A2A — that
exchange gets a contextId. Agent B calls an MCP tool to look something up
— that call rides on an Mcp-Session-Id scoped to Agent B’s connection to
that MCP server, which has no defined relationship to the A2A contextId
that caused it. If the tool then hits a third-party API, that call carries
neither identifier, because neither protocol defined a place for it to go.
Three hops, two protocol-native correlation fields, zero shared thread
between them. Reconstructing the chain means matching the A2A contextId to
roughly-simultaneous entries in the MCP server’s session log to
roughly-simultaneous entries in the third-party API’s access log — the exact
timestamp-stitching exercise correlation IDs exist to make unnecessary,
except now it’s worse, because you’re stitching across two different ID
schemes that were each designed to stop exactly at the edge of their own
protocol.
Why this isn’t a spec bug to wait out
It’s tempting to read this as a gap the next A2A or MCP revision will close.
It probably won’t, for a structural reason: contextId and
Mcp-Session-Id are correctly scoped for what each protocol is responsible
for. A2A doesn’t know or care what its receiving agent does with a task
internally; MCP doesn’t know or care what triggered the tool call it’s
asked to run. Neither spec has a natural place to own an identifier that
spans both of them plus whatever third-party API sits behind either one —
that’s not a hole in either protocol, it’s the edge of what either protocol
is scoped to govern. Waiting for a future spec revision to solve a
cross-protocol problem that neither protocol is responsible for is a bet
against the wrong committee.
The business case: a correlation ID that lives above the protocol, not inside it
If neither protocol will own end-to-end correlation, the identifier has to come from something that sits above both of them — present at every hop regardless of whether that hop happens to be A2A, MCP, or a plain REST call. Fullmakt brokers the credential behind every one of those calls, which makes credential issuance that chokepoint:
- A correlation ID is stamped once, at the top of a chain, and attached to every credential Fullmakt dispenses afterward — independent of whether the hop that consumes it is an A2A task, an MCP tool call, or a direct API request, because the broker doesn’t need either protocol’s own ID scheme to attach one of its own.
- The A2A
contextIdand the MCPMcp-Session-Idaren’t replaced — they’re outranked. Both still do their protocol-local job well; Fullmakt’s correlation ID is the layer that ties a givencontextIdto a givenMcp-Session-Idto a given downstream API call, because it’s present at all three. - The record ties back to the requesting identity, not just the acting one, so a chain that crosses from A2A into MCP and back doesn’t lose track of which human or upstream task authorized the first hop by the time it reaches the third.
- Reconstruction is a query against one system, not a manual join across an A2A agent’s task log, an MCP server’s session log, and a third-party API’s access log that were never going to share an ID scheme on their own.
This is the practical answer to a question every team adopting both
protocols eventually asks: “our agents already have contextId and session
IDs — don’t we already have tracing?” No — you have two protocols each doing
their own narrow, correct job. Tracing needs an identifier that outranks
both, which means it has to live at a layer above either spec: the one
component in the path that has to see every hop to issue a credential for
it.
FAQ
Can’t A2A or MCP just add a trace-context field to close this gap? Either could add one, but it would only cover calls made over that protocol. The actual gap is between protocols — an A2A hop handing off to an MCP hop — which no single protocol’s extension can close on its own, since neither is responsible for what happens on the other side of that boundary.
Is contextId useless for tracing, then?
No — it’s exactly right for what it’s for: correlating tasks within one
A2A relationship. The mistake is assuming it substitutes for an end-to-end
trace, when its scope stops at the edge of that one relationship by design.
Does OpenTelemetry solve this if both agents instrument it?
It helps wherever both sides choose to propagate a W3C traceparent header
voluntarily. It doesn’t help with a third-party agent or MCP server you
don’t control and that never adopted it — which is the common case, not the
edge case, in a real multi-vendor agent chain.
Where does the correlation ID actually get attached if not in the A2A or MCP message itself? At credential issuance. Every hop in the chain needs a live credential to call anything, and that issuance step is the one place guaranteed to see the call regardless of which protocol carried it — so that’s where a broker-level ID gets stamped, alongside whatever protocol-native ID that hop also carries.
Check any multi-agent workflow that mixes A2A and MCP in production: pull
one contextId and try to find every MCP session and downstream API call it
actually caused. If that search means opening three different logging
systems and matching on timestamps, the contextId was never the trace ID —
it just looked like one.