Skip to content

feat(runtime): configurable MCP server on BuiltInAgent (Bearer + per-call user header)#4420

Merged
mme merged 9 commits into
mainfrom
lukas/cpk-7526-copilotkitruntimev2-configurable-mcp-server-on-builtinagent
May 7, 2026
Merged

feat(runtime): configurable MCP server on BuiltInAgent (Bearer + per-call user header)#4420
mme merged 9 commits into
mainfrom
lukas/cpk-7526-copilotkitruntimev2-configurable-mcp-server-on-builtinagent

Conversation

@lukasmoschitz
Copy link
Copy Markdown
Contributor

@lukasmoschitz lukasmoschitz commented Apr 29, 2026

Summary

Closes CPK-7526.

@copilotkit/runtime/v2's BuiltInAgent already supported MCP servers, but only with static headers set at client construction. CopilotKit Intelligence's /mcp endpoint requires a two-axis auth contract that breaks under static headers:

  1. Static project BearerAuthorization: Bearer cpk-{projectId}_{shortToken}_{longToken}. Same on every call.
  2. Per-call end-user headerX-Cpki-User-Id. Fresh per outbound MCP HTTP request — never cached. Determines which user's persistent bash sandbox + /threads view a call sees.

Without per-call user-id, every demo session for a given project would share one bash sandbox, breaking the user-isolation contract that cpki.shell_contexts and ThreadsFs enforce on the platform side.

This PR adds the missing per-call resolver hook and is the precondition for CPK-7527 (Phase 4b SL integration), which wires it into Intelligence/demos/simple-agent.

What's in here

Public API surface

import {
  BuiltInAgent,
  INTELLIGENCE_USER_ID_HEADER,
  // re-exported from @ai-sdk/mcp:
  type MCPClient,
  type MCPTransport,
  type OAuthClientProvider,
} from "@copilotkit/runtime/v2";

