Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 15 additions & 2 deletions .github/actions/setup-codeql-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ runs:
echo " CodeQL Version: $CODEQL_VERSION"
echo " Cache Key: $CODEQL_CACHE_KEY"

- name: Cache `gh-codeql` extension and CodeQL packages
id: cache-codeql
- name: Cache `gh-codeql` extension and CodeQL packages (Unix)
id: cache-codeql-unix
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: |
Expand All @@ -86,6 +87,18 @@ runs:
restore-keys: |
gh-codeql-${{ runner.os }}-${{ steps.codeql-version.outputs.codeql-version }}-

- name: Cache `gh-codeql` extension and CodeQL packages (Windows)
id: cache-codeql-windows
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: |
~\AppData\Local\GitHub\gh-codeql
~\.codeql\packages
key: ${{ steps.codeql-version.outputs.codeql-cache-key }}
restore-keys: |
gh-codeql-${{ runner.os }}-${{ steps.codeql-version.outputs.codeql-version }}-

# Install GitHub CLI CodeQL extension and set `codeql` CLI version
- name: Install GitHub CLI CodeQL extension and set version
id: install-gh-codeql
Expand Down
48 changes: 38 additions & 10 deletions .github/workflows/client-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ on:
paths:
- '.github/actions/setup-codeql-environment/action.yml'
- '.github/workflows/client-integration-tests.yml'
- '.node-version'
- '.codeql-version'
- '.node-version'
- 'client/**'
- 'server/**'
pull_request:
branches: [main]
paths:
- '.github/actions/setup-codeql-environment/action.yml'
- '.github/workflows/client-integration-tests.yml'
- '.node-version'
- '.codeql-version'
- '.node-version'
- 'client/**'
- 'server/**'
workflow_dispatch:
Expand All @@ -26,7 +26,13 @@ permissions:

jobs:
integration-tests:
runs-on: ubuntu-latest
name: Integration Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

env:
HTTP_HOST: 'localhost'
Expand All @@ -44,9 +50,14 @@ jobs:
cache: 'npm'
node-version-file: '.node-version'

