Skip to content

Commit a66c4a2

Browse files
committed
fix: Address PR #24 review feedback
- Replace hardcoded version in language-server.ts with dynamic getPackageVersion() that reads from package.json (cached) - Add getUserWorkspaceDir() for user-relative path resolution that falls back to process.cwd() in npm-installed (non-monorepo) layouts - Honor CODEQL_MCP_TMP_DIR env var in temp-dir.ts for read-only package root scenarios (e.g., npm global installs) - Move session-data-manager default storage from packageRoot to getProjectTmpBase() so it respects CODEQL_MCP_TMP_DIR override - Fix misleading test name: "should accept valid CODEQL_PATH" was actually testing rejection of non-existent paths - Add skipIf(win32) guard on sh-dependent PATH prepend test
1 parent 6e2e0e2 commit a66c4a2

8 files changed

Lines changed: 101 additions & 29 deletions

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

Lines changed: 31 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 2 additions & 2 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: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { executeCodeQLCommand, executeQLTCommand, CLIExecutionResult } from './c
88
import { logger } from '../utils/logger';
99
import { evaluateQueryResults, QueryEvaluationResult, extractQueryMetadata } from './query-results-evaluator';
1010
import { getOrCreateLogDirectory } from './log-directory-manager';
11-
import { packageRootDir, resolveToolQueryPackPath, workspaceRootDir } from '../utils/package-paths';
11+
import { getUserWorkspaceDir, packageRootDir, resolveToolQueryPackPath } from '../utils/package-paths';
1212
import { writeFileSync, rmSync, existsSync, mkdirSync } from 'fs';
1313
import { basename, dirname, isAbsolute, join, resolve } from 'path';
1414
import { createProjectTempDir } from '../utils/temp-dir';
@@ -215,19 +215,21 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
215215
case 'codeql_test_run':
216216
case 'codeql_resolve_tests':
217217
// Handle tests parameter as positional arguments for test tools.
218-
// Resolve relative paths against workspaceRootDir since the MCP
219-
// server's cwd may not be the repo root.
218+
// Resolve relative paths against the user's effective workspace
219+
// directory. In monorepo layouts this is the repo root; in npm-
220+
// installed layouts it falls back to process.cwd().
220221
if (tests && Array.isArray(tests)) {
222+
const userDir = getUserWorkspaceDir();
221223
positionalArgs = [...positionalArgs, ...(tests as string[]).map(
222-
t => isAbsolute(t) ? t : resolve(workspaceRootDir, t)
224+
t => isAbsolute(t) ? t : resolve(userDir, t)
223225
)];
224226
}
225227
break;
226228

