Skip to content

Commit 24ba5d2

Browse files
Copilotdata-douser
andauthored
Address review round 4: empty string edge cases, process.env guards, log clarity
- BQRS file param: check `file !== undefined` instead of truthiness, validate with `trim()` to catch whitespace-only strings - bqrs_interpret database: always delete key from options regardless of value; fail fast on empty/whitespace-only string with clear error - environment-builder: check `process.env` for inherited values instead of the freshly-constructed `env` object (which is always empty) - result-processor: log "result count unknown" instead of "0 results" when resultCount is null - Added tests: empty string file, whitespace file, empty database param, process.env ENABLE_ANNOTATION_TOOLS preservation Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/f9deedbc-dbc0-4432-9d7b-775d1a9f624f Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent 2cfe94b commit 24ba5d2

File tree

5 files changed

+121
-33
lines changed

5 files changed

+121
-33
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,18 @@ export class EnvironmentBuilder extends DisposableObject {
151151
// The setting controls ENABLE_ANNOTATION_TOOLS and defaults
152152
// MONITORING_STORAGE_LOCATION to the scratch directory so tools work
153153
// out-of-the-box without manual env var configuration.
154-
// Only set these when not already defined, to avoid overriding values
155-
// inherited from a parent process or earlier env setup.
154+
// Respect values inherited from the extension host process environment;
155+
// only apply defaults when not already defined there. The additionalEnv
156+
// block below still overrides everything for advanced users.
156157
const enableAnnotations = config.get<boolean>('enableAnnotationTools', true);
157-
if (!('ENABLE_ANNOTATION_TOOLS' in env)) {
158+
if (typeof process.env.ENABLE_ANNOTATION_TOOLS === 'string') {
159+
env.ENABLE_ANNOTATION_TOOLS = process.env.ENABLE_ANNOTATION_TOOLS;
160+
} else {
158161
env.ENABLE_ANNOTATION_TOOLS = enableAnnotations ? 'true' : 'false';
159162
}
160-
if (
161-
enableAnnotations &&
162-
env.CODEQL_MCP_SCRATCH_DIR &&
163-
!('MONITORING_STORAGE_LOCATION' in env)
164-
) {
163+
if (typeof process.env.MONITORING_STORAGE_LOCATION === 'string') {
164+
env.MONITORING_STORAGE_LOCATION = process.env.MONITORING_STORAGE_LOCATION;
165+
} else if (enableAnnotations && env.CODEQL_MCP_SCRATCH_DIR) {
165166
env.MONITORING_STORAGE_LOCATION = env.CODEQL_MCP_SCRATCH_DIR;
166167
}
167168

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -281,33 +281,26 @@ describe('EnvironmentBuilder', () => {
281281
it('should not overwrite MONITORING_STORAGE_LOCATION if already set in parent env', async () => {
282282
const vscode = await import('vscode');
283283
const origFolders = vscode.workspace.workspaceFolders;
284-
const originalGetConfig = vscode.workspace.getConfiguration;
284+
const origMonLoc = process.env.MONITORING_STORAGE_LOCATION;
285285

286286
try {
287287
(vscode.workspace.workspaceFolders as any) = [
288288
{ uri: { fsPath: '/mock/workspace' }, name: 'ws', index: 0 },
289289
];
290290
// Simulate parent process env with MONITORING_STORAGE_LOCATION already set
291-
vscode.workspace.getConfiguration = () => ({
292-
get: (_key: string, defaultVal?: any) => {
293-
if (_key === 'additionalEnv') return { MONITORING_STORAGE_LOCATION: '/custom/storage/path' };
294-
if (_key === 'additionalDatabaseDirs') return [];
295-
if (_key === 'additionalQueryRunResultsDirs') return [];
296-
if (_key === 'additionalMrvaRunResultsDirs') return [];
297-
return defaultVal;
298-
},
299-
has: () => false,
300-
inspect: () => undefined as any,
301-
update: () => Promise.resolve(),
302-
}) as any;
291+
process.env.MONITORING_STORAGE_LOCATION = '/custom/storage/path';
303292

304293
builder.invalidate();
305294
const env = await builder.build();
306-
// additionalEnv should override the default MONITORING_STORAGE_LOCATION
295+
// process.env value should be preserved
307296
expect(env.MONITORING_STORAGE_LOCATION).toBe('/custom/storage/path');
308297
} finally {
309298
(vscode.workspace.workspaceFolders as any) = origFolders;
310-
vscode.workspace.getConfiguration = originalGetConfig;
299+
if (origMonLoc === undefined) {
300+
delete process.env.MONITORING_STORAGE_LOCATION;
301+
} else {
302+
process.env.MONITORING_STORAGE_LOCATION = origMonLoc;
303+
}
311304
}
312305
});
313306

@@ -380,4 +373,23 @@ describe('EnvironmentBuilder', () => {
380373
vscode.workspace.getConfiguration = originalGetConfig;
381374
}
382375
});
376+
377+
it('should preserve ENABLE_ANNOTATION_TOOLS from parent process environment', async () => {
378+
const origValue = process.env.ENABLE_ANNOTATION_TOOLS;
379+
380+
try {
381+
process.env.ENABLE_ANNOTATION_TOOLS = 'false';
382+
383+
builder.invalidate();
384+
const env = await builder.build();
385+
// Inherited process.env value should be preserved
386+
expect(env.ENABLE_ANNOTATION_TOOLS).toBe('false');
387+
} finally {
388+
if (origValue === undefined) {
389+
delete process.env.ENABLE_ANNOTATION_TOOLS;
390+
} else {
391+
process.env.ENABLE_ANNOTATION_TOOLS = origValue;
392+
}
393+
}
394+
});
383395
});

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,10 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
198198
positionalArgs = [...positionalArgs, ...files as string[]];
199199
}
200200

201-
// Handle file parameter as positional argument for BQRS tools
202-
if (file && name.startsWith('codeql_bqrs_')) {
201+
// Handle file parameter as positional argument for BQRS tools.
202+
// Check for key presence (not truthiness) so that empty strings
203+
// are caught by the validation below rather than silently skipped.
204+
if (file !== undefined && name.startsWith('codeql_bqrs_')) {
203205
// Defensive coercion: handle file value that is an actual array
204206
// or a JSON-encoded array string (e.g. '["/path/to/file.bqrs"]')
205207
let cleanFile = Array.isArray(file) ? (file.length > 0 ? String(file[0]) : '') : String(file);
@@ -213,7 +215,7 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
213215
}
214216
} catch { /* not valid JSON — use as-is */ }
215217
}
216-
if (!cleanFile) {
218+
if (!cleanFile.trim()) {
217219
throw new Error('The "file" parameter for BQRS tools must be a non-empty string path to a .bqrs file.');
218220
}
219221
positionalArgs = [...positionalArgs, cleanFile];
@@ -399,11 +401,19 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
399401
break;
400402
}
401403

