@copilotkit/runtime 1.56.5-canary.1777972218 → 1.57.0

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 (56) hide show
  1. package/dist/agent/index.cjs +8 -41
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +27 -54
  4. package/dist/agent/index.d.cts.map +1 -1
  5. package/dist/agent/index.d.mts +27 -54
  6. package/dist/agent/index.d.mts.map +1 -1
  7. package/dist/agent/index.mjs +10 -43
  8. package/dist/agent/index.mjs.map +1 -1
  9. package/dist/package.cjs +1 -1
  10. package/dist/package.mjs +1 -1
  11. package/dist/v2/index.cjs +0 -2
  12. package/dist/v2/index.d.cts +5 -6
  13. package/dist/v2/index.d.mts +5 -6
  14. package/dist/v2/index.mjs +1 -2
  15. package/dist/v2/runtime/core/runtime.d.cts +0 -1
  16. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  17. package/dist/v2/runtime/core/runtime.d.mts +0 -1
  18. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  19. package/dist/v2/runtime/handlers/intelligence/run.cjs +0 -4
  20. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  21. package/dist/v2/runtime/handlers/intelligence/run.mjs +0 -4
  22. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  23. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  24. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  25. package/dist/v2/runtime/index.d.cts +1 -3
  26. package/dist/v2/runtime/index.d.cts.map +1 -1
  27. package/dist/v2/runtime/index.d.mts +1 -3
  28. package/dist/v2/runtime/index.d.mts.map +1 -1
  29. package/dist/v2/runtime/intelligence-platform/client.cjs +0 -53
  30. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  31. package/dist/v2/runtime/intelligence-platform/client.d.cts +0 -41
  32. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  33. package/dist/v2/runtime/intelligence-platform/client.d.mts +0 -41
  34. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  35. package/dist/v2/runtime/intelligence-platform/client.mjs +0 -53
  36. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  37. package/package.json +2 -2
  38. package/src/agent/__tests__/mcp-clients.test.ts +25 -11
  39. package/src/agent/__tests__/mcp-servers-integration.test.ts +1 -485
  40. package/src/agent/index.ts +62 -139
  41. package/src/v2/runtime/handlers/intelligence/run.ts +0 -9
  42. package/src/v2/runtime/handlers/shared/agent-utils.ts +0 -1
  43. package/src/v2/runtime/index.ts +0 -5
  44. package/src/v2/runtime/intelligence-platform/client.ts +0 -68
  45. package/dist/agent/mcp-transport.cjs +0 -94
  46. package/dist/agent/mcp-transport.cjs.map +0 -1
  47. package/dist/agent/mcp-transport.d.cts +0 -51
  48. package/dist/agent/mcp-transport.d.cts.map +0 -1
  49. package/dist/agent/mcp-transport.d.mts +0 -52
  50. package/dist/agent/mcp-transport.d.mts.map +0 -1
  51. package/dist/agent/mcp-transport.mjs +0 -92
  52. package/dist/agent/mcp-transport.mjs.map +0 -1
  53. package/dist/v2/runtime/intelligence-platform/index.d.cts +0 -2
  54. package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
  55. package/src/agent/mcp-transport.ts +0 -190
  56. package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +0 -190
