@copilotkit/runtime 1.56.4-canary.1777538870 → 1.56.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/agent/converters/tanstack.cjs +6 -0
  2. package/dist/agent/converters/tanstack.cjs.map +1 -1
  3. package/dist/agent/converters/tanstack.mjs +6 -0
  4. package/dist/agent/converters/tanstack.mjs.map +1 -1
  5. package/dist/agent/index.cjs +8 -37
  6. package/dist/agent/index.cjs.map +1 -1
  7. package/dist/agent/index.d.cts +27 -52
  8. package/dist/agent/index.d.cts.map +1 -1
  9. package/dist/agent/index.d.mts +27 -52
  10. package/dist/agent/index.d.mts.map +1 -1
  11. package/dist/agent/index.mjs +9 -38
  12. package/dist/agent/index.mjs.map +1 -1
  13. package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs +8 -1
  14. package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs.map +1 -1
  15. package/dist/lib/runtime/agent-integrations/langgraph/agent.d.cts.map +1 -1
  16. package/dist/lib/runtime/agent-integrations/langgraph/agent.d.mts.map +1 -1
  17. package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs +8 -1
  18. package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs.map +1 -1
  19. package/dist/package.cjs +5 -5
  20. package/dist/package.mjs +5 -5
  21. package/dist/v2/index.cjs +0 -2
  22. package/dist/v2/index.d.cts +5 -6
  23. package/dist/v2/index.d.mts +5 -6
  24. package/dist/v2/index.mjs +1 -2
  25. package/dist/v2/runtime/core/runtime.d.cts +0 -1
  26. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  27. package/dist/v2/runtime/core/runtime.d.mts +0 -1
  28. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  29. package/dist/v2/runtime/endpoints/express.cjs +5 -5
  30. package/dist/v2/runtime/endpoints/express.cjs.map +1 -1
  31. package/dist/v2/runtime/endpoints/express.mjs +5 -5
  32. package/dist/v2/runtime/endpoints/express.mjs.map +1 -1
  33. package/dist/v2/runtime/handlers/intelligence/run.cjs +0 -4
  34. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  35. package/dist/v2/runtime/handlers/intelligence/run.mjs +0 -4
  36. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  37. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  38. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  39. package/dist/v2/runtime/index.d.cts +1 -3
  40. package/dist/v2/runtime/index.d.cts.map +1 -1
  41. package/dist/v2/runtime/index.d.mts +1 -3
  42. package/dist/v2/runtime/index.d.mts.map +1 -1
  43. package/dist/v2/runtime/intelligence-platform/client.cjs +0 -52
  44. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  45. package/dist/v2/runtime/intelligence-platform/client.d.cts +0 -41
  46. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  47. package/dist/v2/runtime/intelligence-platform/client.d.mts +0 -41
  48. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  49. package/dist/v2/runtime/intelligence-platform/client.mjs +0 -52
  50. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  51. package/package.json +6 -6
  52. package/src/agent/__tests__/mcp-clients.test.ts +25 -11
  53. package/src/agent/__tests__/mcp-servers-integration.test.ts +1 -355
  54. package/src/agent/converters/tanstack.ts +18 -0
  55. package/src/agent/index.ts +65 -128
  56. package/src/lib/runtime/agent-integrations/langgraph/agent.ts +8 -1
  57. package/src/v2/runtime/__tests__/express-fetch-bridge.test.ts +1 -1
  58. package/src/v2/runtime/endpoints/express.ts +9 -3
  59. package/src/v2/runtime/handlers/intelligence/run.ts +0 -9
  60. package/src/v2/runtime/handlers/shared/agent-utils.ts +0 -1
  61. package/src/v2/runtime/index.ts +0 -5
  62. package/src/v2/runtime/intelligence-platform/client.ts +0 -67
  63. package/dist/agent/mcp-transport.cjs +0 -94
  64. package/dist/agent/mcp-transport.cjs.map +0 -1
  65. package/dist/agent/mcp-transport.d.cts +0 -51
  66. package/dist/agent/mcp-transport.d.cts.map +0 -1
  67. package/dist/agent/mcp-transport.d.mts +0 -52
  68. package/dist/agent/mcp-transport.d.mts.map +0 -1
  69. package/dist/agent/mcp-transport.mjs +0 -92
  70. package/dist/agent/mcp-transport.mjs.map +0 -1
  71. package/dist/v2/runtime/intelligence-platform/index.d.cts +0 -2
  72. package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
  73. package/src/agent/mcp-transport.ts +0 -190
  74. package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +0 -188
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { BasicAgent, MCPHeaderResolverError } from "../index";
2
+ import { BasicAgent } from "../index";
3
3
  import { EventType } from "@ag-ui/client";