- name: MCP Integration Tests - Install OS dependencies
- name: MCP Integration Tests - Install OS dependencies (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get install -y jq

- name: MCP Integration Tests - Install OS dependencies (Windows)
if: runner.os == 'Windows'
run: choco install jq -y

- name: MCP Integration Tests - Install node dependencies for client and server workspaces
run: npm ci --workspace=client && npm ci --workspace=server

Expand All @@ -57,42 +68,59 @@ jobs:

## Install packs used in the integration tests.
- name: MCP Integration Tests - Install CodeQL packs
shell: bash
run: ./server/scripts/install-packs.sh

## Extract test databases used in the integration tests.
- name: MCP Integration Tests - Extract test databases
shell: bash
run: ./server/scripts/extract-test-databases.sh

## Configure npm to use bash for running scripts on Windows, since the
## integration test scripts are bash scripts that cmd.exe cannot execute.
- name: MCP Integration Tests - Configure npm script shell (Windows)
if: runner.os == 'Windows'
shell: bash
run: npm config set script-shell "$(which bash)"

## Run integration tests. This script builds the server bundle and runs tests.
## We do NOT use 'npm run build-and-test' as it runs query unit tests which
## have a dedicated workflow (query-unit-tests.yml).
- name: MCP Integration Tests - Run integration tests
shell: bash
run: npm run test:integration --workspace=client

- name: MCP Integration Tests - Stop the background MCP server process
if: always()
shell: bash
run: |
if [ -f server.pid ]; then
PID=$(cat server.pid)
echo "Stopping server with PID $PID"
if kill -0 $PID 2>/dev/null; then
kill $PID || true
if kill -0 "$PID" 2>/dev/null; then
kill "$PID" || true
sleep 2
# Force kill if still running
if kill -0 $PID 2>/dev/null; then
if kill -0 "$PID" 2>/dev/null; then
echo "Force killing server process"
kill -9 $PID || true
kill -9 "$PID" || true
fi
else
echo "Server process was not running"
fi
rm server.pid
rm -f server.pid
else
echo "No server.pid file found"
fi

# Clean up log files
if [ -f server.log ]; then
echo "Removing server.log"
rm server.log
rm -f server.log
fi

- name: MCP Integration Tests - Summary
shell: bash
run: |
echo "## Integration Tests Summary (${{ matrix.os }})" >> $GITHUB_STEP_SUMMARY
echo "βœ… MCP server integration tests passed on ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY
4 changes: 2 additions & 2 deletions client/src/ql-mcp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { execSync } from "child_process";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import { fileURLToPath, pathToFileURL } from "url";
import path from "path";

import { IntegrationTestRunner } from "./lib/integration-test-runner.js";
Expand Down Expand Up @@ -807,7 +807,7 @@ async function main() {
}

// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
Expand Down
23 changes: 13 additions & 10 deletions server/dist/ql-mcp-server.js

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

6 changes: 3 additions & 3 deletions server/dist/ql-mcp-server.js.map

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions server/src/lib/cli-tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ import { logger } from '../utils/logger';
import { evaluateQueryResults, QueryEvaluationResult, extractQueryMetadata } from './query-results-evaluator';
import { getOrCreateLogDirectory } from './log-directory-manager';
import { writeFileSync, rmSync, existsSync, mkdirSync } from 'fs';
import { join, dirname, resolve } from 'path';
import { join, dirname, resolve, basename } from 'path';
import { createProjectTempDir } from '../utils/temp-dir';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Calculate the repository root directory
// When running from source: server/src/lib/ -> go up 3 levels to repo root
// When running from bundle: server/dist/ -> go up 2 levels to repo root
// When running from bundle: server/dist/ -> go up 2 levels to repo root
// The bundled file flattens the structure, so we detect based on path
const repoRootDir = __dirname.includes('src/lib')
// Normalize path separators for cross-platform compatibility (Windows uses '\', Unix uses '/')
const normalizedDir = __dirname.replace(/\\/g, '/');
const repoRootDir = normalizedDir.includes('src/lib')
? resolve(__dirname, '..', '..', '..') // From source: server/src/lib -> repo root
: resolve(__dirname, '..', '..'); // From bundle: server/dist -> repo root

Expand Down Expand Up @@ -637,13 +639,13 @@ async function resolveQueryPath(

// Find the query that matches the requested name exactly
const matchingQuery = resolvedQueries.find(queryPath => {
const fileName = queryPath.split('/').pop();
const fileName = basename(queryPath);
// Match exact query name: "PrintAST" should match "PrintAST.ql" only
return fileName === `${queryName}.ql`;
});

if (!matchingQuery) {
logger.error(`Query "${queryName}.ql" not found in pack "${packPath}". Available queries:`, resolvedQueries.map(q => q.split('/').pop()));
logger.error(`Query "${queryName}.ql" not found in pack "${packPath}". Available queries:`, resolvedQueries.map(q => basename(q)));
throw new Error(`Query "${queryName}.ql" not found in pack "${packPath}"`);
}

Expand Down
7 changes: 3 additions & 4 deletions server/src/prompts/workflow-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { basename } from 'path';
import { loadPromptTemplate, processPromptTemplate } from './prompt-loader';
import { logger } from '../utils/logger';

Expand Down Expand Up @@ -154,10 +155,8 @@ export function registerWorkflowPrompts(server: McpServer): void {
// Derive workshop name from query path if not provided
const derivedName =
workshopName ||
queryPath
.split('/')
.pop()
?.replace(/\.(ql|qlref)$/, '')
basename(queryPath)
.replace(/\.(ql|qlref)$/, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') ||
'codeql-workshop';
Expand Down
3 changes: 2 additions & 1 deletion server/src/ql-mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { pathToFileURL } from 'url';
import { registerCodeQLTools, registerCodeQLResources } from './tools';
import { registerLanguageResources } from './resources/language-resources';
import { registerWorkflowPrompts } from './prompts/workflow-prompts';
Expand Down Expand Up @@ -140,6 +141,6 @@ async function main(): Promise<void> {
}

// Start the server if this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}
2 changes: 1 addition & 1 deletion server/src/tools/codeql/language-server-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async function getLanguageServer(options: LanguageServerOptions = {}): Promise<C
await globalLanguageServer.start();

// Use provided workspace URI or default to ql directory
const workspaceUri = `file://${resolve(process.cwd(), 'ql')}`;
const workspaceUri = pathToFileURL(resolve(process.cwd(), 'ql')).href;
await globalLanguageServer.initialize(workspaceUri);

logger.info('CodeQL Language Server started and initialized successfully');
Expand Down
Loading