Skip to content

Commit 78e0e55

Browse files
Copilotdata-douser
andcommitted
feat: backwards-compatible ql-mcp server pack installs for matching CodeQL CLI version
- Add public getCliVersion() to CliResolver with version parsing - Add version-aware pack download to PackInstaller using codeql pack download - Add CLI_VERSION_TO_PACK_VERSION mapping for backwards compatibility (v2.24.0 - v2.25.1) - Add autoDownloadPacks extension config setting (default: true) - Wire autoDownloadPacks config into extension activation - Add comprehensive unit tests for new functionality - Update extension settings documentation Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/c79f4c66-0204-436a-85bb-014f590878a1 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent c822c89 commit 78e0e55

File tree

8 files changed

+451
-12
lines changed

8 files changed

+451
-12
lines changed

docs/vscode/extension.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,14 @@ On activation the extension:
7171

7272
## Settings
7373

74-
| Setting | Default | Description |
75-
| --------------------------------- | ---------- | -------------------------------------------------------------- |
76-
| `codeql-mcp.autoInstall` | `true` | Automatically install/update the MCP server on activation |
77-
| `codeql-mcp.serverVersion` | `"latest"` | npm version to install (`"latest"` or a specific version) |
78-
| `codeql-mcp.serverCommand` | `"node"` | Command to launch the MCP server (override for local dev) |
79-
| `codeql-mcp.watchCodeqlExtension` | `true` | Discover databases and query results from the CodeQL extension |
80-
| `codeql-mcp.additionalEnv` | `{}` | Extra environment variables for the MCP server process |
74+
| Setting | Default | Description |
75+
| --------------------------------- | ---------- | ----------------------------------------------------------------------------------------- |
76+
| `codeql-mcp.autoDownloadPacks` | `true` | Download pre-compiled tool query packs matching the detected CodeQL CLI version from GHCR |
77+
| `codeql-mcp.autoInstall` | `true` | Automatically install/update the MCP server on activation |
78+
| `codeql-mcp.serverVersion` | `"latest"` | npm version to install (`"latest"` or a specific version) |
79+
| `codeql-mcp.serverCommand` | `"node"` | Command to launch the MCP server (override for local dev) |
80+
| `codeql-mcp.watchCodeqlExtension` | `true` | Discover databases and query results from the CodeQL extension |
81+
| `codeql-mcp.additionalEnv` | `{}` | Extra environment variables for the MCP server process |
8182

8283
## Commands
8384

