Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions server/dist/codeql-development-mcp-server.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/dist/codeql-development-mcp-server.js.map

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions server/src/lib/cli-tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { executeCodeQLCommand, executeQLTCommand, CLIExecutionResult } from './c
import { logger } from '../utils/logger';
import { evaluateQueryResults, QueryEvaluationResult, extractQueryMetadata } from './query-results-evaluator';
import { getOrCreateLogDirectory } from './log-directory-manager';
import { packageRootDir, resolveToolQueryPackPath, workspaceRootDir } from '../utils/package-paths';
import { getUserWorkspaceDir, packageRootDir, resolveToolQueryPackPath } from '../utils/package-paths';
import { writeFileSync, rmSync, existsSync, mkdirSync } from 'fs';
import { basename, dirname, isAbsolute, join, resolve } from 'path';
import { createProjectTempDir } from '../utils/temp-dir';
Expand Down Expand Up @@ -215,19 +215,21 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
case 'codeql_test_run':
case 'codeql_resolve_tests':
// Handle tests parameter as positional arguments for test tools.
// Resolve relative paths against workspaceRootDir since the MCP
// server's cwd may not be the repo root.
// Resolve relative paths against the user's effective workspace
// directory. In monorepo layouts this is the repo root; in npm-
// installed layouts it falls back to process.cwd().
if (tests && Array.isArray(tests)) {
const userDir = getUserWorkspaceDir();
positionalArgs = [...positionalArgs, ...(tests as string[]).map(
t => isAbsolute(t) ? t : resolve(workspaceRootDir, t)
t => isAbsolute(t) ? t : resolve(userDir, t)
)];
Comment thread
data-douser marked this conversation as resolved.
}
break;

