Skip to content

Commit f19ee41

Browse files
Copilotdata-douser
andauthored
feat: persist DIL output to a .dil file for codeql_query_compile
After successful compilation with --dump-dil, extract the DIL content from stdout and save it to a dedicated .dil file in a log directory. The file path is appended to the tool response. - Add logDir parameter to codeql_query_compile tool - Create log directory and write .dil file post-execution - Add server unit tests for DIL file persistence - Update integration test assertions to verify DIL file output Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/7ba868e5-1215-4130-bc06-10dfb15ebf14 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent 7e6bf0d commit f19ee41

File tree

7 files changed

+158
-25
lines changed

7 files changed

+158
-25
lines changed

client/integration-tests/primitives/tools/codeql_query_compile/compile_query/test-config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
"toolName": "codeql_query_compile",
33
"arguments": {
44
"query": "server/ql/javascript/examples/src/ExampleQuery1/ExampleQuery1.ql"
5+
},
6+
"assertions": {
7+
"responseContains": ["DIL file:", ".dil"]
58
}
69
}

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35004,7 +35004,7 @@ var require_view = __commonJS({
3500435004
var path3 = __require("node:path");
3500535005
var fs3 = __require("node:fs");
3500635006
var dirname9 = path3.dirname;
35007-
var basename10 = path3.basename;
35007+
var basename11 = path3.basename;
3500835008
var extname3 = path3.extname;
3500935009
var join22 = path3.join;
3501035010
var resolve15 = path3.resolve;
@@ -35043,7 +35043,7 @@ var require_view = __commonJS({
3504335043
var root = roots[i];
3504435044
var loc = resolve15(root, name);
3504535045
var dir = dirname9(loc);
35046-
var file = basename10(loc);
35046+
var file = basename11(loc);
3504735047
path4 = this.resolve(dir, file);
3504835048
}
3504935049
return path4;
@@ -35073,7 +35073,7 @@ var require_view = __commonJS({
3507335073
if (stat && stat.isFile()) {
3507435074
return path4;
3507535075
}
35076-
path4 = join22(dir, basename10(file, ext), "index" + ext);
35076+
path4 = join22(dir, basename11(file, ext), "index" + ext);
3507735077
stat = tryStat(path4);
3507835078
if (stat && stat.isFile()) {
3507935079
return path4;
@@ -38371,7 +38371,7 @@ var require_content_disposition = __commonJS({
3837138371
"use strict";
3837238372
module.exports = contentDisposition;
3837338373
module.exports.parse = parse4;
38374-
var basename10 = __require("path").basename;
38374+
var basename11 = __require("path").basename;
3837538375
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
3837638376
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
3837738377
var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;
@@ -38406,9 +38406,9 @@ var require_content_disposition = __commonJS({
3840638406
if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
3840738407
throw new TypeError("fallback must be ISO-8859-1 string");
3840838408
}
38409-
var name = basename10(filename);
38409+
var name = basename11(filename);
3841038410
var isQuotedString = TEXT_REGEXP.test(name);
38411-
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename10(fallback);
38411+
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename11(fallback);
3841238412
var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
3841338413
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
3841438414
params["filename*"] = name;
@@ -190955,7 +190955,7 @@ function cacheDatabaseAnalyzeResults(params, logger2) {
190955190955
// src/lib/cli-tool-registry.ts
190956190956
init_package_paths();
190957190957
import { existsSync as existsSync6, mkdirSync as mkdirSync8, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
190958-
import { delimiter as delimiter5, dirname as dirname5, isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "path";
190958+
import { delimiter as delimiter5, dirname as dirname5, basename as basename5, isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "path";
190959190959

190960190960
// ../node_modules/js-yaml/dist/js-yaml.mjs
190961190961
function isNothing(subject) {
@@ -193921,6 +193921,11 @@ function registerCLITool(server, definition) {
193921193921
mkdirSync8(outputDir, { recursive: true });
193922193922
}
193923193923
}
193924+
let compileLogDir;
193925+
if (name === "codeql_query_compile" && options["dump-dil"] !== false) {
193926+
compileLogDir = getOrCreateLogDirectory(customLogDir);
193927+
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
193928+
}
193924193929
const rawAdditionalArgs = Array.isArray(options.additionalArgs) ? options.additionalArgs : [];
193925193930
delete options.additionalArgs;
193926193931
const managedFlagNames = /* @__PURE__ */ new Set([
@@ -194016,7 +194021,24 @@ function registerCLITool(server, definition) {
194016194021
const resolvedDb = typeof params.database === "string" ? resolveDatabasePath(params.database) : params.database;
194017194022
cacheDatabaseAnalyzeResults({ ...params, database: resolvedDb, output: options.output, format: options.format }, logger);
194018194023
}
194019-
const processedResult = resultProcessor(result, params);
194024+
let dilFilePath;
194025+
if (name === "codeql_query_compile" && result.success && compileLogDir && result.stdout) {
194026+
try {
194027+
const queryBaseName = query ? basename5(query, ".ql") : "query";
194028+
dilFilePath = join10(compileLogDir, `${queryBaseName}.dil`);
194029+
writeFileSync4(dilFilePath, result.stdout, "utf8");
194030+
logger.info(`Saved DIL output to ${dilFilePath}`);
194031+
} catch (dilError) {
194032+
logger.warn(`Failed to save DIL output: ${dilError}`);
194033+
dilFilePath = void 0;
194034+
}
194035+
}
194036+
let processedResult = resultProcessor(result, params);
194037+
if (dilFilePath) {
194038+
processedResult += `
194039+
194040+
DIL file: ${dilFilePath}`;
194041+
}
194020194042
return {
194021194043
content: [{
194022194044
type: "text",
@@ -195288,7 +195310,7 @@ var codeqlPackLsTool = {
195288195310

195289195311
// src/tools/codeql/profile-codeql-query-from-logs.ts
195290195312
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
195291-
import { basename as basename6, dirname as dirname7, join as join15 } from "path";
195313+
import { basename as basename7, dirname as dirname7, join as join15 } from "path";
195292195314

195293195315
// src/lib/evaluator-log-parser.ts
195294195316
init_logger();
@@ -195620,7 +195642,7 @@ function buildDetailFile(profile, topN) {
195620195642
lines.push("");
195621195643
for (let qIdx = 0; qIdx < profile.queries.length; qIdx++) {
195622195644
const query = profile.queries[qIdx];
195623-
const qName = basename6(query.queryName);
195645+
const qName = basename7(query.queryName);
195624195646
lines.push(`== Query: ${qName} ==`);
195625195647
lines.push(` Total: ${query.totalDurationMs.toFixed(2)}ms | Predicates evaluated: ${query.predicateCount} | Cache hits: ${query.cacheHits}`);
195626195648
lines.push("");
@@ -195779,7 +195801,7 @@ init_cli_executor();
195779195801
init_logger();
195780195802
import { writeFileSync as writeFileSync6, existsSync as existsSync12 } from "fs";
195781195803
import { createReadStream as createReadStream2 } from "fs";
195782-
import { join as join16, dirname as dirname8, basename as basename7 } from "path";
195804+
import { join as join16, dirname as dirname8, basename as basename8 } from "path";
195783195805
import { mkdirSync as mkdirSync10 } from "fs";
195784195806
import { createInterface as createInterface2 } from "readline";
195785195807
async function parseEvaluatorLog2(logPath) {
@@ -195905,7 +195927,7 @@ function formatAsMermaid(profile) {
195905195927
lines.push("```mermaid");
195906195928
lines.push("graph TD");
195907195929
lines.push("");
195908-
lines.push(` QUERY["${basename7(profile.queryName)}<br/>Total: ${profile.totalDuration.toFixed(2)}ms"]`);
195930+
lines.push(` QUERY["${basename8(profile.queryName)}<br/>Total: ${profile.totalDuration.toFixed(2)}ms"]`);
195909195931
lines.push("");
195910195932
profile.pipelines.forEach((pipeline) => {
195911195933
const nodeId = `P${pipeline.eventId}`;
@@ -196036,7 +196058,7 @@ function registerProfileCodeQLQueryTool(server) {
196036196058
...outputFiles.map((f) => ` - ${f}`),
196037196059
"",
196038196060
"Profile Summary:",
196039-
` - Query: ${basename7(profile.queryName)}`,
196061+
` - Query: ${basename8(profile.queryName)}`,
196040196062
` - Total Duration: ${profile.totalDuration.toFixed(2)} ms`,
196041196063
` - Total Pipelines: ${profile.pipelines.length}`,
196042196064
` - Total Events: ${profile.totalEvents}`,
@@ -196073,14 +196095,15 @@ function registerProfileCodeQLQueryTool(server) {
196073196095
// src/tools/codeql/query-compile.ts
196074196096
var codeqlQueryCompileTool = {
196075196097
name: "codeql_query_compile",
196076-
description: "Compile and validate CodeQL queries",
196098+
description: "Compile and validate CodeQL queries. By default, produces a .dil file containing the optimized DIL intermediate representation alongside the compilation output.",
196077196099
command: "codeql",
196078196100
subcommand: "query compile",
196079196101
inputSchema: {
196080196102
query: external_exports.string().describe("Path to the CodeQL query file (.ql)"),
196081196103
database: external_exports.string().optional().describe("Path to the CodeQL database"),
196082196104
"dump-dil": external_exports.boolean().optional().describe("Print the optimized DIL intermediate representation to standard output while compiling. Enabled by default; pass false or --no-dump-dil to disable."),
196083196105
library: external_exports.string().optional().describe("Path to query library"),
196106+
logDir: external_exports.string().optional().describe("Directory to write the .dil file. If not provided, a unique log directory is created automatically."),
196084196107
output: external_exports.string().optional().describe("Output file path"),
196085196108
warnings: external_exports.enum(["hide", "show", "error"]).optional().describe("How to handle compilation warnings"),
196086196109
verbose: external_exports.boolean().optional().describe("Enable verbose output"),
@@ -196737,7 +196760,7 @@ var codeqlResolveTestsTool = {
196737196760

196738196761
// src/tools/codeql/search-ql-code.ts
196739196762
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";
196740-
import { basename as basename8, extname as extname2, join as join19, resolve as resolve9 } from "path";
196763+
import { basename as basename9, extname as extname2, join as join19, resolve as resolve9 } from "path";
196741196764
import { createInterface as createInterface3 } from "readline";
196742196765
init_logger();
196743196766
var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
@@ -196763,7 +196786,7 @@ function collectFiles(paths, extensions, fileCount) {
196763196786
}
196764196787
fileCount.value++;
196765196788
} else if (stat.isDirectory()) {
196766-
if (SKIP_DIRS2.has(basename8(p))) return;
196789+
if (SKIP_DIRS2.has(basename9(p))) return;
196767196790
let realPath;
196768196791
try {
196769196792
realPath = realpathSync2(p);
@@ -198164,7 +198187,7 @@ function registerLanguageResources(server) {
198164198187

198165198188
// src/prompts/workflow-prompts.ts
198166198189
import { access as access2 } from "fs/promises";
198167-
import { basename as basename9, isAbsolute as isAbsolute7, normalize, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
198190+
import { basename as basename10, isAbsolute as isAbsolute7, normalize, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
198168198191
import { fileURLToPath as fileURLToPath3 } from "url";
198169198192

198170198193
// src/prompts/check-for-duplicated-code.prompt.md
@@ -198583,7 +198606,7 @@ ${content}`
198583198606
if (qpResult.blocked) return blockedPathError(qpResult, "query path");
198584198607
const resolvedQueryPath = qpResult.resolvedPath;
198585198608
if (qpResult.warning) warnings.push(qpResult.warning);
198586-
const derivedName = workshopName || basename9(resolvedQueryPath).replace(/\.(ql|qlref)$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-") || "codeql-workshop";
198609+
const derivedName = workshopName || basename10(resolvedQueryPath).replace(/\.(ql|qlref)$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-") || "codeql-workshop";
198587198610
const contextSection = buildWorkshopContext(
198588198611
resolvedQueryPath,
198589198612
language,

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: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { resolveQueryPath } from './query-resolver';
1313
import { cacheDatabaseAnalyzeResults, processQueryRunResults } from './result-processor';
1414
import { getUserWorkspaceDir, packageRootDir } from '../utils/package-paths';
1515
import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
16-
import { delimiter, dirname, isAbsolute, join, resolve } from 'path';
16+
import { delimiter, dirname, basename, isAbsolute, join, resolve } from 'path';
1717
import * as yaml from 'js-yaml';
1818
import { createProjectTempDir } from '../utils/temp-dir';
1919

@@ -556,6 +556,13 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
556556
}
557557
}
558558

559+
// Set up log directory for compile runs to persist DIL output
560+
let compileLogDir: string | undefined;
561+
if (name === 'codeql_query_compile' && options['dump-dil'] !== false) {
562+
compileLogDir = getOrCreateLogDirectory(customLogDir as string | undefined);
563+
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
564+
}
565+
559566
// Extract additionalArgs from options so they are passed as raw CLI
560567
// arguments instead of being transformed into --additionalArgs=value
561568
// by buildCodeQLArgs.
@@ -717,8 +724,29 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
717724
cacheDatabaseAnalyzeResults({ ...params, database: resolvedDb, output: options.output, format: options.format }, logger);
718725
}
719726

727+
// Post-execution: persist DIL output to a .dil file for codeql_query_compile
728+
let dilFilePath: string | undefined;
729+
if (name === 'codeql_query_compile' && result.success && compileLogDir && result.stdout) {
730+
try {
731+
const queryBaseName = query
732+
? basename(query as string, '.ql')
733+
: 'query';
734+
dilFilePath = join(compileLogDir, `${queryBaseName}.dil`);
735+
writeFileSync(dilFilePath, result.stdout, 'utf8');
736+
logger.info(`Saved DIL output to ${dilFilePath}`);
737+
} catch (dilError) {
738+
logger.warn(`Failed to save DIL output: ${dilError}`);
739+
dilFilePath = undefined;
740+
}
741+
}
742+
720743
// Process the result
721-
const processedResult = resultProcessor(result, params);
744+
let processedResult = resultProcessor(result, params);
745+
746+
// Append DIL file path to the response for codeql_query_compile
747+
if (dilFilePath) {
748+
processedResult += `\n\nDIL file: ${dilFilePath}`;
749+
}
722750

723751
return {
724752
content: [{

server/src/tools/codeql/query-compile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { CLIToolDefinition } from '../../lib/cli-tool-registry';
77

88
export const codeqlQueryCompileTool: CLIToolDefinition = {
99
name: 'codeql_query_compile',
10-
description: 'Compile and validate CodeQL queries',
10+
description: 'Compile and validate CodeQL queries. By default, produces a .dil file containing the optimized DIL intermediate representation alongside the compilation output.',
1111
command: 'codeql',
1212
subcommand: 'query compile',
1313
inputSchema: {
@@ -16,6 +16,8 @@ export const codeqlQueryCompileTool: CLIToolDefinition = {
1616
'dump-dil': z.boolean().optional()
1717
.describe('Print the optimized DIL intermediate representation to standard output while compiling. Enabled by default; pass false or --no-dump-dil to disable.'),
1818
library: z.string().optional().describe('Path to query library'),
19+
logDir: z.string().optional()
20+
.describe('Directory to write the .dil file. If not provided, a unique log directory is created automatically.'),
1921
output: z.string().optional().describe('Output file path'),
2022
warnings: z.enum(['hide', 'show', 'error']).optional()
2123
.describe('How to handle compilation warnings'),

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { describe, it, expect, vi, beforeEach } from 'vitest';
6-
import { mkdirSync, rmSync, writeFileSync } from 'fs';
6+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
77
import { join } from 'path';
88
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
99
import { z } from 'zod';
@@ -666,6 +666,76 @@ describe('registerCLITool handler behavior', () => {
666666
expect(positionalArgs).toContain('--dump-dil');
667667
});
668668

669+
it('should save DIL output to a .dil file for codeql_query_compile', async () => {
670+
const definition: CLIToolDefinition = {
671+
name: 'codeql_query_compile',
672+
description: 'Compile query',
673+
command: 'codeql',
674+
subcommand: 'query compile',
675+
inputSchema: {
676+
query: z.string(),
677+
'dump-dil': z.boolean().optional(),
678+
additionalArgs: z.array(z.string()).optional()
679+
}
680+
};
681+
682+
registerCLITool(mockServer, definition);
683+
684+
const handler = (mockServer.registerTool as ReturnType<typeof vi.fn>).mock.calls[0][2];
685+
686+
const dilContent = 'DIL:\n predicate#abc123\n SCAN table\n SELECT col1, col2';
687+
executeCodeQLCommand.mockResolvedValueOnce({
688+
stdout: dilContent,
689+
stderr: '',
690+
success: true
691+
});
692+
693+
const result = await handler({ query: '/path/to/MyQuery.ql' });
694+
695+
// Response should contain the DIL file path
696+
expect(result.content[0].text).toContain('DIL file:');
697+
expect(result.content[0].text).toContain('MyQuery.dil');
698+
699+
// Extract the DIL file path from the response
700+
const match = result.content[0].text.match(/DIL file: (.+)/);
701+
expect(match).not.toBeNull();
702+
const dilFilePath = match![1];
703+
704+
// Verify the .dil file was created with the correct content
705+
expect(existsSync(dilFilePath)).toBe(true);
706+
const fileContent = readFileSync(dilFilePath, 'utf8');
707+
expect(fileContent).toBe(dilContent);
708+
});
709+
710+
it('should not save DIL file when dump-dil is explicitly false for codeql_query_compile', async () => {
711+
const definition: CLIToolDefinition = {
712+
name: 'codeql_query_compile',
713+
description: 'Compile query',
714+
command: 'codeql',
715+
subcommand: 'query compile',
716+
inputSchema: {
717+
query: z.string(),
718+
'dump-dil': z.boolean().optional(),
719+
additionalArgs: z.array(z.string()).optional()
720+
}
721+
};
722+
723+
registerCLITool(mockServer, definition);
724+
725+
const handler = (mockServer.registerTool as ReturnType<typeof vi.fn>).mock.calls[0][2];
726+
727+
executeCodeQLCommand.mockResolvedValueOnce({
728+
stdout: 'Compilation successful',
729+
stderr: '',
730+
success: true
731+
});
732+
733+
const result = await handler({ query: '/path/to/MyQuery.ql', 'dump-dil': false });
734+
735+
// Response should NOT contain DIL file path since dump-dil is disabled
736+
expect(result.content[0].text).not.toContain('DIL file:');
737+
});
738+
669739
it('should pass format to CLI for codeql_bqrs_interpret', async () => {
670740
const definition: CLIToolDefinition = {
671741
name: 'codeql_bqrs_interpret',

server/test/src/tools/codeql/query-compile.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('Query Compile Tool', () => {
2323
expect(schema).toHaveProperty('database');
2424
expect(schema).toHaveProperty('dump-dil');
2525
expect(schema).toHaveProperty('library');
26+
expect(schema).toHaveProperty('logDir');
2627
expect(schema).toHaveProperty('output');
2728
expect(schema).toHaveProperty('warnings');
2829
expect(schema).toHaveProperty('verbose');
@@ -35,6 +36,12 @@ describe('Query Compile Tool', () => {
3536
expect(dumpDil.isOptional()).toBe(true);
3637
});
3738

39+
it('should have logDir as optional string parameter', () => {
40+
const logDir = codeqlQueryCompileTool.inputSchema['logDir'];
41+
expect(logDir).toBeDefined();
42+
expect(logDir.isOptional()).toBe(true);
43+
});
44+
3845
it('should have examples', () => {
3946
expect(codeqlQueryCompileTool.examples).toBeDefined();
4047
expect(codeqlQueryCompileTool.examples!.length).toBeGreaterThan(0);

0 commit comments

Comments
 (0)