Skip to content

Commit 87dcf14

Browse files
Copilotdata-douser
andauthored
Add E2E integration tests for tool validation via InMemoryTransport
Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/306c50e7-31e0-4a39-bba5-9cacb4dd1674 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent ee7f0c0 commit 87dcf14

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

server/test/src/lib/tool-validation.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,142 @@ describe('patchValidateToolInput', () => {
247247
expect(result).toEqual({ foo: 'bar' });
248248
});
249249
});
250+
251+
// ─── E2E with InMemoryTransport ──────────────────────────────────────────────
252+
253+
describe('patchValidateToolInput (E2E with InMemoryTransport)', () => {
254+
it('should report all missing fields in one response via MCP protocol', async () => {
255+
const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js');
256+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
257+
258+
const server = new McpServer({ name: 'e2e-test', version: '1.0.0' });
259+
patchValidateToolInput(server);
260+
261+
server.tool(
262+
'audit_store_findings',
263+
'Store findings',
264+
{
265+
owner: z.string().describe('Owner.'),
266+
repo: z.string().describe('Repo.'),
267+
sourceLocation: z.string().describe('Path.'),
268+
},
269+
async ({ owner, repo, sourceLocation }) => ({
270+
content: [{ type: 'text' as const, text: `${owner}/${repo}:${sourceLocation}` }],
271+
}),
272+
);
273+
274+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
275+
const client = new Client({ name: 'test-client', version: '1.0.0' });
276+
277+
await server.connect(serverTransport);
278+
await client.connect(clientTransport);
279+
280+
try {
281+
// Call with no arguments — should report all three missing fields at once
282+
const result = await client.callTool({
283+
name: 'audit_store_findings',
284+
arguments: {},
285+
});
286+
287+
// The SDK converts McpError into a tool error result
288+
expect(result.isError).toBe(true);
289+
const text = (result.content as Array<{ type: string; text: string }>)
290+
.map((c) => c.text)
291+
.join('');
292+
expect(text).toContain("'owner'");
293+
expect(text).toContain("'repo'");
294+
expect(text).toContain("'sourceLocation'");
295+
expect(text).toContain('must have required properties:');
296+
} finally {
297+
await client.close();
298+
await server.close();
299+
}
300+
});
301+
302+
it('should report single missing field (singular) via MCP protocol', async () => {
303+
const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js');
304+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
305+
306+
const server = new McpServer({ name: 'e2e-test', version: '1.0.0' });
307+
patchValidateToolInput(server);
308+
309+
server.tool(
310+
'annotation_create',
311+
'Create annotation',
312+
{
313+
category: z.string().describe('Cat.'),
314+
entityKey: z.string().describe('Key.'),
315+
},
316+
async ({ category, entityKey }) => ({
317+
content: [{ type: 'text' as const, text: `${category}:${entityKey}` }],
318+
}),
319+
);
320+
321+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
322+
const client = new Client({ name: 'test-client', version: '1.0.0' });
323+
324+
await server.connect(serverTransport);
325+
await client.connect(clientTransport);
326+
327+
try {
328+
// Supply only one of two required fields
329+
const result = await client.callTool({
330+
name: 'annotation_create',
331+
arguments: { category: 'note' },
332+
});
333+
334+
expect(result.isError).toBe(true);
335+
const text = (result.content as Array<{ type: string; text: string }>)
336+
.map((c) => c.text)
337+
.join('');
338+
expect(text).toContain("must have required property 'entityKey'");
339+
// Singular — should NOT contain "properties:"
340+
expect(text).not.toContain('properties:');
341+
} finally {
342+
await client.close();
343+
await server.close();
344+
}
345+
});
346+
347+
it('should succeed when all required fields are provided', async () => {
348+
const { InMemoryTransport } = await import('@modelcontextprotocol/sdk/inMemory.js');
349+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
350+
351+
const server = new McpServer({ name: 'e2e-test', version: '1.0.0' });
352+
patchValidateToolInput(server);
353+
354+
server.tool(
355+
'audit_store_findings',
356+
'Store findings',
357+
{
358+
owner: z.string().describe('Owner.'),
359+
repo: z.string().describe('Repo.'),
360+
},
361+
async ({ owner, repo }) => ({
362+
content: [{ type: 'text' as const, text: `${owner}/${repo}` }],
363+
}),
364+
);
365+
366+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
367+
const client = new Client({ name: 'test-client', version: '1.0.0' });
368+
369+
await server.connect(serverTransport);
370+
await client.connect(clientTransport);
371+
372+
try {
373+
const result = await client.callTool({
374+
name: 'audit_store_findings',
375+
arguments: { owner: 'octocat', repo: 'hello' },
376+
});
377+
378+
expect(result.isError).toBeFalsy();
379+
const text = (result.content as Array<{ type: string; text: string }>)
380+
.map((c) => c.text)
381+
.join('');
382+
expect(text).toBe('octocat/hello');
383+
} finally {
384+
await client.close();
385+
await server.close();
386+
}
387+
});
388+
});

0 commit comments

Comments
 (0)