Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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") {
Comment thread
data-douser marked this conversation as resolved.
// Use the test case directory as the tests path
params.tests = [beforeDir];
Expand Down
64 changes: 61 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,56 @@ 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 if (suggestion && (suggestion in data || suggestion in normalized)) {
} else {
unknownEntries.push({ key, value });
}
}
}
for (const { key } of unknownEntries) {
const hint = suggestPropertyName(key, knownKeys);
const message = 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 +193541,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
121 changes: 121 additions & 0 deletions server/src/lib/param-normalization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* 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; value: unknown }> = [];

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 if (suggestion && (suggestion in data || suggestion in normalized)) {
// Both forms provided β€” prefer the canonical (kebab-case) key,
// silently ignore the alias
} else {
unknownEntries.push({ key, value });
}
Comment thread
data-douser marked this conversation as resolved.
Comment thread
data-douser marked this conversation as resolved.
}
}

// Report truly unknown properties with "did you mean?" hints
for (const { key } of unknownEntries) {
const hint = suggestPropertyName(key, knownKeys);
const message = hint
? `unknown property '${key}' β€” did you mean '${hint}'?`
: `unknown property '${key}'`;
Comment thread
data-douser marked this conversation as resolved.
Outdated
ctx.addIssue({
code: z.ZodIssueCode.custom,
message,
path: [key],
});
}

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

return normalized;
});
}
Loading
Loading