@copilotkit/runtime 1.57.0 → 1.57.1-canary.1778272612
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 +21 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +9 -16
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +9 -16
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +22 -3
- package/dist/agent/index.mjs.map +1 -1
- package/dist/package.cjs +2 -2
- package/dist/package.mjs +2 -2
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/runtime/handlers/intelligence/run.cjs +15 -1
- package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.mjs +15 -1
- 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 +2 -1
- package/dist/v2/runtime/index.d.cts.map +1 -1
- package/dist/v2/runtime/index.d.mts +2 -1
- package/dist/v2/runtime/index.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +22 -0
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +17 -0
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +17 -0
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +22 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/package.json +3 -3
- package/src/agent/__tests__/mcp-clients.test.ts +11 -25
- package/src/agent/index.ts +75 -32
- package/src/v2/runtime/handlers/intelligence/run.ts +33 -5
- package/src/v2/runtime/handlers/shared/agent-utils.ts +4 -6
- package/src/v2/runtime/index.ts +5 -0
- package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +246 -0
- package/src/v2/runtime/intelligence-platform/client.ts +37 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { BasicAgent } from "../../../../agent";
|
|
3
|
+
import { INTELLIGENCE_USER_ID_HEADER } from "../client";
|
|
4
|
+
import { LLMock, MCPMock } from "@copilotkit/aimock";
|
|
5
|
+
import { streamText } from "ai";
|
|
6
|
+
import {
|
|
7
|
+
mockStreamTextResponse,
|
|
8
|
+
textDelta,
|
|
9
|
+
finish,
|
|
10
|
+
collectEvents,
|
|
11
|
+
} from "../../../../agent/__tests__/test-helpers";
|
|
12
|
+
|
|
13
|
+
vi.mock("ai", () => ({
|
|
14
|
+
streamText: vi.fn(),
|
|
15
|
+
tool: vi.fn((config) => config),
|
|
16
|
+
stepCountIs: vi.fn((count: number) => ({ type: "stepCount", count })),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("@ai-sdk/openai", () => ({
|
|
20
|
+
createOpenAI: vi.fn(() => (modelId: string) => ({
|
|
21
|
+
modelId,
|
|
22
|
+
provider: "openai",
|
|
23
|
+
})),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
async function startMcpMock(): Promise<{ url: string; server: LLMock }> {
|
|
27
|
+
const mock = new MCPMock();
|
|
28
|
+
mock.addTool({
|
|
29
|
+
name: "bash",
|
|
30
|
+
description: "Run a bash command",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { command: { type: "string" } },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
mock.onToolCall("bash", () => "ok");
|
|
37
|
+
const server = new LLMock({ port: 0 });
|
|
38
|
+
server.mount("/mcp", mock);
|
|
39
|
+
await server.start();
|
|
40
|
+
return { url: server.url, server };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* aimock redacts `Authorization` to `[REDACTED]` in its journal. Spy on
|
|
45
|
+
* `globalThis.fetch` to read unredacted headers off each outbound request to
|
|
46
|
+
* `mcpUrl`. The spy delegates to the real fetch so the round-trip completes.
|
|
47
|
+
*/
|
|
48
|
+
function spyOnFetch(mcpUrl: string): {
|
|
49
|
+
records: Array<Record<string, string>>;
|
|
50
|
+
restore: () => void;
|
|
51
|
+
} {
|
|
52
|
+
const records: Array<Record<string, string>> = [];
|
|
53
|
+
const realFetch = globalThis.fetch;
|
|
54
|
+
const spy = vi
|
|
55
|
+
.spyOn(globalThis, "fetch")
|
|
56
|
+
.mockImplementation(async (input, init) => {
|
|
57
|
+
const url =
|
|
58
|
+
typeof input === "string"
|
|
59
|
+
? input
|
|
60
|
+
: input instanceof URL
|
|
61
|
+
? input.toString()
|
|
62
|
+
: input.url;
|
|
63
|
+
if (url.startsWith(mcpUrl)) {
|
|
64
|
+
const seen: Record<string, string> = {};
|
|
65
|
+
new Headers(init?.headers ?? {}).forEach((value, key) => {
|
|
66
|
+
seen[key.toLowerCase()] = value;
|
|
67
|
+
});
|
|
68
|
+
records.push(seen);
|
|
69
|
+
}
|
|
70
|
+
return realFetch(input, init);
|
|
71
|
+
});
|
|
72
|
+
return { records, restore: () => spy.mockRestore() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const baseInput = {
|
|
76
|
+
threadId: "thread1",
|
|
77
|
+
runId: "run1",
|
|
78
|
+
messages: [],
|
|
79
|
+
tools: [],
|
|
80
|
+
context: [],
|
|
81
|
+
state: {},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe("BuiltInAgent — Intelligence MCP auto-attach via forwardedProps", () => {
|
|
85
|
+
let llm: LLMock | undefined;
|
|
86
|
+
const originalEnv = process.env;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
process.env = { ...originalEnv };
|
|
91
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(async () => {
|
|
95
|
+
process.env = originalEnv;
|
|
96
|
+
if (llm) {
|
|
97
|
+
await llm.stop().catch(() => {});
|
|
98
|
+
llm = undefined;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("attaches the Intelligence MCP server when forwardedProps carries userId + apiKey + mcpUrl", async () => {
|
|
103
|
+
const { url, server } = await startMcpMock();
|
|
104
|
+
llm = server;
|
|
105
|
+
|
|
106
|
+
const recorder = spyOnFetch(`${url}/mcp`);
|
|
107
|
+
try {
|
|
108
|
+
const agent = new BasicAgent({ model: "openai/gpt-4o" });
|
|
109
|
+
|
|
110
|
+
vi.mocked(streamText).mockReturnValue(
|
|
111
|
+
mockStreamTextResponse([textDelta("hi"), finish()]) as any,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await collectEvents(
|
|
115
|
+
agent["run"]({
|
|
116
|
+
...baseInput,
|
|
117
|
+
forwardedProps: {
|
|
118
|
+
auth: {
|
|
119
|
+
copilotkitIntelligence: {
|
|
120
|
+
userId: "jordan-beamson",
|
|
121
|
+
apiKey: "cpk-proj_short_long",
|
|
122
|
+
mcpUrl: `${url}/mcp`,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(recorder.records.length).toBeGreaterThan(0);
|
|
130
|
+
for (const headers of recorder.records) {
|
|
131
|
+
expect(headers["authorization"]).toBe("Bearer cpk-proj_short_long");
|
|
132
|
+
expect(headers[INTELLIGENCE_USER_ID_HEADER]).toBe("jordan-beamson");
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
recorder.restore();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does NOT attach when forwardedProps is empty (no Intelligence wiring this run)", async () => {
|
|
140
|
+
const { url, server } = await startMcpMock();
|
|
141
|
+
llm = server;
|
|
142
|
+
|
|
143
|
+
const recorder = spyOnFetch(`${url}/mcp`);
|
|
144
|
+
try {
|
|
145
|
+
const agent = new BasicAgent({ model: "openai/gpt-4o" });
|
|
146
|
+
|
|
147
|
+
vi.mocked(streamText).mockReturnValue(
|
|
148
|
+
mockStreamTextResponse([finish()]) as any,
|
|
149
|
+
);
|
|
150
|
+
await collectEvents(agent["run"](baseInput));
|
|
151
|
+
|
|
152
|
+
expect(recorder.records.length).toBe(0);
|
|
153
|
+
} finally {
|
|
154
|
+
recorder.restore();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("does NOT attach when only some of the three props are present", async () => {
|
|
159
|
+
const { url, server } = await startMcpMock();
|
|
160
|
+
llm = server;
|
|
161
|
+
|
|
162
|
+
const recorder = spyOnFetch(`${url}/mcp`);
|
|
163
|
+
try {
|
|
164
|
+
const agent = new BasicAgent({ model: "openai/gpt-4o" });
|
|
165
|
+
|
|
166
|
+
vi.mocked(streamText).mockReturnValue(
|
|
167
|
+
mockStreamTextResponse([finish()]) as any,
|
|
168
|
+
);
|
|
169
|
+
await collectEvents(
|
|
170
|
+
agent["run"]({
|
|
171
|
+
...baseInput,
|
|
172
|
+
forwardedProps: {
|
|
173
|
+
auth: {
|
|
174
|
+
copilotkitIntelligence: {
|
|
175
|
+
// userId + apiKey but no mcpUrl — should not attach.
|
|
176
|
+
userId: "jordan",
|
|
177
|
+
apiKey: "cpk-proj_xx",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(recorder.records.length).toBe(0);
|
|
185
|
+
} finally {
|
|
186
|
+
recorder.restore();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("does NOT attach when the user has already configured a server pointing at the same URL (explicit opt-in wins)", async () => {
|
|
191
|
+
const { url, server } = await startMcpMock();
|
|
192
|
+
llm = server;
|
|
193
|
+
const mcpUrl = `${url}/mcp`;
|
|
194
|
+
|
|
195
|
+
let userFetchCalls = 0;
|
|
196
|
+
const agent = new BasicAgent({
|
|
197
|
+
model: "openai/gpt-4o",
|
|
198
|
+
mcpServers: [
|
|
199
|
+
{
|
|
200
|
+
type: "http",
|
|
201
|
+
url: mcpUrl,
|
|
202
|
+
options: {
|
|
203
|
+
fetch: async (input, init) => {
|
|
204
|
+
userFetchCalls++;
|
|
205
|
+
const h = new Headers(init?.headers ?? {});
|
|
206
|
+
h.set("Authorization", "Bearer user-supplied");
|
|
207
|
+
h.set(INTELLIGENCE_USER_ID_HEADER, "explicit-user");
|
|
208
|
+
return globalThis.fetch(input, { ...init, headers: h });
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const recorder = spyOnFetch(mcpUrl);
|
|
216
|
+
try {
|
|
217
|
+
vi.mocked(streamText).mockReturnValue(
|
|
218
|
+
mockStreamTextResponse([finish()]) as any,
|
|
219
|
+
);
|
|
220
|
+
await collectEvents(
|
|
221
|
+
agent["run"]({
|
|
222
|
+
...baseInput,
|
|
223
|
+
forwardedProps: {
|
|
224
|
+
auth: {
|
|
225
|
+
copilotkitIntelligence: {
|
|
226
|
+
userId: "from-runtime",
|
|
227
|
+
apiKey: "cpk-proj_runtime",
|
|
228
|
+
mcpUrl,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(recorder.records.length).toBeGreaterThan(0);
|
|
236
|
+
// Only the user's fetch wrapper hit the wire — auto-attach skipped.
|
|
237
|
+
for (const headers of recorder.records) {
|
|
238
|
+
expect(headers["authorization"]).toBe("Bearer user-supplied");
|
|
239
|
+
expect(headers[INTELLIGENCE_USER_ID_HEADER]).toBe("explicit-user");
|
|
240
|
+
}
|
|
241
|
+
expect(userFetchCalls).toBeGreaterThan(0);
|
|
242
|
+
} finally {
|
|
243
|
+
recorder.restore();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { logger } from "@copilotkit/shared";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Header name carrying the per-call end-user identity that the CopilotKit
|
|
5
|
+
* Intelligence `/mcp` endpoint requires. Internal CopilotKit machinery — the
|
|
6
|
+
* runtime stamps this onto `agent.headers` after `identifyUser` resolves,
|
|
7
|
+
* and the auto-attach in `configureAgentForRequest` reads it back to gate
|
|
8
|
+
* MCP-server attachment and to populate the outbound `X-Cpki-User-Id`
|
|
9
|
+
* header on every MCP request. Not part of the public user API.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export const INTELLIGENCE_USER_ID_HEADER = "x-cpki-user-id";
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* Error thrown when an Intelligence platform HTTP request returns a non-2xx
|
|
5
17
|
* status. Carries the HTTP {@link status} code so callers can branch on
|
|
@@ -64,6 +76,19 @@ export interface CopilotKitIntelligenceConfig {
|
|
|
64
76
|
wsUrl: string;
|
|
65
77
|
/** API key for authenticating with the intelligence platform */
|
|
66
78
|
apiKey: string;
|
|
79
|
+
/**
|
|
80
|
+
* Enable the Intelligence platform's MCP server (bash + thread tools) on
|
|
81
|
+
* every `BuiltInAgent` run that resolves a user. The auto-attach is
|
|
82
|
+
* implemented in `configureAgentForRequest`: when this flag is `true`
|
|
83
|
+
* AND the runtime's `identifyUser` callback has placed a user-id onto
|
|
84
|
+
* the agent's forwarded headers AND the user has not already configured
|
|
85
|
+
* an MCP server pointing at the same URL, the server is appended to the
|
|
86
|
+
* agent's effective MCP server list for that run.
|
|
87
|
+
*
|
|
88
|
+
* Defaults to `false` — opt-in. Existing intelligence setups continue to
|
|
89
|
+
* work without the bash MCP server unless they flip this flag.
|
|
90
|
+
*/
|
|
91
|
+
mcpServer?: boolean;
|
|
67
92
|
/**
|
|
68
93
|
* Initial listener invoked after a thread is created.
|
|
69
94
|
* Prefer {@link CopilotKitIntelligence.onThreadCreated} for multiple listeners.
|
|
@@ -275,6 +300,7 @@ export class CopilotKitIntelligence {
|
|
|
275
300
|
#runnerWsUrl: string;
|
|
276
301
|
#clientWsUrl: string;
|
|
277
302
|
#apiKey: string;
|
|
303
|
+
#mcpServerEnabled: boolean;
|
|
278
304
|
#threadCreatedListeners = new Set<(thread: ThreadSummary) => void>();
|
|
279
305
|
#threadUpdatedListeners = new Set<(thread: ThreadSummary) => void>();
|
|
280
306
|
#threadDeletedListeners = new Set<(params: ThreadDeletedPayload) => void>();
|
|
@@ -286,6 +312,7 @@ export class CopilotKitIntelligence {
|
|
|
286
312
|
this.#runnerWsUrl = deriveRunnerWsUrl(intelligenceWsUrl);
|
|
287
313
|
this.#clientWsUrl = deriveClientWsUrl(intelligenceWsUrl);
|
|
288
314
|
this.#apiKey = config.apiKey;
|
|
315
|
+
this.#mcpServerEnabled = config.mcpServer ?? false;
|
|
289
316
|
|
|
290
317
|
if (config.onThreadCreated) {
|
|
291
318
|
this.onThreadCreated(config.onThreadCreated);
|
|
@@ -374,6 +401,16 @@ export class CopilotKitIntelligence {
|
|
|
374
401
|
return this.#apiKey;
|
|
375
402
|
}
|
|
376
403
|
|
|
404
|
+
/** @internal Used by the runtime's auto-attach to populate `Authorization`. */
|
|
405
|
+
ɵgetApiKey(): string {
|
|
406
|
+
return this.#apiKey;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** @internal Used by the runtime's auto-attach to gate MCP attachment. */
|
|
410
|
+
ɵisMcpServerEnabled(): boolean {
|
|
411
|
+
return this.#mcpServerEnabled;
|
|
412
|
+
}
|
|
413
|
+
|
|
377
414
|
async #request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
378
415
|
const url = `${this.#apiUrl}${path}`;
|
|
379
416
|
|