Skip to content

[FEATURE]: add mcp.call.before plugin hook for per-call MCP request headers (e.g. sessionID forwarding) #28225

@egze

Description

@egze

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Summary

Add a plugin hook that lets plugins inject HTTP headers into outbound MCP tool calls, with the current sessionID (and callID, server, tool) available in scope. This enables per-session/per-request
header forwarding (e.g. X-Session-Id, X-User-Id) to remote MCP servers — something the existing static headers config and OAuth cannot express, because headers today are bound at transport construction
time.

Current behavior

  • Remote MCP headers (packages/opencode/src/config/mcp.ts:41) are a static Record<string,string> resolved once at config load via {env:...}/{file:...} substitution.
  • Those headers are passed to StreamableHTTPClientTransport / SSEClientTransport via requestInit.headers at construction time (packages/opencode/src/mcp/index.ts:335,342). After construction the
    transport is shared across all sessions, so headers cannot vary per call.
  • The tool execute(args) closure at packages/opencode/src/mcp/index.ts:168 does not carry any session context, so even a per-call template like {session:id} would have nothing to resolve against.

There is therefore no way today — via config, plugin, or otherwise — to attach the current sessionID (or any other per-call value) as a header on outbound MCP HTTP requests.

Proposed shape

A new plugin hook modeled on the existing chat.headers hook (packages/plugin/src/index.ts:256), but fired before each outbound MCP request:

  "mcp.call.before"?: (
    input:  { server: string; tool: string; sessionID: string; callID: string },
    output: { headers: Record<string, string> },
  ) => Promise<void>

Plugin usage:

  export default {
    "mcp.call.before": async (input, output) => {
      output.headers["X-Session-Id"] = input.sessionID
      output.headers["X-Call-Id"]    = input.callID
      if (input.server === "my-server") {
        output.headers["X-User-Id"] = process.env.USER_ID ?? ""
      }
    },
  }

Proposed behavior

  • Hook fires once per client.callTool (and ideally per listTools / getPrompt / readResource too, for consistency — happy to scope down to callTool only in v1).
  • output.headers is pre-populated with the static headers from config; the hook may add, override, or remove keys.
  • Resulting headers are merged into the outbound HTTP request via a custom fetch passed to the SDK transports (requestInit.fetch), since the SDK does not expose per-call header injection. The transport remains
    shared across sessions; only the fetch wrapper is per-call-aware.
  • Per-call context (sessionID, callID, server, tool) is propagated from the tool execute boundary in packages/opencode/src/session/prompt.ts via AsyncLocalStorage (or Effect Context), read inside the
    wrapper-fetch.
  • If multiple plugins register the hook, they run in order and each sees the previous plugin's mutations to output.headers (matching existing hook semantics).
  • Static config headers continues to work unchanged. {env:...}/{file:...} substitution still resolves at load time.

Why this would help

  • Lets users correlate MCP server logs/observability with opencode sessions (one of the most common asks for any tool-running agent).
  • Lets multi-tenant or per-user deployments forward identity to MCP servers without spinning up a separate opencode process per user.
  • Matches the precedent already set by chat.headers for LLM provider HTTP calls — same surface, applied to MCP HTTP calls.
  • Does not introduce a new config schema, new substitution token, or new dependency. Pure plugin addition + one fetch wrapper.

Out of scope (suggested follow-ups)

  • Forwarding inbound REST request headers (from POST /session/:id/message) into MCP calls. That's a separate concern (header allowlist + request-scoped context on the REST side) and can be layered on top of
    this hook later by a plugin once the hook exists.
  • Dynamic header generation via external helper script — already covered by [FEATURE]: add dynamic MCP headersHelper support for remote authentication #27161.

Notes

Happy to open a focused PR with the hook, the AsyncLocalStorage plumbing in session/prompt.ts, the fetch wrapper in mcp/index.ts, tests, docs, and generated SDK/types — if the core team confirms the hook name
and shape are acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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