@@ -1,9 +1,8 @@
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";
6
- import { logger } from "@copilotkit/shared";
7
6
  import {
8
7
  mockStreamTextResponse,
9
8
  textDelta,
@@ -30,81 +29,6 @@ vi.mock("@ai-sdk/openai", () => ({
30
29
  // Do NOT mock @ai-sdk/mcp or @modelcontextprotocol/sdk transports —
31
30
  // we want real HTTP connections to the MCPMock server.
32
31
 
33
- /**
34
- * Spin up an LLMock-mounted MCPMock with a real HTTP listener — the mock
35
- * has to actually respond so the agent's MCP init + tools/list can
36
- * complete and the run progresses to streamText.
37
- */
38
- async function startMcpServerWithJournal(
39
- tools: Array<{ name: string; description?: string }>,
40
- ): Promise<{ mcpUrl: string; llm: LLMock; mcpMock: MCPMock }> {
41
- const mock = new MCPMock();
42
- for (const t of tools) {
43
- mock.addTool({
44
- name: t.name,
45
- description: t.description ?? `${t.name} tool`,
46
- inputSchema: {
47
- type: "object",
48
- properties: { query: { type: "string" } },
49
- },
50
- });
51
- mock.onToolCall(t.name, () => `result from ${t.name}`);
52
- }
53
- const server = new LLMock({ port: 0 });
54
- server.mount("/mcp", mock);
55
- await server.start();
56
- return { mcpUrl: `${server.url}/mcp`, llm: server, mcpMock: mock };
57
- }
58
-
59
- /**
60
- * `server.getRequests()` redacts `Authorization` to `[REDACTED]` (aimock
61
- * privacy feature) — useless when the test needs to see the actual outgoing
62
- * auth value. Spy on `globalThis.fetch` instead and read the headers off
63
- * each call's `RequestInit`. The spy preserves the real fetch so MCPMock
64
- * still responds. Filter to MCP-bound requests by URL substring to ignore
65
- * any unrelated traffic that might land on the recorder.
66
- */
67
- function spyOnFetch(mcpUrl: string): {
68
- records: Array<Record<string, string>>;
69
- restore: () => void;
70
- } {
71
- const records: Array<Record<string, string>> = [];
72
- const realFetch = globalThis.fetch;
73
- const spy = vi
74
- .spyOn(globalThis, "fetch")
75
- .mockImplementation(async (input, init) => {
76
- const url =
77
- typeof input === "string"
78
- ? input
79
- : input instanceof URL
80
- ? input.toString()
81
- : input.url;
82
- if (url.startsWith(mcpUrl)) {
83
- const seen: Record<string, string> = {};
84
- new Headers(init?.headers ?? {}).forEach((value, key) => {
85
- seen[key.toLowerCase()] = value;
86
- });
87
- records.push(seen);
88
- }
89
- return realFetch(input, init);
90
- });
91
- return {
92
- records,
93
- restore: () => spy.mockRestore(),
94
- };
95
- }
96
-
97
- /**
98
- * x-cpki-user-id is NOT in aimock's redaction list, so journal entries
99
- * carry the real value. Use this when comparing per-call values.
100
- */
101
- function userIdsFrom(server: LLMock): string[] {
102
- return server
103
- .getRequests()
104
- .map((entry) => entry.headers?.["x-cpki-user-id"])
105
- .filter((v): v is string => typeof v === "string");
106
- }
107
-
108
32
  describe("mcpServers — real MCP server integration", () => {
109
33
  const originalEnv = process.env;
110
34
  let llm: LLMock;
@@ -446,412 +370,4 @@ describe("mcpServers — real MCP server integration", () => {
446
370
  await llm2.stop().catch(() => {});
447
371
  }
448
372
  });
449
-
450
- describe("static headers + per-call getHeaders", () => {
451
- it("static `headers` are sent on every outbound MCP request (HTTP)", async () => {
452
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
453
- llm = result.llm;
454
- mcpMock = result.mcpMock;
455
-
456
- const recorder = spyOnFetch(result.mcpUrl);
457
- try {
458
- const agent = new BasicAgent({
459
- model: "openai/gpt-4o",
460
- mcpServers: [
461
- {
462
- type: "http",
463
- url: result.mcpUrl,
464
- headers: { Authorization: "Bearer cpk-test-token" },
465
- },
466
- ],
467
- });
468
-
469
- vi.mocked(streamText).mockReturnValue(
470
- mockStreamTextResponse([textDelta("Hello"), finish()]) as any,
471
- );
472
-
473
- await collectEvents(agent["run"](baseInput));
474
-
475
- expect(recorder.records.length).toBeGreaterThan(0);
476
- for (const headers of recorder.records) {
477
- expect(headers["authorization"]).toBe("Bearer cpk-test-token");
478
- }
479
- } finally {
480
- recorder.restore();
481
- }
482
- });
483
-
484
- it("getHeaders runs per outbound HTTP request, not once per session", async () => {
485
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
486
- llm = result.llm;
487
- mcpMock = result.mcpMock;
488
-
489
- // Counter-based resolver: returns a different user-id on every
490
- // invocation. If the SDK opened the connection once and reused
491
- // headers (i.e. cached across calls), all requests would carry
492
- // the same user-id.
493
- let counter = 0;
494
- const resolverInvocations: string[] = [];
495
- const agent = new BasicAgent({
496
- model: "openai/gpt-4o",
497
- mcpServers: [
498
- {
499
- type: "http",
500
- url: result.mcpUrl,
501
- getHeaders: () => {
502
- const id = `user-${counter++}`;
503
- resolverInvocations.push(id);
504
- return { "X-Cpki-User-Id": id };
505
- },
506
- },
507
- ],
508
- });
509
-
510
- vi.mocked(streamText).mockReturnValue(
511
- mockStreamTextResponse([textDelta("ok"), finish()]) as any,
512
- );
513
- await collectEvents(agent["run"](baseInput));
514
-
515
- // The MCP SDK opens with `initialize` and `tools/list`. Both are
516
- // wrapped-fetch invocations, both must hit the resolver.
517
- expect(resolverInvocations.length).toBeGreaterThanOrEqual(2);
518
-
519
- // Distinct values across requests on the wire prove no caching
520
- // happened. x-cpki-user-id is NOT redacted by aimock so we can read
521
- // the actual values from the journal.
522
- const userIds = userIdsFrom(result.llm);
523
- expect(new Set(userIds).size).toBeGreaterThanOrEqual(2);
524
- });
525
-
526
- it("getHeaders receives requestHeaders snapshot + input + mcpServerUrl", async () => {
527
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
528
- llm = result.llm;
529
- mcpMock = result.mcpMock;
530
-
531
- const seenContexts: Array<{
532
- requestHeaders: Record<string, string>;
533
- threadId: string;
534
- mcpServerUrl: string;
535
- }> = [];
536
-
537
- const agent = new BasicAgent({
538
- model: "openai/gpt-4o",
539
- mcpServers: [
540
- {
541
- type: "http",
542
- url: result.mcpUrl,
543
- getHeaders: ({ requestHeaders, input, mcpServerUrl }) => {
544
- seenContexts.push({
545
- requestHeaders: { ...requestHeaders },
546
- threadId: input.threadId,
547
- mcpServerUrl,
548
- });
549
- return { "X-Cpki-User-Id": "anyone" };
550
- },
551
- },
552
- ],
553
- });
554
- // Simulate the runtime's `extractForwardableHeaders` populating headers.
555
- agent.headers = { "x-cpki-user-id": "from-bff" };
556
-
557
- vi.mocked(streamText).mockReturnValue(
558
- mockStreamTextResponse([finish()]) as any,
559
- );
560
- await collectEvents(agent["run"](baseInput));
561
-
562
- expect(seenContexts.length).toBeGreaterThan(0);
563
- const ctx = seenContexts[0];
564
- expect(ctx.requestHeaders["x-cpki-user-id"]).toBe("from-bff");
565
- expect(ctx.threadId).toBe("thread1");
566
- expect(ctx.mcpServerUrl).toBe(result.mcpUrl);
567
- });
568
-
569
- it("getHeaders throwing surfaces RUN_ERROR carrying MCPHeaderResolverError", async () => {
570
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
571
- llm = result.llm;
572
- mcpMock = result.mcpMock;
573
-
574
- const agent = new BasicAgent({
575
- model: "openai/gpt-4o",
576
- mcpServers: [
577
- {
578
- type: "http",
579
- url: result.mcpUrl,
580
- getHeaders: () => {
581
- throw new Error("BFF forgot to forward X-Cpki-User-Id");
582
- },
583
- },
584
- ],
585
- });
586
-
587
- vi.mocked(streamText).mockReturnValue(
588
- mockStreamTextResponse([finish()]) as any,
589
- );
590
-
591
- const events: any[] = [];
592
- try {
593
- await new Promise((resolve, reject) => {
594
- agent["run"](baseInput).subscribe({
595
- next: (event) => events.push(event),
596
- error: (err) => reject(err),
597
- complete: () => resolve(events),
598
- });
599
- });
600
- } catch {
601
- // Expected — resolver threw, fetch wrapper rethrew, transport failed.
602
- }
603
-
604
- const runError = events.find((e) => e.type === EventType.RUN_ERROR);
605
- expect(runError).toBeDefined();
606
- // The wrapped fetch reports through MCPHeaderResolverError so the
607
- // run-error message attributes the failure to the resolver, not the
608
- // transport. We assert message content here; the original cause is
609
- // preserved on the thrown class instance via ES2022 Error.cause
610
- // (visible to subscribers of the Observable error notification, not
611
- // on the AG-UI run-error event payload).
612
- expect(runError?.message).toContain("MCP header resolver");
613
- expect(runError?.message).toContain("BFF forgot to forward");
614
- // The thrown error class is exported so user code can branch on it.
615
- expect(MCPHeaderResolverError).toBeDefined();
616
- });
617
-
618
- it("backwards-compat: existing config with no auth fields still loads tools", async () => {
619
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
620
- llm = result.llm;
621
- mcpMock = result.mcpMock;
622
-
623
- const recorder = spyOnFetch(result.mcpUrl);
624
- try {
625
- const agent = new BasicAgent({
626
- model: "openai/gpt-4o",
627
- mcpServers: [{ type: "http", url: result.mcpUrl }],
628
- });
629
-
630
- vi.mocked(streamText).mockReturnValue(
631
- mockStreamTextResponse([textDelta("ok"), finish()]) as any,
632
- );
633
- await collectEvents(agent["run"](baseInput));
634
-
635
- const callArgs = vi.mocked(streamText).mock.calls[0][0];
636
- expect(callArgs.tools).toHaveProperty("get_weather");
637
- // No Authorization or X-Cpki-User-Id on the wire when no auth fields
638
- // are configured.
639
- for (const headers of recorder.records) {
640
- expect(headers["authorization"]).toBeUndefined();
641
- expect(headers["x-cpki-user-id"]).toBeUndefined();
642
- }
643
- } finally {
644
- recorder.restore();
645
- }
646
- });
647
-
648
- it("getHeaders overrides static `headers` when both set Authorization", async () => {
649
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
650
- llm = result.llm;
651
- mcpMock = result.mcpMock;
652
-
653
- const recorder = spyOnFetch(result.mcpUrl);
654
- try {
655
- const agent = new BasicAgent({
656
- model: "openai/gpt-4o",
657
- mcpServers: [
658
- {
659
- type: "http",
660
- url: result.mcpUrl,
661
- headers: { Authorization: "Bearer cpk-static" },
662
- getHeaders: () => ({ Authorization: "Bearer cpk-resolver-wins" }),
663
- },
664
- ],
665
- });
666
-
667
- vi.mocked(streamText).mockReturnValue(
668
- mockStreamTextResponse([finish()]) as any,
669
- );
670
- await collectEvents(agent["run"](baseInput));
671
-
672
- expect(recorder.records.length).toBeGreaterThan(0);
673
- for (const headers of recorder.records) {
674
- expect(headers["authorization"]).toBe("Bearer cpk-resolver-wins");
675
- }
676
- } finally {
677
- recorder.restore();
678
- }
679
- });
680
-
681
- it("requiresUser=true: server is skipped when no user is on the agent", async () => {
682
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
683
- llm = result.llm;
684
- mcpMock = result.mcpMock;
685
-
686
- const recorder = spyOnFetch(result.mcpUrl);
687
- const resolver = vi.fn(() => ({ "X-Cpki-User-Id": "never" }));
688
- try {
689
- const agent = new BasicAgent({
690
- model: "openai/gpt-4o",
691
- mcpServers: [
692
- {
693
- type: "http",
694
- url: result.mcpUrl,
695
- requiresUser: true,
696
- getHeaders: resolver,
697
- },
698
- ],
699
- });
700
-
701
- vi.mocked(streamText).mockReturnValue(
702
- mockStreamTextResponse([finish()]) as any,
703
- );
704
-
705
- const events = await collectEvents(agent["run"](baseInput));
706
-
707
- expect(
708
- events.find((e) => e.type === EventType.RUN_ERROR),
709
- ).toBeUndefined();
710
- expect(recorder.records.length).toBe(0);
711
- // Skip happens before the transport opens — resolver never invoked.
712
- expect(resolver).not.toHaveBeenCalled();
713
- } finally {
714
- recorder.restore();
715
- }
716
- });
717
-
718
- it("requiresUser=true: server is included as normal when a user is set", async () => {
719
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
720
- llm = result.llm;
721
- mcpMock = result.mcpMock;
722
-
723
- const recorder = spyOnFetch(result.mcpUrl);
724
- try {
725
- const agent = new BasicAgent({
726
- model: "openai/gpt-4o",
727
- mcpServers: [
728
- {
729
- type: "http",
730
- url: result.mcpUrl,
731
- requiresUser: true,
732
- getHeaders: ({ user }) => ({ "X-Cpki-User-Id": user!.id }),
733
- },
734
- ],
735
- });
736
- agent.user = { id: "alice", name: "Alice" };
737
-
738
- vi.mocked(streamText).mockReturnValue(
739
- mockStreamTextResponse([finish()]) as any,
740
- );
741
-
742
- await collectEvents(agent["run"](baseInput));
743
-
744
- expect(recorder.records.length).toBeGreaterThan(0);
745
- for (const headers of recorder.records) {
746
- expect(headers["x-cpki-user-id"]).toBe("alice");
747
- }
748
- } finally {
749
- recorder.restore();
750
- }
751
- });
752
-
753
- it("requiresUser only skips the flagged server: peers without the flag still load tools", async () => {
754
- const flagged = await startMcpServerWithJournal([{ name: "alpha_tool" }]);
755
- const peer = await startMcpServerWithJournal([{ name: "beta_tool" }]);
756
- llm = flagged.llm;
757
- mcpMock = flagged.mcpMock;
758
-
759
- try {
760
- const agent = new BasicAgent({
761
- model: "openai/gpt-4o",
762
- mcpServers: [
763
- { type: "http", url: flagged.mcpUrl, requiresUser: true },
764
- { type: "http", url: peer.mcpUrl },
765
- ],
766
- });
767
-
768
- vi.mocked(streamText).mockReturnValue(
769
- mockStreamTextResponse([finish()]) as any,
770
- );
771
-
772
- await collectEvents(agent["run"](baseInput));
773
-
774
- const callArgs = vi.mocked(streamText).mock.calls[0][0];
775
- expect(callArgs.tools).toHaveProperty("beta_tool");
776
- expect(callArgs.tools).not.toHaveProperty("alpha_tool");
777
- } finally {
778
- await peer.llm.stop().catch(() => {});
779
- }
780
- });
781
-
782
- it("requiresUser=true: skip is logged at warn level", async () => {
783
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
784
- llm = result.llm;
785
- mcpMock = result.mcpMock;
786
-
787
- const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
788
- try {
789
- const agent = new BasicAgent({
790
- model: "openai/gpt-4o",
791
- mcpServers: [
792
- { type: "http", url: result.mcpUrl, requiresUser: true },
793
- ],
794
- });
795
-
796
- vi.mocked(streamText).mockReturnValue(
797
- mockStreamTextResponse([finish()]) as any,
798
- );
799
- await collectEvents(agent["run"](baseInput));
800
-
801
- expect(warnSpy).toHaveBeenCalledWith(
802
- expect.objectContaining({ url: result.mcpUrl }),
803
- expect.stringContaining("Skipping MCP server"),
804
- );
805
- } finally {
806
- warnSpy.mockRestore();
807
- }
808
- });
809
-
810
- it("static headers reach the wire on the SSE transport (regression for the silently-dropped-headers bug)", async () => {
811
- // MCPMock doesn't speak SSE so the connection ultimately fails, but the
812
- // initial GET still goes out via fetch — that's enough to verify the
813
- // transport actually attaches `headers` to the outbound request, which
814
- // a previous direct-SDK construction silently dropped.
815
- const result = await startMcpServerWithJournal([{ name: "get_weather" }]);
816
- llm = result.llm;
817
- mcpMock = result.mcpMock;
818
-
819
- const recorder = spyOnFetch(result.mcpUrl);
820
- try {
821
- const agent = new BasicAgent({
822
- model: "openai/gpt-4o",
823
- mcpServers: [
824
- {
825
- type: "sse",
826
- url: result.mcpUrl,
827
- headers: { "X-Test-Auth": "sse-static-token" },
828
- },
829
- ],
830
- });
831
-
832
- vi.mocked(streamText).mockReturnValue(
833
- mockStreamTextResponse([finish()]) as any,
834
- );
835
-
836
- try {
837
- await new Promise((resolve, reject) => {
838
- agent["run"](baseInput).subscribe({
839
- next: () => {},
840
- error: (err) => reject(err),
841
- complete: () => resolve(undefined),
842
- });
843
- });
844
- } catch {
845
- // Expected — SSE init fails because MCPMock doesn't speak SSE.
846
- }
847
-
848
- expect(recorder.records.length).toBeGreaterThan(0);
849
- for (const headers of recorder.records) {
850
- expect(headers["x-test-auth"]).toBe("sse-static-token");
851
- }
852
- } finally {
853
- recorder.restore();
854
- }
855
- });
856
- });
857
373
  });