diff --git a/.github/workflows/client-integration-tests.yml b/.github/workflows/client-integration-tests.yml index 2852723..991b595 100644 --- a/.github/workflows/client-integration-tests.yml +++ b/.github/workflows/client-integration-tests.yml @@ -26,17 +26,19 @@ permissions: jobs: integration-tests: - name: Integration Tests (${{ matrix.os }}) + name: Integration Tests (${{ matrix.os }}, ${{ matrix.mcp-mode }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + mcp-mode: [http, stdio] os: [ubuntu-latest, windows-latest] env: HTTP_HOST: 'localhost' HTTP_PORT: '3000' + MCP_MODE: ${{ matrix.mcp-mode }} TIMEOUT_SECONDS: '30' URL_SCHEME: 'http' @@ -110,7 +112,7 @@ jobs: run: npm run test:integration --workspace=client - name: MCP Integration Tests - Stop the background MCP server process - if: always() + if: always() && matrix.mcp-mode == 'http' shell: bash run: | if [ -f server.pid ]; then @@ -141,8 +143,8 @@ jobs: - 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 + echo "## Integration Tests Summary (${{ matrix.os }}, ${{ matrix.mcp-mode }})" >> $GITHUB_STEP_SUMMARY + echo "โœ… MCP server integration tests passed on ${{ matrix.os }} with ${{ matrix.mcp-mode }} transport" >> $GITHUB_STEP_SUMMARY codeql-path-tests: name: CODEQL_PATH Tests (${{ matrix.os }}) diff --git a/.gitignore b/.gitignore index bbc674b..c2b3b50 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ codeql-development-mcp-server.code-workspace # Prevent accidentally committing integration test output files in root directory # These should only be in client/integration-tests/primitives/tools/*/after/ directories /evaluator-log.json +/client/query-results/ +/client/query-results.bqrs +/client/query-results.sarif /query-results/ /query-results.bqrs /query-results.sarif diff --git a/client/package.json b/client/package.json index 44175c6..d365715 100644 --- a/client/package.json +++ b/client/package.json @@ -55,8 +55,10 @@ "test:coverage": "echo 'NOOP client test:coverage'", "test:integration": "scripts/run-integration-tests.sh --no-install-packs", "test:integration:default": "ENABLE_MONITORING_TOOLS=false scripts/run-integration-tests.sh --no-install-packs", + "test:integration:http": "MCP_MODE=http scripts/run-integration-tests.sh --no-install-packs", "test:integration:install-packs": "scripts/run-integration-tests.sh", - "test:integration:monitoring": "ENABLE_MONITORING_TOOLS=true scripts/run-integration-tests.sh", + "test:integration:monitoring": "ENABLE_MONITORING_TOOLS=true scripts/run-integration-tests.sh --no-install-packs", + "test:integration:stdio": "MCP_MODE=stdio scripts/run-integration-tests.sh --no-install-packs", "tidy": "npm run lint && npm run format" } } diff --git a/client/scripts/run-integration-tests.sh b/client/scripts/run-integration-tests.sh index 7dc0942..49aa18d 100755 --- a/client/scripts/run-integration-tests.sh +++ b/client/scripts/run-integration-tests.sh @@ -8,8 +8,9 @@ # 2. Monitoring mode (monitoring tools enabled) - tests session_* tools # # Environment Variables: -# HTTP_HOST - Server host (default: localhost) -# HTTP_PORT - Server port (default: 3000) +# MCP_MODE - MCP transport mode (default: stdio, also: http) +# HTTP_HOST - Server host for HTTP mode (default: localhost) +# HTTP_PORT - Server port for HTTP mode (default: 3000) # TIMEOUT_SECONDS - Request timeout (default: 30) # ENABLE_MONITORING_TOOLS - Force a specific mode instead of running both: # "true" = only run with monitoring tools enabled @@ -20,6 +21,7 @@ # ./run-integration-tests.sh # Run in BOTH modes (recommended) # ENABLE_MONITORING_TOOLS=false ./run-integration-tests.sh # Only default mode # ENABLE_MONITORING_TOOLS=true ./run-integration-tests.sh # Only monitoring mode +# MCP_MODE=http ./run-integration-tests.sh # Run using HTTP transport # ./run-integration-tests.sh --tools session_end # Filter to specific tools set -e @@ -29,6 +31,7 @@ CLIENT_DIR="$(dirname "$SCRIPT_DIR")" SERVER_DIR="$(dirname "$CLIENT_DIR")/server" # Set default environment variables +export MCP_MODE="${MCP_MODE:-stdio}" export HTTP_HOST="${HTTP_HOST:-localhost}" export HTTP_PORT="${HTTP_PORT:-3000}" export TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-30}" @@ -65,7 +68,10 @@ for arg in "$@"; do done echo "๐Ÿš€ Starting CodeQL MCP Integration Tests" -echo "Server URL: $URL_SCHEME://$HTTP_HOST:$HTTP_PORT/mcp" +echo "MCP Mode: $MCP_MODE" +if [ "$MCP_MODE" = "http" ]; then + echo "Server URL: $URL_SCHEME://$HTTP_HOST:$HTTP_PORT/mcp" +fi # Step 1: Build and bundle the server code echo "๐Ÿ“ฆ Building CodeQL MCP server bundle..." @@ -81,43 +87,56 @@ else fi cd "$CLIENT_DIR" -export MCP_MODE=http -export MCP_SERVER_URL="$URL_SCHEME://$HTTP_HOST:$HTTP_PORT/mcp" + +# For HTTP mode, set the server URL for the client +if [ "$MCP_MODE" = "http" ]; then + export MCP_SERVER_URL="$URL_SCHEME://$HTTP_HOST:$HTTP_PORT/mcp" +fi # Function to run tests in a specific mode run_tests_in_mode() { local mode_name="$1" local enable_monitoring="$2" - + shift 2 + echo "" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo "๐Ÿงช Running integration tests: $mode_name" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" - + # Set the monitoring tools flag for this run export ENABLE_MONITORING_TOOLS="$enable_monitoring" - - # Start MCP server with current settings - echo "๐Ÿš€ Starting MCP server (monitoring=$enable_monitoring)..." - "$SCRIPT_DIR/start-server.sh" - - # Wait for server startup - echo "โณ Waiting for server startup..." - "$SCRIPT_DIR/wait-for-server.sh" - + + if [ "$MCP_MODE" = "http" ]; then + # HTTP mode: start server in background, run tests, stop server + echo "๐Ÿš€ Starting MCP server (monitoring=$enable_monitoring)..." + "$SCRIPT_DIR/start-server.sh" + + # Wait for server startup + echo "โณ Waiting for server startup..." + "$SCRIPT_DIR/wait-for-server.sh" + else + # stdio mode: client spawns server directly via StdioClientTransport + echo "๐Ÿ“ก Using stdio transport (client spawns server directly)" + fi + # Run the integration tests (skip pack installation since we already did it) echo "๐Ÿงช Running tests..." node src/ql-mcp-client.js integration-tests --no-install-packs "$@" - - # Stop the server before next mode - echo "๐Ÿ›‘ Stopping server..." - "$SCRIPT_DIR/stop-server.sh" + + if [ "$MCP_MODE" = "http" ]; then + # Stop the server before next mode + echo "๐Ÿ›‘ Stopping server..." + "$SCRIPT_DIR/stop-server.sh" + fi } # Trap to ensure cleanup happens even if script fails cleanup() { echo "๐Ÿงน Cleaning up..." - "$SCRIPT_DIR/stop-server.sh" 2>/dev/null || true + if [ "$MCP_MODE" = "http" ]; then + "$SCRIPT_DIR/stop-server.sh" 2>/dev/null || true + fi } trap cleanup EXIT diff --git a/client/src/lib/cli-parser.js b/client/src/lib/cli-parser.js index 7b6573a..a80da46 100644 --- a/client/src/lib/cli-parser.js +++ b/client/src/lib/cli-parser.js @@ -133,6 +133,12 @@ OPTIONS: Example: --timeout 600 --help Display help information + +ENVIRONMENT VARIABLES: + MCP_MODE MCP transport mode: stdio (default) or http + MCP_SERVER_PATH Path to the MCP server JS entry point (stdio mode only) + MCP_SERVER_URL MCP server URL (http mode only, default: http://localhost:3000/mcp) + ENABLE_MONITORING_TOOLS Enable session_* monitoring tools (default: false) `; } diff --git a/client/src/ql-mcp-client.js b/client/src/ql-mcp-client.js index c00cfe4..9d20844 100755 --- a/client/src/ql-mcp-client.js +++ b/client/src/ql-mcp-client.js @@ -8,6 +8,7 @@ /* global URL, setTimeout */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { execSync } from "child_process"; import dotenv from "dotenv"; @@ -36,6 +37,7 @@ class CodeQLMCPClient { constructor(options = {}) { this.client = null; this.transport = null; + this.mcpMode = (process.env.MCP_MODE || "stdio").toLowerCase(); this.serverUrl = process.env.MCP_SERVER_URL || DEFAULT_SERVER_URL; this.timeout = parseInt(options.timeout || process.env.TIMEOUT_SECONDS || "30") * 1000; this.logger = new TestLogger(); @@ -113,14 +115,32 @@ class CodeQLMCPClient { */ async connect() { try { - this.logger.log(`Connecting to MCP server at ${this.serverUrl}`); + this.logger.log(`Connecting to MCP server (mode: ${this.mcpMode})`); this.client = new Client({ name: "codeql-development-mcp-client", version: "1.0.0" }); - this.transport = new StreamableHTTPClientTransport(new URL(this.serverUrl)); + if (this.mcpMode !== "http") { + const repoRoot = path.join(__dirname, "..", ".."); + const serverPath = + process.env.MCP_SERVER_PATH || + path.join(repoRoot, "server", "dist", "codeql-development-mcp-server.js"); + this.transport = new StdioClientTransport({ + command: "node", + args: [serverPath], + cwd: repoRoot, + env: { + ...process.env, + TRANSPORT_MODE: "stdio" + }, + stderr: "pipe" + }); + } else { + this.logger.log(`Server URL: ${this.serverUrl}`); + this.transport = new StreamableHTTPClientTransport(new URL(this.serverUrl)); + } // Set up timeout const connectPromise = this.client.connect(this.transport); @@ -169,7 +189,10 @@ class CodeQLMCPClient { */ async runTests() { this.logger.log("Starting CodeQL MCP Client Integration Tests"); - this.logger.log(`Server URL: ${this.serverUrl}`); + this.logger.log(`MCP Mode: ${this.mcpMode}`); + if (this.mcpMode === "http") { + this.logger.log(`Server URL: ${this.serverUrl}`); + } this.logger.log(`Timeout: ${this.timeout}ms`); // Check CodeQL CLI availability first @@ -194,17 +217,24 @@ class CodeQLMCPClient { } } catch (error) { this.logger.log(`Test execution failed: ${error.message}`, "ERROR"); - } finally { - if (connected) { - await this.disconnect(); - } } - // Print test summary + // Print test summary and set exit code BEFORE disconnect. + // On Windows, StdioClientTransport.close() can cause the Node.js + // process to exit abruptly, so we must report results first. this.logger.printTestSummary(); + const exitCode = this.logger.isSuccess() ? 0 : 1; + process.exitCode = exitCode; + + if (connected) { + try { + await this.disconnect(); + } catch { + // Ignore disconnect errors โ€” results are already reported + } + } - // Exit with appropriate code - process.exit(this.logger.isSuccess() ? 0 : 1); + process.exit(exitCode); } /** @@ -212,7 +242,7 @@ class CodeQLMCPClient { */ async runMonitoringDemo() { this.logger.log("๐Ÿš€ Starting MCP Server Monitoring Demo"); - this.logger.log(`Server URL: ${this.serverUrl}`); + this.logger.log(`MCP Mode: ${this.mcpMode}`); let connected = false; @@ -225,17 +255,24 @@ class CodeQLMCPClient { } } catch (error) { this.logger.log(`Demo execution failed: ${error.message}`, "ERROR"); - } finally { - if (connected) { - await this.disconnect(); - } } - // Print demo summary + // Print summary and set exit code BEFORE disconnect. + // On Windows, StdioClientTransport.close() can cause the Node.js + // process to exit abruptly, so we must report results first. this.logger.printTestSummary(); + const exitCode = this.logger.isSuccess() ? 0 : 1; + process.exitCode = exitCode; - // Exit with appropriate code - process.exit(this.logger.isSuccess() ? 0 : 1); + if (connected) { + try { + await this.disconnect(); + } catch { + // Ignore disconnect errors โ€” results are already reported + } + } + + process.exit(exitCode); } /** @@ -315,7 +352,7 @@ class CodeQLMCPClient { */ async runWorkflowTests() { this.logger.log("๐Ÿ”„ Starting Workflow Integration Tests"); - this.logger.log(`Server URL: ${this.serverUrl}`); + this.logger.log(`MCP Mode: ${this.mcpMode}`); let connected = false; @@ -329,17 +366,24 @@ class CodeQLMCPClient { } } catch (error) { this.logger.log(`Workflow test execution failed: ${error.message}`, "ERROR"); - } finally { - if (connected) { - await this.disconnect(); - } } - // Print test summary + // Print test summary and set exit code BEFORE disconnect. + // On Windows, StdioClientTransport.close() can cause the Node.js + // process to exit abruptly, so we must report results first. this.logger.printTestSummary(); + const exitCode = this.logger.isSuccess() ? 0 : 1; + process.exitCode = exitCode; + + if (connected) { + try { + await this.disconnect(); + } catch { + // Ignore disconnect errors โ€” results are already reported + } + } - // Exit with appropriate code - process.exit(this.logger.isSuccess() ? 0 : 1); + process.exit(exitCode); } /** @@ -347,7 +391,7 @@ class CodeQLMCPClient { */ async runMonitoringIntegrationTests() { this.logger.log("๐Ÿ“Š Starting Monitoring Integration Tests"); - this.logger.log(`Server URL: ${this.serverUrl}`); + this.logger.log(`MCP Mode: ${this.mcpMode}`); let connected = false; @@ -366,17 +410,24 @@ class CodeQLMCPClient { } } catch (error) { this.logger.log(`Monitoring test execution failed: ${error.message}`, "ERROR"); - } finally { - if (connected) { - await this.disconnect(); - } } - // Print test summary + // Print test summary and set exit code BEFORE disconnect. + // On Windows, StdioClientTransport.close() can cause the Node.js + // process to exit abruptly, so we must report results first. this.logger.printTestSummary(); + const exitCode = this.logger.isSuccess() ? 0 : 1; + process.exitCode = exitCode; + + if (connected) { + try { + await this.disconnect(); + } catch { + // Ignore disconnect errors โ€” results are already reported + } + } - // Exit with appropriate code - process.exit(this.logger.isSuccess() ? 0 : 1); + process.exit(exitCode); } /** diff --git a/docs/getting-started.md b/docs/getting-started.md index 4efb6b1..550f91b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,13 +12,13 @@ This guide covers installation, configuration, and usage of the CodeQL Developme ### VS Code Extension (recommended) -The easiest way to get started is the **VS Code extension**, which automates -installation, configuration, and CodeQL CLI discovery. -See the [VS Code Extension guide](./vscode/extension.md) for details. - -The `.vsix` can be downloaded from +The easiest and recommended way to get started is the **VS Code extension** (VSIX +archive bundle), which automates installation, configuration, and CodeQL CLI +discovery. Download the `.vsix` from [GitHub Releases](https://github.com/advanced-security/codeql-development-mcp-server/releases) -or built from source (`npm run package:vsix` at the repository root). +and install it in VS Code via `Extensions: Install from VSIXโ€ฆ` in the Command +Palette, or build from source (`npm run package:vsix` at the repository root). +See the [VS Code Extension guide](./vscode/extension.md) for details. ### From npm diff --git a/docs/public.md b/docs/public.md index 47830c6..ea7cb67 100644 --- a/docs/public.md +++ b/docs/public.md @@ -25,7 +25,22 @@ codeql --version # any recent release ## Installation -### 1. Install the MCP Server (npm) +### Recommended: VS Code Extension (VSIX archive bundle) + +The easiest and recommended way to get started is the **VS Code extension**, +distributed as a `.vsix` archive bundle. It automates installation, +configuration, and CodeQL CLI discovery โ€” no manual `mcp.json` editing required. + +1. Download the latest `.vsix` from + [GitHub Releases](https://github.com/advanced-security/codeql-development-mcp-server/releases). +2. In VS Code, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and + run **Extensions: Install from VSIXโ€ฆ**. +3. Select the downloaded `.vsix` file. + +The extension bundles the MCP server and will register it automatically on +activation. See the [VS Code Extension guide](./vscode/extension.md) for details. + +### Alternative: Install the MCP Server (npm) The server is published as a public package on [npmjs.org](https://www.npmjs.com/package/codeql-development-mcp-server). No authentication or special configuration is needed: @@ -39,7 +54,7 @@ npx -y codeql-development-mcp-server The package ships the bundled server (`dist/codeql-development-mcp-server.js`), production dependencies, and the CodeQL tool query source packs (`ql/*/tools/src/`). -### 2. Install CodeQL Tool Query Packs (optional) +### Alternative: Install CodeQL Tool Query Packs (optional) The server ships with embedded copies of its tool query packs. If you prefer to manage the packs independently, or want to pin a specific version, download them from GHCR: diff --git a/docs/testing.md b/docs/testing.md index ba62ed2..a9c12d4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -40,12 +40,14 @@ Unit tests verify the VS Code extension's TypeScript code outside of the Extensi Integration tests exercise individual MCP tools against a live server instance using the custom MCP client. -- **Client**: `client/src/ql-mcp-client.js` โ€” starts the MCP server, invokes tools, and validates results. +- **Client**: `client/src/ql-mcp-client.js` โ€” connects to the MCP server, invokes tools, and validates results. +- **Transport modes**: The client supports both `stdio` (default) and `http` transport modes, controlled by the `MCP_MODE` environment variable. In `stdio` mode the client spawns the server as a child process via `StdioClientTransport`; in `http` mode it connects to a separately started HTTP server via `StreamableHTTPClientTransport`. - **Test data**: `client/integration-tests/primitives/tools/` โ€” each test has `before/` and `after/` directories that define the initial fixture state and, for file-based tests, the expected final state. - **Run command**: `npm run test:integration:default -w client` (or `npm run test:client` from the repo root). - **Key properties**: - Tests are deterministic and repeatable. - No mocks โ€” tests use real CodeQL databases and queries bundled under `server/ql/`. + - The default transport is `stdio`, matching the primary user experience. - The `before/monitoring-state.json` file supplies tool arguments. For file-based tests, the integration-test runner diffs filesystem state from `before/` to `after/`; for monitoring-based tests, `after/` artifacts are generally not diffed and are only interpreted for specific validations (for example, `codeql_query_run` interpreted output). ### 2b โ€” Extension integration tests @@ -81,6 +83,7 @@ From the repository root: ```bash # Build everything and run all layers (1a + 1b + 2a + 2b) +# Integration tests use stdio transport by default npm run build-and-test # Run only server unit tests (1a) @@ -89,9 +92,13 @@ npm run test:server # Run extension unit tests + integration tests (1b + 2b) npm run test:vscode -# Run only MCP tool integration tests (2a) +# Run only MCP tool integration tests (2a) - stdio mode (default) npm run test:client +# Run MCP tool integration tests with explicit transport mode +MCP_MODE=stdio npm run test:client # stdio transport (default) +MCP_MODE=http npm run test:client # HTTP transport + # Run only extension integration tests (2b) npm run test:integration -w extensions/vscode ```