MCP tool calls should include composerId in _meta to enable per-conversation server-side state

Feature request for product/service

Chat

Describe the request

Cursor’s MCP implementation has two separate tool call paths — one for internal providers (extensions/lease) and one for external HTTP servers (adapter). The internal provider path correctly extracts composerId from the agent stream metadata and passes it through. The external HTTP adapter path receives composerId via IPC but silently drops it, never including it in the _meta sent to the MCP server.

This means external MCP servers (the vast majority of MCP servers) cannot distinguish which Cursor conversation initiated a tool call. Every other major MCP client provides this signal.

How Cursor’s two MCP paths work

Cursor has two distinct tool execution paths:

Path 1: Internal Provider (lease/extension path) — composerId WORKS

Internal MCP providers (Cursor’s own extensions) register via registerMcpProviderToManager. Their callTool receives the full agent stream metadata context including composerId:

// cursor-mcp/dist/main.js — provider factory
callTool: async(t, n, r, i, s) => {
    const o = t.get(ZL)?.composerId;  // ← EXTRACTS composerId from stream metadata
    return e.callTool(n, r, {toolCallId: i, composerId: o});  // ← PASSES IT
}

ZL is Symbol.for("cursor.agentStreamMetadata"). The composerId (the unique UUID for each composer/chat tab) is available here and used for elicitation routing and session reconnection.

Path 2: External HTTP Adapter (callToolForUI) — composerId DROPPED

External MCP servers configured via URL go through a different path. The workbench dispatches the tool call WITH composerId:

// workbench.desktop.main.js — MCP service callTool
u.runCommand(LG.CallTool, {
    name: n,
    args: e,
    identifier: this.serverId,
    toolCallId: i,
    composerId: s,              // ← SENT via IPC
    useLeaseElicitation: t
});

The MCP extension’s RM(GL.CallTool) handler receives this payload as e, but only reads 3 of the 6 fields:

// cursor-mcp/dist/main.js — CallTool handler
RM(GL.CallTool, async(e, t) => {
    // e.composerId EXISTS on the object — but is never read!
    // Only accesses: e.identifier, e.toolCallId, e.useLeaseElicitation
    
    const i = n.getAdapter();
    o = await i.callToolForUI(e.name, e.args, e.toolCallId, s);
    //                        ^^^^^^  ^^^^^^  ^^^^^^^^^^^^
    //                        composerId NOT passed to callToolForUI
});

callToolForUI then builds the HTTP request without composerId:

// cursor-mcp/dist/main.js — callToolForUI
this.client.callTool({
    name: i,
    arguments: t,
    _meta: { progressToken: e }   // ← composerId NOT included
}, ...);

The complete call chain

Workbench callTool(name, args, useLeaseElicitation, toolCallId, abort, composerId)
  │
  ├── Packages: {name, args, identifier, toolCallId, composerId, useLeaseElicitation}
  │                                                  ^^^^^^^^^^
  ├── u.runCommand(LG.CallTool, { ...composerId:s... })
  │
  ↓ IPC to MCP extension process (composerId survives serialization)
  │
  RM(GL.CallTool, async(e, t) => {
  │   // e.composerId is HERE on the object — confirmed via source analysis
  │   // But handler only reads: e.identifier, e.toolCallId, e.useLeaseElicitation
  │
  ├── i.callToolForUI(e.name, e.args, e.toolCallId, elicitProvider)
  │                                                  
  │   // composerId not passed as argument
  │
  ↓ callToolForUI builds HTTP request
  │
  this.client.callTool({
      name, arguments,
      _meta: { progressToken: e }   // ← just progressToken
  })
  │
  ↓ HTTP POST to MCP server
  │
  Server receives: _meta.progressToken only. No conversation identifier.

Proposed Fix

Option A: Pass composerId through callToolForUI to _meta (minimal change)

// RM(GL.CallTool) handler — pass composerId to callToolForUI
- o = await i.callToolForUI(e.name, e.args, e.toolCallId, s)
+ o = await i.callToolForUI(e.name, e.args, e.toolCallId, s, e.composerId)

// callToolForUI — add composerId parameter and include in _meta
- async callToolForUI(e, t, n, r) {
+ async callToolForUI(e, t, n, r, composerId) {
    // ...
-   _meta: { progressToken: e }
+   _meta: { progressToken: e, ...(composerId ? { "cursor/composerId": composerId } : {}) }

Option B: Add composerId alongside progressToken (single-line change in callToolForUI)

If composerId is made available on the adapter instance (already stored in this.deps.currentElicitationProvider map during call):

- _meta: { progressToken: e }
+ _meta: { progressToken: e, "cursor/composerId": this.deps.currentComposerId?.get(this.identifier) }

Precedent: What other clients send

We built a probe MCP server and tested 7 clients. Every client except Cursor provides per-conversation identification:

Client Conversation signal Location Notes
Codex app session_id + thread_id + turn_id _meta.x-codex-turn-metadata Added in openai/codex PR #15190
Codex CLI session_id + turn_id + sandbox _meta.x-codex-turn-metadata Same as app
ChatGPT x-openai-session Custom HTTP header Changes per conversation
Claude Code (local) MCP session ID Mcp-Session-Id header New session per conversation
Claude Code (global) MCP session ID Mcp-Session-Id header Proxied, still per-conversation
Claude.ai sentry-trace_id baggage header Changes per conversation (fragile)
Cursor Nothing N/A Cannot differentiate conversations

Related Forum Discussions

These community posts describe the same underlying need:

  • [Proposal: Include chat tab identifier (window_id) in MCP requests] - [https://forum.cursor.com/t/proposal-include-chat-tab-identifier-window-id-in-mcp-requests/121612] — “multiple chat tabs within a single Cursor instance share the same session_id, making it impossible for the backend to differentiate which chat tab initiated a particular MCP request”
  • [Cursor conversation ID through environment variables] -[https://forum.cursor.com/t/cursor-conversation-id-through-environment-variables/160346] — requests conversationId exposure, citing Claude Code’s CLAUDE_CODE_SESSION_ID
  • [Per-chat session approval for MCP tools] - [https://forum.cursor.com/t/per-chat-session-approval-for-mcp-tools/156343] — requests per-chat tool approval
  • [Chat/Agent Session Metadata for Client, Task, and Cost Attribution] - [https://forum.cursor.com/t/chat-agent-session-metadata-for-client-task-and-cost-attribution/160895] — metadata attachment per session
  • [MCP Cursor makes GET request without Mcp-Session-Id] - [https://forum.cursor.com/t/mcp-cursor-makes-get-request-without-mcp-session-id/146964] — session management issues

Operating System (if it applies)

MacOS