new BuiltInAgent({
  model: "openai/gpt-4o",
  mcpServers: [{
    type: "http",
    url: `${process.env.INTELLIGENCE_API_URL}/mcp`,
    authToken: process.env.INTELLIGENCE_API_KEY!,
    getHeaders: ({ requestHeaders }) => {
      const userId = requestHeaders[INTELLIGENCE_USER_ID_HEADER]?.trim();
      if (!userId) throw new Error("missing user-id");
      return { [INTELLIGENCE_USER_ID_HEADER]: userId };
    },
  }],
});
  • Unified MCPClientConfig shape mirrors @ai-sdk/mcp's MCPTransportConfig (single discriminated type: "http" | "sse", url, headers, authProvider) plus two CopilotKit extensions: authToken (static Bearer shorthand) and getHeaders (per-call resolver invoked on every outbound HTTP request — initialize, tools/list, tools/call, reconnects).
  • MCPRequestContext carries requestHeaders (snapshot of the agent's per-run forwarded headers), input, and mcpServerUrl.
  • MCPHeaderResolverError wraps resolver throws so RUN_ERROR attributes the failure to the resolver via ES2022 Error.cause.
  • BuiltInAgent.headers: Record<string, string> = {} is now declared on the class. The runtime's existing extractForwardableHeaders feature-detect (if (agent.headers) in configureAgentForRequest) only forwards headers to agents that already declare a truthy field — initializing to {} activates that path so the BFF's per-request headers reach the resolver via context.
  • INTELLIGENCE_USER_ID_HEADER constant exported as the canonical header name ("x-cpki-user-id") — re-exported through both intelligence-platform and v2/runtime barrels.

Architecture

The agent's MCP layer now sits directly on @ai-sdk/mcp's stable surface (createMCPClient, MCPClient, MCPTransport) instead of bypassing it to @modelcontextprotocol/sdk. The agent module no longer imports @modelcontextprotocol/sdk at all — that's confined to a small CopilotKitMCPTransport class that implements Vercel's MCPTransport interface and is only instantiated when a getHeaders resolver is present.

For static-only configs (no per-call hooks), the config is handed straight to createMCPClient and Vercel's built-in HttpMCPTransport / SseMCPTransport handle the wire.

MCPClient, MCPTransport, OAuthClientProvider, OAuthTokens, and UnauthorizedError are re-exported from @copilotkit/runtime/v2 so consumers don't need a direct @ai-sdk/mcp dependency.

Side-effect bug fix: SSE static headers

The pre-existing direct-SDK construction passed serverConfig.headers as the wrong-shape options arg to SSEClientTransport, and the SDK silently ignored it — auth on SSE simply didn't work. Vercel's SseMCPTransport correctly applies headers via its commonHeaders() pipeline. The new architecture inherits the fix; a regression test in mcp-servers-integration.test.ts proves static headers now reach the wire on the SSE path.

What's intentionally NOT in here

  • Demo wiring in Intelligence/demos/simple-agent (CPK-7527).
  • MCP support for LangGraph / Mastra runtimes — different integration points; not this ticket.
  • Server-side /mcp route changes in Intelligence (already shipped on mme/integrate-sl).

Notes for reviewers

  • Public-API addition: BuiltInAgent.headers is a real public field, not just an ad-hoc property. It's load-bearing for the runtime's existing header-forwarding feature-detect.
  • Test header inspection: aimock redacts Authorization to [REDACTED] in its journal, so tests that need to verify the actual outgoing Bearer use a vi.spyOn(globalThis, "fetch") recorder instead of the journal. The x-cpki-user-id header isn't redacted, so per-call user-id assertions read from the journal directly.
  • No toMCPServer() helper. Earlier iterations of this PR shipped a CopilotKitIntelligence.toMCPServer() shortcut. It conflated thread management (the class's actual job) with MCP transport configuration and baked in opinionated behavior that not every deployment will want; dropped in favor of the inline pattern shown above using the exported INTELLIGENCE_USER_ID_HEADER constant. Same line count as the helper-call form, more flexible.
  • Pre-existing tsc --noEmit OOM: pnpm nx run @copilotkit/runtime:check-types runs out of heap on main at 8GB. Reproduces on a clean checkout without any changes from this PR. Worth tracking as a separate issue. Build (tsdown) is unaffected; the full vitest suite (1419 tests, 101 files) passes.

Test plan

  • pnpm nx run @copilotkit/runtime:test -- mcp-servers-integration — 15 cases pass (8 existing + 6 per-call/static-header + 1 new SSE regression test)
  • pnpm nx run @copilotkit/runtime:test -- mcp-clients — 8 cases pass (existing user-managed-clients suite, including MCPClientMCPClientProvider type-compat check)
  • pnpm nx run @copilotkit/runtime:test --skip-nx-cache — full suite 1419/1419
  • pnpm nx run @copilotkit/runtime:buildtsdown clean
  • oxlint on changed files — 0 errors
  • Manual end-to-end against a local Intel server + simple-agent BFF — deferred to CPK-7527 since the demo wiring lives there

@linear
Copy link
Copy Markdown

linear Bot commented Apr 29, 2026

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-with-your-data Ready Ready Preview, Comment May 7, 2026 0:46am
docs Ready Ready Preview, Comment May 7, 2026 0:46am
form-filling Ready Ready Preview, Comment May 7, 2026 0:46am
research-canvas Ready Ready Preview, Comment May 7, 2026 0:46am
travel Ready Ready Preview, Comment May 7, 2026 0:46am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

📣 Social Copy Generator

Generate social media copies (Twitter/X, LinkedIn, Blog Post) for this PR using Claude.

  • Generate social media copies

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 29, 2026

Open in StackBlitz

@copilotkit/a2ui-renderer

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/a2ui-renderer@4420

@copilotkit/agentcore-runner

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/agentcore-runner@4420

@copilotkitnext/angular

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkitnext/angular@4420

@copilotkit/core

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/core@4420

@copilotkit/react-core

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-core@4420

@copilotkit/react-native

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-native@4420

@copilotkit/react-textarea

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-textarea@4420

@copilotkit/react-ui

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-ui@4420

@copilotkit/runtime

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/runtime@4420

@copilotkit/runtime-client-gql

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/runtime-client-gql@4420

@copilotkit/sdk-js

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/sdk-js@4420

@copilotkit/shared

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/shared@4420

@copilotkit/sqlite-runner

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/sqlite-runner@4420

@copilotkit/voice

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/voice@4420

@copilotkit/web-inspector

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/web-inspector@4420

commit: 5dad3de

lukasmoschitz added a commit that referenced this pull request Apr 29, 2026
The original PR for per-call MCP header resolution went one layer below
@ai-sdk/mcp and instantiated @modelcontextprotocol/sdk's transports
directly. Vercel has since graduated MCP support to a stable surface —
createMCPClient, MCPClient, MCPClientConfig, MCPTransport,
MCPTransportConfig, OAuthClientProvider — and the rest of the agent
already runs on Vercel (streamText, tool, message conversion). Going
deeper just for MCP was an architectural inconsistency without a
payoff.

Re-architect on top of the stable Vercel surface:

- Drop the deprecated experimental_createMCPClient alias for stable
  createMCPClient. The agent module no longer imports
  @modelcontextprotocol/sdk; that's confined to mcp-transport.ts.
- Replace the discriminated MCPClientConfigHTTP|SSE pair with a unified
  MCPClientConfig shape that mirrors Vercel's MCPTransportConfig and
  adds the two CopilotKit extensions Vercel doesn't cover: authToken
  (Bearer shorthand) and getHeaders (per-call resolver).
- New helper openMcpClient(config, requestHeaders, input) branches on
  whether getHeaders is set: if so, instantiate CopilotKitMCPTransport
  (new file) and pass to createMCPClient as a custom transport; if not,
  hand the static-only config straight to createMCPClient and let
  Vercel's HttpMCPTransport / SseMCPTransport handle the wire.
- CopilotKitMCPTransport implements @ai-sdk/mcp's MCPTransport interface.
  Internally it wraps StreamableHTTPClientTransport with a fetch that
  applies authToken + getHeaders per outbound HTTP request — same
  pattern the buildHttpTransportOptions helper used, just behind a
  proper transport contract.
- MCPClientProvider becomes Pick<MCPClient, "tools">. Existing inline
  mocks ({ tools: vi.fn() }) still satisfy the structural type, and
  Vercel-built MCPClient instances pass through without a wrapper.
- intelligence.toMCPServer() returns the unified MCPClientConfig and
  tightens the user-id guard to userId?.trim() so whitespace-only
  values don't slip through.
- Re-export MCPClient, MCPTransport, OAuthClientProvider, OAuthTokens,
  UnauthorizedError from @copilotkit/runtime/v2 so consumers don't
  need a direct @ai-sdk/mcp dependency.
- Side-effect bug fix: SSE config's static headers now actually reach
  the wire. The old direct-SDK construction passed the headers map as
  the wrong-shape options arg and the SDK silently ignored it; Vercel's
  SseMCPTransport applies them via commonHeaders() on every request.

Refs CPK-7526, addresses PR #4420 review feedback.
mme added 4 commits May 6, 2026 16:15
Convenience re-exports so consumers wiring custom MCP clients (via
`mcpClients`) or custom transports don't need to add `@ai-sdk/mcp`
to their dependencies just to type the values.
…sors

Adds `mcpServer?: boolean` to `CopilotKitIntelligenceConfig` (default
`false`). When true, the runtime emits the per-request bag the agent
needs to attach the platform's MCP server.

Internal accessors `ɵisMcpServerEnabled()` and `ɵgetApiKey()` round
out the existing `ɵgetApiUrl()`. Used by the runtime layer in the
forthcoming auto-attach commit; not part of the public surface.
…rwardedProps

Runtime side (intelligence/run.ts): when `runtime.intelligence.mcpServer`
is enabled, build a `copilotkitIntelligence` bag carrying the resolved
user-id, project apiKey, and the platform MCP URL, and pass it through
on `RunAgentInput.forwardedProps` to the agent. Skipped when the flag
is off — runs that don't go through this Intelligence path simply
don't see the bag.

Agent side (BuiltInAgent's run code): if `forwardedProps.copilotkitIntelligence`
contains all three string values AND the user's static `config.mcpServers`
doesn't already include the same URL, append a per-request
`MCPClientConfigHTTP`. Its `options.fetch` closes over apiKey + userId
and stamps `Authorization: Bearer <apiKey>` and `X-Cpki-User-Id:
<userId>` on every outbound MCP call. The custom fetch is the MCP
TypeScript SDK's documented extension point for per-request header
injection — no extra wrapper class, no separate framework concept.

The agent class is otherwise untouched: no new fields, no per-request
side channels, no typed reference to `CopilotKitIntelligence`. Other
agents that don't read `forwardedProps.copilotkitIntelligence` ignore
the bag.

Pulls in the AI SDK's stable `createMCPClient` export (rename from
`experimental_createMCPClient`) — the experimental name was deprecated;
`mcp-clients.test.ts`'s mock setup follows.
Four cases at the agent layer (BuiltInAgent reading forwardedProps):
  * attaches when `copilotkitIntelligence` carries all three strings
    (userId, apiKey, mcpUrl) — outbound headers carry Authorization +
    X-Cpki-User-Id.
  * does NOT attach when the bag is absent (no Intelligence wiring on
    this run).
  * does NOT attach when the bag is partial (e.g. mcpUrl missing).
  * does NOT attach when the user has already configured an MCP server
    pointing at the same URL — explicit user config wins, with the
    user's headers and resolver hitting the wire.
@BenTaylorDev
Copy link
Copy Markdown
Contributor

Things Claude wanted me to tell you:

  • The cast in handlers/intelligence/run.ts:87 — (agent as unknown as { user?: { id, name } }).user = ....
    MiddlewareCapableAgent already extends AbstractAgent with user? in agent-utils.ts:16. The handler should
    accept that type (or a narrow shared type) instead of double-casting. Loses type safety for no real reason.
  • INTELLIGENCE_USER_ID_HEADER is hard-coded inside client.ts:9 — fine for encapsulation, but the helper is
    closed over a single header name. If/when a self-hosted Intelligence variant wants a different name, this
    becomes a fork point. Probably worth a comment that it's deliberately not configurable.
  • Concurrency assumption is implicit: setting agent.user before agent.run() only works because
    cloneAgentForRequest returns a fresh instance per request. Worth a one-liner on BuiltInAgent.user saying
    "set per request on a cloned agent; do not set on a shared singleton."
  • Doc-table footnote: the SSE row says "if you need it, use HTTP" but the SSE bug-fix commit means SSE now
    applies static headers correctly. A tiny note that SSE supports static headers and only getHeaders is
    HTTP-only would prevent confusion.

…rops.auth.copilotkitIntelligence

Move the per-request Intelligence MCP bag from
forwardedProps.copilotkitIntelligence to
forwardedProps.auth.copilotkitIntelligence so the Intelligence-side
redaction policy strips it. The 'auth' namespace is the convention for
credentials; persistence sinks (Postgres, Redis, S3) and FE replay
paths in apps/realtime-gateway already strip everything under it.

Updates:
- Emitter (handlers/intelligence/run.ts): merge the bag into a single
  forwardedProps.auth object alongside any upstream auth keys, and
  only emit the auth namespace when there is something to put in it.
- Reader (agent/index.ts): read from forwardedProps.auth.copilotkitIntelligence
  instead of forwardedProps.copilotkitIntelligence.
- Tests (intelligence-mcp-helper.test.ts): three fixtures rewritten
  to the nested shape.
mme and others added 3 commits May 7, 2026 14:28
…tkitruntimev2-configurable-mcp-server-on-builtinagent
…DER constant

Two CR comments addressed:

- Rename the local destructure of forwardedProps.auth.copilotkitIntelligence
  from 'cki' to 'cpki' so it matches the project-wide abbreviation already
  used in metadata fields (cpki_event_id, cpki_event_seq, etc).

- Replace the inline 'X-Cpki-User-Id' string literal with the existing
  INTELLIGENCE_USER_ID_HEADER constant exported from intelligence-platform/client.
  Applies to the runtime auto-attach in agent/index.ts and to the three
  test sites in intelligence-mcp-helper.test.ts so the user-side and
  runtime-side stay in sync.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants