Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions server/dist/codeql-development-mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -200644,6 +200644,66 @@ function registerSarifDiffRunsTool(server) {
);
}

// src/lib/tool-validation.ts
function formatAllValidationErrors(error2) {
const { issues } = error2;
if (issues.length === 0) return "Unknown validation error";
const missingRequired = [];
const otherErrors = [];
for (const issue2 of issues) {
const path3 = issue2.path.join(".");
if (issue2.code === "invalid_type" && issue2.received === "undefined" && path3) {
missingRequired.push(`'${path3}'`);
} else {
otherErrors.push(path3 ? `${path3}: ${issue2.message}` : issue2.message);
}
}
const parts = [];
if (missingRequired.length === 1) {
parts.push(`must have required property ${missingRequired[0]}`);
} else if (missingRequired.length > 1) {
parts.push(`must have required properties: ${missingRequired.join(", ")}`);
}
parts.push(...otherErrors);
return parts.join("; ");
}
function resolveZodSchema(inputSchema) {
if (!inputSchema || typeof inputSchema !== "object") return void 0;
const schema2 = inputSchema;
if ("_def" in schema2 || "_zod" in schema2) {
return inputSchema;
}
const values = Object.values(schema2);
if (values.length > 0 && values.every(
(v) => typeof v === "object" && v !== null && ("_def" in v || "_zod" in v || typeof v.parse === "function")
)) {
return external_exports.object(schema2);
}
return void 0;
}
function patchValidateToolInput(server) {
const instance = server;
const originalValidateToolInput = instance.validateToolInput.bind(instance);
instance.validateToolInput = async function(tool, args, toolName) {
if (!tool.inputSchema) {
return void 0;
}
const schema2 = resolveZodSchema(tool.inputSchema);
if (!schema2) {
return originalValidateToolInput(tool, args, toolName);
}
const parseResult = await schema2.safeParseAsync(args);
if (!parseResult.success) {
const errorMessage = formatAllValidationErrors(parseResult.error);
throw new McpError(
ErrorCode.InvalidParams,
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`
);
}
return parseResult.data;
};
}

// src/codeql-development-mcp-server.ts
init_cli_executor();
init_server_manager();
Expand All @@ -200662,6 +200722,7 @@ async function startServer(mode = "stdio") {
name: PACKAGE_NAME,
version: VERSION
});
patchValidateToolInput(server);
registerCodeQLTools(server);
registerLSPTools(server);
registerCodeQLResources(server);
Expand Down
8 changes: 4 additions & 4 deletions server/dist/codeql-development-mcp-server.js.map

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions server/src/codeql-development-mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { registerAuditTools } from './tools/audit-tools';
import { registerCacheTools } from './tools/cache-tools';
import { registerSarifTools } from './tools/sarif-tools';
import { sessionDataManager } from './lib/session-data-manager';
import { patchValidateToolInput } from './lib/tool-validation';
import { resolveCodeQLBinary, validateCodeQLBinaryReachable } from './lib/cli-executor';
import { initServerManager, shutdownServerManager } from './lib/server-manager';
import { packageRootDir } from './utils/package-paths';
Expand Down Expand Up @@ -60,6 +61,10 @@ export async function startServer(mode: 'stdio' | 'http' = 'stdio'): Promise<Mcp
version: VERSION,
});

// Override the SDK's default one-at-a-time error reporting so that all
// validation violations are surfaced in a single response.
patchValidateToolInput(server);

// Register CodeQL tools (legacy high-level helpers)
registerCodeQLTools(server);

Expand Down
138 changes: 138 additions & 0 deletions server/src/lib/tool-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Tool input validation enhancement for the MCP server.
*
* The upstream MCP SDK (`getParseErrorMessage` in `zod-compat.js`) extracts
* only the *first* Zod issue when a tool call fails validation. This module
* overrides `McpServer.validateToolInput` so that **all** issues are surfaced
* in a single, human-readable error message.
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

// ─── Error formatting ────────────────────────────────────────────────────────

/**
* Format all Zod validation issues into a single, human-readable message.
*
* - Groups missing-required-field errors into one line
* (`must have required properties: 'a', 'b'`).
* - Appends any other validation errors individually.
*/
export function formatAllValidationErrors(error: z.ZodError): string {
const { issues } = error;

if (issues.length === 0) return 'Unknown validation error';

// Partition into "required-field missing" vs "everything else"
const missingRequired: string[] = [];
const otherErrors: string[] = [];

for (const issue of issues) {
const path = issue.path.join('.');

if (issue.code === 'invalid_type' && issue.received === 'undefined' && path) {
missingRequired.push(`'${path}'`);
} else {
otherErrors.push(path ? `${path}: ${issue.message}` : issue.message);
}
}

const parts: string[] = [];

if (missingRequired.length === 1) {
parts.push(`must have required property ${missingRequired[0]}`);
} else if (missingRequired.length > 1) {
parts.push(`must have required properties: ${missingRequired.join(', ')}`);
}

parts.push(...otherErrors);

return parts.join('; ');
}

// ─── Schema resolution ───────────────────────────────────────────────────────

/**
* Resolve the tool's `inputSchema` into a parsable Zod schema.
*
* Handles both:
* - Raw Zod shapes (`{ owner: z.string(), ... }`) β€” wraps with `z.object()`
* - Pre-built Zod schemas (ZodObject, ZodEffects, etc.) β€” returns as-is
*/
function resolveZodSchema(inputSchema: unknown): z.ZodTypeAny | undefined {
if (!inputSchema || typeof inputSchema !== 'object') return undefined;

const schema = inputSchema as Record<string, unknown>;

// Already a Zod schema instance (has _def for Zod v3 or _zod for v4)
if ('_def' in schema || '_zod' in schema) {
return inputSchema as z.ZodTypeAny;
}

// Check for raw Zod shape (all values are Zod schemas)
const values = Object.values(schema);
if (
values.length > 0 &&
values.every(
(v) =>
typeof v === 'object' &&
v !== null &&
('_def' in (v as Record<string, unknown>) ||
'_zod' in (v as Record<string, unknown>) ||
typeof (v as Record<string, unknown>).parse === 'function'),
)
) {
return z.object(schema as z.ZodRawShape);
}

return undefined;
}

// ─── Instance patch ──────────────────────────────────────────────────────────

/**
* Patch `validateToolInput` on the given McpServer **instance** so that
* **all** validation errors are reported in a single response instead of
* only the first one.
*
* Call this once after constructing the McpServer and before connecting
* any transport.
*/
export function patchValidateToolInput(server: McpServer): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = server as any;

// Capture the original so we can delegate for unrecognized schema types
const originalValidateToolInput = instance.validateToolInput.bind(instance);

instance.validateToolInput = async function (
tool: { inputSchema?: unknown },
args: unknown,
toolName: string,
): Promise<unknown> {
if (!tool.inputSchema) {
return undefined;
}

const schema = resolveZodSchema(tool.inputSchema);
if (!schema) {
// Unrecognized schema type β€” delegate to the original SDK validation
// so mis-registered tools don't accidentally bypass input validation.
return originalValidateToolInput(tool, args, toolName);
}

const parseResult = await schema.safeParseAsync(args);

if (!parseResult.success) {
const errorMessage = formatAllValidationErrors(parseResult.error);
throw new McpError(
ErrorCode.InvalidParams,
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`,
);
}

return parseResult.data;
};
}
Loading
Loading