Skip to content

Expose LLM tool-call id to MCP tool invocations #1332

@lxwl-fht

Description

@lxwl-fht

Expose LLM tool-call id to MCP tool invocations

Background

When the SDK invokes an MCP tool, our host process needs to know the LLM-side tool call id that triggered the call, and (ideally) propagate it to the MCP server. We use this id for:

  • Correlating MCP server-side audit logs with LLM turns
  • Distributed tracing across the LLM → CLI → MCP server boundary
  • Reconciling outputs from multiple parallel tool calls in the same turn

Today, the id is already tracked inside the CLI — it surfaces in:

  • ToolInvocation.toolCallId (local tools registered via tools[])
  • tool.execution_start.data.toolCallId (session event stream, read-only)
  • kind:"mcp" PermissionRequest's toolCallId (only fires on permission path)

But none of these reaches the MCP server, and none lets the host inject the id into the outbound tools/call request.

Asks

Either of the following would unblock us. Option A is preferred because it requires no host-side code.

Option A — CLI auto-injects _meta on outbound MCP tools/call

When the CLI sends a tools/call JSON-RPC request to an MCP server, include the id in the standard MCP _meta request field:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": { ... },
    "_meta": {
      "copilot/toolCallId": "<toolCallId>"
    }
  }
}

_meta is part of the MCP spec for out-of-band request metadata. It sits alongside arguments, isn't part of inputSchema, isn't visible to the LLM, and strict additionalProperties: false schemas don't reject it. Servers that don't read _meta are unaffected — zero breaking change.

Optionally include sessionId, parentToolCallId, and W3C traceparent to match the richness already present in tool.execution_start.data.

Option B — Add toolCallId to PreToolUseHookInput

Currently:

export interface PreToolUseHookInput extends BaseHookInput {
  toolName: string;
  toolArgs: unknown;
}

Proposed (additive, back-compatible):

export interface PreToolUseHookInput extends BaseHookInput {
  toolName: string;
  toolArgs: unknown;
  toolCallId?: string;          // LLM-side tool call id
}

With the id available in the hook, hosts can use PreToolUseHookOutput.modifiedArgs to inject the id into MCP tools/call arguments themselves. This is strictly less clean than Option A (pollutes arguments, requires schema cooperation), but it gives hosts a self-service escape hatch.

Today's workaround (and why it's bad)

The only way to pass toolCallId to an MCP server today is to reimplement the MCP server as a host-side proxy registered via tools[], where ToolInvocation.toolCallId is available. The host then runs its own MCP client and forwards calls, injecting the id.

Costs of that workaround:

  • Lose kind:"mcp" PermissionRequest metadata (serverName, toolTitle, readOnly)
  • Reimplement MCP lifecycle: process spawn, reconnect, timeouts
  • Reimplement OAuth flow (mcp.oauth_required event no longer fires)
  • Lose tool.execution_start.mcpServerName / mcpToolName telemetry
  • Tools show up as kind:"custom-tool" in UIs that branch on tool kind

For functionality the CLI already has internally, this is a lot of duplicated machinery on the host side.

Summary

Option A is the simplest, smallest, and most aligned with the MCP spec — one place in the CLI's outbound tools/call construction needs the id added to _meta. Option B is a fallback if Option A is not feasible.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions