Expose callId in MCP tool call _meta for all servers

Open 💬 3 comments Opened May 4, 2026 by daniloag

What variant of Codex are you using?

CLI

What feature would you like to see?

Context

When Codex dispatches an MCP tool call, the tool call's call_id is passed to build_mcp_tool_call_request_meta, but only included in _meta for the codex_apps server, nested under _codex_apps.call_id. Custom MCP servers receive no identifier for the originating tool call.

Ask

Add callId at the top level of _meta for all MCP servers, following the same pattern as threadId (which is already injected unconditionally via with_mcp_tool_call_thread_id_meta).

Use cases:

  • Tracing / observability: correlate client-side spans with server-side processing.
  • Audit logging: track which model action triggered which server-side operation.
  • Request deduplication: detect retried or duplicate tool calls on the server side.

Additional information

call_id is already a parameter of build_mcp_tool_call_request_meta. It just needs to be inserted unconditionally.

Happy to submit a PR with tests if the team is open to this change.

View original on GitHub ↗

3 Comments

vaaraio · 3 months ago

From the audit-log consumer side, callId at the top of _meta for every MCP server is the stable correlation key that lets per-call audit records chain to the host session without server-specific nesting.

Once callId lives at _meta.callId unconditionally, downstream runtime-evidence pipelines stop needing per-server adapters to find it. Concretely, in Vaara's MCP proxy (github.com/vaaraio/vaara, src/vaara/integrations/mcp_proxy.py) the audit record emitted per tools/call carries an action_id that maps directly to the originating model action. With callId unwrapped at the top of _meta, the proxy can correlate Codex-side spans to action_id without falling back to threadId-plus-timestamp heuristics.

The same surface unblocks a few other things. SEP-2787 (modelcontextprotocol/modelcontextprotocol#2787) attestation envelopes that bind per-call commitments key off the call identifier. Transparency-log inclusion proofs anchored on callId let the client verify "this annotation derives from record N in the audit log" without trusting the host. All of these become awkward without a stable top-level callId.

The patch you proposed (unconditional injection alongside threadId via the existing with_mcp_tool_call_thread_id_meta shape) is the cleanest place for it.

Rul1an · 3 months ago

This seems like the right place to add the field. A top-level _meta.callId gives MCP servers a stable host-side correlation key for the logical tool call, separate from the JSON-RPC request id and separate from transport trace context.

One boundary I would keep explicit is that callId is an opaque correlation identifier. Servers can log it, attach it to spans, use it for best-effort deduplication of retries, and join it with host-side tool-call telemetry. They should not treat it by itself as authorization, provenance, or attestation evidence.

That distinction matters because the adjacent identifiers have different jobs. traceparent and tracestate carry distributed trace context. The MCP or JSON-RPC request id correlates a protocol request and response. A host callId identifies the model-side tool-call action. A signed attestation or audit receipt could later choose to bind that call id into its own envelope, but the call id alone does not prove package provenance, policy approval, argument integrity, or execution outcome.

The contract I would find easiest to implement against is small: callId is generated by the host, stable for one logical tool call including retries of that same call, opaque, non-secret, and interpreted within host or session context rather than as a global security principal.

That keeps this issue nicely scoped: expose the host call id consistently for observability, audit-log joins, and deduplication, while leaving stronger evidence semantics to explicit attestation or receipt layers.

m13v · 3 months ago

the pattern here shows up in every MCP host that injects request context: the useful field lands in _meta for the first-party server first (here, _codex_apps.call_id) and custom servers fly blind until someone files this. threadId rides along unconditionally, but it scopes the session, it can't tell one call from a retry of that call. harmless for read-only servers, brutal for side-effecting ones. on a server driving desktop actions, a retried call with no callId looks identical to a fresh one, so a timed-out click gets replayed and you double-type a field. hoist callId next to threadId.