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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Every CodeQL language that supports Models-as-Data upstream (`cpp`, `csharp`, `g

### Fixed

- **`codeql_query_run` did not auto-cache results for `@kind problem` / `@kind path-problem` / `@kind graph` queries when `format` was not provided** — The query result post-processor only ran BQRS interpretation (and therefore only populated the query results cache) when the caller passed an explicit `format`. The tool description already documented that `format` defaults based on `@kind`, but the implementation returned early. The post-processor now reads the query's `@kind` metadata and defaults `format` to `sarif-latest` for `problem`/`path-problem` queries and `graphtext` for `graph` queries, so SARIF/graphtext output is generated and cached automatically. Explicitly-provided `format` values continue to take precedence. ([#268](https://github.com/advanced-security/codeql-development-mcp-server/pull/268))
- **`query_results_cache_retrieve` rejected by GitHub Copilot Chat (HTTP 400 invalid schema)** — The `lineRange` and `resultIndices` parameters were defined with `z.tuple([...])`, which the MCP SDK serialized to a bare-array JSON Schema value (e.g. `[{"type":"integer"}, {"type":"integer"}]`). GitHub Copilot Chat enforces strict JSON Schema validation and rejected the entire `ql-mcp` server with `"... is not of type 'object', 'boolean'"`. Both parameters now use `z.object({ start, end })` so they serialize to a valid `type: "object"` JSON Schema. Tool callers must now pass `{ "lineRange": { "start": 1, "end": 10 } }` instead of `{ "lineRange": [1, 10] }`. ([#263](https://github.com/advanced-security/codeql-development-mcp-server/pull/263))

## [v2.25.2] — 2026-04-15
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"sessions": [
{
"id": "integration_test_session",
"calls": [
{
"tool": "codeql_query_run",
"timestamp": "2026-05-08T22:00:00.000Z",
"status": "success"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"sessions": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"toolName": "codeql_query_run",
"arguments": {
"query": "server/ql/javascript/examples/src/ExampleQuery1/ExampleQuery1.ql",
"database": "server/ql/javascript/examples/test/ExampleQuery1/ExampleQuery1.testproj"
},
"assertions": {
"responseContains": [
"Query results interpreted successfully with format: sarif-latest",
"Results cached with key:"
]
}
}
47 changes: 33 additions & 14 deletions server/dist/codeql-development-mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -190807,12 +190807,12 @@ function getDefaultExtension(format) {
return ".txt";
}
}
async function interpretBQRSFile(bqrsPath, queryPath, format, outputPath, logger2) {
async function interpretBQRSFile(bqrsPath, queryPath, format, outputPath, logger2, metadata) {
try {
const metadata = await extractQueryMetadata(queryPath);
const queryMetadata = metadata ?? await extractQueryMetadata(queryPath);
const missingFields = [];
if (!metadata.id) missingFields.push("id");
if (!metadata.kind) missingFields.push("kind");
if (!queryMetadata.id) missingFields.push("id");
if (!queryMetadata.kind) missingFields.push("kind");
if (missingFields.length > 0) {
return {
error: `Query metadata is incomplete. Missing required field(s): ${missingFields.join(", ")}. Ensure the query file contains @id and @kind metadata.`,
Expand All @@ -190822,12 +190822,12 @@ async function interpretBQRSFile(bqrsPath, queryPath, format, outputPath, logger
success: false
};
}
const sanitizedId = (metadata.id || "").replace(/[^a-zA-Z0-9_/:-]/g, "");
const sanitizedKind = (metadata.kind || "").replace(/[^a-zA-Z0-9_-]/g, "");
const sanitizedId = (queryMetadata.id || "").replace(/[^a-zA-Z0-9_/:-]/g, "");
const sanitizedKind = (queryMetadata.kind || "").replace(/[^a-zA-Z0-9_-]/g, "");
const graphFormats = ["graphtext", "dgml", "dot"];
if (graphFormats.includes(format) && metadata.kind !== "graph") {
if (graphFormats.includes(format) && queryMetadata.kind !== "graph") {
return {
error: `Format '${format}' is only compatible with @kind graph queries, but this query has @kind ${metadata.kind}`,
error: `Format '${format}' is only compatible with @kind graph queries, but this query has @kind ${queryMetadata.kind}`,
exitCode: 1,
stderr: "",
stdout: "",
Expand Down Expand Up @@ -190865,9 +190865,6 @@ async function processQueryRunResults(result, params, logger2) {
queryLanguage,
queryName
} = params;
if (!format && !evaluationFunction) {
return result;
}
if (!output) {
return result;
}
Expand All @@ -190878,22 +190875,44 @@ async function processQueryRunResults(result, params, logger2) {
} else if (!queryPath && queryName && queryLanguage) {
queryPath = await resolveQueryPath(params, logger2);
}
let effectiveFormat = format;
let queryMetadata;
if (queryPath) {
try {
queryMetadata = await extractQueryMetadata(queryPath);
} catch (metaErr) {
logger2.error("Failed to extract query metadata:", metaErr);
}
}
if (!effectiveFormat && !evaluationFunction && queryMetadata) {
if (queryMetadata.kind === "problem" || queryMetadata.kind === "path-problem") {
effectiveFormat = "sarif-latest";
} else if (queryMetadata.kind === "graph") {
effectiveFormat = "graphtext";
}
if (effectiveFormat) {
logger2.info(`No format specified; defaulting to '${effectiveFormat}' for @kind ${queryMetadata.kind} query`);
}
}
if (!effectiveFormat && !evaluationFunction) {
return result;
}
if (!queryPath) {
logger2.error("Cannot determine query path for interpretation/evaluation");
return {
...result,
stdout: result.stdout + "\n\nWarning: Query interpretation skipped - could not determine query path"
};
}
if (format) {
const outputFormat = format;
if (effectiveFormat) {
const outputFormat = effectiveFormat;
let outputFilePath = interpretedOutput;
if (!outputFilePath) {
const ext = getDefaultExtension(outputFormat);
outputFilePath = bqrsPath.replace(".bqrs", ext);
}
logger2.info(`Interpreting query results from ${bqrsPath} with format: ${outputFormat}`);
const interpretResult = await interpretBQRSFile(bqrsPath, queryPath, outputFormat, outputFilePath, logger2);
const interpretResult = await interpretBQRSFile(bqrsPath, queryPath, outputFormat, outputFilePath, logger2, queryMetadata);
if (interpretResult.success) {
let enhancedOutput = result.stdout;
enhancedOutput += `
Expand Down
4 changes: 2 additions & 2 deletions server/dist/codeql-development-mcp-server.js.map

Large diffs are not rendered by default.

69 changes: 51 additions & 18 deletions server/src/lib/result-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { mkdirSync, readFileSync } from 'fs';
import { createHash } from 'crypto';
import { CLIExecutionResult, executeCodeQLCommand, getActualCodeqlVersion } from './cli-executor';
import { readDatabaseMetadata } from './database-resolver';
import { evaluateQueryResults, extractQueryMetadata, QueryEvaluationResult } from './query-results-evaluator';
import { evaluateQueryResults, extractQueryMetadata, QueryEvaluationResult, QueryMetadata } from './query-results-evaluator';
import { resolveQueryPath } from './query-resolver';
import { collectAllRules, decomposeSarifByRule, getRuleDisplayName } from './sarif-utils';
import { sessionDataManager } from './session-data-manager';
Expand Down Expand Up @@ -67,22 +67,27 @@ export function getDefaultExtension(format: string): string {

/**
* Interpret a BQRS file using `codeql bqrs interpret`.
*
* If `metadata` is provided, the caller-supplied query metadata is reused
* (avoiding a redundant file open/read in `extractQueryMetadata`); otherwise
* the metadata is read from `queryPath`.
*/
export async function interpretBQRSFile(
bqrsPath: string,
queryPath: string,
format: string,
outputPath: string,
logger: ProcessorLogger,
metadata?: QueryMetadata,
): Promise<CLIExecutionResult> {
try {
// Extract query metadata to get id and kind
const metadata = await extractQueryMetadata(queryPath);
// Extract query metadata to get id and kind, unless the caller already did.
const queryMetadata = metadata ?? await extractQueryMetadata(queryPath);

// Validate required metadata fields
const missingFields = [];
if (!metadata.id) missingFields.push('id');
if (!metadata.kind) missingFields.push('kind');
if (!queryMetadata.id) missingFields.push('id');
if (!queryMetadata.kind) missingFields.push('kind');

if (missingFields.length > 0) {
return {
Expand All @@ -95,14 +100,14 @@ export async function interpretBQRSFile(
}

// Sanitize metadata values to prevent command injection
const sanitizedId = (metadata.id || '').replace(/[^a-zA-Z0-9_/:-]/g, '');
const sanitizedKind = (metadata.kind || '').replace(/[^a-zA-Z0-9_-]/g, '');
const sanitizedId = (queryMetadata.id || '').replace(/[^a-zA-Z0-9_/:-]/g, '');
const sanitizedKind = (queryMetadata.kind || '').replace(/[^a-zA-Z0-9_-]/g, '');

// Validate format for query kind
const graphFormats = ['graphtext', 'dgml', 'dot'];
if (graphFormats.includes(format) && metadata.kind !== 'graph') {
if (graphFormats.includes(format) && queryMetadata.kind !== 'graph') {
return {
error: `Format '${format}' is only compatible with @kind graph queries, but this query has @kind ${metadata.kind}`,
error: `Format '${format}' is only compatible with @kind graph queries, but this query has @kind ${queryMetadata.kind}`,
exitCode: 1,
stderr: '',
stdout: '',
Expand Down Expand Up @@ -161,11 +166,6 @@ export async function processQueryRunResults(
queryName,
} = params;

// If no format or evaluationFunction specified, return as-is
if (!format && !evaluationFunction) {
return result;
}

// Ensure output (bqrs file) was generated
if (!output) {
return result;
Expand All @@ -183,6 +183,38 @@ export async function processQueryRunResults(
queryPath = await resolveQueryPath(params, logger);
}

// Resolve effective format: when caller did not explicitly request a format
// and is not using the legacy evaluationFunction, infer one from the query's
// @kind metadata so that results from problem/path-problem/graph queries are
// automatically interpreted and cached.
//
// The metadata is also reused below by `interpretBQRSFile` to avoid a second
// file read for the same query.
let effectiveFormat: string | undefined = format as string | undefined;
let queryMetadata: QueryMetadata | undefined;
if (queryPath) {
try {
queryMetadata = await extractQueryMetadata(queryPath);
} catch (metaErr) {
logger.error('Failed to extract query metadata:', metaErr);
}
}
Comment thread
data-douser marked this conversation as resolved.
if (!effectiveFormat && !evaluationFunction && queryMetadata) {
if (queryMetadata.kind === 'problem' || queryMetadata.kind === 'path-problem') {
effectiveFormat = 'sarif-latest';
} else if (queryMetadata.kind === 'graph') {
effectiveFormat = 'graphtext';
}
if (effectiveFormat) {
logger.info(`No format specified; defaulting to '${effectiveFormat}' for @kind ${queryMetadata.kind} query`);
}
Comment thread
data-douser marked this conversation as resolved.
}

// If no format (explicit or inferred) and no evaluationFunction, return as-is
if (!effectiveFormat && !evaluationFunction) {
return result;
}

if (!queryPath) {
logger.error('Cannot determine query path for interpretation/evaluation');
return {
Expand All @@ -192,8 +224,8 @@ export async function processQueryRunResults(
}

// Handle new format parameter (preferred approach)
if (format) {
const outputFormat = format as string;
if (effectiveFormat) {
const outputFormat = effectiveFormat;

// Determine output path
let outputFilePath = interpretedOutput as string | undefined;
Expand All @@ -204,8 +236,9 @@ export async function processQueryRunResults(

logger.info(`Interpreting query results from ${bqrsPath} with format: ${outputFormat}`);

// Interpret the BQRS file
const interpretResult = await interpretBQRSFile(bqrsPath, queryPath, outputFormat, outputFilePath, logger);
// Interpret the BQRS file (reusing already-extracted metadata to avoid
// a second file read of the query source).
const interpretResult = await interpretBQRSFile(bqrsPath, queryPath, outputFormat, outputFilePath, logger, queryMetadata);

if (interpretResult.success) {
let enhancedOutput = result.stdout;
Expand Down
Loading
Loading