227229
case 'codeql_query_run': {
228230
// Resolve database path to absolute path if it's relative
229231
if (options.database && typeof options.database === 'string' && !isAbsolute(options.database)) {
230-
options.database = resolve(workspaceRootDir, options.database);
232+
options.database = resolve(getUserWorkspaceDir(), options.database);
231233
logger.info(`Resolved database path to: ${options.database}`);
232234
}
233235

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

397399
// Add --additional-packs for commands that need to access local test packs.

server/src/lib/language-server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { delimiter, join } from 'path';
1111
import { logger } from '../utils/logger';
1212
import { getProjectTmpDir } from '../utils/temp-dir';
1313
import { getResolvedCodeQLDir } from './cli-executor';
14+
import { getPackageVersion } from '../utils/package-paths';
1415

1516
export interface LSPMessage {
1617
jsonrpc: '2.0';
@@ -243,7 +244,7 @@ export class CodeQLLanguageServer extends EventEmitter {
243244
processId: process.pid,
244245
clientInfo: {
245246
name: 'codeql-development-mcp-server',
246-
version: '2.23.9'
247+
version: getPackageVersion()
247248
},
248249
capabilities: {
249250
textDocument: {

server/src/lib/session-data-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { JSONFileSync } from 'lowdb/node';
88
import { mkdirSync, writeFileSync } from 'fs';
99
import { join } from 'path';
1010
import { randomUUID } from 'crypto';
11-
import { getPackageRootDir } from '../utils/package-paths';
11+
import { getProjectTmpBase } from '../utils/temp-dir';
1212
import {
1313
QueryDevelopmentSession,
1414
QueryState,
@@ -403,6 +403,6 @@ function parseBoolEnv(envVar: string | undefined, defaultValue: boolean): boolea
403403

404404
// Export singleton instance with environment variable support
405405
export const sessionDataManager = new SessionDataManager({
406-
storageLocation: process.env.MONITORING_STORAGE_LOCATION || join(getPackageRootDir(), '.ql-mcp-tracking'),
406+
storageLocation: process.env.MONITORING_STORAGE_LOCATION || join(getProjectTmpBase(), 'ql-mcp-tracking'),
407407
enableMonitoringTools: parseBoolEnv(process.env.ENABLE_MONITORING_TOOLS, false),
408408
});

server/src/utils/package-paths.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,48 @@ export function resolveToolQueryPackPath(language: string, packageRoot?: string)
8686
return resolve(pkgRoot, 'ql', language, 'tools', 'src');
8787
}
8888

89+
/**
90+
* Read the package version from the nearest package.json.
91+
*
92+
* Cached at first call so the file is read at most once per process.
93+
*/
94+
let _cachedVersion: string | undefined;
95+
export function getPackageVersion(): string {
96+
if (_cachedVersion !== undefined) return _cachedVersion;
97+
try {
98+
const pkgPath = resolve(getPackageRootDir(), 'package.json');
99+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
100+
_cachedVersion = pkg.version ?? '0.0.0';
101+
} catch {
102+
_cachedVersion = '0.0.0';
103+
}
104+
return _cachedVersion as string;
105+
}
106+
107+
/**
108+
* Get the effective workspace directory for resolving user-supplied relative
109+
* paths (test directories, database paths, pack dirs, etc.).
110+
*
111+
* In a monorepo checkout the workspace root is the monorepo parent. In an
112+
* npm-installed layout, `workspaceRootDir` falls back to `packageRootDir`
113+
* which may be read-only and is not the user's project. In that case we
114+
* fall back to `process.cwd()` so that relative paths resolve against the
115+
* directory the user actually invoked the server from.
116+
*
117+
* Override with `CODEQL_MCP_WORKSPACE` for deterministic behavior.
118+
*/
119+
export function getUserWorkspaceDir(): string {
120+
if (process.env.CODEQL_MCP_WORKSPACE) {
121+
return process.env.CODEQL_MCP_WORKSPACE;
122+
}
123+
// When workspaceRootDir === packageRootDir we are NOT in a monorepo
124+
// (npm-installed), so fall back to process.cwd().
125+
if (workspaceRootDir === packageRootDir) {
126+
return process.cwd();
127+
}
128+
return workspaceRootDir;
129+
}
130+
89131
// Pre-computed values for use throughout the server
90132
export const packageRootDir = getPackageRootDir();
91133
export const workspaceRootDir = getWorkspaceRootDir(packageRootDir);

server/src/utils/temp-dir.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import { getPackageRootDir } from './package-paths';
1313

1414
/**
1515
* Base directory for all project-local temporary data.
16-
* Stored under `<packageRoot>/.tmp` and excluded from version control.
16+
*
17+
* Resolution order:
18+
* 1. `CODEQL_MCP_TMP_DIR` environment variable — for read-only package root
19+
* scenarios (e.g., npm global installs where the package directory is not
20+
* writable).
21+
* 2. `<packageRoot>/.tmp` — default; excluded from version control.
1722
*/
18-
const PROJECT_TMP_BASE = join(getPackageRootDir(), '.tmp');
23+
const PROJECT_TMP_BASE = process.env.CODEQL_MCP_TMP_DIR || join(getPackageRootDir(), '.tmp');
1924

2025
/**
2126
* Return the project-local `.tmp` base directory, creating it if needed.

server/test/src/lib/cli-executor.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -635,11 +635,9 @@ describe('resolveCodeQLBinary', () => {
635635
expect(() => resolveCodeQLBinary()).toThrow('does not exist');
636636
});
637637

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

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

0 commit comments

Comments
 (0)