@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.
- package/dist/agent/index.cjs +8 -41
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +27 -54
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +27 -54
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +10 -43
- package/dist/agent/index.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- 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/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 -53
- 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 -53
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/package.json +2 -2
- package/src/agent/__tests__/mcp-clients.test.ts +25 -11
- package/src/agent/__tests__/mcp-servers-integration.test.ts +1 -485
- package/src/agent/index.ts +62 -139
- 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 -68
- 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 -190
|
@@ -1,9 +1,8 @@
|
|
|
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";
|
|
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
|
});
|