case 'codeql_query_run': {
// Resolve database path to absolute path if it's relative
if (options.database && typeof options.database === 'string' && !isAbsolute(options.database)) {
options.database = resolve(workspaceRootDir, options.database);
options.database = resolve(getUserWorkspaceDir(), options.database);
logger.info(`Resolved database path to: ${options.database}`);
}

Expand Down Expand Up @@ -389,9 +391,9 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
let cwd: string | undefined;
if ((name === 'codeql_pack_install' || name === 'codeql_pack_ls') && (dir || packDir)) {
const rawCwd = (dir || packDir) as string;
// Resolve relative paths against the workspace root, not process.cwd(),
// since the MCP server's cwd may differ (especially in VS Code).
cwd = isAbsolute(rawCwd) ? rawCwd : resolve(workspaceRootDir, rawCwd);
// Resolve relative paths against the user's effective workspace
// directory rather than a potentially read-only package root.
cwd = isAbsolute(rawCwd) ? rawCwd : resolve(getUserWorkspaceDir(), rawCwd);
}

// Add --additional-packs for commands that need to access local test packs.
Expand Down
3 changes: 2 additions & 1 deletion server/src/lib/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { delimiter, join } from 'path';
import { logger } from '../utils/logger';
import { getProjectTmpDir } from '../utils/temp-dir';
import { getResolvedCodeQLDir } from './cli-executor';
import { getPackageVersion } from '../utils/package-paths';
Comment thread
data-douser marked this conversation as resolved.
Outdated

export interface LSPMessage {
jsonrpc: '2.0';
Expand Down Expand Up @@ -243,7 +244,7 @@ export class CodeQLLanguageServer extends EventEmitter {
processId: process.pid,
clientInfo: {
name: 'codeql-development-mcp-server',
version: '2.23.9'
version: getPackageVersion()
},
capabilities: {
textDocument: {
Expand Down
4 changes: 2 additions & 2 deletions server/src/lib/session-data-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { JSONFileSync } from 'lowdb/node';
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
import { getPackageRootDir } from '../utils/package-paths';
import { getProjectTmpBase } from '../utils/temp-dir';
import {
QueryDevelopmentSession,
QueryState,
Expand Down Expand Up @@ -403,6 +403,6 @@ function parseBoolEnv(envVar: string | undefined, defaultValue: boolean): boolea

// Export singleton instance with environment variable support
export const sessionDataManager = new SessionDataManager({
storageLocation: process.env.MONITORING_STORAGE_LOCATION || join(getPackageRootDir(), '.ql-mcp-tracking'),
storageLocation: process.env.MONITORING_STORAGE_LOCATION || join(getProjectTmpBase(), 'ql-mcp-tracking'),
Comment thread
data-douser marked this conversation as resolved.
Outdated
enableMonitoringTools: parseBoolEnv(process.env.ENABLE_MONITORING_TOOLS, false),
});
42 changes: 42 additions & 0 deletions server/src/utils/package-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,48 @@ export function resolveToolQueryPackPath(language: string, packageRoot?: string)
return resolve(pkgRoot, 'ql', language, 'tools', 'src');
}

/**
* Read the package version from the nearest package.json.
*
* Cached at first call so the file is read at most once per process.
*/
let _cachedVersion: string | undefined;
export function getPackageVersion(): string {
if (_cachedVersion !== undefined) return _cachedVersion;
try {
const pkgPath = resolve(getPackageRootDir(), 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
_cachedVersion = pkg.version ?? '0.0.0';
} catch {
_cachedVersion = '0.0.0';
}
return _cachedVersion as string;
}
Comment thread
data-douser marked this conversation as resolved.

/**
* Get the effective workspace directory for resolving user-supplied relative
* paths (test directories, database paths, pack dirs, etc.).
*
* In a monorepo checkout the workspace root is the monorepo parent. In an
* npm-installed layout, `workspaceRootDir` falls back to `packageRootDir`
* which may be read-only and is not the user's project. In that case we
* fall back to `process.cwd()` so that relative paths resolve against the
* directory the user actually invoked the server from.
*
* Override with `CODEQL_MCP_WORKSPACE` for deterministic behavior.
*/
export function getUserWorkspaceDir(): string {
if (process.env.CODEQL_MCP_WORKSPACE) {
return process.env.CODEQL_MCP_WORKSPACE;
}
// When workspaceRootDir === packageRootDir we are NOT in a monorepo
// (npm-installed), so fall back to process.cwd().
if (workspaceRootDir === packageRootDir) {
return process.cwd();
}
return workspaceRootDir;
}
Comment thread
data-douser marked this conversation as resolved.

// Pre-computed values for use throughout the server
export const packageRootDir = getPackageRootDir();
export const workspaceRootDir = getWorkspaceRootDir(packageRootDir);
9 changes: 7 additions & 2 deletions server/src/utils/temp-dir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import { getPackageRootDir } from './package-paths';

/**
* Base directory for all project-local temporary data.
* Stored under `<packageRoot>/.tmp` and excluded from version control.
*
* Resolution order:
* 1. `CODEQL_MCP_TMP_DIR` environment variable — for read-only package root
* scenarios (e.g., npm global installs where the package directory is not
* writable).
* 2. `<packageRoot>/.tmp` — default; excluded from version control.
*/
const PROJECT_TMP_BASE = join(getPackageRootDir(), '.tmp');
const PROJECT_TMP_BASE = process.env.CODEQL_MCP_TMP_DIR || join(getPackageRootDir(), '.tmp');
Comment thread
data-douser marked this conversation as resolved.
Outdated

/**
* Return the project-local `.tmp` base directory, creating it if needed.
Expand Down
11 changes: 5 additions & 6 deletions server/test/src/lib/cli-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,11 +635,9 @@ describe('resolveCodeQLBinary', () => {
expect(() => resolveCodeQLBinary()).toThrow('does not exist');
});

it('should accept a valid absolute CODEQL_PATH pointing to an existing file', () => {
// Use /bin/echo as a stand-in for an existing file named "codeql"
// We can't actually test with a real codeql binary in unit tests,
// so test the basename validation and existence check separately.
// Here we test that a non-existent but well-named path is rejected for non-existence.
it('should reject non-existent CODEQL_PATH even with valid basename', () => {
// Verify that a well-named but non-existent path is rejected for non-existence
// (the basename 'codeql' passes validation, but the file doesn't exist).
process.env.CODEQL_PATH = '/tmp/nonexistent-dir/codeql';
expect(() => resolveCodeQLBinary()).toThrow('does not exist');
});
Expand Down Expand Up @@ -694,8 +692,9 @@ describe('CODEQL_PATH - PATH prepend integration', () => {
expect(getResolvedCodeQLDir()).toBeNull();
});

it('should prepend CODEQL_PATH directory to child process PATH', async () => {
it.skipIf(process.platform === 'win32')('should prepend CODEQL_PATH directory to child process PATH', async () => {
// Create a temporary directory with a fake "codeql" script
// Skipped on Windows: uses sh and #!/bin/sh shebang
const tmpDir = createProjectTempDir('codeql-path-prepend-test-');
const codeqlPath = join(tmpDir, 'codeql');
writeFileSync(codeqlPath, '#!/bin/sh\necho test', { mode: 0o755 });
Expand Down
Loading