4
4
  import { streamText } from "ai";
5
5
  import { LLMock, MCPMock } from "@copilotkit/aimock";
@@ -29,81 +29,6 @@ vi.mock("@ai-sdk/openai", () => ({
29
29
  // Do NOT mock @ai-sdk/mcp or @modelcontextprotocol/sdk transports —
30
30
  // we want real HTTP connections to the MCPMock server.
31
31
 
32
- /**
33
- * Spin up an LLMock-mounted MCPMock with a real HTTP listener — the mock
34
- * has to actually respond so the agent's MCP init + tools/list can
35
- * complete and the run progresses to streamText.
36
- */
37
- async function startMcpServerWithJournal(
38
- tools: Array<{ name: string; description?: string }>,
39
- ): Promise<{ mcpUrl: string; llm: LLMock; mcpMock: MCPMock }> {
40
- const mock = new MCPMock();
41
- for (const t of tools) {
42
- mock.addTool({
43
- name: t.name,
44
- description: t.description ?? `${t.name} tool`,
45
- inputSchema: {
46
- type: "object",
47
- properties: { query: { type: "string" } },
48
- },
49
- });
50
- mock.onToolCall(t.name, () => `result from ${t.name}`);
51
- }
52
- const server = new LLMock({ port: 0 });
53
- server.mount("/mcp", mock);
54
- await server.start();
55
- return { mcpUrl: `${server.url}/mcp`, llm: server, mcpMock: mock };
56
- }
57
-
58
- /**
59
- * `server.getRequests()` redacts `Authorization` to `[REDACTED]` (aimock
60
- * privacy feature) — useless when the test needs to see the actual outgoing
61
- * auth value. Spy on `globalThis.fetch` instead and read the headers off
62
- * each call's `RequestInit`. The spy preserves the real fetch so MCPMock
63
- * still responds. Filter to MCP-bound requests by URL substring to ignore
64
- * any unrelated traffic that might land on the recorder.
65
- */
66
- function spyOnFetch(mcpUrl: string): {
67
- records: Array<Record<string, string>>;
68
- restore: () => void;
69
- } {
70
- const records: Array<Record<string, string>> = [];
71
- const realFetch = globalThis.fetch;
72
- const spy = vi
73
- .spyOn(globalThis, "fetch")
74
- .mockImplementation(async (input, init) => {
75
- const url =
76
- typeof input === "string"
77
- ? input
78
- : input instanceof URL
79
- ? input.toString()
80
- : input.url;
81
- if (url.startsWith(mcpUrl)) {
82
- const seen: Record<string, string> = {};
83
- new Headers(init?.headers ?? {}).forEach((value, key) => {
84
- seen[key.toLowerCase()] = value;
85
- });
86
- records.push(seen);
87
- }
88
- return realFetch(input, init);
89
- });
90
- return {
91
- records,
92
- restore: () => spy.mockRestore(),
93
- };
94
- }
95
-
96
- /**
97
- * x-cpki-user-id is NOT in aimock's redaction list, so journal entries
98
- * carry the real value. Use this when comparing per-call values.
99
- */
100
- function userIdsFrom(server: LLMock): string[] {
101
- return server
102
- .getRequests()
103
- .map((entry) => entry.headers?.["x-cpki-user-id"])
104
- .filter((v): v is string => typeof v === "string");
105
- }
106
-
107
32
  describe("mcpServers — real MCP server integration", () => {
108
33
  const originalEnv = process.env;
109
34
  let llm: LLMock;
@@ -445,283 +370,4 @@ describe("mcpServers — real MCP server integration", () => {
445
370
  await llm2.stop().catch(() => {});
446
371
  }
447
372
  });
448
-
449
- describe("static headers + per-call getHeaders", () => {
450
- it("static `headers` are sent on every outbound MCP request (HTTP)", async () => {
451
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
452
- llm = result.llm;
453
- mcpMock = result.mcpMock;
454
-
455
- const recorder = spyOnFetch(result.mcpUrl);
456
- try {
457
- const agent = new BasicAgent({
458
- model: "openai/gpt-4o",
459
- mcpServers: [
460
- {
461
- type: "http",
462
- url: result.mcpUrl,
463
- headers: { Authorization: "Bearer cpk-test-token" },
464
- },
465
- ],
466
- });
467
-
468
- vi.mocked(streamText).mockReturnValue(
469
- mockStreamTextResponse([textDelta("Hello"), finish()]) as any,
470
- );
471
-
472
- await collectEvents(agent["run"](baseInput));
473
-
474
- expect(recorder.records.length).toBeGreaterThan(0);
475
- for (const headers of recorder.records) {
476
- expect(headers["authorization"]).toBe("Bearer cpk-test-token");
477
- }
478
- } finally {
479
- recorder.restore();
480
- }
481
- });
482
-
483
- it("getHeaders runs per outbound HTTP request, not once per session", async () => {
484
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
485
- llm = result.llm;
486
- mcpMock = result.mcpMock;
487
-
488
- // Counter-based resolver: returns a different user-id on every
489
- // invocation. If the SDK opened the connection once and reused
490
- // headers (i.e. cached across calls), all requests would carry
491
- // the same user-id.
492
- let counter = 0;
493
- const resolverInvocations: string[] = [];
494
- const agent = new BasicAgent({
495
- model: "openai/gpt-4o",
496
- mcpServers: [
497
- {
498
- type: "http",
499
- url: result.mcpUrl,
500
- getHeaders: () => {
501
- const id = `user-${counter++}`;
502
- resolverInvocations.push(id);
503
- return { "X-Cpki-User-Id": id };
504
- },
505
- },
506
- ],
507
- });
508
-
509
- vi.mocked(streamText).mockReturnValue(
510
- mockStreamTextResponse([textDelta("ok"), finish()]) as any,
511
- );
512
- await collectEvents(agent["run"](baseInput));
513
-
514
- // The MCP SDK opens with `initialize` and `tools/list`. Both are
515
- // wrapped-fetch invocations, both must hit the resolver.
516
- expect(resolverInvocations.length).toBeGreaterThanOrEqual(2);
517
-
518
- // Distinct values across requests on the wire prove no caching
519
- // happened. x-cpki-user-id is NOT redacted by aimock so we can read
520
- // the actual values from the journal.
521
- const userIds = userIdsFrom(result.llm);
522
- expect(new Set(userIds).size).toBeGreaterThanOrEqual(2);
523
- });
524
-
525
- it("getHeaders receives requestHeaders snapshot + input + mcpServerUrl", async () => {
526
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
527
- llm = result.llm;
528
- mcpMock = result.mcpMock;
529
-
530
- const seenContexts: Array<{
531
- requestHeaders: Record<string, string>;
532
- threadId: string;
533
- mcpServerUrl: string;
534
- }> = [];
535
-
536
- const agent = new BasicAgent({
537
- model: "openai/gpt-4o",
538
- mcpServers: [
539
- {
540
- type: "http",
541
- url: result.mcpUrl,
542
- getHeaders: ({ requestHeaders, input, mcpServerUrl }) => {
543
- seenContexts.push({
544
- requestHeaders: { ...requestHeaders },
545
- threadId: input.threadId,
546
- mcpServerUrl,
547
- });
548
- return { "X-Cpki-User-Id": "anyone" };
549
- },
550
- },
551
- ],
552
- });
553
- // Simulate the runtime's `extractForwardableHeaders` populating headers.
554
- agent.headers = { "x-cpki-user-id": "from-bff" };
555
-
556
- vi.mocked(streamText).mockReturnValue(
557
- mockStreamTextResponse([finish()]) as any,
558
- );
559
- await collectEvents(agent["run"](baseInput));
560
-
561
- expect(seenContexts.length).toBeGreaterThan(0);
562
- const ctx = seenContexts[0];
563
- expect(ctx.requestHeaders["x-cpki-user-id"]).toBe("from-bff");
564
- expect(ctx.threadId).toBe("thread1");
565
- expect(ctx.mcpServerUrl).toBe(result.mcpUrl);
566
- });
567
-
568
- it("getHeaders throwing surfaces RUN_ERROR carrying MCPHeaderResolverError", async () => {
569
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
570
- llm = result.llm;
571
- mcpMock = result.mcpMock;
572
-
573
- const agent = new BasicAgent({
574
- model: "openai/gpt-4o",
575
- mcpServers: [
576
- {
577
- type: "http",
578
- url: result.mcpUrl,
579
- getHeaders: () => {
580
- throw new Error("BFF forgot to forward X-Cpki-User-Id");
581
- },
582
- },
583
- ],
584
- });
585
-
586
- vi.mocked(streamText).mockReturnValue(
587
- mockStreamTextResponse([finish()]) as any,
588
- );
589
-
590
- const events: any[] = [];
591
- try {
592
- await new Promise((resolve, reject) => {
593
- agent["run"](baseInput).subscribe({
594
- next: (event) => events.push(event),
595
- error: (err) => reject(err),
596
- complete: () => resolve(events),
597
- });
598
- });
599
- } catch {
600
- // Expected — resolver threw, fetch wrapper rethrew, transport failed.
601
- }
602
-
603
- const runError = events.find((e) => e.type === EventType.RUN_ERROR);
604
- expect(runError).toBeDefined();
605
- // The wrapped fetch reports through MCPHeaderResolverError so the
606
- // run-error message attributes the failure to the resolver, not the
607
- // transport. We assert message content here; the original cause is
608
- // preserved on the thrown class instance via ES2022 Error.cause
609
- // (visible to subscribers of the Observable error notification, not
610
- // on the AG-UI run-error event payload).
611
- expect(runError?.message).toContain("MCP header resolver");
612
- expect(runError?.message).toContain("BFF forgot to forward");
613
- // The thrown error class is exported so user code can branch on it.
614
- expect(MCPHeaderResolverError).toBeDefined();
615
- });
616
-
617
- it("backwards-compat: existing config with no auth fields still loads tools", async () => {
618
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
619
- llm = result.llm;
620
- mcpMock = result.mcpMock;
621
-
622
- const recorder = spyOnFetch(result.mcpUrl);
623
- try {
624
- const agent = new BasicAgent({
625
- model: "openai/gpt-4o",
626
- mcpServers: [{ type: "http", url: result.mcpUrl }],
627
- });
628
-
629
- vi.mocked(streamText).mockReturnValue(
630
- mockStreamTextResponse([textDelta("ok"), finish()]) as any,
631
- );
632
- await collectEvents(agent["run"](baseInput));
633
-
634
- const callArgs = vi.mocked(streamText).mock.calls[0][0];
635
- expect(callArgs.tools).toHaveProperty("get_weather");
636
- // No Authorization or X-Cpki-User-Id on the wire when no auth fields
637
- // are configured.
638
- for (const headers of recorder.records) {
639
- expect(headers["authorization"]).toBeUndefined();
640
- expect(headers["x-cpki-user-id"]).toBeUndefined();
641
- }
642
- } finally {
643
- recorder.restore();
644
- }
645
- });
646
-
647
- it("getHeaders overrides static `headers` when both set Authorization", async () => {
648
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
649
- llm = result.llm;
650
- mcpMock = result.mcpMock;
651
-
652
- const recorder = spyOnFetch(result.mcpUrl);
653
- try {
654
- const agent = new BasicAgent({
655
- model: "openai/gpt-4o",
656
- mcpServers: [
657
- {
658
- type: "http",
659
- url: result.mcpUrl,
660
- headers: { Authorization: "Bearer cpk-static" },
661
- getHeaders: () => ({ Authorization: "Bearer cpk-resolver-wins" }),
662
- },
663
- ],
664
- });
665
-
666
- vi.mocked(streamText).mockReturnValue(
667
- mockStreamTextResponse([finish()]) as any,
668
- );
669
- await collectEvents(agent["run"](baseInput));
670
-
671
- expect(recorder.records.length).toBeGreaterThan(0);
672
- for (const headers of recorder.records) {
673
- expect(headers["authorization"]).toBe("Bearer cpk-resolver-wins");
674
- }
675
- } finally {
676
- recorder.restore();
677
- }
678
- });
679
-
680
- it("static headers reach the wire on the SSE transport (regression for the silently-dropped-headers bug)", async () => {
681
- // MCPMock doesn't speak SSE so the connection ultimately fails, but the
682
- // initial GET still goes out via fetch — that's enough to verify the
683
- // transport actually attaches `headers` to the outbound request, which
684
- // a previous direct-SDK construction silently dropped.
685
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
686
- llm = result.llm;
687
- mcpMock = result.mcpMock;
688
-
689
- const recorder = spyOnFetch(result.mcpUrl);
690
- try {
691
- const agent = new BasicAgent({
692
- model: "openai/gpt-4o",
693
- mcpServers: [
694
- {
695
- type: "sse",
696
- url: result.mcpUrl,
697
- headers: { "X-Test-Auth": "sse-static-token" },
698
- },
699
- ],
700
- });
701
-
702
- vi.mocked(streamText).mockReturnValue(
703
- mockStreamTextResponse([finish()]) as any,
704
- );
705
-
706
- try {
707
- await new Promise((resolve, reject) => {
708
- agent["run"](baseInput).subscribe({
709
- next: () => {},
710
- error: (err) => reject(err),
711
- complete: () => resolve(undefined),
712
- });
713
- });
714
- } catch {
715
- // Expected — SSE init fails because MCPMock doesn't speak SSE.
716
- }
717
-
718
- expect(recorder.records.length).toBeGreaterThan(0);
719
- for (const headers of recorder.records) {
720
- expect(headers["x-test-auth"]).toBe("sse-static-token");
721
- }
722
- } finally {
723
- recorder.restore();
724
- }
725
- });
726
- });
727
373
  });
@@ -266,12 +266,30 @@ export async function* convertTanStackStream(
266
266
  }
267
267
  }
268
268
 
269
+ // TanStack's chat() engine runs a multi-turn agent loop: after the model
270
+ // returns tool calls, the engine tries to execute them and re-prompt. This
271
+ // produces a second round of TOOL_CALL_START / TOOL_CALL_END events that
272
+ // duplicate the ones from the first streaming pass. The CopilotKit runtime
273
+ // handles tool execution externally (via the frontend SDK), so we must stop
274
+ // converting events once the TanStack adapter signals the first turn is
275
+ // complete with RUN_FINISHED.
276
+ let runFinished = false;
277
+
269
278
  for await (const chunk of stream) {
270
279
  if (abortSignal.aborted) break;
271
280
 
272
281
  const raw = chunk as Record<string, unknown>;
273
282
  const type = raw.type as string;
274
283
 
284
+ // Stop converting after the first RUN_FINISHED — any subsequent events
285
+ // come from TanStack's internal tool-execution loop and would produce
286
+ // duplicate TOOL_CALL_END events that violate the ag-ui verify middleware.
287
+ if (type === "RUN_FINISHED") {
288
+ runFinished = true;
289
+ continue;
290
+ }
291
+ if (runFinished) continue;
292
+
275
293
  if (type === "TEXT_MESSAGE_CONTENT" && raw.delta != null) {
276
294
  yield* closeReasoningIfOpen();
277
295
  const textEvent: TextMessageChunkEvent = {
@@ -1,6 +1,8 @@
1
- import type {
1
+ import {
2
+ AbstractAgent,
2
3
  BaseEvent,
3
4
  RunAgentInput,
5
+ EventType,
4
6
  Message,
5
7
  ReasoningEndEvent,
6
8
  ReasoningMessageContentEvent,
@@ -18,9 +20,9 @@ import type {
18
20
  StateSnapshotEvent,
19
21
  StateDeltaEvent,
20
22
  } from "@ag-ui/client";
21
- import { AbstractAgent, EventType } from "@ag-ui/client";
22
23
  import type { AgentCapabilities } from "@ag-ui/core";
23
- import type {
24
+ import {
25
+ streamText,
24
26
  LanguageModel,
25
27
  ModelMessage,
26
28
  AssistantModelMessage,
@@ -32,12 +34,12 @@ import type {
32
34
  TextPart,
33
35
  ImagePart,
34
36
  FilePart,
37
+ tool as createVercelAISDKTool,
35
38
  ToolChoice,
36
39
  ToolSet,
40
+ stepCountIs,
37
41
  } from "ai";
38
- import { streamText, tool as createVercelAISDKTool, stepCountIs } from "ai";
39
- import { createMCPClient } from "@ai-sdk/mcp";
40
- import type { MCPClient } from "@ai-sdk/mcp";
42
+ import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
41
43
  import { Observable } from "rxjs";
42
44
  import { createOpenAI } from "@ai-sdk/openai";
43
45
  import { createAnthropic } from "@ai-sdk/anthropic";
@@ -50,13 +52,11 @@ import { schemaToJsonSchema } from "@copilotkit/shared";
50
52
  import { jsonSchema as aiJsonSchema } from "ai";
51
53
  import { convertAISDKStream } from "./converters/aisdk";
52
54
  import { convertTanStackStream } from "./converters/tanstack";
53
- import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
54
55
  import {
55
- CopilotKitMCPTransport,
56
- MCPHeaderResolverError,
57
- type MCPRequestContext,
58
- type MCPRuntimeUser,
59
- } from "./mcp-transport";
56
+ StreamableHTTPClientTransport,
57
+ StreamableHTTPClientTransportOptions,
58
+ } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
59
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
60
60
  import { randomUUID } from "@copilotkit/shared";
61
61
 
62
62
  /**
@@ -113,112 +113,58 @@ export type BuiltInAgentModel =
113
113
  */
114
114
  export type ModelSpecifier = string | LanguageModel;
115
115
 
116
- // MCPRequestContext and MCPHeaderResolverError now live in mcp-transport.ts.
117
- // Re-export so existing imports of these symbols from agent/index continue to
118
- // work.
119
- export { MCPHeaderResolverError, type MCPRequestContext };
120
-
121
116
  /**
122
- * MCP Client configuration for HTTP transport.
117
+ * MCP Client configuration for HTTP transport
123
118
  */
124
119
  export interface MCPClientConfigHTTP {
125
- /** Type of MCP client */
126
- type: "http";
127
- /** URL of the MCP server */
128
- url: string;
129
120
  /**
130
- * Optional transport options for the underlying
131
- * `StreamableHTTPClientTransport`. Pre-existing escape hatch for advanced
132
- * use cases (custom `fetch`, `requestInit`, OAuth `authProvider`, etc.).
121
+ * Type of MCP client
133
122
  */
134
- options?: StreamableHTTPClientTransportOptions;
123
+ type: "http";
135
124
  /**
136
- * Static HTTP headers, merged into every outbound request to this server.
137
- * For per-call values, use {@link MCPClientConfigHTTP.getHeaders} instead.
125
+ * URL of the MCP server
138
126
  */
139
- headers?: Record<string, string>;
127
+ url: string;
140
128
  /**
141
- * Per-call header resolver. Invoked on **every** outbound HTTP request to
142
- * this server (initialize, tools/list, tools/call, SSE reconnects). The
143
- * returned headers are merged on top of `headers` and any
144
- * `options.requestInit.headers`, so a resolver can override either.
145
- *
146
- * Throwing from the resolver causes the agent run to emit `RUN_ERROR`
147
- * carrying a {@link MCPHeaderResolverError}.
129
+ * Optional transport options for HTTP client
148
130
  */
149
- getHeaders?: (
150
- ctx: MCPRequestContext,
151
- ) => Record<string, string> | Promise<Record<string, string>>;
131
+ options?: StreamableHTTPClientTransportOptions;
152
132
  }
153
133
 
154
134
  /**
155
- * MCP Client configuration for SSE transport.
135
+ * MCP Client configuration for SSE transport
156
136
  */
157
137
  export interface MCPClientConfigSSE {
158
- /** Type of MCP client */
138
+ /**
139
+ * Type of MCP client
140
+ */
159
141
  type: "sse";
160
- /** URL of the MCP server */
142
+ /**
143
+ * URL of the MCP server
144
+ */
161
145
  url: string;
162
- /** Optional HTTP headers (e.g., for authentication). */
146
+ /**
147
+ * Optional HTTP headers (e.g., for authentication)
148
+ */
163
149
  headers?: Record<string, string>;
164
150
  }
165
151
 
166
152
  /**
167
- * MCP Client configuration.
153
+ * MCP Client configuration
168
154
  */
169
155
  export type MCPClientConfig = MCPClientConfigHTTP | MCPClientConfigSSE;
170
156
 
171
157
  /**
172
- * A user-managed MCP client that provides tools to the agent. Structural
173
- * alias for `@ai-sdk/mcp`'s `MCPClient.tools` slice pass any value built by
174
- * `createMCPClient()` directly, or supply a custom `{ tools(): ... }` object
175
- * for tests/caching layers.
158
+ * A user-managed MCP client that provides tools to the agent.
159
+ * The user is responsible for creating, configuring, and closing the client.
160
+ * Compatible with the return type of @ai-sdk/mcp's createMCPClient().
176
161
  *
177
- * Unlike `mcpServers`, the agent does NOT create or close these clients. The
178
- * user controls the full lifecycle.
162
+ * Unlike mcpServers, the agent does NOT create or close these clients.
163
+ * This allows persistent connections, custom auth, and tool caching.
179
164
  */
180
- export type MCPClientProvider = Pick<MCPClient, "tools">;
181
-
182
- /**
183
- * Open an MCP client for the given server config.
184
- *
185
- * - HTTP always goes through {@link CopilotKitMCPTransport} (preserves the
186
- * pre-existing `options` escape hatch and adds per-call `getHeaders`
187
- * resolution).
188
- * - SSE goes through `@ai-sdk/mcp`'s `createMCPClient`, whose built-in
189
- * `SseMCPTransport` correctly applies static `headers` on every outbound
190
- * request.
191
- */
192
- async function openMcpClient(
193
- config: MCPClientConfig,
194
- context: {
195
- requestHeaders: Record<string, string>;
196
- input: RunAgentInput;
197
- user?: MCPRuntimeUser;
198
- },
199
- ): Promise<MCPClient> {
200
- if (config.type === "http") {
201
- const transport = new CopilotKitMCPTransport({
202
- url: config.url,
203
- headers: config.headers,
204
- getHeaders: config.getHeaders,
205
- options: config.options,
206
- requestHeaders: context.requestHeaders,
207
- input: context.input,
208
- user: context.user,
209
- });
210
- return createMCPClient({ transport });
211
- }
212
-
213
- // SSE: hand to Vercel's transport. Static `headers` are applied via the
214
- // SseMCPTransport's common-header pipeline.
215
- return createMCPClient({
216
- transport: {
217
- type: "sse",
218
- url: config.url,
219
- headers: config.headers,
220
- },
221
- });
165
+ export interface MCPClientProvider {
166
+ /** Return tools to be merged into the agent's tool set. */
167
+ tools(): Promise<ToolSet>;
222
168
  }
223
169
 
224
170
  /**
@@ -905,21 +851,6 @@ function isFactoryConfig(
905
851
 
906
852
  export class BuiltInAgent extends AbstractAgent {
907
853
  private abortController?: AbortController;
908
- /**
909
- * Headers populated per-request by the runtime's
910
- * `extractForwardableHeaders` (the incoming request's `Authorization` +
911
- * every `x-*` header, lower-cased). Available to MCP header resolvers via
912
- * {@link MCPRequestContext.requestHeaders}; kept here as a plain field so
913
- * the runtime's `configureAgentForRequest` feature-detect activates.
914
- */
915
- public headers: Record<string, string> = {};
916
- /**
917
- * End-user identity for the current request, populated by the runtime by
918
- * invoking `identifyUser(request)` (Intelligence mode). Surfaced to MCP
919
- * header resolvers via {@link MCPRequestContext.user}; remains undefined
920
- * for runs that aren't going through a runtime with `identifyUser` set.
921
- */
922
- public user?: { id: string; name: string };
923
854
 
924
855
  constructor(private config: BuiltInAgentConfiguration) {
925
856
  super();
@@ -1173,7 +1104,7 @@ export class BuiltInAgent extends AbstractAgent {
1173
1104
  }
1174
1105
 
1175
1106
  // Set up MCP clients if configured and process the stream
1176
- const mcpClients: MCPClient[] = [];
1107
+ const mcpClients: Array<{ close: () => Promise<void> }> = [];
1177
1108
 
1178
1109
  (async () => {
1179
1110
  let terminalEventEmitted = false;
@@ -1258,27 +1189,33 @@ export class BuiltInAgent extends AbstractAgent {
1258
1189
 
1259
1190
  // Initialize MCP clients and get their tools
1260
1191
  if (this.config.mcpServers && this.config.mcpServers.length > 0) {
1261
- // Snapshot the agent's per-run state (forwarded headers + user)
1262
- // once at run-start. Resolvers see this immutable snapshot for the
1263
- // lifetime of the run, including any reconnections fired after the
1264
- // initial run completes.
1265
- const requestHeaders: Record<string, string> = { ...this.headers };
1266
- const user = this.user ? { ...this.user } : undefined;
1267
-
1268
1192
  for (const serverConfig of this.config.mcpServers) {
1269
- const mcpClient = await openMcpClient(serverConfig, {
1270
- requestHeaders,
1271
- input,
1272
- user,
1273
- });
1274
- mcpClients.push(mcpClient);
1275
-
1276
- // Get tools from this MCP server and merge with existing tools
1277
- const mcpTools = await mcpClient.tools();
1278
- streamTextParams.tools = {
1279
- ...streamTextParams.tools,
1280
- ...mcpTools,
1281
- } as ToolSet;
1193
+ let transport;
1194
+
1195
+ if (serverConfig.type === "http") {
1196
+ const url = new URL(serverConfig.url);
1197
+ transport = new StreamableHTTPClientTransport(
1198
+ url,
1199
+ serverConfig.options,
1200
+ );
1201
+ } else if (serverConfig.type === "sse") {
1202
+ transport = new SSEClientTransport(
1203
+ new URL(serverConfig.url),
1204
+ serverConfig.headers,
1205
+ );
1206
+ }
1207
+
1208
+ if (transport) {
1209
+ const mcpClient = await createMCPClient({ transport });
1210
+ mcpClients.push(mcpClient);
1211
+
1212
+ // Get tools from this MCP server and merge with existing tools
1213
+ const mcpTools = await mcpClient.tools();
1214
+ streamTextParams.tools = {
1215
+ ...streamTextParams.tools,
1216
+ ...mcpTools,
1217
+ } as ToolSet;
1218
+ }
1282
1219
  }
1283
1220
  }
1284
1221