Repository navigation
Expand file tree
/
Copy pathmcp_and_agents.e2e.test.ts
More file actions
386 lines (320 loc) · 13.7 KB
/
Copy pathmcp_and_agents.e2e.test.ts
File metadata and controls
386 lines (320 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import type {
CopilotSession,
CustomAgentConfig,
MCPStdioServerConfig,
MCPServerConfig,
} from "../../src/index.js";
import { approveAll, defineTool } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs");
const TEST_HARNESS_DIR = dirname(TEST_MCP_SERVER);
function createTestMcpServers(...serverNames: string[]): Record<string, MCPServerConfig> {
return Object.fromEntries(
serverNames.map((name) => [
name,
{
type: "local",
command: "node",
args: [TEST_MCP_SERVER],
workingDirectory: TEST_HARNESS_DIR,
tools: ["*"],
} as MCPStdioServerConfig,
])
);
}
async function waitForMcpServerStatus(
session: CopilotSession,
serverName: string,
expectedStatus = "connected"
): Promise<void> {
const deadline = Date.now() + 60_000;
let lastStatus = "<not listed>";
while (Date.now() < deadline) {
const result = await session.rpc.mcp.list();
const server = result.servers.find((s) => s.name === serverName);
if (server?.status === expectedStatus) {
return;
}
lastStatus = server?.status ?? "<not listed>";
await new Promise((resolve) => setTimeout(resolve, 200));
}
throw new Error(`${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}`);
}
describe("MCP Servers and Custom Agents", async () => {
const { copilotClient: client, openAiEndpoint } = await createSdkTestContext();
describe("MCP Servers", () => {
it("should accept MCP server configuration on session create", async () => {
const mcpServers = createTestMcpServers("test-server");
const session = await client.createSession({
onPermissionRequest: approveAll,
mcpServers,
});
expect(session.sessionId).toBeDefined();
await waitForMcpServerStatus(session, "test-server");
// Simple interaction to verify session works
const message = await session.sendAndWait({
prompt: "What is 2+2?",
});
expect(message?.data.content).toContain("4");
await session.disconnect();
});
it("should accept MCP server configuration without args", async () => {
const mcpServers: Record<string, MCPServerConfig> = {
"test-server": {
type: "local",
command: "git",
tools: ["*"],
} as MCPStdioServerConfig,
};
const session = await client.createSession({
onPermissionRequest: approveAll,
mcpServers,
});
expect(session.sessionId).toBeDefined();
await session.disconnect();
});
it("should accept MCP server configuration on session resume", async () => {
// Create a session first
const session1 = await client.createSession({ onPermissionRequest: approveAll });
const sessionId = session1.sessionId;
// Resume with MCP servers
const mcpServers = createTestMcpServers("test-server");
const session2 = await client.resumeSession(sessionId, {
onPermissionRequest: approveAll,
mcpServers,
});
expect(session2.sessionId).toBe(sessionId);
await waitForMcpServerStatus(session2, "test-server");
await session2.disconnect();
});
it("should handle multiple MCP servers", async () => {
const mcpServers = createTestMcpServers("server1", "server2");
const session = await client.createSession({
onPermissionRequest: approveAll,
mcpServers,
});
expect(session.sessionId).toBeDefined();
await waitForMcpServerStatus(session, "server1");
await waitForMcpServerStatus(session, "server2");
await session.disconnect();
});
it("should pass literal env values to MCP server subprocess", async () => {
const mcpServers: Record<string, MCPServerConfig> = {
"env-echo": {
type: "local",
command: "node",
args: [TEST_MCP_SERVER],
tools: ["*"],
env: { TEST_SECRET: "hunter2" },
workingDirectory: TEST_HARNESS_DIR,
} as MCPStdioServerConfig,
};
const session = await client.createSession({
mcpServers,
onPermissionRequest: approveAll,
});
expect(session.sessionId).toBeDefined();
await waitForMcpServerStatus(session, "env-echo");
const message = await session.sendAndWait({
prompt: "Use the env-echo/get_env tool to read the TEST_SECRET environment variable. Reply with just the value, nothing else.",
});
expect(message?.data.content).toContain("hunter2");
await session.disconnect();
});
});
describe("Custom Agents", () => {
it("should accept custom agent configuration on session create", async () => {
const customAgents: CustomAgentConfig[] = [
{
name: "test-agent",
displayName: "Test Agent",
description: "A test agent for SDK testing",
prompt: "You are a helpful test agent.",
infer: true,
},
];
const session = await client.createSession({
onPermissionRequest: approveAll,
customAgents,
});
expect(session.sessionId).toBeDefined();
// Simple interaction to verify session works
const message = await session.sendAndWait({
prompt: "What is 5+5?",
});
expect(message?.data.content).toContain("10");
await session.disconnect();
});
it("should accept custom agent configuration on session resume", async () => {
// Create a session first
const session1 = await client.createSession({ onPermissionRequest: approveAll });
const sessionId = session1.sessionId;
await session1.sendAndWait({ prompt: "What is 1+1?" });
// Resume with custom agents
const customAgents: CustomAgentConfig[] = [
{
name: "resume-agent",
displayName: "Resume Agent",
description: "An agent added on resume",
prompt: "You are a resume test agent.",
},
];
const session2 = await client.resumeSession(sessionId, {
onPermissionRequest: approveAll,
customAgents,
});
expect(session2.sessionId).toBe(sessionId);
const message = await session2.sendAndWait({
prompt: "What is 6+6?",
});
expect(message?.data.content).toContain("12");
await session2.disconnect();
});
it("should handle custom agent with tools configuration", async () => {
const customAgents: CustomAgentConfig[] = [
{
name: "tool-agent",
displayName: "Tool Agent",
description: "An agent with specific tools",
prompt: "You are an agent with specific tools.",
tools: ["bash", "edit"],
infer: true,
},
];
const session = await client.createSession({
onPermissionRequest: approveAll,
customAgents,
});
expect(session.sessionId).toBeDefined();
await session.disconnect();
});
it("should handle custom agent with MCP servers", async () => {
const customAgents: CustomAgentConfig[] = [
{
name: "mcp-agent",
displayName: "MCP Agent",
description: "An agent with its own MCP servers",
prompt: "You are an agent with MCP servers.",
mcpServers: {
...createTestMcpServers("agent-server"),
},
},
];
const session = await client.createSession({
onPermissionRequest: approveAll,
customAgents,
});
expect(session.sessionId).toBeDefined();
await session.disconnect();
});
it("should handle multiple custom agents", async () => {
const customAgents: CustomAgentConfig[] = [
{
name: "agent1",
displayName: "Agent One",
description: "First agent",
prompt: "You are agent one.",
},
{
name: "agent2",
displayName: "Agent Two",
description: "Second agent",
prompt: "You are agent two.",
infer: false,
},
];
const session = await client.createSession({
onPermissionRequest: approveAll,
customAgents,
});
expect(session.sessionId).toBeDefined();
await session.disconnect();
});
});
describe("Combined Configuration", () => {
it("should accept both MCP servers and custom agents", async () => {
const mcpServers = createTestMcpServers("shared-server");
const customAgents: CustomAgentConfig[] = [
{
name: "combined-agent",
displayName: "Combined Agent",
description: "An agent using shared MCP servers",
prompt: "You are a combined test agent.",
},
];
const session = await client.createSession({
onPermissionRequest: approveAll,
mcpServers,
customAgents,
});
expect(session.sessionId).toBeDefined();
await waitForMcpServerStatus(session, "shared-server");
await session.disconnect();
});
});
describe("Default Agent Tool Exclusion", () => {
it("should hide excluded tools from default agent", async () => {
const secretTool = defineTool("secret_tool", {
description: "A secret tool hidden from the default agent",
parameters: z.object({
input: z.string().describe("Input to process"),
}),
handler: ({ input }) => `SECRET:${input}`,
});
const session = await client.createSession({
onPermissionRequest: approveAll,
tools: [secretTool],
defaultAgent: {
excludedTools: ["secret_tool"],
},
});
// Ask about the tool — the default agent should not see it
const message = await session.sendAndWait({
prompt: "Do you have access to a tool called secret_tool? Answer yes or no.",
});
// Sanity-check the replayed response (not the actual exclusion assertion)
expect(message?.data.content?.toLowerCase()).toContain("no");
// The real assertion: verify the runtime excluded the tool from the CAPI request
const exchanges = await openAiEndpoint.getExchanges();
const toolNames = exchanges.flatMap((e) =>
(e.request.tools ?? []).map((t) => ("function" in t ? t.function.name : ""))
);
expect(toolNames).not.toContain("secret_tool");
await session.disconnect();
});
it("should accept defaultAgent configuration on session resume", async () => {
const session1 = await client.createSession({ onPermissionRequest: approveAll });
const sessionId = session1.sessionId;
await session1.sendAndWait({ prompt: "What is 3+3?" });
const secretTool = defineTool("secret_tool", {
description: "A secret tool hidden from the default agent",
parameters: z.object({
input: z.string().describe("Input to process"),
}),
handler: ({ input }) => `SECRET:${input}`,
});
const session2 = await client.resumeSession(sessionId, {
onPermissionRequest: approveAll,
tools: [secretTool],
defaultAgent: {
excludedTools: ["secret_tool"],
},
});
expect(session2.sessionId).toBe(sessionId);
const message = await session2.sendAndWait({
prompt: "What is 4+4?",
});
expect(message?.data.content).toContain("8");
await session2.disconnect();
});
});
});