@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.
Files changed (38) hide show
  1. package/dist/agent/index.cjs +21 -2
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +9 -16
  4. package/dist/agent/index.d.cts.map +1 -1
  5. package/dist/agent/index.d.mts +9 -16
  6. package/dist/agent/index.d.mts.map +1 -1
  7. package/dist/agent/index.mjs +22 -3
  8. package/dist/agent/index.mjs.map +1 -1
  9. package/dist/package.cjs +2 -2
  10. package/dist/package.mjs +2 -2
  11. package/dist/v2/index.d.cts +2 -2
  12. package/dist/v2/index.d.mts +2 -2
  13. package/dist/v2/runtime/handlers/intelligence/run.cjs +15 -1
  14. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  15. package/dist/v2/runtime/handlers/intelligence/run.mjs +15 -1
  16. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  17. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  18. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  19. package/dist/v2/runtime/index.d.cts +2 -1
  20. package/dist/v2/runtime/index.d.cts.map +1 -1
  21. package/dist/v2/runtime/index.d.mts +2 -1
  22. package/dist/v2/runtime/index.d.mts.map +1 -1
  23. package/dist/v2/runtime/intelligence-platform/client.cjs +22 -0
  24. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  25. package/dist/v2/runtime/intelligence-platform/client.d.cts +17 -0
  26. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  27. package/dist/v2/runtime/intelligence-platform/client.d.mts +17 -0
  28. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  29. package/dist/v2/runtime/intelligence-platform/client.mjs +22 -1
  30. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  31. package/package.json +3 -3
  32. package/src/agent/__tests__/mcp-clients.test.ts +11 -25
  33. package/src/agent/index.ts +75 -32
  34. package/src/v2/runtime/handlers/intelligence/run.ts +33 -5
  35. package/src/v2/runtime/handlers/shared/agent-utils.ts +4 -6
  36. package/src/v2/runtime/index.ts +5 -0
  37. package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +246 -0
  38. 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