@copilotkit/runtime 1.55.1 → 1.55.2-canary.test-01
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 +20 -0
- package/dist/agent/converters/aisdk.cjs +215 -0
- package/dist/agent/converters/aisdk.cjs.map +1 -0
- package/dist/agent/converters/aisdk.d.cts +18 -0
- package/dist/agent/converters/aisdk.d.cts.map +1 -0
- package/dist/agent/converters/aisdk.d.mts +18 -0
- package/dist/agent/converters/aisdk.d.mts.map +1 -0
- package/dist/agent/converters/aisdk.mjs +214 -0
- package/dist/agent/converters/aisdk.mjs.map +1 -0
- package/dist/agent/converters/index.d.mts +3 -0
- package/dist/agent/converters/tanstack.cjs +180 -0
- package/dist/agent/converters/tanstack.cjs.map +1 -0
- package/dist/agent/converters/tanstack.d.cts +68 -0
- package/dist/agent/converters/tanstack.d.cts.map +1 -0
- package/dist/agent/converters/tanstack.d.mts +68 -0
- package/dist/agent/converters/tanstack.d.mts.map +1 -0
- package/dist/agent/converters/tanstack.mjs +178 -0
- package/dist/agent/converters/tanstack.mjs.map +1 -0
- package/dist/agent/index.cjs +111 -17
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +61 -4
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +62 -4
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +111 -17
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.cjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.cts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.mts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.mjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +4 -3
- package/dist/package.mjs +4 -3
- package/dist/service-adapters/anthropic/utils.cjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +1 -1
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +1 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +5 -0
- package/dist/v2/index.d.cts +4 -2
- package/dist/v2/index.d.mts +4 -2
- package/dist/v2/index.mjs +3 -1
- package/dist/v2/runtime/core/fetch-handler.cjs +43 -3
- package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.mjs +43 -3
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +26 -0
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +26 -0
- package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
- package/dist/v2/runtime/core/hooks.cjs.map +1 -1
- package/dist/v2/runtime/core/hooks.d.cts +13 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +13 -0
- package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
- package/dist/v2/runtime/core/hooks.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/threads.cjs +179 -0
- package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -0
- package/dist/v2/runtime/handlers/intelligence/threads.mjs +173 -0
- package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -0
- package/package.json +5 -4
- package/src/agent/__tests__/agent-test-helpers.ts +446 -0
- package/src/agent/__tests__/agent.test.ts +593 -0
- package/src/agent/__tests__/converter-aisdk.test.ts +692 -0
- package/src/agent/__tests__/converter-custom.test.ts +319 -0
- package/src/agent/__tests__/converter-tanstack-input.test.ts +211 -0
- package/src/agent/__tests__/converter-tanstack.test.ts +314 -0
- package/src/agent/__tests__/mcp-servers-integration.test.ts +373 -0
- package/src/agent/__tests__/multimodal-tanstack.test.ts +284 -0
- package/src/agent/__tests__/test-helpers.ts +12 -8
- package/src/agent/converters/aisdk.ts +326 -0
- package/src/agent/converters/index.ts +7 -0
- package/src/agent/converters/tanstack.ts +286 -0
- package/src/agent/index.ts +245 -26
- package/src/lib/integrations/nextjs/pages-router.ts +1 -0
- package/src/lib/runtime/copilot-runtime.ts +21 -12
- package/src/lib/runtime/mcp-tools-utils.ts +1 -1
- package/src/service-adapters/anthropic/utils.ts +1 -1
- package/src/service-adapters/openai/utils.ts +1 -1
- package/src/v2/runtime/__tests__/fetch-router.test.ts +76 -0
- package/src/v2/runtime/__tests__/mcp-apps-middleware-integration.test.ts +275 -0
- package/src/v2/runtime/core/fetch-handler.ts +55 -4
- package/src/v2/runtime/core/fetch-router.ts +48 -0
- package/src/v2/runtime/core/hooks.ts +6 -1
- package/src/v2/runtime/handlers/handle-threads.ts +1 -0
- package/src/v2/runtime/handlers/intelligence/threads.ts +28 -0
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
type MCPTool,
|
|
65
65
|
extractParametersFromSchema,
|
|
66
66
|
} from "./mcp-tools-utils";
|
|
67
|
-
import { BuiltInAgent, type
|
|
67
|
+
import { BuiltInAgent, type BuiltInAgentClassicConfig } from "../../agent";
|
|
68
68
|
// Define the function type alias here or import if defined elsewhere
|
|
69
69
|
type CreateMCPClientFunction = (
|
|
70
70
|
config: MCPEndpointConfig,
|
|
@@ -328,7 +328,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
328
328
|
params?: CopilotRuntimeConstructorParams<T>;
|
|
329
329
|
private observability?: CopilotObservabilityConfig;
|
|
330
330
|
// Cache MCP tools per endpoint to avoid re-fetching repeatedly
|
|
331
|
-
private mcpToolsCache: Map<string,
|
|
331
|
+
private mcpToolsCache: Map<string, BuiltInAgentClassicConfig["tools"]> =
|
|
332
332
|
new Map();
|
|
333
333
|
private runtimeArgs: CopilotRuntimeOptions;
|
|
334
334
|
private _instance: CopilotRuntimeVNext;
|
|
@@ -449,7 +449,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
449
449
|
// Receive this.params.action and turn it into the AbstractAgent tools
|
|
450
450
|
private getToolsFromActions(
|
|
451
451
|
actions: ActionsConfiguration<any>,
|
|
452
|
-
):
|
|
452
|
+
): BuiltInAgentClassicConfig["tools"] {
|
|
453
453
|
// Resolve actions to an array (handle function case)
|
|
454
454
|
const actionsArray =
|
|
455
455
|
typeof actions === "function"
|
|
@@ -472,7 +472,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
472
472
|
|
|
473
473
|
private assignToolsToAgents(
|
|
474
474
|
agents: Record<string, AbstractAgent>,
|
|
475
|
-
tools:
|
|
475
|
+
tools: BuiltInAgentClassicConfig["tools"],
|
|
476
476
|
): Record<string, AbstractAgent> {
|
|
477
477
|
if (!tools?.length) {
|
|
478
478
|
return agents;
|
|
@@ -481,12 +481,21 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
481
481
|
const enrichedAgents: Record<string, AbstractAgent> = { ...agents };
|
|
482
482
|
|
|
483
483
|
for (const [agentId, agent] of Object.entries(enrichedAgents)) {
|
|
484
|
-
const existingConfig = (Reflect.get(agent, "config") ??
|
|
485
|
-
|
|
486
|
-
|
|
484
|
+
const existingConfig = (Reflect.get(agent, "config") ?? {}) as Record<
|
|
485
|
+
string,
|
|
486
|
+
unknown
|
|
487
|
+
>;
|
|
487
488
|
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
// Skip factory-mode agents — they don't have a tools property
|
|
490
|
+
if ("factory" in existingConfig) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const classicConfig = existingConfig as BuiltInAgentClassicConfig;
|
|
495
|
+
const existingTools = classicConfig.tools ?? [];
|
|
496
|
+
|
|
497
|
+
const updatedConfig: BuiltInAgentClassicConfig = {
|
|
498
|
+
...classicConfig,
|
|
490
499
|
tools: [...existingTools, ...tools],
|
|
491
500
|
};
|
|
492
501
|
|
|
@@ -657,7 +666,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
657
666
|
// Optionally accepts request-scoped properties to merge request-provided mcpServers
|
|
658
667
|
private async getToolsFromMCP(options?: {
|
|
659
668
|
properties?: Record<string, unknown>;
|
|
660
|
-
}): Promise<
|
|
669
|
+
}): Promise<BuiltInAgentClassicConfig["tools"]> {
|
|
661
670
|
const runtimeMcpServers = (this.params?.mcpServers ??
|
|
662
671
|
[]) as MCPEndpointConfig[];
|
|
663
672
|
const createMCPClient = this.params?.createMCPClient as
|
|
@@ -702,7 +711,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
702
711
|
return Array.from(byUrl.values());
|
|
703
712
|
})();
|
|
704
713
|
|
|
705
|
-
const allTools:
|
|
714
|
+
const allTools: BuiltInAgentClassicConfig["tools"] = [];
|
|
706
715
|
|
|
707
716
|
for (const config of effectiveEndpoints) {
|
|
708
717
|
const endpointUrl = config.endpoint;
|
|
@@ -717,7 +726,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
717
726
|
const client = await createMCPClient(config);
|
|
718
727
|
const toolsMap = await client.tools();
|
|
719
728
|
|
|
720
|
-
const toolDefs:
|
|
729
|
+
const toolDefs: BuiltInAgentClassicConfig["tools"] = Object.entries(
|
|
721
730
|
toolsMap,
|
|
722
731
|
).map(([toolName, tool]: [string, MCPTool]) => {
|
|
723
732
|
const params: Parameter[] = extractParametersFromSchema(tool);
|
|
@@ -32,7 +32,7 @@ export function limitMessagesToTokenCount(
|
|
|
32
32
|
|
|
33
33
|
let cutoff: boolean = false;
|
|
34
34
|
|
|
35
|
-
const reversedMessages = [...messages].
|
|
35
|
+
const reversedMessages = [...messages].toReversed();
|
|
36
36
|
for (const message of reversedMessages) {
|
|
37
37
|
if (message.role === "system") {
|
|
38
38
|
result.unshift(message);
|
|
@@ -40,7 +40,7 @@ export function limitMessagesToTokenCount(
|
|
|
40
40
|
|
|
41
41
|
let cutoff: boolean = false;
|
|
42
42
|
|
|
43
|
-
const reversedMessages = [...messages].
|
|
43
|
+
const reversedMessages = [...messages].toReversed();
|
|
44
44
|
for (const message of reversedMessages) {
|
|
45
45
|
if (["system", "developer"].includes(message.role)) {
|
|
46
46
|
result.unshift(message);
|
|
@@ -50,6 +50,57 @@ describe("fetch-router", () => {
|
|
|
50
50
|
expect(result).toBeNull();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("matches GET /threads", () => {
|
|
54
|
+
const result = matchRoute("/api/copilotkit/threads", basePath);
|
|
55
|
+
expect(result).toEqual({ method: "threads/list" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("matches POST /threads/subscribe", () => {
|
|
59
|
+
const result = matchRoute("/api/copilotkit/threads/subscribe", basePath);
|
|
60
|
+
expect(result).toEqual({ method: "threads/subscribe" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("matches PATCH /threads/:threadId", () => {
|
|
64
|
+
const result = matchRoute("/api/copilotkit/threads/thread-abc", basePath);
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
method: "threads/update",
|
|
67
|
+
threadId: "thread-abc",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("matches POST /threads/:threadId/archive", () => {
|
|
72
|
+
const result = matchRoute(
|
|
73
|
+
"/api/copilotkit/threads/thread-abc/archive",
|
|
74
|
+
basePath,
|
|
75
|
+
);
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
method: "threads/archive",
|
|
78
|
+
threadId: "thread-abc",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("matches GET /threads/:threadId/messages", () => {
|
|
83
|
+
const result = matchRoute(
|
|
84
|
+
"/api/copilotkit/threads/thread-abc/messages",
|
|
85
|
+
basePath,
|
|
86
|
+
);
|
|
87
|
+
expect(result).toEqual({
|
|
88
|
+
method: "threads/messages",
|
|
89
|
+
threadId: "thread-abc",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("handles URL-encoded threadId in thread routes", () => {
|
|
94
|
+
const result = matchRoute(
|
|
95
|
+
"/api/copilotkit/threads/thread%2F123",
|
|
96
|
+
basePath,
|
|
97
|
+
);
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
method: "threads/update",
|
|
100
|
+
threadId: "thread/123",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
53
104
|
it("returns null when basePath is a prefix but not a segment boundary", () => {
|
|
54
105
|
const result = matchRoute("/api/copilotkitextra/info", basePath);
|
|
55
106
|
expect(result).toBeNull();
|
|
@@ -124,6 +175,31 @@ describe("fetch-router", () => {
|
|
|
124
175
|
expect(result).toBeNull();
|
|
125
176
|
});
|
|
126
177
|
|
|
178
|
+
it("matches /threads suffix", () => {
|
|
179
|
+
const result = matchRoute("/anything/threads");
|
|
180
|
+
expect(result).toEqual({ method: "threads/list" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("matches /threads/subscribe suffix", () => {
|
|
184
|
+
const result = matchRoute("/anything/threads/subscribe");
|
|
185
|
+
expect(result).toEqual({ method: "threads/subscribe" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("matches /threads/:threadId suffix", () => {
|
|
189
|
+
const result = matchRoute("/anything/threads/t1");
|
|
190
|
+
expect(result).toEqual({ method: "threads/update", threadId: "t1" });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("matches /threads/:threadId/archive suffix", () => {
|
|
194
|
+
const result = matchRoute("/anything/threads/t1/archive");
|
|
195
|
+
expect(result).toEqual({ method: "threads/archive", threadId: "t1" });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("matches /threads/:threadId/messages suffix", () => {
|
|
199
|
+
const result = matchRoute("/anything/threads/t1/messages");
|
|
200
|
+
expect(result).toEqual({ method: "threads/messages", threadId: "t1" });
|
|
201
|
+
});
|
|
202
|
+
|
|
127
203
|
it("works with deeply nested mount prefix", () => {
|
|
128
204
|
const result = matchRoute("/api/v2/copilotkit/agent/a1/run");
|
|
129
205
|
expect(result).toEqual({ method: "agent/run", agentId: "a1" });
|
|
@@ -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
|
+
});
|
|
@@ -46,6 +46,14 @@ import { handleConnectAgent } from "../handlers/handle-connect";
|
|
|
46
46
|
import { handleStopAgent } from "../handlers/handle-stop";
|
|
47
47
|
import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
|
|
48
48
|
import { handleTranscribe } from "../handlers/handle-transcribe";
|
|
49
|
+
import {
|
|
50
|
+
handleListThreads,
|
|
51
|
+
handleSubscribeToThreads,
|
|
52
|
+
handleUpdateThread,
|
|
53
|
+
handleArchiveThread,
|
|
54
|
+
handleDeleteThread,
|
|
55
|
+
handleGetThreadMessages,
|
|
56
|
+
} from "../handlers/handle-threads";
|
|
49
57
|
import {
|
|
50
58
|
parseMethodCall,
|
|
51
59
|
createJsonRequest,
|
|
@@ -306,6 +314,31 @@ function dispatchRoute(
|
|
|
306
314
|
return handleGetRuntimeInfo({ runtime, request });
|
|
307
315
|
case "transcribe":
|
|
308
316
|
return handleTranscribe({ runtime, request });
|
|
317
|
+
case "threads/list":
|
|
318
|
+
return handleListThreads({ runtime, request });
|
|
319
|
+
case "threads/subscribe":
|
|
320
|
+
return handleSubscribeToThreads({ runtime, request });
|
|
321
|
+
case "threads/update":
|
|
322
|
+
if (request.method.toUpperCase() === "DELETE") {
|
|
323
|
+
return handleDeleteThread({
|
|
324
|
+
runtime,
|
|
325
|
+
request,
|
|
326
|
+
threadId: route.threadId,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return handleUpdateThread({ runtime, request, threadId: route.threadId });
|
|
330
|
+
case "threads/archive":
|
|
331
|
+
return handleArchiveThread({
|
|
332
|
+
runtime,
|
|
333
|
+
request,
|
|
334
|
+
threadId: route.threadId,
|
|
335
|
+
});
|
|
336
|
+
case "threads/messages":
|
|
337
|
+
return handleGetThreadMessages({
|
|
338
|
+
runtime,
|
|
339
|
+
request,
|
|
340
|
+
threadId: route.threadId,
|
|
341
|
+
});
|
|
309
342
|
}
|
|
310
343
|
}
|
|
311
344
|
|
|
@@ -376,10 +409,28 @@ function validateHttpMethod(
|
|
|
376
409
|
route: RouteInfo,
|
|
377
410
|
): Response | null {
|
|
378
411
|
const method = httpMethod.toUpperCase();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
|
|
413
|
+
switch (route.method) {
|
|
414
|
+
case "info":
|
|
415
|
+
case "threads/list":
|
|
416
|
+
case "threads/messages":
|
|
417
|
+
if (method === "GET") return null;
|
|
418
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
419
|
+
Allow: "GET",
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
case "threads/update":
|
|
423
|
+
if (method === "PATCH" || method === "DELETE") return null;
|
|
424
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
425
|
+
Allow: "PATCH, DELETE",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
if (method === "POST") return null;
|
|
430
|
+
return jsonResponse({ error: "Method not allowed" }, 405, {
|
|
431
|
+
Allow: "POST",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
383
434
|
}
|
|
384
435
|
|
|
385
436
|
/* ------------------------------------------------------------------------------------------------
|
|
@@ -108,5 +108,53 @@ function matchSegments(path: string): RouteInfo | null {
|
|
|
108
108
|
return { method: "agent/stop", agentId, threadId };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// /threads/subscribe (2 segments)
|
|
112
|
+
if (
|
|
113
|
+
len >= 2 &&
|
|
114
|
+
segments[len - 2] === "threads" &&
|
|
115
|
+
segments[len - 1] === "subscribe"
|
|
116
|
+
) {
|
|
117
|
+
return { method: "threads/subscribe" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// /threads/:threadId/messages (3 segments)
|
|
121
|
+
if (
|
|
122
|
+
len >= 3 &&
|
|
123
|
+
segments[len - 3] === "threads" &&
|
|
124
|
+
segments[len - 1] === "messages"
|
|
125
|
+
) {
|
|
126
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
127
|
+
if (!threadId) return null;
|
|
128
|
+
return { method: "threads/messages", threadId };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// /threads/:threadId/archive (3 segments)
|
|
132
|
+
if (
|
|
133
|
+
len >= 3 &&
|
|
134
|
+
segments[len - 3] === "threads" &&
|
|
135
|
+
segments[len - 1] === "archive"
|
|
136
|
+
) {
|
|
137
|
+
const threadId = safeDecodeURIComponent(segments[len - 2]!);
|
|
138
|
+
if (!threadId) return null;
|
|
139
|
+
return { method: "threads/archive", threadId };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// /threads/:threadId (2 segments) — update or delete
|
|
143
|
+
if (
|
|
144
|
+
len >= 2 &&
|
|
145
|
+
segments[len - 2] === "threads" &&
|
|
146
|
+
segments[len - 1] !== "subscribe"
|
|
147
|
+
) {
|
|
148
|
+
const threadId = safeDecodeURIComponent(segments[len - 1]!);
|
|
149
|
+
if (!threadId) return null;
|
|
150
|
+
// Disambiguated by HTTP method in the handler
|
|
151
|
+
return { method: "threads/update", threadId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// /threads (1 segment) — list
|
|
155
|
+
if (len >= 1 && segments[len - 1] === "threads") {
|
|
156
|
+
return { method: "threads/list" };
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
return null;
|
|
112
160
|
}
|
|
@@ -38,7 +38,12 @@ export type RouteInfo =
|
|
|
38
38
|
| { method: "agent/connect"; agentId: string }
|
|
39
39
|
| { method: "agent/stop"; agentId: string; threadId: string }
|
|
40
40
|
| { method: "info" }
|
|
41
|
-
| { method: "transcribe" }
|
|
41
|
+
| { method: "transcribe" }
|
|
42
|
+
| { method: "threads/list" }
|
|
43
|
+
| { method: "threads/subscribe" }
|
|
44
|
+
| { method: "threads/update"; threadId: string }
|
|
45
|
+
| { method: "threads/archive"; threadId: string }
|
|
46
|
+
| { method: "threads/messages"; threadId: string };
|
|
42
47
|
|
|
43
48
|
/* ------------------------------------------------------------------------------------------------
|
|
44
49
|
* Hook contexts
|
|
@@ -231,3 +231,31 @@ export async function handleDeleteThread({
|
|
|
231
231
|
return errorResponse("Failed to delete thread", 500);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
export async function handleGetThreadMessages({
|
|
236
|
+
runtime,
|
|
237
|
+
request,
|
|
238
|
+
threadId,
|
|
239
|
+
}: ThreadMutationParams): Promise<Response> {
|
|
240
|
+
const intelligenceRuntime = requireIntelligenceRuntime(runtime);
|
|
241
|
+
if (isHandlerResponse(intelligenceRuntime)) {
|
|
242
|
+
return intelligenceRuntime;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const user = await resolveIntelligenceUser({
|
|
247
|
+
runtime: intelligenceRuntime,
|
|
248
|
+
request,
|
|
249
|
+
});
|
|
250
|
+
if (isHandlerResponse(user)) return user;
|
|
251
|
+
|
|
252
|
+
const data = await intelligenceRuntime.intelligence.getThreadMessages({
|
|
253
|
+
threadId,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return Response.json(data);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.error({ err: error, threadId }, "Error getting thread messages");
|
|
259
|
+
return errorResponse("Failed to get thread messages", 500);
|
|
260
|
+
}
|
|
261
|
+
}
|