extensions/vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@
8080
"default": [],
8181
"description": "Additional directories containing query run result subdirectories. Appended to CODEQL_QUERY_RUN_RESULTS_DIRS alongside the vscode-codeql query storage path."
8282
},
83+
"codeql-mcp.autoDownloadPacks": {
84+
"type": "boolean",
85+
"default": true,
86+
"markdownDescription": "Automatically download pre-compiled tool query packs from GHCR that match the detected CodeQL CLI version. When disabled, the extension uses only the VSIX-bundled packs via `codeql pack install` (which requires compilation against the local CLI). Disable this if you want strict use of the bundled packs for the same overall version of both CodeQL CLI and ql-mcp server."
87+
},
8388
"codeql-mcp.autoInstall": {
8489
"type": "boolean",
8590
"default": true,

extensions/vscode/src/codeql/cli-resolver.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const KNOWN_LOCATIONS = [
3131
*/
3232
export class CliResolver extends DisposableObject {
3333
private cachedPath: string | undefined | null = null; // null = not yet resolved
34+
private cachedVersion: string | undefined;
3435

3536
constructor(
3637
private readonly logger: Logger,
@@ -39,6 +40,17 @@ export class CliResolver extends DisposableObject {
3940
super();
4041
}
4142

43+
/**
44+
* Get the CodeQL CLI version string (e.g. '2.25.1').
45+
*
46+
* Returns the cached version detected during the most recent `resolve()`,
47+
* or `undefined` if no CLI has been resolved yet or the version could not
48+
* be parsed.
49+
*/
50+
getCliVersion(): string | undefined {
51+
return this.cachedVersion;
52+
}
53+
4254
/** Resolve the CodeQL CLI path. Returns `undefined` if not found. */
4355
async resolve(): Promise<string | undefined> {
4456
if (this.cachedPath !== null) {
@@ -93,19 +105,35 @@ export class CliResolver extends DisposableObject {
93105
/** Clear the cached path so the next `resolve()` re-probes. */
94106
invalidateCache(): void {
95107
this.cachedPath = null;
108+
this.cachedVersion = undefined;
96109
}
97110

98111
/** Check if a path exists and responds to `--version`. */
99112
private async validateBinary(binaryPath: string): Promise<boolean> {
100113
try {
101114
await access(binaryPath, constants.X_OK);
102-
await this.getVersion(binaryPath);
115+
const versionOutput = await this.getVersion(binaryPath);
116+
this.cachedVersion = CliResolver.parseVersionString(versionOutput);
103117
return true;
104118
} catch {
105119
return false;
106120
}
107121
}
108122

123+
/**
124+
* Parse a version number from the `codeql --version` output.
125+
*
126+
* Recognises both legacy and current formats:
127+
* - "CodeQL command-line toolchain release 2.19.0."
128+
* - "CodeQL CLI 2.25.1"
129+
*
130+
* Returns the bare version (e.g. '2.25.1') or `undefined` if not parseable.
131+
*/
132+
static parseVersionString(versionOutput: string): string | undefined {
133+
const match = /(\d+\.\d+\.\d+)/.exec(versionOutput);
134+
return match?.[1];
135+
}
136+
109137
/**
110138
* Discover the CodeQL CLI binary from the `vscode-codeql` extension's
111139
* managed distribution directory.

extensions/vscode/src/extension.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,12 @@ export async function activate(
170170
logger.info('Auto-install enabled — starting background setup...');
171171
logger.info(`Install directory: ${serverManager.getInstallDir?.() ?? 'unknown'}`);
172172
logger.info(`Server launch: ${serverManager.getDescription?.() ?? 'unknown'}`);
173+
const autoDownloadPacks = config.get<boolean>('autoDownloadPacks', true);
173174
// Run in background — don't block activation
174175
void (async () => {
175176
try {
176177
await serverManager.ensureInstalled();
177-
await packInstaller.installAll();
178+
await packInstaller.installAll({ downloadForCliVersion: autoDownloadPacks });
178179
mcpProvider.fireDidChange();
179180
logger.info('✅ MCP server setup complete. Server is ready to be started.');
180181
} catch (err) {

extensions/vscode/src/server/pack-installer.ts

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export interface PackInstallOptions {
2525
* The bundled copy is always preferred so that the packs used by
2626
* `codeql pack install` match the server code running from the VSIX.
2727
*
28+
* When the detected CodeQL CLI version differs from the version the
29+
* VSIX was built against, the installer downloads pre-compiled packs
30+
* from GHCR for the matching CLI version via `codeql pack download`.
31+
* This ensures backwards compatibility across published CLI versions.
32+
*
2833
* CodeQL library dependencies (e.g. `codeql/javascript-all`) must be
2934
* fetched from GHCR via `codeql pack install`. This class automates
3035
* the `codeql-development-mcp-server-setup-packs` step documented in
@@ -43,6 +48,28 @@ export class PackInstaller extends DisposableObject {
4348
'swift',
4449
] as const;
4550

51+
/**
52+
* Maps CodeQL CLI versions to the ql-mcp tools pack version published
53+
* for that CLI release. Each ql-mcp stable release is built against a
54+
* specific CodeQL CLI version, and the published pack version matches
55+
* the ql-mcp release version.
56+
*
57+
* Compatibility range: v2.24.0 (initial public release) through the
58+
* current release.
59+
*/
60+
static readonly CLI_VERSION_TO_PACK_VERSION: ReadonlyMap<string, string> =
61+
new Map([
62+
['2.24.0', '2.24.0'],
63+
['2.24.1', '2.24.1'],
64+
['2.24.2', '2.24.2'],
65+
['2.24.3', '2.24.3'],
66+
['2.25.0', '2.25.0'],
67+
['2.25.1', '2.25.1'],
68+
]);
69+
70+
/** Pack scope/prefix for all ql-mcp tools packs on GHCR. */
71+
static readonly PACK_SCOPE = 'advanced-security';
72+
4673
constructor(
4774
private readonly cliResolver: CliResolver,
4875
private readonly serverManager: ServerManager,
@@ -74,11 +101,43 @@ export class PackInstaller extends DisposableObject {
74101
);
75102
}
76103

104+
/**
105+
* Derive the target CodeQL CLI version from the extension's own
106+
* package version. The base version (X.Y.Z, stripping any
107+
* pre-release suffix like `-next.1`) corresponds to the CodeQL CLI
108+
* release the VSIX was built against.
109+
*/
110+
getTargetCliVersion(): string {
111+
const extensionVersion = this.serverManager.getExtensionVersion();
112+
return PackInstaller.baseVersion(extensionVersion);
113+
}
114+
115+
/**
116+
* Look up the ql-mcp tools pack version to download for a given
117+
* CodeQL CLI version.
118+
*
119+
* Returns the pack version string, or `undefined` if the CLI version
120+
* has no known compatible pack release.
121+
*/
122+
static getPackVersionForCli(cliVersion: string): string | undefined {
123+
const base = PackInstaller.baseVersion(cliVersion);
124+
return PackInstaller.CLI_VERSION_TO_PACK_VERSION.get(base);
125+
}
126+
77127
/**
78128
* Install CodeQL pack dependencies for all (or specified) languages.
79-
* Requires the npm package to be installed locally first (via ServerManager).
129+
*
130+
* When `downloadForCliVersion` is `true` (the default), the installer
131+
* detects the actual CodeQL CLI version and, if it differs from what
132+
* the VSIX was built against, downloads pre-compiled packs from GHCR
133+
* for the matching CLI version. When the CLI version matches, or when
134+
* downloading is disabled, falls back to `codeql pack install` on the
135+
* bundled pack sources.
80136
*/
81-
async installAll(options?: PackInstallOptions): Promise<void> {
137+
async installAll(options?: PackInstallOptions & {
138+
/** Download packs matching the detected CLI version (default: true). */
139+
downloadForCliVersion?: boolean;
140+
}): Promise<void> {
82141
const codeqlPath = await this.cliResolver.resolve();
83142
if (!codeqlPath) {
84143
this.logger.warn(
@@ -87,10 +146,87 @@ export class PackInstaller extends DisposableObject {
87146
return;
88147
}
89148

90-
const qlRoot = this.getQlpackRoot();
91149
const languages =
92150
options?.languages ?? [...PackInstaller.SUPPORTED_LANGUAGES];
93151

152+
const downloadEnabled = options?.downloadForCliVersion ?? true;
153+
const actualCliVersion = this.cliResolver.getCliVersion();
154+
const targetCliVersion = this.getTargetCliVersion();
155+
156+
if (downloadEnabled && actualCliVersion && actualCliVersion !== targetCliVersion) {
157+
this.logger.info(
158+
`CodeQL CLI version ${actualCliVersion} differs from VSIX target ${targetCliVersion}. ` +
159+
'Attempting to download compatible tool query packs...',
160+
);
161+
const downloaded = await this.downloadPacksForCliVersion(
162+
codeqlPath, actualCliVersion, languages,
163+
);
164+
if (downloaded) {
165+
return;
166+
}
167+
this.logger.info(
168+
'Pack download did not succeed for all languages — falling back to bundled pack install.',
169+
);
170+
}
171+
172+
// Default path: install dependencies for bundled packs
173+
await this.installBundledPacks(codeqlPath, languages);
174+
}
175+
176+
/**
177+
* Download pre-compiled tool query packs from GHCR for the specified
178+
* CodeQL CLI version.
179+
*
180+
* Returns `true` if all requested languages were downloaded
181+
* successfully, `false` otherwise.
182+
*/
183+
async downloadPacksForCliVersion(
184+
codeqlPath: string,
185+
cliVersion: string,
186+
languages: string[],
187+
): Promise<boolean> {
188+
const packVersion = PackInstaller.getPackVersionForCli(cliVersion);
189+
if (!packVersion) {
190+
this.logger.warn(
191+
`No known ql-mcp pack version for CodeQL CLI ${cliVersion}. ` +
192+
'Falling back to bundled packs.',
193+
);
194+
return false;
195+
}
196+
197+
this.logger.info(
198+
`Downloading ql-mcp tool query packs v${packVersion} for CodeQL CLI ${cliVersion}...`,
199+
);
200+
201+
let allSucceeded = true;
202+
for (const lang of languages) {
203+
const packRef =
204+
`${PackInstaller.PACK_SCOPE}/ql-mcp-${lang}-tools-src@${packVersion}`;
205+
this.logger.info(`Downloading ${packRef}...`);
206+
try {
207+
await this.runCodeqlPackDownload(codeqlPath, packRef);
208+
this.logger.info(`Downloaded ${packRef}.`);
209+
} catch (err) {
210+
this.logger.error(
211+
`Failed to download ${packRef}: ${err instanceof Error ? err.message : String(err)}`,
212+
);
213+
allSucceeded = false;
214+
}
215+
}
216+
return allSucceeded;
217+
}
218+
219+
/**
220+
* Install pack dependencies for bundled packs using `codeql pack install`.
221+
* This is the original behaviour and serves as the fallback when pack
222+
* download is disabled or unavailable.
223+
*/
224+
private async installBundledPacks(
225+
codeqlPath: string,
226+
languages: string[],
227+
): Promise<void> {
228+
const qlRoot = this.getQlpackRoot();
229+
94230
for (const lang of languages) {
95231
const packDir = join(qlRoot, 'ql', lang, 'tools', 'src');
96232

@@ -136,4 +272,37 @@ export class PackInstaller extends DisposableObject {
136272
);
137273
});
138274
}
275+
276+
/** Run `codeql pack download` for a pack reference (e.g. scope/name@version). */
277+
private runCodeqlPackDownload(
278+
codeqlPath: string,
279+
packRef: string,
280+
): Promise<void> {
281+
return new Promise((resolve, reject) => {
282+
execFile(
283+
codeqlPath,
284+
['pack', 'download', packRef],
285+
{ timeout: 300_000 },
286+
(err, _stdout, stderr) => {
287+
if (err) {
288+
reject(
289+
new Error(`codeql pack download failed: ${stderr || err.message}`),
290+
);
291+
return;
292+
}
293+
resolve();
294+
},
295+
);
296+
});
297+
}
298+
299+
/**
300+
* Strip any pre-release suffix from a semver string, returning
301+
* only the `MAJOR.MINOR.PATCH` portion.
302+
*/
303+
static baseVersion(version: string): string {
304+
const stripped = version.startsWith('v') ? version.slice(1) : version;
305+
const match = /^(\d+\.\d+\.\d+)/.exec(stripped);
306+
return match ? match[1] : stripped;
307+
}
139308
}

0 commit comments

Comments
 (0)