@copilotkit/runtime 1.55.1 → 1.55.2-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @copilotkit/runtime
2
2
 
3
+ ## 1.55.2-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @copilotkit/shared@1.55.2-next.0
8
+
3
9
  ## 1.55.1
4
10
 
5
11
  ### Patch Changes
package/dist/package.cjs CHANGED
@@ -5,7 +5,7 @@ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
5
5
  var require_package = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
6
6
  module.exports = {
7
7
  "name": "@copilotkit/runtime",
8
- "version": "1.55.1",
8
+ "version": "1.55.2-next.0",
9
9
  "private": false,
10
10
  "keywords": [
11
11
  "ai",
@@ -81,7 +81,7 @@ var require_package = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, m
81
81
  "attw": "attw --pack . --profile node16"
82
82
  },
83
83
  "dependencies": {
84
- "@ag-ui/a2ui-middleware": "0.0.3",
84
+ "@ag-ui/a2ui-middleware": "0.0.4",
85
85
  "@ag-ui/client": "0.0.52",
86
86
  "@ag-ui/core": "0.0.52",
87
87
  "@ag-ui/encoder": "0.0.52",
@@ -123,6 +123,7 @@ var require_package = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, m
123
123
  "zod": "^3.23.3"
124
124
  },
125
125
  "devDependencies": {
126
+ "@copilotkit/aimock": "^1.10.0",
126
127
  "@swc/core": "1.5.28",
127
128
  "@types/cors": "^2.8.17",
128
129
  "@types/express": "^4.17.21",
package/dist/package.mjs CHANGED
@@ -5,7 +5,7 @@ import { __commonJSMin } from "./_virtual/_rolldown/runtime.mjs";
5
5
  var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6
6
  module.exports = {
7
7
  "name": "@copilotkit/runtime",
8
- "version": "1.55.1",
8
+ "version": "1.55.2-next.0",
9
9
  "private": false,
10
10
  "keywords": [
11
11
  "ai",
@@ -81,7 +81,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
81
81
  "attw": "attw --pack . --profile node16"
82
82
  },
83
83
  "dependencies": {
84
- "@ag-ui/a2ui-middleware": "0.0.3",
84
+ "@ag-ui/a2ui-middleware": "0.0.4",
85
85
  "@ag-ui/client": "0.0.52",
86
86
  "@ag-ui/core": "0.0.52",
87
87
  "@ag-ui/encoder": "0.0.52",
@@ -123,6 +123,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
123
123
  "zod": "^3.23.3"
124
124
  },
125
125
  "devDependencies": {
126
+ "@copilotkit/aimock": "^1.10.0",
126
127
  "@swc/core": "1.5.28",
127
128
  "@types/cors": "^2.8.17",
128
129
  "@types/express": "^4.17.21",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/runtime",
3
- "version": "1.55.1",
3
+ "version": "1.55.2-next.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -76,7 +76,7 @@
76
76
  "access": "public"
77
77
  },
78
78
  "dependencies": {
79
- "@ag-ui/a2ui-middleware": "0.0.3",
79
+ "@ag-ui/a2ui-middleware": "0.0.4",
80
80
  "@ag-ui/client": "0.0.52",
81
81
  "@ag-ui/core": "0.0.52",
82
82
  "@ag-ui/encoder": "0.0.52",
@@ -115,9 +115,10 @@
115
115
  "uuid": "^10.0.0",
116
116
  "ws": "^8.18.0",
117
117
  "zod": "^3.23.3",
118
- "@copilotkit/shared": "1.55.1"
118
+ "@copilotkit/shared": "1.55.2-next.0"
119
119
  },
120
120
  "devDependencies": {
121
+ "@copilotkit/aimock": "^1.10.0",
121
122
  "@swc/core": "1.5.28",
122
123
  "@types/cors": "^2.8.17",
123
124
  "@types/express": "^4.17.21",
@@ -0,0 +1,373 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { BasicAgent } from "../index";
3
+ import { EventType } from "@ag-ui/client";
4
+ import { streamText } from "ai";
5
+ import { LLMock, MCPMock } from "@copilotkit/aimock";
6
+ import {
7
+ mockStreamTextResponse,
8
+ textDelta,
9
+ finish,
10
+ collectEvents,
11
+ toolCall,
12
+ toolResult,
13
+ } from "./test-helpers";
14
+
15
+ // Mock the ai module — we don't want real LLM calls
16
+ vi.mock("ai", () => ({
17
+ streamText: vi.fn(),
18
+ tool: vi.fn((config) => config),
19
+ stepCountIs: vi.fn((count: number) => ({ type: "stepCount", count })),
20
+ }));
21
+
22
+ vi.mock("@ai-sdk/openai", () => ({
23
+ createOpenAI: vi.fn(() => (modelId: string) => ({
24
+ modelId,
25
+ provider: "openai",
26
+ })),
27
+ }));
28
+
29
+ // Do NOT mock @ai-sdk/mcp or @modelcontextprotocol/sdk transports —
30
+ // we want real HTTP connections to the MCPMock server.
31
+
32
+ describe("mcpServers — real MCP server integration", () => {
33
+ const originalEnv = process.env;
34
+ let llm: LLMock;
35
+ let mcpMock: MCPMock;
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ process.env = { ...originalEnv };
40
+ process.env.OPENAI_API_KEY = "test-key";
41
+ });
42
+
43
+ afterEach(async () => {
44
+ process.env = originalEnv;
45
+ if (llm) {
46
+ await llm.stop().catch(() => {});
47
+ }
48
+ });
49
+
50
+ const baseInput = {
51
+ threadId: "thread1",
52
+ runId: "run1",
53
+ messages: [],
54
+ tools: [],
55
+ context: [],
56
+ state: {},
57
+ };
58
+
59
+ /**
60
+ * Start an LLMock with an MCPMock mounted at /mcp.
61
+ * Returns the full MCP endpoint URL.
62
+ */
63
+ async function startMcpServer(
64
+ tools: Array<{ name: string; description?: string }>,
65
+ ): Promise<{ mcpUrl: string; llm: LLMock; mcpMock: MCPMock }> {
66
+ const mock = new MCPMock();
67
+ for (const t of tools) {
68
+ mock.addTool({
69
+ name: t.name,
70
+ description: t.description ?? `${t.name} tool`,
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: { query: { type: "string" } },
74
+ },
75
+ });
76
+ mock.onToolCall(t.name, () => `result from ${t.name}`);
77
+ }
78
+
79
+ const server = new LLMock({ port: 0 });
80
+ server.mount("/mcp", mock);
81
+ await server.start();
82
+
83
+ return {
84
+ mcpUrl: `${server.url}/mcp`,
85
+ llm: server,
86
+ mcpMock: mock,
87
+ };
88
+ }
89
+
90
+ it("HTTP transport fetches tools from MCPMock", async () => {
91
+ const result = await startMcpServer([
92
+ { name: "get_weather", description: "Get the weather" },
93
+ ]);
94
+ llm = result.llm;
95
+ mcpMock = result.mcpMock;
96
+
97
+ const agent = new BasicAgent({
98
+ model: "openai/gpt-4o",
99
+ mcpServers: [{ type: "http", url: result.mcpUrl }],
100
+ });
101
+
102
+ vi.mocked(streamText).mockReturnValue(
103
+ mockStreamTextResponse([textDelta("Hello"), finish()]) as any,
104
+ );
105
+
106
+ await collectEvents(agent["run"](baseInput));
107
+
108
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
109
+ expect(callArgs.tools).toHaveProperty("get_weather");
110
+ });
111
+
112
+ it("SSE transport against MCPMock emits RUN_ERROR or completes without crash", async () => {
113
+ // MCPMock only supports Streamable HTTP, not SSE.
114
+ // The agent should emit RUN_ERROR when SSE connection fails.
115
+ const result = await startMcpServer([
116
+ { name: "get_weather", description: "Get the weather" },
117
+ ]);
118
+ llm = result.llm;
119
+ mcpMock = result.mcpMock;
120
+
121
+ const agent = new BasicAgent({
122
+ model: "openai/gpt-4o",
123
+ mcpServers: [{ type: "sse", url: result.mcpUrl }],
124
+ });
125
+
126
+ vi.mocked(streamText).mockReturnValue(
127
+ mockStreamTextResponse([finish()]) as any,
128
+ );
129
+
130
+ // Collect events manually — the Observable may error after emitting RUN_ERROR
131
+ const events: any[] = [];
132
+ try {
133
+ await new Promise((resolve, reject) => {
134
+ agent["run"](baseInput).subscribe({
135
+ next: (event: any) => events.push(event),
136
+ error: (err: any) => reject(err),
137
+ complete: () => resolve(events),
138
+ });
139
+ });
140
+ // If it completes without error, that's also acceptable (graceful fallthrough)
141
+ } catch {
142
+ // Expected — SSE transport failure should emit RUN_ERROR then error
143
+ }
144
+
145
+ const hasRunError = events.some((e) => e.type === EventType.RUN_ERROR);
146
+ // Either we got a RUN_ERROR or streamText was never called (connection failed before tools fetch)
147
+ expect(hasRunError || !vi.mocked(streamText).mock.calls.length).toBe(true);
148
+ });
149
+
150
+ it("tool call round-trip emits TOOL_CALL_START, TOOL_CALL_RESULT, and TEXT_MESSAGE_CHUNK", async () => {
151
+ const result = await startMcpServer([
152
+ { name: "get_weather", description: "Get the weather" },
153
+ ]);
154
+ llm = result.llm;
155
+ mcpMock = result.mcpMock;
156
+
157
+ const agent = new BasicAgent({
158
+ model: "openai/gpt-4o",
159
+ mcpServers: [{ type: "http", url: result.mcpUrl }],
160
+ });
161
+
162
+ vi.mocked(streamText).mockReturnValue(
163
+ mockStreamTextResponse([
164
+ toolCall("tc1", "get_weather", { query: "NYC" }),
165
+ toolResult("tc1", "get_weather", "Sunny 72F"),
166
+ textDelta("The weather is sunny."),
167
+ finish(),
168
+ ]) as any,
169
+ );
170
+
171
+ const events = await collectEvents(agent["run"](baseInput));
172
+
173
+ const types = events.map((e: any) => e.type);
174
+ expect(types).toContain(EventType.TOOL_CALL_START);
175
+ expect(types).toContain(EventType.TOOL_CALL_RESULT);
176
+ expect(types).toContain(EventType.TEXT_MESSAGE_CHUNK);
177
+
178
+ // Verify the tool call result content
179
+ const resultEvent = events.find(
180
+ (e: any) => e.type === EventType.TOOL_CALL_RESULT,
181
+ ) as any;
182
+ expect(resultEvent.toolCallId).toBe("tc1");
183
+ expect(resultEvent.content).toContain("Sunny 72F");
184
+ });
185
+
186
+ it("MCP clients are cleaned up after completion — second run creates fresh connections", async () => {
187
+ const result = await startMcpServer([
188
+ { name: "get_weather", description: "Get the weather" },
189
+ ]);
190
+ llm = result.llm;
191
+ mcpMock = result.mcpMock;
192
+
193
+ const agent = new BasicAgent({
194
+ model: "openai/gpt-4o",
195
+ mcpServers: [{ type: "http", url: result.mcpUrl }],
196
+ });
197
+
198
+ // First run
199
+ vi.mocked(streamText).mockReturnValue(
200
+ mockStreamTextResponse([textDelta("Run 1"), finish()]) as any,
201
+ );
202
+ const events1 = await collectEvents(agent["run"](baseInput));
203
+ expect(events1.some((e: any) => e.type === EventType.RUN_FINISHED)).toBe(
204
+ true,
205
+ );
206
+
207
+ // Second run — should succeed with fresh MCP client connections
208
+ vi.mocked(streamText).mockReturnValue(
209
+ mockStreamTextResponse([textDelta("Run 2"), finish()]) as any,
210
+ );
211
+ const events2 = await collectEvents(agent["run"](baseInput));
212
+ expect(events2.some((e: any) => e.type === EventType.RUN_FINISHED)).toBe(
213
+ true,
214
+ );
215
+
216
+ // streamText was called twice (once per run), each time with MCP tools
217
+ expect(vi.mocked(streamText).mock.calls).toHaveLength(2);
218
+ expect(vi.mocked(streamText).mock.calls[0][0].tools).toHaveProperty(
219
+ "get_weather",
220
+ );
221
+ expect(vi.mocked(streamText).mock.calls[1][0].tools).toHaveProperty(
222
+ "get_weather",
223
+ );
224
+ });
225
+
226
+ it("unreachable MCP server emits RUN_ERROR", async () => {
227
+ const agent = new BasicAgent({
228
+ model: "openai/gpt-4o",
229
+ mcpServers: [{ type: "http", url: "http://localhost:59999" }],
230
+ });
231
+
232
+ vi.mocked(streamText).mockReturnValue(
233
+ mockStreamTextResponse([finish()]) as any,
234
+ );
235
+
236
+ const events: any[] = [];
237
+ try {
238
+ await new Promise((resolve, reject) => {
239
+ agent["run"](baseInput).subscribe({
240
+ next: (event: any) => events.push(event),
241
+ error: (err: any) => reject(err),
242
+ complete: () => resolve(events),
243
+ });
244
+ });
245
+ } catch {
246
+ // Expected — connection refused should cause an error
247
+ }
248
+
249
+ expect(events.some((e) => e.type === EventType.RUN_ERROR)).toBe(true);
250
+ // streamText should not have been called since MCP init failed
251
+ expect(streamText).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("MCP clients are cleaned up after streamText error — subsequent run still works", async () => {
255
+ const result = await startMcpServer([
256
+ { name: "get_weather", description: "Get the weather" },
257
+ ]);
258
+ llm = result.llm;
259
+ mcpMock = result.mcpMock;
260
+
261
+ const agent = new BasicAgent({
262
+ model: "openai/gpt-4o",
263
+ mcpServers: [{ type: "http", url: result.mcpUrl }],
264
+ });
265
+
266
+ // First run — streamText throws an error
267
+ vi.mocked(streamText).mockImplementation(() => {
268
+ throw new Error("LLM connection failed");
269
+ });
270
+
271
+ const events1: any[] = [];
272
+ try {
273
+ await new Promise((resolve, reject) => {
274
+ agent["run"](baseInput).subscribe({
275
+ next: (event: any) => events1.push(event),
276
+ error: (err: any) => reject(err),
277
+ complete: () => resolve(events1),
278
+ });
279
+ });
280
+ } catch {
281
+ // Expected — streamText threw
282
+ }
283
+
284
+ // Should have emitted RUN_ERROR
285
+ expect(events1.some((e) => e.type === EventType.RUN_ERROR)).toBe(true);
286
+
287
+ // Second run — streamText works normally, proving MCP cleanup happened
288
+ vi.mocked(streamText).mockReturnValue(
289
+ mockStreamTextResponse([textDelta("Recovery"), finish()]) as any,
290
+ );
291
+ const events2 = await collectEvents(agent["run"](baseInput));
292
+ expect(events2.some((e: any) => e.type === EventType.RUN_FINISHED)).toBe(
293
+ true,
294
+ );
295
+
296
+ // The second run should have MCP tools available
297
+ const secondCallArgs = vi.mocked(streamText).mock.calls[1][0];
298
+ expect(secondCallArgs.tools).toHaveProperty("get_weather");
299
+ });
300
+
301
+ it("MCP tool descriptions are passed to streamText tools config", async () => {
302
+ const result = await startMcpServer([
303
+ { name: "get_weather", description: "Get the weather" },
304
+ ]);
305
+ llm = result.llm;
306
+ mcpMock = result.mcpMock;
307
+
308
+ const agent = new BasicAgent({
309
+ model: "openai/gpt-4o",
310
+ mcpServers: [{ type: "http", url: result.mcpUrl }],
311
+ });
312
+
313
+ vi.mocked(streamText).mockReturnValue(
314
+ mockStreamTextResponse([textDelta("Hello"), finish()]) as any,
315
+ );
316
+
317
+ await collectEvents(agent["run"](baseInput));
318
+
319
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
320
+ expect(callArgs.tools).toHaveProperty("get_weather");
321
+ // The MCP tool should include the description from the MCPMock server
322
+ expect(callArgs.tools.get_weather.description).toBe("Get the weather");
323
+ });
324
+
325
+ it("multiple MCP servers merge tools from both", async () => {
326
+ // First server with get_weather
327
+ const result1 = await startMcpServer([
328
+ { name: "get_weather", description: "Get the weather" },
329
+ ]);
330
+ llm = result1.llm;
331
+
332
+ // Second server with search_docs
333
+ const mock2 = new MCPMock();
334
+ mock2.addTool({
335
+ name: "search_docs",
336
+ description: "Search documentation",
337
+ inputSchema: {
338
+ type: "object",
339
+ properties: { query: { type: "string" } },
340
+ },
341
+ });
342
+ mock2.onToolCall("search_docs", () => "doc results");
343
+
344
+ const llm2 = new LLMock({ port: 0 });
345
+ llm2.mount("/mcp", mock2);
346
+ await llm2.start();
347
+
348
+ try {
349
+ const agent = new BasicAgent({
350
+ model: "openai/gpt-4o",
351
+ mcpServers: [
352
+ { type: "http", url: result1.mcpUrl },
353
+ { type: "http", url: `${llm2.url}/mcp` },
354
+ ],
355
+ });
356
+
357
+ vi.mocked(streamText).mockReturnValue(
358
+ mockStreamTextResponse([
359
+ textDelta("Both tools available"),
360
+ finish(),
361
+ ]) as any,
362
+ );
363
+
364
+ await collectEvents(agent["run"](baseInput));
365
+
366
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
367
+ expect(callArgs.tools).toHaveProperty("get_weather");
368
+ expect(callArgs.tools).toHaveProperty("search_docs");
369
+ } finally {
370
+ await llm2.stop().catch(() => {});
371
+ }
372
+ });
373
+ });
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import {
3
+ AbstractAgent,
4
+ RunAgentInput,
5
+ BaseEvent,
6
+ EventType,
7
+ } from "@ag-ui/client";
8
+ import { Observable } from "rxjs";
9
+ import { LLMock, MCPMock } from "@copilotkit/aimock";
10
+ import { MCPAppsMiddleware, getServerHash } from "@ag-ui/mcp-apps-middleware";
11
+
12
+ /**
13
+ * A minimal next-agent that emits RUN_STARTED and RUN_FINISHED.
14
+ * Used as the downstream agent when the middleware should NOT delegate.
15
+ */
16
+ class MockNextAgent extends AbstractAgent {
17
+ run(input: RunAgentInput): Observable<BaseEvent> {
18
+ return new Observable((subscriber) => {
19
+ subscriber.next({
20
+ type: EventType.RUN_STARTED,
21
+ threadId: input.threadId,
22
+ runId: input.runId,
23
+ } as BaseEvent);
24
+ subscriber.next({
25
+ type: EventType.RUN_FINISHED,
26
+ threadId: input.threadId,
27
+ runId: input.runId,
28
+ } as BaseEvent);
29
+ subscriber.complete();
30
+ });
31
+ }
32
+
33
+ clone(): AbstractAgent {
34
+ return new MockNextAgent();
35
+ }
36
+
37
+ protected connect(): ReturnType<AbstractAgent["connect"]> {
38
+ throw new Error("not used");
39
+ }
40
+ }
41
+
42
+ function createRunInput(overrides: Partial<RunAgentInput> = {}): RunAgentInput {
43
+ return {
44
+ threadId: "thread-1",
45
+ runId: "run-1",
46
+ state: {},
47
+ messages: [],
48
+ tools: [],
49
+ context: [],
50
+ forwardedProps: undefined,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ async function collectEvents(
56
+ observable: Observable<BaseEvent>,
57
+ ): Promise<BaseEvent[]> {
58
+ const events: BaseEvent[] = [];
59
+ await new Promise<void>((resolve, reject) => {
60
+ observable.subscribe({
61
+ next: (event) => events.push(event),
62
+ error: reject,
63
+ complete: resolve,
64
+ });
65
+ });
66
+ return events;
67
+ }
68
+
69
+ describe("MCPAppsMiddleware integration", () => {
70
+ let llm: LLMock;
71
+ let mcpMock: MCPMock;
72
+
73
+ afterEach(async () => {
74
+ if (llm) {
75
+ await llm.stop().catch(() => {});
76
+ }
77
+ });
78
+
79
+ async function startMcpServer(): Promise<string> {
80
+ mcpMock = new MCPMock();
81
+ mcpMock.addTool({
82
+ name: "get_weather",
83
+ description: "Get the weather",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: { city: { type: "string" } },
87
+ },
88
+ });
89
+ mcpMock.onToolCall("get_weather", (args: unknown) => {
90
+ const parsed = args as { city?: string };
91
+ return `Weather in ${parsed.city || "unknown"}: sunny`;
92
+ });
93
+ mcpMock.addResource(
94
+ {
95
+ uri: "app://dashboard",
96
+ name: "Dashboard",
97
+ mimeType: "text/plain",
98
+ },
99
+ { text: "Dashboard content here" },
100
+ );
101
+
102
+ llm = new LLMock({ port: 0 });
103
+ llm.mount("/mcp", mcpMock);
104
+ await llm.start();
105
+ return `${llm.url}/mcp`;
106
+ }
107
+
108
+ it("can be created with mcpServers config pointing at MCPMock URL", async () => {
109
+ const mcpUrl = await startMcpServer();
110
+
111
+ const middleware = new MCPAppsMiddleware({
112
+ mcpServers: [{ type: "http", url: mcpUrl }],
113
+ });
114
+
115
+ expect(middleware).toBeInstanceOf(MCPAppsMiddleware);
116
+ });
117
+
118
+ it("proxies tools/call through to MCPMock and returns results", async () => {
119
+ const mcpUrl = await startMcpServer();
120
+
121
+ const serverConfig = { type: "http" as const, url: mcpUrl };
122
+ const serverHash = getServerHash(serverConfig);
123
+
124
+ const middleware = new MCPAppsMiddleware({
125
+ mcpServers: [serverConfig],
126
+ });
127
+
128
+ const input = createRunInput({
129
+ forwardedProps: {
130
+ __proxiedMCPRequest: {
131
+ serverHash,
132
+ method: "tools/call",
133
+ params: {
134
+ name: "get_weather",
135
+ arguments: { city: "NYC" },
136
+ },
137
+ },
138
+ },
139
+ });
140
+
141
+ const mockAgent = new MockNextAgent();
142
+ const events = await collectEvents(middleware.run(input, mockAgent));
143
+
144
+ // Should have RUN_STARTED and RUN_FINISHED
145
+ const types = events.map((e) => e.type);
146
+ expect(types).toContain(EventType.RUN_STARTED);
147
+ expect(types).toContain(EventType.RUN_FINISHED);
148
+
149
+ // RUN_FINISHED should contain the MCP tool result
150
+ const runFinished = events.find(
151
+ (e) => e.type === EventType.RUN_FINISHED,
152
+ ) as BaseEvent & { result?: unknown };
153
+ expect(runFinished).toBeDefined();
154
+ expect(runFinished.result).toBeDefined();
155
+
156
+ // The result should contain the tool's text content
157
+ const result = runFinished.result as { content?: unknown[] };
158
+ expect(result.content).toBeDefined();
159
+ expect(Array.isArray(result.content)).toBe(true);
160
+
161
+ const textContent = (
162
+ result.content as Array<{ type: string; text?: string }>
163
+ ).find((c) => c.type === "text");
164
+ expect(textContent).toBeDefined();
165
+ expect(textContent!.text).toContain("sunny");
166
+ });
167
+
168
+ it("non-proxied request delegates to next agent", async () => {
169
+ const mcpUrl = await startMcpServer();
170
+
171
+ const middleware = new MCPAppsMiddleware({
172
+ mcpServers: [{ type: "http", url: mcpUrl }],
173
+ });
174
+
175
+ // Input WITHOUT __proxiedMCPRequest — should delegate to MockNextAgent
176
+ const input = createRunInput();
177
+
178
+ const mockAgent = new MockNextAgent();
179
+
180
+ const events = await collectEvents(middleware.run(input, mockAgent));
181
+
182
+ // MockNextAgent's run should have been called (delegation happened)
183
+ // The middleware calls runNextWithState which internally calls next.run,
184
+ // but since processStream wraps it, we check the output events instead
185
+ const types = events.map((e) => e.type);
186
+ expect(types).toContain(EventType.RUN_STARTED);
187
+ expect(types).toContain(EventType.RUN_FINISHED);
188
+ });
189
+
190
+ it("wrong serverHash returns error in RUN_FINISHED result", async () => {
191
+ const mcpUrl = await startMcpServer();
192
+
193
+ const middleware = new MCPAppsMiddleware({
194
+ mcpServers: [{ type: "http", url: mcpUrl }],
195
+ });
196
+
197
+ const input = createRunInput({
198
+ forwardedProps: {
199
+ __proxiedMCPRequest: {
200
+ serverHash: "nonexistent-hash-value",
201
+ method: "tools/call",
202
+ params: {
203
+ name: "get_weather",
204
+ arguments: { city: "NYC" },
205
+ },
206
+ },
207
+ },
208
+ });
209
+
210
+ const mockAgent = new MockNextAgent();
211
+ const events = await collectEvents(middleware.run(input, mockAgent));
212
+
213
+ // Should still get RUN_STARTED and RUN_FINISHED
214
+ const types = events.map((e) => e.type);
215
+ expect(types).toContain(EventType.RUN_STARTED);
216
+ expect(types).toContain(EventType.RUN_FINISHED);
217
+
218
+ // RUN_FINISHED should contain an error about unknown server
219
+ const runFinished = events.find(
220
+ (e) => e.type === EventType.RUN_FINISHED,
221
+ ) as BaseEvent & { result?: unknown };
222
+ expect(runFinished).toBeDefined();
223
+ const result = runFinished.result as { error?: string };
224
+ expect(result.error).toBeDefined();
225
+ expect(result.error).toContain("nonexistent-hash-value");
226
+ });
227
+
228
+ it("proxies resources/read through to MCPMock and returns results", async () => {
229
+ const mcpUrl = await startMcpServer();
230
+
231
+ const serverConfig = { type: "http" as const, url: mcpUrl };
232
+ const serverHash = getServerHash(serverConfig);
233
+
234
+ const middleware = new MCPAppsMiddleware({
235
+ mcpServers: [serverConfig],
236
+ });
237
+
238
+ const input = createRunInput({
239
+ forwardedProps: {
240
+ __proxiedMCPRequest: {
241
+ serverHash,
242
+ method: "resources/read",
243
+ params: { uri: "app://dashboard" },
244
+ },
245
+ },
246
+ });
247
+
248
+ const mockAgent = new MockNextAgent();
249
+ const events = await collectEvents(middleware.run(input, mockAgent));
250
+
251
+ // Should have RUN_STARTED and RUN_FINISHED
252
+ const types = events.map((e) => e.type);
253
+ expect(types).toContain(EventType.RUN_STARTED);
254
+ expect(types).toContain(EventType.RUN_FINISHED);
255
+
256
+ // RUN_FINISHED should contain the resource content
257
+ const runFinished = events.find(
258
+ (e) => e.type === EventType.RUN_FINISHED,
259
+ ) as BaseEvent & { result?: unknown };
260
+ expect(runFinished).toBeDefined();
261
+ expect(runFinished.result).toBeDefined();
262
+
263
+ // The result should contain resource contents
264
+ const result = runFinished.result as { contents?: unknown[] };
265
+ expect(result.contents).toBeDefined();
266
+ expect(Array.isArray(result.contents)).toBe(true);
267
+
268
+ const resource = (
269
+ result.contents as Array<{ uri: string; text?: string }>
270
+ )[0];
271
+ expect(resource).toBeDefined();
272
+ expect(resource.uri).toBe("app://dashboard");
273
+ expect(resource.text).toContain("Dashboard content here");
274
+ });
275
+ });