402-
case 'codeql_bqrs_interpret':
404+
case 'codeql_bqrs_interpret': {
403405
// Map 'database' to '--source-archive' and '--source-location-prefix'
404-
// for codeql bqrs interpret (only when not explicitly provided)
405-
if (options.database) {
406-
const dbPath = resolveDatabasePath(options.database as string);
406+
// for codeql bqrs interpret (only when not explicitly provided).
407+
// Always delete the synthetic 'database' key to prevent it from
408+
// being forwarded as an unsupported CLI flag.
409+
const dbValue = options.database;
410+
delete options.database;
411+
if (dbValue !== undefined) {
412+
const dbStr = String(dbValue).trim();
413+
if (!dbStr) {
414+
throw new Error('The "database" parameter must be a non-empty path to a CodeQL database.');
415+
}
416+
const dbPath = resolveDatabasePath(dbStr);
407417
if (!options['source-archive']) {
408418
const srcZipPath = join(dbPath, 'src.zip');
409419
const srcDirPath = join(dbPath, 'src');
@@ -412,7 +422,6 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
412422
} else if (existsSync(srcDirPath)) {
413423
options['source-archive'] = srcDirPath;
414424
} else {
415-
delete options.database;
416425
throw new Error(
417426
`CodeQL database at "${dbPath}" does not contain a source archive (expected "src.zip" file or "src" directory).`,
418427
);
@@ -432,9 +441,9 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
432441
// codeql-database.yml missing or unparseable — skip prefix resolution
433442
}
434443
}
435-
delete options.database;
436444
}
437445
break;
446+
}
438447

