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
4 changes: 2 additions & 2 deletions client/src/lib/integration-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -1084,8 +1084,8 @@ export class IntegrationTestRunner {
throw new Error(`Test directory ${beforeDir} not found for ${toolName}/${testCase}`);
}
} else if (toolName === "codeql_resolve_queries") {
// Use the test case directory as the queries path
params.path = beforeDir;
// Use the static examples directory which already contains installed QL packs
params.directory = path.join(staticPath, "src");
} else if (toolName === "codeql_resolve_tests") {
// Use the test case directory as the tests path
params.tests = [beforeDir];
Expand Down
63 changes: 60 additions & 3 deletions server/dist/codeql-development-mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -188583,6 +188583,55 @@ function getOrCreateLogDirectory(logDir) {
return uniqueLogDir;
}

// src/lib/param-normalization.ts
function camelToKebabCase(key) {
return key.replace(/[A-Z]/g, (ch) => "-" + ch.toLowerCase());
}
function kebabToCamelCase(key) {
return key.replace(/-([a-z])/g, (_match, ch) => ch.toUpperCase());
}
function suggestPropertyName(key, knownKeys) {
const kebab = camelToKebabCase(key);
if (kebab !== key && knownKeys.has(kebab)) return kebab;
const snakeToKebab = key.replace(/_/g, "-");
if (snakeToKebab !== key && knownKeys.has(snakeToKebab)) return snakeToKebab;
const camel = kebabToCamelCase(key);
if (camel !== key && knownKeys.has(camel)) return camel;
return void 0;
}
function buildEnhancedToolSchema(shape) {
const knownKeys = new Set(Object.keys(shape));
return external_exports.object(shape).passthrough().transform((data, ctx) => {
const normalized = {};
const unknownEntries = [];
for (const [key, value] of Object.entries(data)) {
if (knownKeys.has(key)) {
normalized[key] = value;
} else {
const suggestion = suggestPropertyName(key, knownKeys);
if (suggestion && !(suggestion in data) && !(suggestion in normalized)) {
normalized[suggestion] = value;
} else {
const isDuplicate = !!suggestion && (suggestion in data || suggestion in normalized);
unknownEntries.push({ key, hint: suggestion, isDuplicate });
}
}
}
for (const { key, hint, isDuplicate } of unknownEntries) {
const message = isDuplicate && hint ? `duplicate property: both '${key}' and its canonical form '${hint}' were provided; use only '${hint}'` : hint ? `unknown property '${key}' \u2014 did you mean '${hint}'?` : `unknown property '${key}'`;
ctx.addIssue({
code: external_exports.ZodIssueCode.custom,
message,
path: [key]
});
}
if (unknownEntries.length > 0) {
return external_exports.NEVER;
}
return normalized;
});
}

// src/lib/query-resolver.ts
init_package_paths();
import { basename as basename3 } from "path";
Expand Down Expand Up @@ -193491,10 +193540,18 @@ function registerCLITool(server, definition) {
inputSchema,
resultProcessor = defaultCLIResultProcessor
} = definition;
server.tool(
const enhancedSchema = buildEnhancedToolSchema(inputSchema);
server.registerTool(
name,
description,
inputSchema,
{
description,
// The enhanced schema is a pre-built ZodEffects (passthrough + transform).
// Using registerTool() instead of tool() because the latter's argument
// parsing rejects ZodEffects as "unrecognized objects". registerTool()
// passes inputSchema directly to getZodSchemaObject(), which correctly
// recognises any Zod schema instance.
inputSchema: enhancedSchema
},
async (params) => {
const tempDirsToCleanup = [];
try {
Expand Down
6 changes: 3 additions & 3 deletions server/dist/codeql-development-mcp-server.js.map

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions server/src/lib/cli-tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CLIExecutionResult, executeCodeQLCommand, executeQLTCommand } from './c
import { readDatabaseMetadata, resolveDatabasePath } from './database-resolver';
import { logger } from '../utils/logger';
import { getOrCreateLogDirectory } from './log-directory-manager';
import { buildEnhancedToolSchema } from './param-normalization';
import { resolveQueryPath } from './query-resolver';
import { cacheDatabaseAnalyzeResults, processQueryRunResults } from './result-processor';
import { getUserWorkspaceDir, packageRootDir } from '../utils/package-paths';
Expand Down Expand Up @@ -91,7 +92,14 @@ export const defaultCLIResultProcessor = (
};

/**
* Register a CLI tool with the MCP server
* Register a CLI tool with the MCP server.
*
* The raw `inputSchema` shape is wrapped by {@link buildEnhancedToolSchema}
* so that:
* - camelCase / snake_case variants of kebab-case keys are silently
* normalised (e.g. `sourceRoot` β†’ `source-root`);
* - truly unknown properties are rejected with a helpful error that names
* the unrecognized key and, where possible, suggests the correct name.
*/
export function registerCLITool(server: McpServer, definition: CLIToolDefinition): void {
const {
Expand All @@ -103,10 +111,21 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
resultProcessor = defaultCLIResultProcessor
} = definition;

server.tool(
// Build enhanced schema that normalises property-name variants and
// produces actionable error messages for truly unknown keys.
const enhancedSchema = buildEnhancedToolSchema(inputSchema);

server.registerTool(
name,
description,
inputSchema,
{
description,
// The enhanced schema is a pre-built ZodEffects (passthrough + transform).
// Using registerTool() instead of tool() because the latter's argument
// parsing rejects ZodEffects as "unrecognized objects". registerTool()
// passes inputSchema directly to getZodSchemaObject(), which correctly
// recognises any Zod schema instance.
inputSchema: enhancedSchema,
},
async (params: Record<string, unknown>) => {
// Track temporary directories for cleanup
const tempDirsToCleanup: string[] = [];
Expand Down
123 changes: 123 additions & 0 deletions server/src/lib/param-normalization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Parameter normalization utilities for CLI tool schemas.
*
* Provides camelCase β†’ kebab-case key normalization and
* "did you mean?" suggestions for unrecognized property names.
*/

import { z } from 'zod';

// ─── String-case conversion helpers ──────────────────────────────────────────

/**
* Convert a camelCase string to kebab-case.
* Example: "sourceRoot" β†’ "source-root"
*/
export function camelToKebabCase(key: string): string {
return key.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
}

/**
* Convert a kebab-case string to camelCase.
* Example: "source-root" β†’ "sourceRoot"
*/
export function kebabToCamelCase(key: string): string {
return key.replace(/-([a-z])/g, (_match, ch: string) => ch.toUpperCase());
}

// ─── Suggestion logic ────────────────────────────────────────────────────────

/**
* Given an unrecognized property name and the set of known schema keys,
* return the most likely intended key (or `undefined` if no close match).
*
* Resolution order:
* 1. camelCase β†’ kebab-case (e.g. "sourceRoot" β†’ "source-root")
* 2. snake_case β†’ kebab-case (e.g. "source_root" β†’ "source-root")
* 3. kebab-case β†’ camelCase (e.g. "source-root" β†’ "sourceRoot")
*/
export function suggestPropertyName(
key: string,
knownKeys: ReadonlySet<string>,
): string | undefined {
// 1. camelCase β†’ kebab-case
const kebab = camelToKebabCase(key);
if (kebab !== key && knownKeys.has(kebab)) return kebab;

// 2. snake_case β†’ kebab-case
const snakeToKebab = key.replace(/_/g, '-');
if (snakeToKebab !== key && knownKeys.has(snakeToKebab)) return snakeToKebab;

// 3. kebab-case β†’ camelCase
const camel = kebabToCamelCase(key);
if (camel !== key && knownKeys.has(camel)) return camel;

return undefined;
}

// ─── Schema builder ──────────────────────────────────────────────────────────

/**
* Build an enhanced Zod schema from a raw tool input shape.
*
* The returned schema:
* - Accepts additional (unknown) properties without client-side rejection
* (`passthrough` mode β†’ JSON Schema `additionalProperties: true`).
* - Normalizes camelCase / snake_case keys to their kebab-case equivalents
* when a matching schema key exists.
* - Rejects truly unknown properties with a helpful error that names the
* unrecognized key and, when possible, suggests the correct name.
*/
export function buildEnhancedToolSchema(
shape: Record<string, z.ZodTypeAny>,
): z.ZodTypeAny {
const knownKeys = new Set(Object.keys(shape));

return z
.object(shape)
.passthrough()
.transform((data, ctx) => {
const normalized: Record<string, unknown> = {};
const unknownEntries: Array<{ key: string; hint?: string; isDuplicate: boolean }> = [];

for (const [key, value] of Object.entries(data)) {
if (knownKeys.has(key)) {
// Known key β€” keep as-is
normalized[key] = value;
} else {
// Try to find a kebab-case equivalent
const suggestion = suggestPropertyName(key, knownKeys);
if (suggestion && !(suggestion in data) && !(suggestion in normalized)) {
// Silently normalize to the canonical kebab-case key
normalized[suggestion] = value;
} else {
// Either no suggestion (truly unknown) or the canonical key is
// already present. Capture the suggestion so the error message
// can include a helpful hint.
const isDuplicate = !!suggestion && (suggestion in data || suggestion in normalized);
unknownEntries.push({ key, hint: suggestion, isDuplicate });
}
}
}

// Report unknown / duplicate properties with actionable messages
for (const { key, hint, isDuplicate } of unknownEntries) {
const message = isDuplicate && hint
? `duplicate property: both '${key}' and its canonical form '${hint}' were provided; use only '${hint}'`
: hint
? `unknown property '${key}' β€” did you mean '${hint}'?`
: `unknown property '${key}'`;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message,
path: [key],
});
}

if (unknownEntries.length > 0) {
return z.NEVER;
}

return normalized;
});
}
Loading
Loading