Skip to content

Commit 7a590b4

Browse files
Copilotdata-douser
andauthored
Fix bqrs_interpret: auto-resolve --source-location-prefix from database metadata
When the `database` parameter is provided to `codeql_bqrs_interpret`, read `codeql-database.yml` and extract `sourceLocationPrefix` to auto-set `--source-location-prefix`. This fixes the "Missing required argument(s): --source-location-prefix" error reported in v2.25.1-next.2 evaluation. TDD: Added 2 handler behavior tests in cli-tool-registry.test.ts and 9 definition tests in bqrs-interpret.test.ts. Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/6ff74bab-c637-4e18-a5dc-92e3065583f4 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent ea5a11c commit 7a590b4

6 files changed

Lines changed: 197 additions & 20 deletions

File tree

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -184635,7 +184635,7 @@ function cacheDatabaseAnalyzeResults(params, logger2) {
184635184635

184636184636
// src/lib/cli-tool-registry.ts
184637184637
init_package_paths();
184638-
import { writeFileSync as writeFileSync4, rmSync, existsSync as existsSync6, mkdirSync as mkdirSync8, realpathSync } from "fs";
184638+
import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync7, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
184639184639
import { delimiter as delimiter5, dirname as dirname5, isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "path";
184640184640

184641184641
// ../node_modules/js-yaml/dist/js-yaml.mjs
@@ -187500,6 +187500,15 @@ function registerCLITool(server, definition) {
187500187500
`CodeQL database at "${dbPath}" does not contain a source archive (expected "src.zip" file or "src" directory).`
187501187501
);
187502187502
}
187503+
const dbYmlPath = join10(dbPath, "codeql-database.yml");
187504+
try {
187505+
const dbYml = readFileSync7(dbYmlPath, "utf8");
187506+
const dbMeta = load(dbYml);
187507+
if (dbMeta?.sourceLocationPrefix && typeof dbMeta.sourceLocationPrefix === "string") {
187508+
options["source-location-prefix"] = dbMeta.sourceLocationPrefix;
187509+
}
187510+
} catch {
187511+
}
187503187512
delete options.database;
187504187513
}
187505187514
break;
@@ -187793,7 +187802,7 @@ var codeqlBqrsInterpretTool = {
187793187802
file: external_exports.string().describe("The BQRS file to interpret"),
187794187803
format: external_exports.enum(["csv", "sarif-latest", "sarifv2.1.0", "graphtext", "dgml", "dot"]).describe("Output format: csv (comma-separated), sarif-latest/sarifv2.1.0 (SARIF), graphtext/dgml/dot (graph formats, only for @kind graph queries)"),
187795187804
output: createCodeQLSchemas.output(),
187796-
database: external_exports.string().optional().describe("Path to the CodeQL database, used to resolve source archive context for SARIF interpretation (provides file contents and snippets)"),
187805+
database: external_exports.string().optional().describe("Path to the CodeQL database. When provided, auto-resolves --source-archive (for file contents/snippets) and --source-location-prefix (from codeql-database.yml metadata) so SARIF results include full source context"),
187797187806
t: external_exports.array(external_exports.string()).describe('Query metadata key=value pairs in KEY=VALUE format. At least "kind" and "id" must be specified. Example: ["kind=problem", "id=js/sql-injection"]. Common keys: kind (problem|path-problem|graph|metric|diagnostic), id (query identifier like js/xss)'),
187798187807
"max-paths": external_exports.number().optional().describe("Maximum number of paths to produce for each alert with paths (default: 4)"),
187799187808
"sarif-add-file-contents": external_exports.boolean().optional().describe("[SARIF only] Include full file contents for all files referenced in results"),
@@ -188351,7 +188360,7 @@ var codeqlGenerateQueryHelpTool = {
188351188360
};
188352188361

188353188362
// src/tools/codeql/list-databases.ts
188354-
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
188363+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
188355188364
import { join as join12 } from "path";
188356188365

188357188366
// src/lib/discovery-config.ts
@@ -188376,7 +188385,7 @@ function getQueryRunResultsDirs() {
188376188385
init_logger();
188377188386
function parseDatabaseYml(ymlPath) {
188378188387
try {
188379-
const content = readFileSync8(ymlPath, "utf-8");
188388+
const content = readFileSync9(ymlPath, "utf-8");
188380188389
const info = {};
188381188390
for (const line of content.split("\n")) {
188382188391
const trimmed = line.trim();
@@ -188505,7 +188514,7 @@ function registerListDatabasesTool(server) {
188505188514
}
188506188515

188507188516
// src/tools/codeql/list-mrva-run-results.ts
188508-
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
188517+
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
188509188518
import { join as join13 } from "path";
188510188519
init_logger();
188511188520
var NUMERIC_DIR_PATTERN = /^\d+$/;
@@ -188541,7 +188550,7 @@ async function discoverMrvaRunResults(resultsDirs, runId) {
188541188550
const timestampPath = join13(entryPath, "timestamp");
188542188551
if (existsSync9(timestampPath)) {
188543188552
try {
188544-
timestamp2 = readFileSync9(timestampPath, "utf-8").trim();
188553+
timestamp2 = readFileSync10(timestampPath, "utf-8").trim();
188545188554
} catch {
188546188555
}
188547188556
}
@@ -188597,7 +188606,7 @@ function discoverRepoResults(runPath) {
188597188606
const repoTaskPath = join13(repoPath, "repo_task.json");
188598188607
if (existsSync9(repoTaskPath)) {
188599188608
try {
188600-
const raw = readFileSync9(repoTaskPath, "utf-8");
188609+
const raw = readFileSync10(repoTaskPath, "utf-8");
188601188610
const task = JSON.parse(raw);
188602188611
if (typeof task.analysisStatus === "string") {
188603188612
analysisStatus = task.analysisStatus;
@@ -188692,7 +188701,7 @@ function registerListMrvaRunResultsTool(server) {
188692188701
}
188693188702

188694188703
// src/tools/codeql/list-query-run-results.ts
188695-
import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
188704+
import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
188696188705
import { join as join14 } from "path";
188697188706
init_logger();
188698188707
var QUERY_RUN_DIR_PATTERN = /^(.+\.ql)-(.+)$/;
@@ -188764,14 +188773,14 @@ async function discoverQueryRunResults(resultsDirs, filter) {
188764188773
const timestampPath = join14(entryPath, "timestamp");
188765188774
if (existsSync10(timestampPath)) {
188766188775
try {
188767-
timestamp2 = readFileSync10(timestampPath, "utf-8").trim();
188776+
timestamp2 = readFileSync11(timestampPath, "utf-8").trim();
188768188777
} catch {
188769188778
}
188770188779
}
188771188780
let metadata = {};
188772188781
if (hasQueryLog) {
188773188782
try {
188774-
const logContent = readFileSync10(join14(entryPath, "query.log"), "utf-8");
188783+
const logContent = readFileSync11(join14(entryPath, "query.log"), "utf-8");
188775188784
metadata = parseQueryLogMetadata(logContent);
188776188785
} catch {
188777188786
}
@@ -189881,7 +189890,7 @@ function registerQuickEvaluateTool(server) {
189881189890

189882189891
// src/tools/codeql/read-database-source.ts
189883189892
var import_adm_zip = __toESM(require_adm_zip(), 1);
189884-
import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
189893+
import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
189885189894
import { join as join18, resolve as resolve7 } from "path";
189886189895
import { fileURLToPath as fileURLToPath2 } from "url";
189887189896
init_logger();
@@ -190043,7 +190052,7 @@ Directory contains ${availableEntries.length} entries. Use read_database_source
190043190052
);
190044190053
}
190045190054
const fullPath = join18(srcDirPath, matchedRelative);
190046-
const rawContent = readFileSync11(fullPath, "utf-8");
190055+
const rawContent = readFileSync12(fullPath, "utf-8");
190047190056
const { content, effectiveEnd, effectiveStart, totalLines } = applyLineRange(
190048190057
rawContent,
190049190058
startLine,
@@ -190391,7 +190400,7 @@ var codeqlResolveTestsTool = {
190391190400
};
190392190401

190393190402
// src/tools/codeql/search-ql-code.ts
190394-
import { closeSync as closeSync2, createReadStream as createReadStream3, fstatSync as fstatSync2, lstatSync, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "fs";
190403+
import { closeSync as closeSync2, createReadStream as createReadStream3, fstatSync as fstatSync2, lstatSync, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync13, realpathSync as realpathSync2 } from "fs";
190395190404
import { basename as basename8, extname as extname2, join as join19, resolve as resolve9 } from "path";
190396190405
import { createInterface as createInterface3 } from "readline";
190397190406
init_logger();
@@ -190467,7 +190476,7 @@ async function searchFile(filePath, regex, contextLines, maxCollect) {
190467190476
}
190468190477
let content;
190469190478
try {
190470-
content = readFileSync12(fd, "utf-8");
190479+
content = readFileSync13(fd, "utf-8");
190471190480
} catch {
190472190481
try {
190473190482
closeSync2(fd);

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

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

server/src/lib/cli-tool-registry.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getOrCreateLogDirectory } from './log-directory-manager';
1111
import { resolveQueryPath } from './query-resolver';
1212
import { cacheDatabaseAnalyzeResults, processQueryRunResults } from './result-processor';
1313
import { getUserWorkspaceDir, packageRootDir } from '../utils/package-paths';
14-
import { writeFileSync, rmSync, existsSync, mkdirSync, realpathSync } from 'fs';
14+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'fs';
1515
import { delimiter, dirname, isAbsolute, join, resolve } from 'path';
1616
import * as yaml from 'js-yaml';
1717
import { createProjectTempDir } from '../utils/temp-dir';
@@ -384,7 +384,8 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
384384
}
385385

386386
case 'codeql_bqrs_interpret':
387-
// Map 'database' to '--source-archive' for codeql bqrs interpret
387+
// Map 'database' to '--source-archive' and '--source-location-prefix'
388+
// for codeql bqrs interpret
388389
if (options.database) {
389390
const dbPath = resolveDatabasePath(options.database as string);
390391
const srcZipPath = join(dbPath, 'src.zip');
@@ -399,6 +400,17 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
399400
`CodeQL database at "${dbPath}" does not contain a source archive (expected "src.zip" file or "src" directory).`,
400401
);
401402
}
403+
// Auto-resolve --source-location-prefix from codeql-database.yml
404+
const dbYmlPath = join(dbPath, 'codeql-database.yml');
405+
try {
406+
const dbYml = readFileSync(dbYmlPath, 'utf8');
407+
const dbMeta = yaml.load(dbYml) as Record<string, unknown> | undefined;
408+
if (dbMeta?.sourceLocationPrefix && typeof dbMeta.sourceLocationPrefix === 'string') {
409+
options['source-location-prefix'] = dbMeta.sourceLocationPrefix;
410+
}
411+
} catch {
412+
// codeql-database.yml missing or unparseable — skip prefix resolution
413+
}
402414
delete options.database;
403415
}
404416
break;

server/src/tools/codeql/bqrs-interpret.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const codeqlBqrsInterpretTool: CLIToolDefinition = {
1616
.describe('Output format: csv (comma-separated), sarif-latest/sarifv2.1.0 (SARIF), graphtext/dgml/dot (graph formats, only for @kind graph queries)'),
1717
output: createCodeQLSchemas.output(),
1818
database: z.string().optional()
19-
.describe('Path to the CodeQL database, used to resolve source archive context for SARIF interpretation (provides file contents and snippets)'),
19+
.describe('Path to the CodeQL database. When provided, auto-resolves --source-archive (for file contents/snippets) and --source-location-prefix (from codeql-database.yml metadata) so SARIF results include full source context'),
2020
t: z.array(z.string())
2121
.describe('Query metadata key=value pairs in KEY=VALUE format. At least "kind" and "id" must be specified. Example: ["kind=problem", "id=js/sql-injection"]. Common keys: kind (problem|path-problem|graph|metric|diagnostic), id (query identifier like js/xss)'),
2222
'max-paths': z.number().optional()

server/test/src/lib/cli-tool-registry.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,107 @@ describe('registerCLITool handler behavior', () => {
549549
);
550550
});
551551

552+
it('should auto-resolve source-location-prefix from codeql-database.yml for bqrs_interpret with database', async () => {
553+
// Create a temp database directory with codeql-database.yml containing sourceLocationPrefix
554+
const tmpDir = createTestTempDir('bqrs-interpret-slp');
555+
const dbDir = join(tmpDir, 'test-db');
556+
mkdirSync(dbDir, { recursive: true });
557+
writeFileSync(join(dbDir, 'codeql-database.yml'), [
558+
'primaryLanguage: javascript',
559+
'sourceLocationPrefix: /Users/dev/my-project',
560+
'creationMetadata:',
561+
' cliVersion: 2.25.1',
562+
].join('\n'), 'utf8');
563+
// Create src.zip so source-archive resolution succeeds
564+
writeFileSync(join(dbDir, 'src.zip'), '', 'utf8');
565+
566+
const definition: CLIToolDefinition = {
567+
name: 'codeql_bqrs_interpret',
568+
description: 'Interpret BQRS',
569+
command: 'codeql',
570+
subcommand: 'bqrs interpret',
571+
inputSchema: {
572+
file: z.string(),
573+
database: z.string().optional(),
574+
format: z.string().optional()
575+
}
576+
};
577+
578+
registerCLITool(mockServer, definition);
579+
580+
const handler = (mockServer.tool as ReturnType<typeof vi.fn>).mock.calls[0][3];
581+
582+
executeCodeQLCommand.mockResolvedValueOnce({
583+
stdout: 'Interpreted',
584+
stderr: '',
585+
success: true
586+
});
587+
588+
try {
589+
await handler({ file: '/path/to/results.bqrs', database: dbDir, format: 'sarif-latest' });
590+
591+
const callArgs = executeCodeQLCommand.mock.calls[0];
592+
const options = callArgs[1] as Record<string, unknown>;
593+
// source-location-prefix should be auto-resolved from codeql-database.yml
594+
expect(options['source-location-prefix']).toBe('/Users/dev/my-project');
595+
// source-archive should point to src.zip
596+
expect(options['source-archive']).toBe(join(dbDir, 'src.zip'));
597+
// database should be removed from options (not passed to CLI)
598+
expect(options).not.toHaveProperty('database');
599+
} finally {
600+
rmSync(tmpDir, { recursive: true, force: true });
601+
}
602+
});
603+
604+
it('should not set source-location-prefix when codeql-database.yml has no sourceLocationPrefix for bqrs_interpret', async () => {
605+
// Create a temp database directory with codeql-database.yml WITHOUT sourceLocationPrefix
606+
const tmpDir = createTestTempDir('bqrs-interpret-no-slp');
607+
const dbDir = join(tmpDir, 'test-db');
608+
mkdirSync(dbDir, { recursive: true });
609+
writeFileSync(join(dbDir, 'codeql-database.yml'), [
610+
'primaryLanguage: javascript',
611+
'creationMetadata:',
612+
' cliVersion: 2.25.1',
613+
].join('\n'), 'utf8');
614+
// Create src directory as fallback source archive
615+
mkdirSync(join(dbDir, 'src'), { recursive: true });
616+
617+
const definition: CLIToolDefinition = {
618+
name: 'codeql_bqrs_interpret',
619+
description: 'Interpret BQRS',
620+
command: 'codeql',
621+
subcommand: 'bqrs interpret',
622+
inputSchema: {
623+
file: z.string(),
624+
database: z.string().optional(),
625+
format: z.string().optional()
626+
}
627+
};
628+
629+
registerCLITool(mockServer, definition);
630+
631+
const handler = (mockServer.tool as ReturnType<typeof vi.fn>).mock.calls[0][3];
632+
633+
executeCodeQLCommand.mockResolvedValueOnce({
634+
stdout: 'Interpreted',
635+
stderr: '',
636+
success: true
637+
});
638+
639+
try {
640+
await handler({ file: '/path/to/results.bqrs', database: dbDir, format: 'sarif-latest' });
641+
642+
const callArgs = executeCodeQLCommand.mock.calls[0];
643+
const options = callArgs[1] as Record<string, unknown>;
644+
// source-location-prefix should NOT be set when not in codeql-database.yml
645+
expect(options).not.toHaveProperty('source-location-prefix');
646+
// source-archive should fall back to src directory
647+
expect(options['source-archive']).toBe(join(dbDir, 'src'));
648+
} finally {
649+
rmSync(tmpDir, { recursive: true, force: true });
650+
}
651+
});
652+
552653
it('should NOT pass format to CLI options for tools where format should not be on CLI', async () => {
553654
const definition: CLIToolDefinition = {
554655
name: 'codeql_query_run',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Tests for codeql_bqrs_interpret tool definition
3+
*
4+
* Validates the tool schema, including the database parameter for
5+
* source archive context and source-location-prefix auto-resolution.
6+
*/
7+
8+
import { describe, expect, it } from 'vitest';
9+
import { codeqlBqrsInterpretTool } from '../../../../src/tools/codeql/bqrs-interpret';
10+
11+
describe('codeql_bqrs_interpret tool definition', () => {
12+
it('should have correct tool name', () => {
13+
expect(codeqlBqrsInterpretTool.name).toBe('codeql_bqrs_interpret');
14+
});
15+
16+
it('should use codeql bqrs interpret subcommand', () => {
17+
expect(codeqlBqrsInterpretTool.command).toBe('codeql');
18+
expect(codeqlBqrsInterpretTool.subcommand).toBe('bqrs interpret');
19+
});
20+
21+
it('should have file as required positional input (string, not array)', () => {
22+
expect(codeqlBqrsInterpretTool.inputSchema).toHaveProperty('file');
23+
// Verify it's a string schema, not an array schema
24+
const fileSchema = codeqlBqrsInterpretTool.inputSchema.file;
25+
// Parse should accept a string
26+
expect(fileSchema.parse('/path/to/results.bqrs')).toBe('/path/to/results.bqrs');
27+
// Parse should reject an array
28+
expect(() => fileSchema.parse(['/path/to/results.bqrs'])).toThrow();
29+
});
30+
31+
it('should support format parameter', () => {
32+
expect(codeqlBqrsInterpretTool.inputSchema).toHaveProperty('format');
33+
});
34+
35+
it('should support database parameter (optional, for source archive context)', () => {
36+
expect(codeqlBqrsInterpretTool.inputSchema).toHaveProperty('database');
37+
});
38+
39+
it('should support t parameter (query metadata key=value pairs)', () => {
40+
expect(codeqlBqrsInterpretTool.inputSchema).toHaveProperty('t');
41+
});
42+
43+
it('should support output parameter', () => {
44+
expect(codeqlBqrsInterpretTool.inputSchema).toHaveProperty('output');
45+
});
46+
47+
it('should have examples', () => {
48+
expect(codeqlBqrsInterpretTool.examples).toBeDefined();
49+
expect(codeqlBqrsInterpretTool.examples!.length).toBeGreaterThan(0);
50+
});
51+
52+
it('should have a custom result processor', () => {
53+
expect(codeqlBqrsInterpretTool.resultProcessor).toBeDefined();
54+
});
55+
});

0 commit comments

Comments
 (0)