439448
case 'codeql_query_compile':
440449
case 'codeql_resolve_metadata':

server/src/lib/result-processor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ export function cacheDatabaseAnalyzeResults(
382382
resultCount,
383383
});
384384

385-
logger.info(`Cached database-analyze results with key: ${cacheKey} (${resultCount ?? 0} results)`);
385+
logger.info(`Cached database-analyze results with key: ${cacheKey} (${resultCount != null ? `${resultCount} results` : 'result count unknown'})`);
386386
} catch (err) {
387387
logger.error('Failed to cache database-analyze results:', err);
388388
}

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,4 +1301,70 @@ describe('registerCLITool handler behavior', () => {
13011301
rmSync(tmpDir, { recursive: true, force: true });
13021302
}
13031303
});
1304+
1305+
it('should return error when file parameter is an empty string for BQRS tools', async () => {
1306+
const definition: CLIToolDefinition = {
1307+
name: 'codeql_bqrs_info',
1308+
description: 'BQRS info',
1309+
command: 'codeql',
1310+
subcommand: 'bqrs info',
1311+
inputSchema: {
1312+
file: z.string()
1313+
}
1314+
};
1315+
1316+
registerCLITool(mockServer, definition);
1317+
1318+
const handler = (mockServer.tool as ReturnType<typeof vi.fn>).mock.calls[0][3];
1319+
1320+
const result = await handler({ file: '' });
1321+
expect(result.isError).toBe(true);
1322+
expect(result.content[0].text).toContain('The "file" parameter for BQRS tools must be a non-empty string path to a .bqrs file.');
1323+
});
1324+
1325+
it('should return error when file parameter is a whitespace-only string for BQRS tools', async () => {
1326+
const definition: CLIToolDefinition = {
1327+
name: 'codeql_bqrs_decode',
1328+
description: 'Decode BQRS',
1329+
command: 'codeql',
1330+
subcommand: 'bqrs decode',
1331+
inputSchema: {
1332+
file: z.string()
1333+
}
1334+
};
1335+
1336+
registerCLITool(mockServer, definition);
1337+
1338+
const handler = (mockServer.tool as ReturnType<typeof vi.fn>).mock.calls[0][3];
1339+
1340+
const result = await handler({ file: ' ' });
1341+
expect(result.isError).toBe(true);
1342+
expect(result.content[0].text).toContain('The "file" parameter for BQRS tools must be a non-empty string path to a .bqrs file.');
1343+
});
1344+
1345+
it('should return error when database parameter is an empty string for bqrs_interpret', async () => {
1346+
const definition: CLIToolDefinition = {
1347+
name: 'codeql_bqrs_interpret',
1348+
description: 'Interpret BQRS',
1349+
command: 'codeql',
1350+
subcommand: 'bqrs interpret',
1351+
inputSchema: {
1352+
file: z.string(),
1353+
database: z.string().optional(),
1354+
format: z.string().optional()
1355+
}
1356+
};
1357+
1358+
registerCLITool(mockServer, definition);
1359+
1360+
const handler = (mockServer.tool as ReturnType<typeof vi.fn>).mock.calls[0][3];
1361+
1362+
const result = await handler({
1363+
file: '/path/to/results.bqrs',
1364+
database: '',
1365+
format: 'sarif-latest'
1366+
});
1367+
expect(result.isError).toBe(true);
1368+
expect(result.content[0].text).toContain('The "database" parameter must be a non-empty path to a CodeQL database.');
1369+
});
13041370
});

0 commit comments

Comments
 (0)