@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.
- package/dist/agent/converters/tanstack.cjs +6 -0
- package/dist/agent/converters/tanstack.cjs.map +1 -1
- package/dist/agent/converters/tanstack.mjs +6 -0
- package/dist/agent/converters/tanstack.mjs.map +1 -1
- package/dist/agent/index.cjs +8 -37
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +27 -52
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +27 -52
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +9 -38
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs +8 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.cjs.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.d.cts.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.d.mts.map +1 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs +8 -1
- package/dist/lib/runtime/agent-integrations/langgraph/agent.mjs.map +1 -1
- package/dist/package.cjs +5 -5
- package/dist/package.mjs +5 -5
- package/dist/v2/index.cjs +0 -2
- package/dist/v2/index.d.cts +5 -6
- package/dist/v2/index.d.mts +5 -6
- package/dist/v2/index.mjs +1 -2
- package/dist/v2/runtime/core/runtime.d.cts +0 -1
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +0 -1
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/endpoints/express.cjs +5 -5
- package/dist/v2/runtime/endpoints/express.cjs.map +1 -1
- package/dist/v2/runtime/endpoints/express.mjs +5 -5
- package/dist/v2/runtime/endpoints/express.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.cjs +0 -4
- package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.mjs +0 -4
- package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -3
- package/dist/v2/runtime/index.d.cts.map +1 -1
- package/dist/v2/runtime/index.d.mts +1 -3
- package/dist/v2/runtime/index.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +0 -52
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +0 -41
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +0 -41
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +0 -52
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/package.json +6 -6
- package/src/agent/__tests__/mcp-clients.test.ts +25 -11
- package/src/agent/__tests__/mcp-servers-integration.test.ts +1 -355
- package/src/agent/converters/tanstack.ts +18 -0
- package/src/agent/index.ts +65 -128
- package/src/lib/runtime/agent-integrations/langgraph/agent.ts +8 -1
- package/src/v2/runtime/__tests__/express-fetch-bridge.test.ts +1 -1
- package/src/v2/runtime/endpoints/express.ts +9 -3
- package/src/v2/runtime/handlers/intelligence/run.ts +0 -9
- package/src/v2/runtime/handlers/shared/agent-utils.ts +0 -1
- package/src/v2/runtime/index.ts +0 -5
- package/src/v2/runtime/intelligence-platform/client.ts +0 -67
- package/dist/agent/mcp-transport.cjs +0 -94
- package/dist/agent/mcp-transport.cjs.map +0 -1
- package/dist/agent/mcp-transport.d.cts +0 -51
- package/dist/agent/mcp-transport.d.cts.map +0 -1
- package/dist/agent/mcp-transport.d.mts +0 -52
- package/dist/agent/mcp-transport.d.mts.map +0 -1
- package/dist/agent/mcp-transport.mjs +0 -92
- package/dist/agent/mcp-transport.mjs.map +0 -1
- package/dist/v2/runtime/intelligence-platform/index.d.cts +0 -2
- package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
- package/src/agent/mcp-transport.ts +0 -190
- 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
|
|
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 = {
|
package/src/agent/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
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
|
|
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 {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
123
|
+
type: "http";
|
|
135
124
|
/**
|
|
136
|
-
*
|
|
137
|
-
* For per-call values, use {@link MCPClientConfigHTTP.getHeaders} instead.
|
|
125
|
+
* URL of the MCP server
|
|
138
126
|
*/
|
|
139
|
-
|
|
127
|
+
url: string;
|
|
140
128
|
/**
|
|
141
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Type of MCP client
|
|
140
|
+
*/
|
|
159
141
|
type: "sse";
|
|
160
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* URL of the MCP server
|
|
144
|
+
*/
|
|
161
145
|
url: string;
|
|
162
|
-
/**
|
|
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.
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
|
178
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|