Skip to content

Commit 29da2f5

Browse files
committed
Fix tool issues found during explain-codeql-query workflow testing
- search_ql_code: add missing await in tool handler; skip .codeql, node_modules, and .git directories to avoid duplicate results from compiled pack caches - cli-tool-registry: extract resolveDatabasePath helper for multi-language DB root auto-resolution; apply to codeql_query_run, codeql_database_analyze, and codeql_resolve_database - environment-builder: route CODEQL_MCP_TMP_DIR to workspace-local .codeql/ql-mcp scratch directory (configurable via scratchDir setting); add CODEQL_MCP_WORKSPACE_FOLDERS env var - query-file-finder: add contextual hints array for missing tests, documentation, and expected results
1 parent 73b1fef commit 29da2f5

File tree

10 files changed

+277
-71
lines changed

10 files changed

+277
-71
lines changed

extensions/vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
"default": "latest",
109109
"description": "The npm version of codeql-development-mcp-server to install. Use 'latest' for the most recent release."
110110
},
111+
"codeql-mcp.scratchDir": {
112+
"type": "string",
113+
"default": ".codeql/ql-mcp",
114+
"markdownDescription": "Workspace-relative path for the ql-mcp scratch directory used for temporary files (query logs, external predicates, etc). The `.codeql/` parent is shared with other CodeQL CLI commands like `codeql pack bundle`. Set to an absolute path to override workspace-relative resolution."
115+
},
111116
"codeql-mcp.watchCodeqlExtension": {
112117
"type": "boolean",
113118
"default": true,

extensions/vscode/src/bridge/environment-builder.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { delimiter, join } from 'path';
2+
import { delimiter, isAbsolute, join } from 'path';
33
import { DisposableObject } from '../common/disposable';
44
import type { Logger } from '../common/logger';
55
import type { CliResolver } from '../codeql/cli-resolver';
@@ -64,17 +64,34 @@ export class EnvironmentBuilder extends DisposableObject {
6464
env.CODEQL_PATH = cliPath;
6565
}
6666

67-
// Workspace root
67+
// Workspace root and all workspace folders
6868
const workspaceFolders = vscode.workspace.workspaceFolders;
6969
if (workspaceFolders && workspaceFolders.length > 0) {
7070
env.CODEQL_MCP_WORKSPACE = workspaceFolders[0].uri.fsPath;
71+
env.CODEQL_MCP_WORKSPACE_FOLDERS = workspaceFolders
72+
.map((f) => f.uri.fsPath)
73+
.join(delimiter);
7174
}
7275

73-
// Temp directory for MCP server scratch files
74-
env.CODEQL_MCP_TMP_DIR = join(
75-
this.context.globalStorageUri.fsPath,
76-
'tmp',
77-
);
76+
// Workspace-local scratch directory for tool output (query logs, etc.)
77+
// Defaults to `.codeql/ql-mcp` within the first workspace folder.
78+
// This is also used as CODEQL_MCP_TMP_DIR so that the server writes
79+
// all temporary output (query logs, external predicate CSVs) inside
80+
// the workspace, avoiding out-of-workspace file access prompts.
81+
const scratchRelative = config.get<string>('scratchDir', '.codeql/ql-mcp');
82+
if (workspaceFolders && workspaceFolders.length > 0) {
83+
const scratchDir = isAbsolute(scratchRelative)
84+
? scratchRelative
85+
: join(workspaceFolders[0].uri.fsPath, scratchRelative);
86+
env.CODEQL_MCP_SCRATCH_DIR = scratchDir;
87+
env.CODEQL_MCP_TMP_DIR = scratchDir;
88+
} else {
89+
// No workspace — fall back to extension globalStorage
90+
env.CODEQL_MCP_TMP_DIR = join(
91+
this.context.globalStorageUri.fsPath,
92+
'tmp',
93+
);
94+
}
7895

7996
// Additional packs path — include vscode-codeql's database storage
8097
// so the MCP server can discover databases registered there

extensions/vscode/test/bridge/environment-builder.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,42 @@ describe('EnvironmentBuilder', () => {
8787
expect(env.TRANSPORT_MODE).toBe('stdio');
8888
});
8989

90-
it('should include CODEQL_MCP_TMP_DIR under global storage', async () => {
90+
it('should include CODEQL_MCP_TMP_DIR under global storage when no workspace', async () => {
9191
const env = await builder.build();
9292
expect(env.CODEQL_MCP_TMP_DIR).toBe('/mock/global-storage/codeql-mcp/tmp');
9393
});
9494

95+
it('should set CODEQL_MCP_TMP_DIR to workspace scratch dir when workspace folders exist', async () => {
96+
const vscode = await import('vscode');
97+
const origFolders = vscode.workspace.workspaceFolders;
98+
(vscode.workspace.workspaceFolders as any) = [
99+
{ uri: { fsPath: '/mock/workspace' }, name: 'ws', index: 0 },
100+
];
101+
102+
builder.invalidate();
103+
const env = await builder.build();
104+
expect(env.CODEQL_MCP_TMP_DIR).toBe('/mock/workspace/.codeql/ql-mcp');
105+
expect(env.CODEQL_MCP_SCRATCH_DIR).toBe('/mock/workspace/.codeql/ql-mcp');
106+
107+
(vscode.workspace.workspaceFolders as any) = origFolders;
108+
});
109+
110+
it('should set CODEQL_MCP_WORKSPACE_FOLDERS with all workspace folder paths', async () => {
111+
const vscode = await import('vscode');
112+
const { delimiter } = await import('path');
113+
const origFolders = vscode.workspace.workspaceFolders;
114+
(vscode.workspace.workspaceFolders as any) = [
115+
{ uri: { fsPath: '/mock/ws-a' }, name: 'a', index: 0 },
116+
{ uri: { fsPath: '/mock/ws-b' }, name: 'b', index: 1 },
117+
];
118+
119+
builder.invalidate();
120+
const env = await builder.build();
121+
expect(env.CODEQL_MCP_WORKSPACE_FOLDERS).toBe(['/mock/ws-a', '/mock/ws-b'].join(delimiter));
122+
123+
(vscode.workspace.workspaceFolders as any) = origFolders;
124+
});
125+
95126
it('should include CODEQL_ADDITIONAL_PACKS with database storage path', async () => {
96127
const env = await builder.build();
97128
expect(env.CODEQL_ADDITIONAL_PACKS).toBeDefined();

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

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31255,7 +31255,7 @@ var require_view = __commonJS({
3125531255
var path4 = __require("node:path");
3125631256
var fs3 = __require("node:fs");
3125731257
var dirname8 = path4.dirname;
31258-
var basename7 = path4.basename;
31258+
var basename8 = path4.basename;
3125931259
var extname3 = path4.extname;
3126031260
var join19 = path4.join;
3126131261
var resolve14 = path4.resolve;
@@ -31294,7 +31294,7 @@ var require_view = __commonJS({
3129431294
var root = roots[i];
3129531295
var loc = resolve14(root, name);
3129631296
var dir = dirname8(loc);
31297-
var file = basename7(loc);
31297+
var file = basename8(loc);
3129831298
path5 = this.resolve(dir, file);
3129931299
}
3130031300
return path5;
@@ -31324,7 +31324,7 @@ var require_view = __commonJS({
3132431324
if (stat && stat.isFile()) {
3132531325
return path5;
3132631326
}
31327-
path5 = join19(dir, basename7(file, ext), "index" + ext);
31327+
path5 = join19(dir, basename8(file, ext), "index" + ext);
3132831328
stat = tryStat(path5);
3132931329
if (stat && stat.isFile()) {
3133031330
return path5;
@@ -34608,7 +34608,7 @@ var require_content_disposition = __commonJS({
3460834608
"use strict";
3460934609
module.exports = contentDisposition;
3461034610
module.exports.parse = parse4;
34611-
var basename7 = __require("path").basename;
34611+
var basename8 = __require("path").basename;
3461234612
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
3461334613
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
3461434614
var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;
@@ -34643,9 +34643,9 @@ var require_content_disposition = __commonJS({
3464334643
if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
3464434644
throw new TypeError("fallback must be ISO-8859-1 string");
3464534645
}
34646-
var name = basename7(filename);
34646+
var name = basename8(filename);
3464734647
var isQuotedString = TEXT_REGEXP.test(name);
34648-
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename7(fallback);
34648+
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename8(fallback);
3464934649
var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
3465034650
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
3465134651
params["filename*"] = name;
@@ -57007,6 +57007,26 @@ init_package_paths();
5700757007
init_temp_dir();
5700857008
import { writeFileSync as writeFileSync2, rmSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
5700957009
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "path";
57010+
function resolveDatabasePath(dbPath) {
57011+
if (existsSync4(join6(dbPath, "codeql-database.yml"))) {
57012+
return dbPath;
57013+
}
57014+
try {
57015+
const entries = readdirSync2(dbPath);
57016+
for (const entry of entries) {
57017+
const candidate = join6(dbPath, entry);
57018+
try {
57019+
if (statSync2(candidate).isDirectory() && existsSync4(join6(candidate, "codeql-database.yml"))) {
57020+
logger.info(`Resolved multi-language database directory: ${dbPath} -> ${candidate}`);
57021+
return candidate;
57022+
}
57023+
} catch {
57024+
}
57025+
}
57026+
} catch {
57027+
}
57028+
return dbPath;
57029+
}
5701057030
var defaultCLIResultProcessor = (result, _params) => {
5701157031
if (!result.success) {
5701257032
return `Command failed (exit code ${result.exitCode || "unknown"}):
@@ -57122,25 +57142,7 @@ function registerCLITool(server, definition) {
5712257142
positionalArgs = [...positionalArgs, qlref];
5712357143
}
5712457144
if (options.database && name === "codeql_resolve_database") {
57125-
let dbPath = options.database;
57126-
if (!existsSync4(join6(dbPath, "codeql-database.yml"))) {
57127-
try {
57128-
const entries = readdirSync2(dbPath);
57129-
for (const entry of entries) {
57130-
const candidate = join6(dbPath, entry);
57131-
try {
57132-
if (statSync2(candidate).isDirectory() && existsSync4(join6(candidate, "codeql-database.yml"))) {
57133-
logger.info(`Resolved database directory: ${dbPath} -> ${candidate}`);
57134-
dbPath = candidate;
57135-
break;
57136-
}
57137-
} catch {
57138-
}
57139-
}
57140-
} catch {
57141-
}
57142-
}
57143-
positionalArgs = [...positionalArgs, dbPath];
57145+
positionalArgs = [...positionalArgs, resolveDatabasePath(options.database)];
5714457146
delete options.database;
5714557147
}
5714657148
if (options.database && name === "codeql_database_create") {
@@ -57149,7 +57151,7 @@ function registerCLITool(server, definition) {
5714957151
}
5715057152
if (name === "codeql_database_analyze") {
5715157153
if (options.database) {
57152-
positionalArgs = [...positionalArgs, options.database];
57154+
positionalArgs = [...positionalArgs, resolveDatabasePath(options.database)];
5715357155
delete options.database;
5715457156
}
5715557157
if (options.queries) {
@@ -57180,6 +57182,9 @@ function registerCLITool(server, definition) {
5718057182
options.database = resolve4(getUserWorkspaceDir(), options.database);
5718157183
logger.info(`Resolved database path to: ${options.database}`);
5718257184
}
57185+
if (options.database && typeof options.database === "string") {
57186+
options.database = resolveDatabasePath(options.database);
57187+
}
5718357188
const resolvedQuery = await resolveQueryPath(params, logger);
5718457189
if (resolvedQuery) {
5718557190
positionalArgs = [...positionalArgs, resolvedQuery];
@@ -60737,6 +60742,18 @@ async function findCodeQLQueryFiles(queryFilePath, language, resolveMetadata = t
6073760742
}
6073860743
const testPackPath = findNearestQlpack(testDir);
6073960744
const testPackDir = testPackPath ? path.dirname(testPackPath) : testDir;
60745+
const hints = [];
60746+
if (!testDirectory.exists) {
60747+
hints.push("No test directory found. To run this query you will need a user-provided database (databasePath). Test-driven profiling is not available without tests.");
60748+
} else if (testCodePaths.length === 0) {
60749+
hints.push("Test directory exists but contains no test source code files. Consider creating test code to enable test-driven workflows.");
60750+
}
60751+
if (!expectedResultsPath.exists && testDirectory.exists) {
60752+
hints.push("No .expected file found. Run codeql_test_run to generate initial expected results, then verify them.");
60753+
}
60754+
if (!documentationPath.exists) {
60755+
hints.push("No query documentation (.md) file found. Use the document_codeql_query prompt to generate one.");
60756+
}
6074060757
return {
6074160758
queryName,
6074260759
language: detectedLanguage,
@@ -60759,6 +60776,7 @@ async function findCodeQLQueryFiles(queryFilePath, language, resolveMetadata = t
6075960776
testDatabaseDir: testDatabasePath.path
6076060777
}
6076160778
},
60779+
hints,
6076260780
metadata,
6076360781
missingFiles,
6076460782
packMetadata,
@@ -62884,13 +62902,14 @@ var codeqlResolveTestsTool = {
6288462902

6288562903
// src/tools/codeql/search-ql-code.ts
6288662904
import { createReadStream as createReadStream3, lstatSync, readdirSync as readdirSync8, readFileSync as readFileSync9, realpathSync } from "fs";
62887-
import { extname as extname2, join as join15, resolve as resolve9 } from "path";
62905+
import { basename as basename6, extname as extname2, join as join15, resolve as resolve9 } from "path";
6288862906
import { createInterface as createInterface3 } from "readline";
6288962907
init_logger();
6289062908
var MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
6289162909
var MAX_FILES_TRAVERSED = 1e4;
6289262910
var MAX_CONTEXT_LINES = 50;
6289362911
var MAX_MAX_RESULTS = 1e4;
62912+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".codeql", "node_modules", ".git"]);
6289462913
function collectFiles(paths, extensions, fileCount) {
6289562914
const files = [];
6289662915
const visitedDirs = /* @__PURE__ */ new Set();
@@ -62909,6 +62928,7 @@ function collectFiles(paths, extensions, fileCount) {
6290962928
}
6291062929
fileCount.value++;
6291162930
} else if (stat.isDirectory()) {
62931+
if (SKIP_DIRS2.has(basename6(p))) return;
6291262932
let realPath;
6291362933
try {
6291462934
realPath = realpathSync(p);
@@ -63066,7 +63086,7 @@ function registerSearchQlCodeTool(server) {
6306663086
},
6306763087
async ({ pattern, paths, includeExtensions, caseSensitive, contextLines, maxResults }) => {
6306863088
try {
63069-
const result = searchQlCode({
63089+
const result = await searchQlCode({
6307063090
pattern,
6307163091
paths,
6307263092
includeExtensions,
@@ -64185,7 +64205,7 @@ function registerLanguageResources(server) {
6418564205
}
6418664206

6418764207
// src/prompts/workflow-prompts.ts
64188-
import { basename as basename6 } from "path";
64208+
import { basename as basename7 } from "path";
6418964209

6419064210
// src/prompts/document-codeql-query.prompt.md
6419164211
var document_codeql_query_prompt_default = '---\nagent: agent\n---\n\n# Document a CodeQL Query\n\nThis prompt guides you through creating or updating documentation for a CodeQL query file. The documentation is stored as a sibling file to the query with a standardized markdown format.\n\n## Purpose\n\nThe `document_codeql_query` prompt creates/updates **query documentation files** for a specific version of a CodeQL query. Documentation files are stored alongside the query file and provide concise yet comprehensive information about what the query does.\n\nFor creating **workshop learning content** with detailed explanations and visual diagrams, use the `explain_codeql_query` prompt instead.\n\n## Required Inputs\n\n- **queryPath**: Path to the CodeQL query file (`.ql` or `.qlref`)\n- **language**: Target programming language (actions, cpp, csharp, go, java, javascript, python, ruby, swift)\n\n## Documentation File Conventions\n\n### File Location and Naming\n\nFor a query file `QueryFileBaseName.ql`, the documentation file should be:\n\n- **Primary**: `QueryFileBaseName.md` (markdown format, preferred)\n- **Legacy**: `QueryFileBaseName.qhelp` (XML-based query help format)\n\nDocumentation files are **siblings** to the query file (same directory).\n\n### Handling Existing Documentation\n\n1. **No documentation exists**: Create new `QueryFileBaseName.md` file\n2. **`.md` file exists**: Update the existing markdown file\n3. **`.qhelp` file exists**: Use #codeql_generate_query-help tool to convert to markdown, then update\n\n## Workflow Checklist\n\nUse the following MCP server tools to gather context before creating documentation:\n\n### Phase 1: Query Discovery\n\n- [ ] **Step 1: Locate query files**\n - Tool: #find_codeql_query_files\n - Parameters: `queryPath` = provided query path\n - Gather: Query source file path, existing documentation files, test files\n - Check: Does `QueryFileBaseName.md` or `QueryFileBaseName.qhelp` exist?\n\n- [ ] **Step 2: Read query metadata**\n - Tool: #codeql_resolve_metadata\n - Parameters: `query` = query file path\n - Gather: @name, @description, @kind, @id, @tags, @precision, @severity\n\n### Phase 2: Convert Existing qhelp (if needed)\n\n- [ ] **Step 3: Convert qhelp to markdown** (only if `.qhelp` exists)\n - Tool: #codeql_generate_query-help\n - Parameters: `query` = query file path, `format` = "markdown"\n - Use output as starting point for updated documentation\n\n### Phase 3: Gather Query Context\n\n- [ ] **Step 4: Validate query structure**\n - Tool: #validate_codeql_query\n - Parameters: `query` = query source code\n - Gather: Structural validation, suggestions\n - Note: This is a heuristic check only \u2014 for full validation, use #codeql_query_compile\n\n- [ ] **Step 5: Explore query types** (if deeper understanding needed)\n - Tool: #codeql_lsp_definition \u2014 navigate to class/predicate definitions\n - Tool: #codeql_lsp_completion \u2014 explore member predicates on types used in the query\n - Parameters: `file_path`, `line` (0-based), `character` (0-based), `workspace_uri` (pack root)\n - Run #codeql_pack_install first \u2014 LSP tools require resolved dependencies\n\n- [ ] **Step 6: Run tests** (if tests exist from Step 1)\n - Tool: #codeql_test_run\n - Parameters: `tests` = test directories\n - Gather: Pass/fail status, confirms query behavior\n\n### Phase 4: Create/Update Documentation\n\nBased on gathered context, create or update the documentation file.\n\n## Documentation Format\n\nThe documentation file (`QueryFileBaseName.md`) should follow this standardized format with these sections:\n\n### Section 1: Title and Description\n\n- H1 heading with the query name from @name metadata\n- One paragraph description from @description, expanded if needed\n\n### Section 2: Metadata Table\n\nA table with these rows:\n\n- ID: The @id value in backticks\n- Kind: The @kind value (problem, path-problem, etc.)\n- Severity: The @severity value\n- Precision: The @precision value\n- Tags: The @tags values\n\n### Section 3: Overview\n\nConcise explanation of what vulnerability/issue this query detects and why it matters. 2-4 sentences.\n\n### Section 4: Recommendation\n\nBrief guidance on how developers should fix issues flagged by this query. Include code patterns to use or avoid.\n\n### Section 5: Example\n\nTwo subsections:\n\n- **Vulnerable Code**: A code block showing a pattern that would be flagged by this query\n- **Fixed Code**: A code block showing the corrected version of the code\n\nUse the appropriate language identifier for the code blocks (e.g., `javascript`, `python`, `java`).\n\n### Section 6: References\n\nA list of links to:\n\n- Relevant CWE if security query\n- Relevant documentation or standards\n- CodeQL documentation for related concepts\n\n## Output Actions\n\nAfter generating documentation content:\n\n1. **For new documentation**: Create the file at `[QueryDirectory]/QueryFileBaseName.md`\n2. **For existing `.md` file**: Update the file with new content, preserving any custom sections\n3. **For existing `.qhelp` file**: Create new `.md` file (keeping `.qhelp` for backward compatibility)\n\n## Important Notes\n\n- **Be concise**: Documentation should be brief but complete. This is reference documentation, not tutorial content.\n- **Keep it current**: Documentation should reflect the current behavior of the query.\n- **Use examples from tests**: If unit tests exist, use those code patterns as examples.\n- **Standard format**: Always use the format above for consistency across all query documentation.\n- **Metadata accuracy**: Ensure documented metadata matches actual query metadata.\n- **For workshops**: Use `explain_codeql_query` prompt when creating workshop content that requires deeper explanations and visual diagrams.\n';
@@ -64395,7 +64415,7 @@ ${content}`
6439564415
workshopCreationWorkflowSchema.shape,
6439664416
async ({ queryPath, language, workshopName, numStages }) => {
6439764417
const template = loadPromptTemplate("workshop-creation-workflow.prompt.md");
64398-
const derivedName = workshopName || basename6(queryPath).replace(/\.(ql|qlref)$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-") || "codeql-workshop";
64418+
const derivedName = workshopName || basename7(queryPath).replace(/\.(ql|qlref)$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-") || "codeql-workshop";
6439964419
const contextSection = buildWorkshopContext(
6440064420
queryPath,
6440164421
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.

0 commit comments

Comments
 (0)