Skip to content

Commit 1ffd305

Browse files
authored
[UPDATE PRIMITIVE] Report all validation errors at once instead of one-at-a-time (#227)
1 parent 5f88dc2 commit 1ffd305

File tree

5 files changed

+595
-4
lines changed

5 files changed

+595
-4
lines changed

server/dist/codeql-development-mcp-server.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200644,6 +200644,66 @@ function registerSarifDiffRunsTool(server) {
200644200644
);
200645200645
}
200646200646

200647+
// src/lib/tool-validation.ts
200648+
function formatAllValidationErrors(error2) {
200649+
const { issues } = error2;
200650+
if (issues.length === 0) return "Unknown validation error";
200651+
const missingRequired = [];
200652+
const otherErrors = [];
200653+
for (const issue2 of issues) {
200654+
const path3 = issue2.path.join(".");
200655+
if (issue2.code === "invalid_type" && issue2.received === "undefined" && path3) {
200656+
missingRequired.push(`'${path3}'`);
200657+
} else {
200658+
otherErrors.push(path3 ? `${path3}: ${issue2.message}` : issue2.message);
200659+
}
200660+
}
200661+
const parts = [];
200662+
if (missingRequired.length === 1) {
200663+
parts.push(`must have required property ${missingRequired[0]}`);
200664+
} else if (missingRequired.length > 1) {
200665+
parts.push(`must have required properties: ${missingRequired.join(", ")}`);
200666+
}
200667+
parts.push(...otherErrors);
200668+
return parts.join("; ");
200669+
}
200670+
function resolveZodSchema(inputSchema) {
200671+
if (!inputSchema || typeof inputSchema !== "object") return void 0;
200672+
const schema2 = inputSchema;
200673+
if ("_def" in schema2 || "_zod" in schema2) {
200674+
return inputSchema;
200675+
}
200676+
const values = Object.values(schema2);
200677+
if (values.length > 0 && values.every(
200678+
(v) => typeof v === "object" && v !== null && ("_def" in v || "_zod" in v || typeof v.parse === "function")
200679+
)) {
200680+
return external_exports.object(schema2);
200681+
}
200682+
return void 0;
200683+
}
200684+
function patchValidateToolInput(server) {
200685+
const instance = server;
200686+
const originalValidateToolInput = instance.validateToolInput.bind(instance);
200687+
instance.validateToolInput = async function(tool, args, toolName) {
200688+
if (!tool.inputSchema) {
200689+
return void 0;
200690+
}
200691+
const schema2 = resolveZodSchema(tool.inputSchema);
200692+
if (!schema2) {
200693+
return originalValidateToolInput(tool, args, toolName);
200694+
}
200695+
const parseResult = await schema2.safeParseAsync(args);
200696+
if (!parseResult.success) {
200697+
const errorMessage = formatAllValidationErrors(parseResult.error);
200698+
throw new McpError(
200699+
ErrorCode.InvalidParams,
200700+
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`
200701+
);
200702+
}
200703+
return parseResult.data;
200704+
};
200705+
}
200706+
200647200707
// src/codeql-development-mcp-server.ts
200648200708
init_cli_executor();
200649200709
init_server_manager();
@@ -200662,6 +200722,7 @@ async function startServer(mode = "stdio") {
200662200722
name: PACKAGE_NAME,
200663200723
version: VERSION
200664200724
});
200725+
patchValidateToolInput(server);
200665200726
registerCodeQLTools(server);
200666200727
registerLSPTools(server);
200667200728
registerCodeQLResources(server);

server/dist/codeql-development-mcp-server.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/codeql-development-mcp-server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { registerAuditTools } from './tools/audit-tools';
2222
import { registerCacheTools } from './tools/cache-tools';
2323
import { registerSarifTools } from './tools/sarif-tools';
2424
import { sessionDataManager } from './lib/session-data-manager';
25+
import { patchValidateToolInput } from './lib/tool-validation';
2526
import { resolveCodeQLBinary, validateCodeQLBinaryReachable } from './lib/cli-executor';
2627
import { initServerManager, shutdownServerManager } from './lib/server-manager';
2728
import { packageRootDir } from './utils/package-paths';
@@ -60,6 +61,10 @@ export async function startServer(mode: 'stdio' | 'http' = 'stdio'): Promise<Mcp
6061
version: VERSION,
6162
});
6263

64+
// Override the SDK's default one-at-a-time error reporting so that all
65+
// validation violations are surfaced in a single response.
66+
patchValidateToolInput(server);
67+
6368
// Register CodeQL tools (legacy high-level helpers)
6469
registerCodeQLTools(server);
6570

server/src/lib/tool-validation.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Tool input validation enhancement for the MCP server.
3+
*
4+
* The upstream MCP SDK (`getParseErrorMessage` in `zod-compat.js`) extracts
5+
* only the *first* Zod issue when a tool call fails validation. This module
6+
* overrides `McpServer.validateToolInput` so that **all** issues are surfaced
7+
* in a single, human-readable error message.
8+
*/
9+
10+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
12+
import { z } from 'zod';
13+
14+
// ─── Error formatting ────────────────────────────────────────────────────────
15+
16+
/**
17+
* Format all Zod validation issues into a single, human-readable message.
18+
*
19+
* - Groups missing-required-field errors into one line
20+
* (`must have required properties: 'a', 'b'`).
21+
* - Appends any other validation errors individually.
22+
*/
23+
export function formatAllValidationErrors(error: z.ZodError): string {
24+
const { issues } = error;
25+
26+
if (issues.length === 0) return 'Unknown validation error';
27+
28+
// Partition into "required-field missing" vs "everything else"
29+
const missingRequired: string[] = [];
30+
const otherErrors: string[] = [];
31+
32+
for (const issue of issues) {
33+
const path = issue.path.join('.');
34+
35+
if (issue.code === 'invalid_type' && issue.received === 'undefined' && path) {
36+
missingRequired.push(`'${path}'`);
37+
} else {
38+
otherErrors.push(path ? `${path}: ${issue.message}` : issue.message);
39+
}
40+
}
41+
42+
const parts: string[] = [];
43+
44+
if (missingRequired.length === 1) {
45+
parts.push(`must have required property ${missingRequired[0]}`);
46+
} else if (missingRequired.length > 1) {
47+
parts.push(`must have required properties: ${missingRequired.join(', ')}`);
48+
}
49+
50+
parts.push(...otherErrors);
51+
52+
return parts.join('; ');
53+
}
54+
55+
// ─── Schema resolution ───────────────────────────────────────────────────────
56+
57+
/**
58+
* Resolve the tool's `inputSchema` into a parsable Zod schema.
59+
*
60+
* Handles both:
61+
* - Raw Zod shapes (`{ owner: z.string(), ... }`) — wraps with `z.object()`
62+
* - Pre-built Zod schemas (ZodObject, ZodEffects, etc.) — returns as-is
63+
*/
64+
function resolveZodSchema(inputSchema: unknown): z.ZodTypeAny | undefined {
65+
if (!inputSchema || typeof inputSchema !== 'object') return undefined;
66+
67+
const schema = inputSchema as Record<string, unknown>;
68+
69+
// Already a Zod schema instance (has _def for Zod v3 or _zod for v4)
70+
if ('_def' in schema || '_zod' in schema) {
71+
return inputSchema as z.ZodTypeAny;
72+
}
73+
74+
// Check for raw Zod shape (all values are Zod schemas)
75+
const values = Object.values(schema);
76+
if (
77+
values.length > 0 &&
78+
values.every(
79+
(v) =>
80+
typeof v === 'object' &&
81+
v !== null &&
82+
('_def' in (v as Record<string, unknown>) ||
83+
'_zod' in (v as Record<string, unknown>) ||
84+
typeof (v as Record<string, unknown>).parse === 'function'),
85+
)
86+
) {
87+
return z.object(schema as z.ZodRawShape);
88+
}
89+
90+
return undefined;
91+
}
92+
93+
// ─── Instance patch ──────────────────────────────────────────────────────────
94+
95+
/**
96+
* Patch `validateToolInput` on the given McpServer **instance** so that
97+
* **all** validation errors are reported in a single response instead of
98+
* only the first one.
99+
*
100+
* Call this once after constructing the McpServer and before connecting
101+
* any transport.
102+
*/
103+
export function patchValidateToolInput(server: McpServer): void {
104+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105+
const instance = server as any;
106+
107+
// Capture the original so we can delegate for unrecognized schema types
108+
const originalValidateToolInput = instance.validateToolInput.bind(instance);
109+
110+
instance.validateToolInput = async function (
111+
tool: { inputSchema?: unknown },
112+
args: unknown,
113+
toolName: string,
114+
): Promise<unknown> {
115+
if (!tool.inputSchema) {
116+
return undefined;
117+
}
118+
119+
const schema = resolveZodSchema(tool.inputSchema);
120+
if (!schema) {
121+
// Unrecognized schema type — delegate to the original SDK validation
122+
// so mis-registered tools don't accidentally bypass input validation.
123+
return originalValidateToolInput(tool, args, toolName);
124+
}
125+
126+
const parseResult = await schema.safeParseAsync(args);
127+
128+
if (!parseResult.success) {
129+
const errorMessage = formatAllValidationErrors(parseResult.error);
130+
throw new McpError(
131+
ErrorCode.InvalidParams,
132+
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`,
133+
);
134+
}
135+
136+
return parseResult.data;
137+
};
138+
}

0 commit comments

Comments
 (0)