diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d50b3af..8223171 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,12 @@ on: jobs: test: - name: Run Unit Tests + name: Test (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20.x, 22.x, 24.x] steps: - name: Checkout code @@ -18,7 +22,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 24.x + node-version: ${{ matrix.node-version }} cache: "npm" cache-dependency-path: mcp-server/package-lock.json @@ -26,6 +30,8 @@ jobs: working-directory: ./mcp-server run: npm ci - - name: Run tests + # Includes package-environments.integration.test.ts (workspace ESM/CJS), + # package-published-esm.integration.test.ts (npm pack + import "@currents/mcp"). + - name: Build and run tests working-directory: ./mcp-server run: npm run test:run diff --git a/Dockerfile b/Dockerfile index 0213248..e0fbac2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # syntax=docker/dockerfile:1 # Build stage -FROM node:lts-alpine AS build +FROM node:20-alpine AS build WORKDIR /app # Install dependencies @@ -10,14 +10,15 @@ COPY mcp-server/package.json mcp-server/package-lock.json ./ RUN npm ci --production=false # Copy source code -COPY mcp-server/tsconfig.json ./tsconfig.json +COPY mcp-server/tsconfig.json mcp-server/tsconfig.cjs.json ./ +COPY mcp-server/scripts ./scripts COPY mcp-server/src ./src # Build the project RUN npm run build # Runtime stage -FROM node:lts-alpine AS runtime +FROM node:20-alpine AS runtime WORKDIR /app # Copy build artifacts and dependencies diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json index d891477..a50ac03 100644 --- a/mcp-server/package-lock.json +++ b/mcp-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@currents/mcp", - "version": "2.2.11", + "version": "2.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@currents/mcp", - "version": "2.2.11", + "version": "2.2.15", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.25.2", + "@modelcontextprotocol/sdk": "^1.28.0", "commander": "^12.1.0", "pino": "^9.9.5", "pino-pretty": "^13.1.1", @@ -20,10 +20,14 @@ }, "devDependencies": { "@release-it/conventional-changelog": "^10.0.0", + "@types/node": "^20.19.0", "release-it": "^19.0.3", "rimraf": "^6.0.1", "typescript": "^5.9.3", "vitest": "^4.0.18" + }, + "engines": { + "node": ">=20" } }, "node_modules/@conventional-changelog/git-client": { @@ -865,9 +869,9 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", + "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -1483,6 +1487,16 @@ "url": "https://ko-fi.com/dangreen" } }, + "node_modules/@simple-libs/child-process-utils/node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@simple-libs/stream-utils": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.1.0.tgz", @@ -1499,6 +1513,16 @@ "url": "https://ko-fi.com/dangreen" } }, + "node_modules/@simple-libs/stream-utils/node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1538,10 +1562,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", - "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } diff --git a/mcp-server/package.json b/mcp-server/package.json index d28b711..3f9fb47 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -15,17 +15,26 @@ "bin": "./build/index.js", "version": "2.2.16", "description": "Currents MCP server", - "main": "index.js", + "main": "./build/cjs/api.js", + "module": "./build/api.js", + "types": "./build/api.d.ts", + "exports": { + ".": { + "types": "./build/api.d.ts", + "import": "./build/api.js", + "require": "./build/cjs/api.js" + } + }, "scripts": { - "build": "tsc && chmod 755 build/index.js", + "build": "tsc && tsc -p tsconfig.cjs.json && node ./scripts/write-cjs-package-json.mjs && chmod 755 build/index.js", "publish:mcp": "npm run publish:npm", "publish:npm": "npm run rm && npm run build && ./publish.cjs", "start": "node build/index.js", "rm": "rimraf dist", "test": "vitest", "test:ui": "vitest --ui", - "test:run": "vitest run", - "test:coverage": "vitest run --coverage", + "test:run": "npm run build && vitest run", + "test:coverage": "npm run build && vitest run --coverage", "release": "release-it", "release:dry": "release-it --dry-run" }, @@ -36,14 +45,18 @@ "url": "https://github.com/currents-dev/currents-mcp.git" }, "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.25.2", + "@modelcontextprotocol/sdk": "^1.28.0", "commander": "^12.1.0", "pino": "^9.9.5", "pino-pretty": "^13.1.1", "zod": "^4.3.5" }, "devDependencies": { + "@types/node": "^20.19.0", "@release-it/conventional-changelog": "^10.0.0", "release-it": "^19.0.3", "rimraf": "^6.0.1", diff --git a/mcp-server/scripts/write-cjs-package-json.mjs b/mcp-server/scripts/write-cjs-package-json.mjs new file mode 100644 index 0000000..84d17aa --- /dev/null +++ b/mcp-server/scripts/write-cjs-package-json.mjs @@ -0,0 +1,8 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const cjsDir = join(root, "build", "cjs"); +mkdirSync(cjsDir, { recursive: true }); +writeFileSync(join(cjsDir, "package.json"), `${JSON.stringify({ type: "commonjs" })}\n`); diff --git a/mcp-server/src/api.ts b/mcp-server/src/api.ts new file mode 100644 index 0000000..331059c --- /dev/null +++ b/mcp-server/src/api.ts @@ -0,0 +1 @@ +export { startMcpServer } from "./server.js"; diff --git a/mcp-server/src/cli-bin.integration.test.ts b/mcp-server/src/cli-bin.integration.test.ts new file mode 100644 index 0000000..6ed7b7a --- /dev/null +++ b/mcp-server/src/cli-bin.integration.test.ts @@ -0,0 +1,98 @@ +/** + * Packaged **CLI** integration tests: the `bin` field and `npx` behavior for a + * tarball shaped like a publish (not the programmatic `exports` entry; see + * `package-published-esm.integration.test.ts` for `import "@currents/mcp"`). + * + + * 1. Prerequisite: `build/index.js` exists (`npm run test:run` runs `build` + * first). If missing, the suite is skipped so `vitest` without a prior + * build does not fail noisily. + * 2. `packTarball`: `npm pack` from the package root → one `.tgz` under a + * temp dir. Contents follow `package.json` `files` and npm’s pack rules + * (same artifact shape as registry install, minus release-only publish.cjs + * mutations). + */ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const buildIndex = path.join(root, "build", "index.js"); + +function packTarball(packDest: string): string { + // Respect `files` and standard pack rules; do not mutate package.json (unlike release `publish.cjs`). + execFileSync("npm", ["pack", "--pack-destination", packDest], { + cwd: root, + stdio: ["ignore", "pipe", "pipe"], + }); + const tgz = readdirSync(packDest).filter((f) => f.endsWith(".tgz")); + if (tgz.length !== 1) { + throw new Error(`expected one .tgz in ${packDest}, got: ${tgz.join(", ")}`); + } + return path.join(packDest, tgz[0]); +} + +describe.skipIf(!existsSync(buildIndex))( + "packaged CLI (npx / bin)", + { timeout: 60_000 }, + () => { + /** + * `npx -y --package mcp`: npm treats the tarball as the + * package to install transiently; `mcp` is the bin name from that package’s + * `package.json` `bin` map (not the scoped package name). The child should + * start the MCP server and log the “live” line (stdio MCP servers run until + * stdin closes; we cap wall time with `spawnSync` timeout). Accept either + * that log line or process timeout as success so slow CI still passes. + * */ + it("starts via npx --package tarball mcp", () => { + const packDir = mkdtempSync(path.join(tmpdir(), "mcp-pack-")); + const tarball = packTarball(packDir); + const r = spawnSync("npx", ["-y", "--package", tarball, "mcp"], { + cwd: packDir, + timeout: 45_000, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + env: { + ...process.env, + // Required for server startup; value is unused in this smoke test. + CURRENTS_API_KEY: "vitest-cli-pack-smoke", + }, + }); + const combined = `${r.stdout ?? ""}${r.stderr ?? ""}`; + const timedOut = + r.error != null && "code" in r.error && r.error.code === "ETIMEDOUT"; + expect(combined.includes("Currents MCP Server is live") || timedOut).toBe( + true + ); + }); + + /* + * Second `it` — consumer project + `node_modules/.bin`: + * - `npm init -y` and `npm install ` in a fresh temp project. npm links + * `node_modules/.bin/mcp` (or `mcp.cmd` on Windows) to the packed CLI. + * - Assert the shim exists. This catches broken `bin`, wrong `files` (missing + * `build/index.js`), or install layout issues without spawning the server. + * */ + it("exposes mcp bin after npm install from tarball", () => { + const packDir = mkdtempSync(path.join(tmpdir(), "mcp-pack-")); + const installDir = mkdtempSync(path.join(tmpdir(), "mcp-install-")); + const tarball = packTarball(packDir); + execFileSync("npm", ["init", "-y"], { + cwd: installDir, + stdio: "ignore", + }); + execFileSync("npm", ["install", tarball], { + cwd: installDir, + stdio: "ignore", + }); + const binDir = path.join(installDir, "node_modules", ".bin"); + const hasMcp = + existsSync(path.join(binDir, "mcp")) || + existsSync(path.join(binDir, "mcp.cmd")); + expect(hasMcp).toBe(true); + }); + } +); diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index ae1e088..e79104a 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -1,288 +1,18 @@ #!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { CURRENTS_API_KEY } from "./lib/env.js"; +import { MISSING_CURRENTS_API_KEY_MESSAGE } from "./lib/env.js"; import { logger } from "./lib/logger.js"; -// Actions tools -import { listActionsTool } from "./tools/actions/list-actions.js"; -import { createActionTool } from "./tools/actions/create-action.js"; -import { getActionTool } from "./tools/actions/get-action.js"; -import { updateActionTool } from "./tools/actions/update-action.js"; -import { deleteActionTool } from "./tools/actions/delete-action.js"; -import { enableActionTool } from "./tools/actions/enable-action.js"; -import { disableActionTool } from "./tools/actions/disable-action.js"; -import { listAffectedTestsTool } from "./tools/actions/list-affected-tests.js"; -import { getAffectedTestExecutionsTool } from "./tools/actions/get-affected-test-executions.js"; -import { getAffectedTestExecutionsByActionTool } from "./tools/actions/get-affected-test-executions-by-action.js"; -// Projects tools -import { getProjectsTool } from "./tools/projects/get-projects.js"; -import { getProjectTool } from "./tools/projects/get-project.js"; -import { getProjectInsightsTool } from "./tools/projects/get-project-insights.js"; -// Runs tools -import { getRunsTool } from "./tools/runs/get-runs.js"; -import { getRunDetailsTool } from "./tools/runs/get-run.js"; -import { findRunTool } from "./tools/runs/find-run.js"; -import { cancelRunTool } from "./tools/runs/cancel-run.js"; -import { resetRunTool } from "./tools/runs/reset-run.js"; -import { deleteRunTool } from "./tools/runs/delete-run.js"; -import { cancelRunByGithubCITool } from "./tools/runs/cancel-run-github-ci.js"; -// Specs tools -import { getSpecFilesPerformanceTool } from "./tools/specs/get-spec-files-performance.js"; -import { getSpecInstancesTool } from "./tools/specs/get-spec-instances.js"; -// Tests tools -import { getTestResultsTool } from "./tools/tests/get-test-results.js"; -import { getTestsPerformanceTool } from "./tools/tests/get-tests-performance.js"; -import { getTestSignatureTool } from "./tools/tests/get-tests-signature.js"; -// Errors tools -import { getErrorsExplorerTool } from "./tools/errors/get-errors-explorer.js"; -// Webhooks tools -import { listWebhooksTool } from "./tools/webhooks/list-webhooks.js"; -import { createWebhookTool } from "./tools/webhooks/create-webhook.js"; -import { getWebhookTool } from "./tools/webhooks/get-webhook.js"; -import { updateWebhookTool } from "./tools/webhooks/update-webhook.js"; -import { deleteWebhookTool } from "./tools/webhooks/delete-webhook.js"; - -if (CURRENTS_API_KEY === "") { - logger.error("CURRENTS_API_KEY env variable is not set."); -} - -const server = new McpServer({ - name: "currents", - version: "1.0.0", -}); - -// Actions API tools -server.tool( - "currents-list-actions", - "List all actions for a project with optional filtering. Actions are rules that automatically modify test behavior (skip, quarantine, tag). Supports filtering by status (active/disabled/archived/expired) and search by name. Requires a projectId.", - listActionsTool.schema, - listActionsTool.handler -); - -server.tool( - "currents-create-action", - "Create a new action for a project. Actions define rules that automatically skip, quarantine, or tag tests based on conditions like test title, file path, git branch, etc. Requires projectId, name, action array, and matcher object.", - createActionTool.schema, - createActionTool.handler -); - -server.tool( - "currents-get-action", - "Get a single action by ID. The actionId is globally unique, so projectId is not required. Returns full action details including matcher conditions and current status.", - getActionTool.schema, - getActionTool.handler -); - -server.tool( - "currents-update-action", - "Update an existing action. The actionId is globally unique. You can update name, description, action array, matcher, or expiration date. All fields are optional.", - updateActionTool.schema, - updateActionTool.handler -); - -server.tool( - "currents-delete-action", - "Delete (archive) an action. This is a soft delete - the action will be marked as archived but not permanently removed. The actionId is globally unique.", - deleteActionTool.schema, - deleteActionTool.handler -); - -server.tool( - "currents-enable-action", - "Enable a disabled action. Changes the action status from disabled to active, making it apply to matching tests again. The actionId is globally unique.", - enableActionTool.schema, - enableActionTool.handler -); - -server.tool( - "currents-disable-action", - "Disable an active action. Changes the action status to disabled, temporarily preventing it from applying to tests. The actionId is globally unique.", - disableActionTool.schema, - disableActionTool.handler -); - -server.tool( - "currents-list-affected-tests", - "List tests affected by actions (quarantine, skip, tag) for a project within a date range. Returns aggregated data grouped by test signature. Supports filtering by action types, action ID, status, and search. Requires projectId, date_start, and date_end. (Preview endpoint: fields and path may change.)", - listAffectedTestsTool.schema, - listAffectedTestsTool.handler -); - -server.tool( - "currents-get-affected-test-executions", - "Get execution details for a specific affected test (by signature) within a date range. Returns individual test execution records with action info. Uses cursor-based pagination. Requires projectId, signature, date_start, and date_end.", - getAffectedTestExecutionsTool.schema, - getAffectedTestExecutionsTool.handler -); - -server.tool( - "currents-get-affected-test-executions-by-action", - "List test executions where a specific action/rule was applied, within a date range. Uses cursor-based pagination. Requires actionId, date_start, and date_end.", - getAffectedTestExecutionsByActionTool.schema, - getAffectedTestExecutionsByActionTool.handler -); - -// Projects API tools -server.tool( - "currents-get-projects", - "Retrieves projects available in the Currents platform. Supports cursor-based pagination with limit, starting_after, ending_before parameters, or set fetchAll=true for automatic pagination. This is a prerequisite for using any other tools that require project-specific information.", - getProjectsTool.schema, - getProjectsTool.handler -); - -server.tool( - "currents-get-project", - "Get a single project by ID. Returns project details including name, creation date, failFast setting, inactivity timeout, and default branch name.", - getProjectTool.schema, - getProjectTool.handler -); - -server.tool( - "currents-get-project-insights", - "Get aggregated run and test metrics for a project within a date range. Returns overall metrics and timeline data with configurable resolution (1h/1d/1w). Supports filtering by tags, branches, groups, and authors. Requires projectId, date_start, and date_end.", - getProjectInsightsTool.schema, - getProjectInsightsTool.handler -); - -// Runs API tools -server.tool( - "currents-get-runs", - "Retrieves a list of runs for a specific project with optional filtering. Supports filtering by branch, tags (with AND/OR operators), status (PASSED/FAILED/RUNNING/FAILING), completion state, date range, commit author, and search by ciBuildId or commit message. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", - getRunsTool.schema, - getRunsTool.handler -); - -server.tool( - "currents-get-run-details", - "Retrieves details of a specific test run. Requires a user-provided runId.", - getRunDetailsTool.schema, - getRunDetailsTool.handler -); - -server.tool( - "currents-find-run", - "Find a run by query parameters. Returns the most recent completed run matching the criteria. Can search by ciBuildId (exact match) or by branch/tags. Supports pwLastRun flag for Playwright last run info. Requires projectId.", - findRunTool.schema, - findRunTool.handler -); - -server.tool( - "currents-cancel-run", - "Cancel a run in progress. This will stop the run and mark it as cancelled. Requires runId.", - cancelRunTool.schema, - cancelRunTool.handler -); - -server.tool( - "currents-reset-run", - "Reset failed spec files in a run to allow re-execution. Requires runId and machineId array (1-63 machine IDs). Optionally supports batched orchestration.", - resetRunTool.schema, - resetRunTool.handler -); - -server.tool( - "currents-delete-run", - "Delete a run and all associated data. This is a permanent deletion. Requires runId.", - deleteRunTool.schema, - deleteRunTool.handler -); - -server.tool( - "currents-cancel-run-github-ci", - "Cancel a run by GitHub Actions workflow run ID and attempt number. Optionally scope by projectId or ciBuildId. Requires githubRunId and githubRunAttempt.", - cancelRunByGithubCITool.schema, - cancelRunByGithubCITool.handler -); - -// Specs API tools -server.tool( - "currents-get-spec-instance", - "Retrieves debugging data from a specific execution of a test spec file by instanceId.", - getSpecInstancesTool.schema, - getSpecInstancesTool.handler -); - -server.tool( - "currents-get-spec-files-performance", - "Retrieves spec files performance metrics for a specific project within a date range. Supports ordering by avgDuration, failedExecutions, failureRate, flakeRate, flakyExecutions, fullyReported, overallExecutions, suiteSize, timeoutExecutions, or timeoutRate. Supports filtering by tags, branches, groups, and authors. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", - getSpecFilesPerformanceTool.schema, - getSpecFilesPerformanceTool.handler -); - -// Tests API tools -server.tool( - "currents-get-tests-performance", - "Retrieves aggregated test metrics for a specific project within a date range. Supports ordering by failures, passes, flakiness, duration, executions, title, and various delta metrics. Supports filtering by spec name, test title, tags, branches, groups, authors, minimum executions, test state, and annotations. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", - getTestsPerformanceTool.schema, - getTestsPerformanceTool.handler -); - -server.tool( - "currents-get-tests-signatures", - "Generates a unique test signature based on project, spec file path, and test title. The test title can be a string or array of strings (for nested describe blocks). Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", - getTestSignatureTool.schema, - getTestSignatureTool.handler -); - -server.tool( - "currents-get-test-results", - "Retrieves historical test execution results for a specific test signature. Supports filtering by date range, branch, tags, git author, test status (passed/failed/pending/skipped), run group, flaky status, and annotations. Requires the test signature. If the signature is not known, first call 'currents-get-tests-signatures'.", - getTestResultsTool.schema, - getTestResultsTool.handler -); - -// Errors API tools -server.tool( - "currents-get-errors-explorer", - "Get aggregated error metrics for a project within a date range. Supports filtering by error_target, error_message, error_category, error_action, tags, branches, authors, and groups. Supports grouping by target, action, category, or message. Returns error counts, affected tests and branches, with timeline data. Requires projectId, date_start, and date_end.", - getErrorsExplorerTool.schema, - getErrorsExplorerTool.handler -); - -// Webhooks API tools -server.tool( - "currents-list-webhooks", - "List all webhooks for a project. Webhooks allow you to receive HTTP POST notifications when certain events occur in your test runs: RUN_FINISH (run completed), RUN_START (run started), RUN_TIMEOUT (run timed out), RUN_CANCELED (run was cancelled). Requires a projectId.", - listWebhooksTool.schema, - listWebhooksTool.handler -); - -server.tool( - "currents-create-webhook", - "Create a new webhook for a project. Specify the URL to receive POST notifications, optional custom headers (as JSON string), events to trigger on (RUN_FINISH, RUN_START, RUN_TIMEOUT, RUN_CANCELED), and an optional label. Requires projectId and url.", - createWebhookTool.schema, - createWebhookTool.handler -); - -server.tool( - "currents-get-webhook", - "Get a single webhook by ID. The hookId is a UUID. Returns full webhook details including url, headers, events, label, and timestamps.", - getWebhookTool.schema, - getWebhookTool.handler -); - -server.tool( - "currents-update-webhook", - "Update an existing webhook. You can update the url, headers (as JSON string), hookEvents array, or label. All fields are optional. The hookId is a UUID.", - updateWebhookTool.schema, - updateWebhookTool.handler -); - -server.tool( - "currents-delete-webhook", - "Delete a webhook. This permanently removes the webhook. The hookId is a UUID.", - deleteWebhookTool.schema, - deleteWebhookTool.handler -); - -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - logger.debug("🚀 Currents MCP Server is live"); - await new Promise(() => {}); -} - -main().catch((error) => { - logger.error("Fatal error in main():", error); +import { startMcpServer } from "./server.js"; + +startMcpServer().catch((error) => { + if ( + error instanceof Error && + error.message === MISSING_CURRENTS_API_KEY_MESSAGE + ) { + logger.error( + "CURRENTS_API_KEY is not set. Add your Currents API key to the environment (for example: export CURRENTS_API_KEY=)." + ); + } else { + logger.error({ err: error }, "Fatal error in main()"); + } process.exit(1); }); diff --git a/mcp-server/src/lib/env.ts b/mcp-server/src/lib/env.ts index 077a47c..691ffa4 100644 --- a/mcp-server/src/lib/env.ts +++ b/mcp-server/src/lib/env.ts @@ -1,3 +1,7 @@ export const CURRENTS_API_URL = process.env.CURRENTS_API_URL || "https://api.currents.dev/v1"; -export const CURRENTS_API_KEY = process.env.CURRENTS_API_KEY || ""; +export const CURRENTS_API_KEY = (process.env.CURRENTS_API_KEY ?? "").trim(); + +/** Machine-readable; CLI maps this to a short user message without a stack trace. */ +export const MISSING_CURRENTS_API_KEY_MESSAGE = + "CURRENTS_API_KEY env variable is not set."; diff --git a/mcp-server/src/package-environments.integration.test.ts b/mcp-server/src/package-environments.integration.test.ts new file mode 100644 index 0000000..a088fba --- /dev/null +++ b/mcp-server/src/package-environments.integration.test.ts @@ -0,0 +1,60 @@ +/** + * Workspace build integration tests: programmatic `startMcpServer` entry from a + * real Node child process (not Vitest’s module graph). + * + * Flow (each `it`): + * 1. `npm run test:run` has already run `build`, so `build/` and `build/cjs/` + * exist under the package root (`mcp-server/`). + * 2. Spawn `process.execPath` (Node) with a small script under + * `test/fixtures/*.mjs` or `*.cjs`. + * 3. Set `cwd` to that package root so paths and semantics match “consumer runs + * next to a checked-out / linked package,” not the Vitest test file’s dir. + * 4. The fixture imports the **built** API: + * - ESM: relative file URL to `build/api.js` (Node ESM resolution, `.js` + * extension required). + * - CJS: `require("../../build/cjs/api.js")` with `build/cjs/package.json` + * `type: commonjs` so nested `.js` files load as CommonJS. + * - CJS + dynamic `import()`: CommonJS script `import()`s the ESM build; + * exercises interop from a `.cjs` entry. + * 5. Each script prints a single token to stdout; the parent asserts it to + * detect load/execute failures (missing files, wrong export shape, etc.). + * + * What this does *not* cover: installing from `npm pack` / registry or resolving + * the package **name** `@currents/mcp` via `package.json` `exports` (see + * `package-published-esm.integration.test.ts`). + */ +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const root = fileURLToPath(new URL("..", import.meta.url)); + +describe("package consumers (Node ESM, CJS require, CJS dynamic import)", () => { + it("loads programmatic API from ESM", () => { + const out = execFileSync( + process.execPath, + [path.join(root, "test", "fixtures", "consumer-esm.mjs")], + { cwd: root, encoding: "utf-8" } + ); + expect(out.trim()).toBe("esm-ok"); + }); + + it("loads programmatic API from CJS require (build/cjs)", () => { + const out = execFileSync( + process.execPath, + [path.join(root, "test", "fixtures", "consumer-cjs.cjs")], + { cwd: root, encoding: "utf-8" } + ); + expect(out.trim()).toBe("cjs-require-ok"); + }); + + it("loads the ESM build via import() from a CommonJS script", () => { + const out = execFileSync( + process.execPath, + [path.join(root, "test", "fixtures", "consumer-cjs-dynamic-import.cjs")], + { cwd: root, encoding: "utf-8" } + ); + expect(out.trim()).toBe("cjs-dynamic-import-ok"); + }); +}); diff --git a/mcp-server/src/package-published-esm.integration.test.ts b/mcp-server/src/package-published-esm.integration.test.ts new file mode 100644 index 0000000..3d16589 --- /dev/null +++ b/mcp-server/src/package-published-esm.integration.test.ts @@ -0,0 +1,88 @@ +/** + * Published-artifact ESM integration test: resolve the **package name** + * `@currents/mcp` using `package.json` `exports` after an install from a + * tarball (same shape users get from the registry). + * + * Why a separate file from `package-environments.integration.test.ts`: + * workspace tests import `build/*.js` via relative paths; they never ask Node + * to apply `"exports"` for the scoped name. This suite closes that gap. + * + * Flow (`it`): + * 1. Prerequisite: `build/index.js` exists (`test:run` runs `build` first). + * If missing, the suite is skipped (same gate as CLI pack tests). + * 2. `packTarball`: run `npm pack` with `cwd` = package root. npm creates a + * `.tgz` containing exactly what publishing would ship (`package.json` + * `files`, plus auto-included `package.json`). That includes `build/` and + * the `exports` map pointing `"import"` → `./build/api.js`. + * 3. Create two temp dirs: one receives the `.tgz`, one is a minimal consumer + * project (`npm init -y`, then `npm install `). npm unpacks into + * `/node_modules/@currents/mcp`. + * 4. Copy `test/fixtures/consumer-published-esm.mjs` into the consumer project + * as `run-published-esm.mjs`. The script must live under `installDir` so + * Node walks upward to **that** `node_modules` for bare specifiers like + * `@currents/mcp` (a fixture left in the repo tree would not see the temp + * install’s `node_modules`). + * 5. Run `node run-published-esm.mjs` with `cwd = installDir`. Node loads the + * ESM entry via the `"import"` condition and must find `startMcpServer`. + * 6. Assert stdout `published-esm-ok`. Failures here typically mean broken + * `exports`, missing files from the packed `build/`, or a bad dual-package + * layout. + * */ +import { execFileSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdtempSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const buildIndex = path.join(root, "build", "index.js"); + +/** Run `npm pack` from the package root and return the path to the single `.tgz` in `packDest`. */ +function packTarball(packDest: string): string { + // Respect `files` and standard pack rules; do not mutate package.json (unlike release `publish.cjs`). + execFileSync("npm", ["pack", "--pack-destination", packDest], { + cwd: root, + stdio: ["ignore", "pipe", "pipe"], + }); + const tgz = readdirSync(packDest).filter((f) => f.endsWith(".tgz")); + if (tgz.length !== 1) { + throw new Error(`expected one .tgz in ${packDest}, got: ${tgz.join(", ")}`); + } + return path.join(packDest, tgz[0]); +} + +describe.skipIf(!existsSync(buildIndex))( + "published tarball (package exports, ESM)", + { timeout: 120_000 }, + () => { + it('resolves `import "@currents/mcp"` after npm install ', () => { + const packDir = mkdtempSync(path.join(tmpdir(), "mcp-pack-published-")); + const installDir = mkdtempSync( + path.join(tmpdir(), "mcp-install-published-") + ); + const tarball = packTarball(packDir); + execFileSync("npm", ["init", "-y"], { + cwd: installDir, + stdio: "ignore", + }); + execFileSync("npm", ["install", tarball], { + cwd: installDir, + stdio: "ignore", + }); + const fixture = path.join( + root, + "test", + "fixtures", + "consumer-published-esm.mjs" + ); + const runner = path.join(installDir, "run-published-esm.mjs"); + copyFileSync(fixture, runner); + const out = execFileSync(process.execPath, [runner], { + cwd: installDir, + encoding: "utf-8", + }); + expect(out.trim()).toBe("published-esm-ok"); + }); + } +); diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts new file mode 100644 index 0000000..b554c42 --- /dev/null +++ b/mcp-server/src/server.ts @@ -0,0 +1,379 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CURRENTS_API_KEY, + MISSING_CURRENTS_API_KEY_MESSAGE, +} from "./lib/env.js"; +import { logger } from "./lib/logger.js"; +// Actions tools +import { listActionsTool } from "./tools/actions/list-actions.js"; +import { createActionTool } from "./tools/actions/create-action.js"; +import { getActionTool } from "./tools/actions/get-action.js"; +import { updateActionTool } from "./tools/actions/update-action.js"; +import { deleteActionTool } from "./tools/actions/delete-action.js"; +import { enableActionTool } from "./tools/actions/enable-action.js"; +import { disableActionTool } from "./tools/actions/disable-action.js"; +import { listAffectedTestsTool } from "./tools/actions/list-affected-tests.js"; +import { getAffectedTestExecutionsTool } from "./tools/actions/get-affected-test-executions.js"; +import { getAffectedTestExecutionsByActionTool } from "./tools/actions/get-affected-test-executions-by-action.js"; +// Projects tools +import { getProjectsTool } from "./tools/projects/get-projects.js"; +import { getProjectTool } from "./tools/projects/get-project.js"; +import { getProjectInsightsTool } from "./tools/projects/get-project-insights.js"; +// Runs tools +import { getRunsTool } from "./tools/runs/get-runs.js"; +import { getRunDetailsTool } from "./tools/runs/get-run.js"; +import { findRunTool } from "./tools/runs/find-run.js"; +import { cancelRunTool } from "./tools/runs/cancel-run.js"; +import { resetRunTool } from "./tools/runs/reset-run.js"; +import { deleteRunTool } from "./tools/runs/delete-run.js"; +import { cancelRunByGithubCITool } from "./tools/runs/cancel-run-github-ci.js"; +// Specs tools +import { getSpecFilesPerformanceTool } from "./tools/specs/get-spec-files-performance.js"; +import { getSpecInstancesTool } from "./tools/specs/get-spec-instances.js"; +// Tests tools +import { getTestResultsTool } from "./tools/tests/get-test-results.js"; +import { getTestsPerformanceTool } from "./tools/tests/get-tests-performance.js"; +import { getTestSignatureTool } from "./tools/tests/get-tests-signature.js"; +// Errors tools +import { getErrorsExplorerTool } from "./tools/errors/get-errors-explorer.js"; +// Webhooks tools +import { listWebhooksTool } from "./tools/webhooks/list-webhooks.js"; +import { createWebhookTool } from "./tools/webhooks/create-webhook.js"; +import { getWebhookTool } from "./tools/webhooks/get-webhook.js"; +import { updateWebhookTool } from "./tools/webhooks/update-webhook.js"; +import { deleteWebhookTool } from "./tools/webhooks/delete-webhook.js"; + +const server = new McpServer({ + name: "currents", + version: "1.0.0", +}); + +// Actions API tools +server.registerTool( + "currents-list-actions", + { + description: + "List all actions for a project with optional filtering. Actions are rules that automatically modify test behavior (skip, quarantine, tag). Supports filtering by status (active/disabled/archived/expired) and search by name. Requires a projectId.", + inputSchema: listActionsTool.schema, + }, + listActionsTool.handler +); + +server.registerTool( + "currents-create-action", + { + description: + "Create a new action for a project. Actions define rules that automatically skip, quarantine, or tag tests based on conditions like test title, file path, git branch, etc. Requires projectId, name, action array, and matcher object.", + inputSchema: createActionTool.schema, + }, + createActionTool.handler +); + +server.registerTool( + "currents-get-action", + { + description: + "Get a single action by ID. The actionId is globally unique, so projectId is not required. Returns full action details including matcher conditions and current status.", + inputSchema: getActionTool.schema, + }, + getActionTool.handler +); + +server.registerTool( + "currents-update-action", + { + description: + "Update an existing action. The actionId is globally unique. You can update name, description, action array, matcher, or expiration date. All fields are optional.", + inputSchema: updateActionTool.schema, + }, + updateActionTool.handler +); + +server.registerTool( + "currents-delete-action", + { + description: + "Delete (archive) an action. This is a soft delete - the action will be marked as archived but not permanently removed. The actionId is globally unique.", + inputSchema: deleteActionTool.schema, + }, + deleteActionTool.handler +); + +server.registerTool( + "currents-enable-action", + { + description: + "Enable a disabled action. Changes the action status from disabled to active, making it apply to matching tests again. The actionId is globally unique.", + inputSchema: enableActionTool.schema, + }, + enableActionTool.handler +); + +server.registerTool( + "currents-disable-action", + { + description: + "Disable an active action. Changes the action status to disabled, temporarily preventing it from applying to tests. The actionId is globally unique.", + inputSchema: disableActionTool.schema, + }, + disableActionTool.handler +); + +server.registerTool( + "currents-list-affected-tests", + { + description: + "List tests affected by actions (quarantine, skip, tag) for a project within a date range. Returns aggregated data grouped by test signature. Supports filtering by action types, action ID, status, and search. Requires projectId, date_start, and date_end. (Preview endpoint: fields and path may change.)", + inputSchema: listAffectedTestsTool.schema, + }, + listAffectedTestsTool.handler +); + +server.registerTool( + "currents-get-affected-test-executions", + { + description: + "Get execution details for a specific affected test (by signature) within a date range. Returns individual test execution records with action info. Uses cursor-based pagination. Requires projectId, signature, date_start, and date_end.", + inputSchema: getAffectedTestExecutionsTool.schema, + }, + getAffectedTestExecutionsTool.handler +); + +server.registerTool( + "currents-get-affected-test-executions-by-action", + { + description: + "List test executions where a specific action/rule was applied, within a date range. Uses cursor-based pagination. Requires actionId, date_start, and date_end.", + inputSchema: getAffectedTestExecutionsByActionTool.schema, + }, + getAffectedTestExecutionsByActionTool.handler +); + +// Projects API tools +server.registerTool( + "currents-get-projects", + { + description: + "Retrieves projects available in the Currents platform. Supports cursor-based pagination with limit, starting_after, ending_before parameters, or set fetchAll=true for automatic pagination. This is a prerequisite for using any other tools that require project-specific information.", + inputSchema: getProjectsTool.schema, + }, + getProjectsTool.handler +); + +server.registerTool( + "currents-get-project", + { + description: + "Get a single project by ID. Returns project details including name, creation date, failFast setting, inactivity timeout, and default branch name.", + inputSchema: getProjectTool.schema, + }, + getProjectTool.handler +); + +server.registerTool( + "currents-get-project-insights", + { + description: + "Get aggregated run and test metrics for a project within a date range. Returns overall metrics and timeline data with configurable resolution (1h/1d/1w). Supports filtering by tags, branches, groups, and authors. Requires projectId, date_start, and date_end.", + inputSchema: getProjectInsightsTool.schema, + }, + getProjectInsightsTool.handler +); + +// Runs API tools +server.registerTool( + "currents-get-runs", + { + description: + "Retrieves a list of runs for a specific project with optional filtering. Supports filtering by branch, tags (with AND/OR operators), status (PASSED/FAILED/RUNNING/FAILING), completion state, date range, commit author, and search by ciBuildId or commit message. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", + inputSchema: getRunsTool.schema, + }, + getRunsTool.handler +); + +server.registerTool( + "currents-get-run-details", + { + description: + "Retrieves details of a specific test run. Requires a user-provided runId.", + inputSchema: getRunDetailsTool.schema, + }, + getRunDetailsTool.handler +); + +server.registerTool( + "currents-find-run", + { + description: + "Find a run by query parameters. Returns the most recent completed run matching the criteria. Can search by ciBuildId (exact match) or by branch/tags. Supports pwLastRun flag for Playwright last run info. Requires projectId.", + inputSchema: findRunTool.schema, + }, + findRunTool.handler +); + +server.registerTool( + "currents-cancel-run", + { + description: + "Cancel a run in progress. This will stop the run and mark it as cancelled. Requires runId.", + inputSchema: cancelRunTool.schema, + }, + cancelRunTool.handler +); + +server.registerTool( + "currents-reset-run", + { + description: + "Reset failed spec files in a run to allow re-execution. Requires runId and machineId array (1-63 machine IDs). Optionally supports batched orchestration.", + inputSchema: resetRunTool.schema, + }, + resetRunTool.handler +); + +server.registerTool( + "currents-delete-run", + { + description: + "Delete a run and all associated data. This is a permanent deletion. Requires runId.", + inputSchema: deleteRunTool.schema, + }, + deleteRunTool.handler +); + +server.registerTool( + "currents-cancel-run-github-ci", + { + description: + "Cancel a run by GitHub Actions workflow run ID and attempt number. Optionally scope by projectId or ciBuildId. Requires githubRunId and githubRunAttempt.", + inputSchema: cancelRunByGithubCITool.schema, + }, + cancelRunByGithubCITool.handler +); + +// Specs API tools +server.registerTool( + "currents-get-spec-instance", + { + description: + "Retrieves debugging data from a specific execution of a test spec file by instanceId.", + inputSchema: getSpecInstancesTool.schema, + }, + getSpecInstancesTool.handler +); + +server.registerTool( + "currents-get-spec-files-performance", + { + description: + "Retrieves spec files performance metrics for a specific project within a date range. Supports ordering by avgDuration, failedExecutions, failureRate, flakeRate, flakyExecutions, fullyReported, overallExecutions, suiteSize, timeoutExecutions, or timeoutRate. Supports filtering by tags, branches, groups, and authors. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", + inputSchema: getSpecFilesPerformanceTool.schema, + }, + getSpecFilesPerformanceTool.handler +); + +// Tests API tools +server.registerTool( + "currents-get-tests-performance", + { + description: + "Retrieves aggregated test metrics for a specific project within a date range. Supports ordering by failures, passes, flakiness, duration, executions, title, and various delta metrics. Supports filtering by spec name, test title, tags, branches, groups, authors, minimum executions, test state, and annotations. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", + inputSchema: getTestsPerformanceTool.schema, + }, + getTestsPerformanceTool.handler +); + +server.registerTool( + "currents-get-tests-signatures", + { + description: + "Generates a unique test signature based on project, spec file path, and test title. The test title can be a string or array of strings (for nested describe blocks). Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.", + inputSchema: getTestSignatureTool.schema, + }, + getTestSignatureTool.handler +); + +server.registerTool( + "currents-get-test-results", + { + description: + "Retrieves historical test execution results for a specific test signature. Supports filtering by date range, branch, tags, git author, test status (passed/failed/pending/skipped), run group, flaky status, and annotations. Requires the test signature. If the signature is not known, first call 'currents-get-tests-signatures'.", + inputSchema: getTestResultsTool.schema, + }, + getTestResultsTool.handler +); + +// Errors API tools +server.registerTool( + "currents-get-errors-explorer", + { + description: + "Get aggregated error metrics for a project within a date range. Supports filtering by error_target, error_message, error_category, error_action, tags, branches, authors, and groups. Supports grouping by target, action, category, or message. Returns error counts, affected tests and branches, with timeline data. Requires projectId, date_start, and date_end.", + inputSchema: getErrorsExplorerTool.schema, + }, + getErrorsExplorerTool.handler +); + +// Webhooks API tools +server.registerTool( + "currents-list-webhooks", + { + description: + "List all webhooks for a project. Webhooks allow you to receive HTTP POST notifications when certain events occur in your test runs: RUN_FINISH (run completed), RUN_START (run started), RUN_TIMEOUT (run timed out), RUN_CANCELED (run was cancelled). Requires a projectId.", + inputSchema: listWebhooksTool.schema, + }, + listWebhooksTool.handler +); + +server.registerTool( + "currents-create-webhook", + { + description: + "Create a new webhook for a project. Specify the URL to receive POST notifications, optional custom headers (as JSON string), events to trigger on (RUN_FINISH, RUN_START, RUN_TIMEOUT, RUN_CANCELED), and an optional label. Requires projectId and url.", + inputSchema: createWebhookTool.schema, + }, + createWebhookTool.handler +); + +server.registerTool( + "currents-get-webhook", + { + description: + "Get a single webhook by ID. The hookId is a UUID. Returns full webhook details including url, headers, events, label, and timestamps.", + inputSchema: getWebhookTool.schema, + }, + getWebhookTool.handler +); + +server.registerTool( + "currents-update-webhook", + { + description: + "Update an existing webhook. You can update the url, headers (as JSON string), hookEvents array, or label. All fields are optional. The hookId is a UUID.", + inputSchema: updateWebhookTool.schema, + }, + updateWebhookTool.handler +); + +server.registerTool( + "currents-delete-webhook", + { + description: + "Delete a webhook. This permanently removes the webhook. The hookId is a UUID.", + inputSchema: deleteWebhookTool.schema, + }, + deleteWebhookTool.handler +); + +/** Starts the MCP server over stdio (used by the CLI and programmatic embedders). */ +export async function startMcpServer(): Promise { + if (!CURRENTS_API_KEY) { + throw new Error(MISSING_CURRENTS_API_KEY_MESSAGE); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + logger.debug("🚀 Currents MCP Server is live"); + await new Promise(() => {}); +} diff --git a/mcp-server/src/tools/actions/create-action.ts b/mcp-server/src/tools/actions/create-action.ts index da5d10b..245b411 100644 --- a/mcp-server/src/tools/actions/create-action.ts +++ b/mcp-server/src/tools/actions/create-action.ts @@ -151,6 +151,6 @@ const handler = async ({ }; export const createActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/delete-action.ts b/mcp-server/src/tools/actions/delete-action.ts index 821df76..1bac890 100644 --- a/mcp-server/src/tools/actions/delete-action.ts +++ b/mcp-server/src/tools/actions/delete-action.ts @@ -33,6 +33,6 @@ const handler = async ({ actionId }: z.infer) => { }; export const deleteActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/disable-action.ts b/mcp-server/src/tools/actions/disable-action.ts index 8fd0082..9f49d67 100644 --- a/mcp-server/src/tools/actions/disable-action.ts +++ b/mcp-server/src/tools/actions/disable-action.ts @@ -33,6 +33,6 @@ const handler = async ({ actionId }: z.infer) => { }; export const disableActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/enable-action.ts b/mcp-server/src/tools/actions/enable-action.ts index 874b954..06dab4a 100644 --- a/mcp-server/src/tools/actions/enable-action.ts +++ b/mcp-server/src/tools/actions/enable-action.ts @@ -33,6 +33,6 @@ const handler = async ({ actionId }: z.infer) => { }; export const enableActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/get-action.ts b/mcp-server/src/tools/actions/get-action.ts index 21fa586..f66e9c2 100644 --- a/mcp-server/src/tools/actions/get-action.ts +++ b/mcp-server/src/tools/actions/get-action.ts @@ -33,6 +33,6 @@ const handler = async ({ actionId }: z.infer) => { }; export const getActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/get-affected-test-executions-by-action.ts b/mcp-server/src/tools/actions/get-affected-test-executions-by-action.ts index 2492955..b05be0b 100644 --- a/mcp-server/src/tools/actions/get-affected-test-executions-by-action.ts +++ b/mcp-server/src/tools/actions/get-affected-test-executions-by-action.ts @@ -90,6 +90,6 @@ const handler = async ({ }; export const getAffectedTestExecutionsByActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/get-affected-test-executions.ts b/mcp-server/src/tools/actions/get-affected-test-executions.ts index cd54b73..3d7b0a0 100644 --- a/mcp-server/src/tools/actions/get-affected-test-executions.ts +++ b/mcp-server/src/tools/actions/get-affected-test-executions.ts @@ -95,6 +95,6 @@ const handler = async ({ }; export const getAffectedTestExecutionsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/list-actions.ts b/mcp-server/src/tools/actions/list-actions.ts index 44a0126..3613f6c 100644 --- a/mcp-server/src/tools/actions/list-actions.ts +++ b/mcp-server/src/tools/actions/list-actions.ts @@ -61,6 +61,6 @@ const handler = async ({ }; export const listActionsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/list-affected-tests.ts b/mcp-server/src/tools/actions/list-affected-tests.ts index fabd3bd..bd9ea55 100644 --- a/mcp-server/src/tools/actions/list-affected-tests.ts +++ b/mcp-server/src/tools/actions/list-affected-tests.ts @@ -119,6 +119,6 @@ const handler = async ({ }; export const listAffectedTestsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/actions/update-action.ts b/mcp-server/src/tools/actions/update-action.ts index e83d4e9..18918af 100644 --- a/mcp-server/src/tools/actions/update-action.ts +++ b/mcp-server/src/tools/actions/update-action.ts @@ -166,6 +166,6 @@ const handler = async ({ }; export const updateActionTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/errors/get-errors-explorer.ts b/mcp-server/src/tools/errors/get-errors-explorer.ts index 1fd9b79..23fce23 100644 --- a/mcp-server/src/tools/errors/get-errors-explorer.ts +++ b/mcp-server/src/tools/errors/get-errors-explorer.ts @@ -199,6 +199,6 @@ const handler = async ({ }; export const getErrorsExplorerTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/projects/get-project-insights.ts b/mcp-server/src/tools/projects/get-project-insights.ts index fa41455..e343f8c 100644 --- a/mcp-server/src/tools/projects/get-project-insights.ts +++ b/mcp-server/src/tools/projects/get-project-insights.ts @@ -95,6 +95,6 @@ const handler = async ({ }; export const getProjectInsightsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/projects/get-project.ts b/mcp-server/src/tools/projects/get-project.ts index e1b9183..1a9f574 100644 --- a/mcp-server/src/tools/projects/get-project.ts +++ b/mcp-server/src/tools/projects/get-project.ts @@ -33,6 +33,6 @@ const handler = async ({ projectId }: z.infer) => { }; export const getProjectTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/projects/get-projects.ts b/mcp-server/src/tools/projects/get-projects.ts index 31425c8..c6c7e85 100644 --- a/mcp-server/src/tools/projects/get-projects.ts +++ b/mcp-server/src/tools/projects/get-projects.ts @@ -100,6 +100,6 @@ const handler = async ({ }; export const getProjectsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/cancel-run-github-ci.ts b/mcp-server/src/tools/runs/cancel-run-github-ci.ts index ea62365..3b37696 100644 --- a/mcp-server/src/tools/runs/cancel-run-github-ci.ts +++ b/mcp-server/src/tools/runs/cancel-run-github-ci.ts @@ -82,6 +82,6 @@ const handler = async ({ }; export const cancelRunByGithubCITool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/cancel-run.ts b/mcp-server/src/tools/runs/cancel-run.ts index e535eba..896c8a5 100644 --- a/mcp-server/src/tools/runs/cancel-run.ts +++ b/mcp-server/src/tools/runs/cancel-run.ts @@ -33,6 +33,6 @@ const handler = async ({ runId }: z.infer) => { }; export const cancelRunTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/delete-run.ts b/mcp-server/src/tools/runs/delete-run.ts index 6b46635..d24d69b 100644 --- a/mcp-server/src/tools/runs/delete-run.ts +++ b/mcp-server/src/tools/runs/delete-run.ts @@ -33,6 +33,6 @@ const handler = async ({ runId }: z.infer) => { }; export const deleteRunTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/find-run.ts b/mcp-server/src/tools/runs/find-run.ts index ddc0b17..0a8f920 100644 --- a/mcp-server/src/tools/runs/find-run.ts +++ b/mcp-server/src/tools/runs/find-run.ts @@ -76,6 +76,6 @@ const handler = async ({ }; export const findRunTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/get-run.ts b/mcp-server/src/tools/runs/get-run.ts index 8deb92b..9d2470e 100644 --- a/mcp-server/src/tools/runs/get-run.ts +++ b/mcp-server/src/tools/runs/get-run.ts @@ -30,6 +30,6 @@ const handler = async ({ runId }: z.infer) => { }; export const getRunDetailsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/get-runs.ts b/mcp-server/src/tools/runs/get-runs.ts index fbb2f56..39422c8 100644 --- a/mcp-server/src/tools/runs/get-runs.ts +++ b/mcp-server/src/tools/runs/get-runs.ts @@ -154,6 +154,6 @@ const handler = async ({ }; export const getRunsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/runs/reset-run.ts b/mcp-server/src/tools/runs/reset-run.ts index ee6d12c..12921de 100644 --- a/mcp-server/src/tools/runs/reset-run.ts +++ b/mcp-server/src/tools/runs/reset-run.ts @@ -67,6 +67,6 @@ const handler = async ({ }; export const resetRunTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/specs/get-spec-files-performance.ts b/mcp-server/src/tools/specs/get-spec-files-performance.ts index e2a7219..e69b53e 100644 --- a/mcp-server/src/tools/specs/get-spec-files-performance.ts +++ b/mcp-server/src/tools/specs/get-spec-files-performance.ts @@ -153,6 +153,6 @@ const handler = async ({ }; export const getSpecFilesPerformanceTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/specs/get-spec-instances.ts b/mcp-server/src/tools/specs/get-spec-instances.ts index 6985750..fb7d544 100644 --- a/mcp-server/src/tools/specs/get-spec-instances.ts +++ b/mcp-server/src/tools/specs/get-spec-instances.ts @@ -32,6 +32,6 @@ const handler = async ({ instanceId }: z.infer) => { }; export const getSpecInstancesTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/tests/get-test-results.ts b/mcp-server/src/tools/tests/get-test-results.ts index 85daa98..3cd2472 100644 --- a/mcp-server/src/tools/tests/get-test-results.ts +++ b/mcp-server/src/tools/tests/get-test-results.ts @@ -145,6 +145,6 @@ const handler = async ({ }; export const getTestResultsTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/tests/get-tests-performance.ts b/mcp-server/src/tools/tests/get-tests-performance.ts index 13f0af5..f87d87c 100644 --- a/mcp-server/src/tools/tests/get-tests-performance.ts +++ b/mcp-server/src/tools/tests/get-tests-performance.ts @@ -192,6 +192,6 @@ const handler = async ({ }; export const getTestsPerformanceTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/tests/get-tests-signature.ts b/mcp-server/src/tools/tests/get-tests-signature.ts index 8478bfb..d186498 100644 --- a/mcp-server/src/tools/tests/get-tests-signature.ts +++ b/mcp-server/src/tools/tests/get-tests-signature.ts @@ -69,6 +69,6 @@ const handler = async ({ }; export const getTestSignatureTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/create-webhook.ts b/mcp-server/src/tools/webhooks/create-webhook.ts index 5dccf70..f22c779 100644 --- a/mcp-server/src/tools/webhooks/create-webhook.ts +++ b/mcp-server/src/tools/webhooks/create-webhook.ts @@ -86,6 +86,6 @@ const handler = async ({ }; export const createWebhookTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/delete-webhook.ts b/mcp-server/src/tools/webhooks/delete-webhook.ts index f9ad7aa..203fa42 100644 --- a/mcp-server/src/tools/webhooks/delete-webhook.ts +++ b/mcp-server/src/tools/webhooks/delete-webhook.ts @@ -37,6 +37,6 @@ const handler = async ({ }; export const deleteWebhookTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/get-webhook.ts b/mcp-server/src/tools/webhooks/get-webhook.ts index 1d3538f..d37c1ed 100644 --- a/mcp-server/src/tools/webhooks/get-webhook.ts +++ b/mcp-server/src/tools/webhooks/get-webhook.ts @@ -37,6 +37,6 @@ const handler = async ({ }; export const getWebhookTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/list-webhooks.ts b/mcp-server/src/tools/webhooks/list-webhooks.ts index 452d4e4..d23649e 100644 --- a/mcp-server/src/tools/webhooks/list-webhooks.ts +++ b/mcp-server/src/tools/webhooks/list-webhooks.ts @@ -42,6 +42,6 @@ const handler = async ({ }; export const listWebhooksTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/update-webhook.ts b/mcp-server/src/tools/webhooks/update-webhook.ts index 1187a45..5ac2461 100644 --- a/mcp-server/src/tools/webhooks/update-webhook.ts +++ b/mcp-server/src/tools/webhooks/update-webhook.ts @@ -81,6 +81,6 @@ const handler = async ({ }; export const updateWebhookTool = { - schema: zodSchema.shape, + schema: zodSchema, handler, }; diff --git a/mcp-server/src/tools/webhooks/webhooks.test.ts b/mcp-server/src/tools/webhooks/webhooks.test.ts index 7bbbcac..5a9f201 100644 --- a/mcp-server/src/tools/webhooks/webhooks.test.ts +++ b/mcp-server/src/tools/webhooks/webhooks.test.ts @@ -63,7 +63,7 @@ describe("listWebhooksTool", () => { it("should have correct schema structure", () => { expect(listWebhooksTool.schema).toBeDefined(); - expect(listWebhooksTool.schema.projectId).toBeDefined(); + expect(listWebhooksTool.schema.shape.projectId).toBeDefined(); }); }); @@ -113,7 +113,7 @@ describe("getWebhookTool", () => { it("should have correct schema structure", () => { expect(getWebhookTool.schema).toBeDefined(); - expect(getWebhookTool.schema.hookId).toBeDefined(); + expect(getWebhookTool.schema.shape.hookId).toBeDefined(); }); }); @@ -208,11 +208,11 @@ describe("createWebhookTool", () => { it("should have correct schema structure", () => { expect(createWebhookTool.schema).toBeDefined(); - expect(createWebhookTool.schema.projectId).toBeDefined(); - expect(createWebhookTool.schema.url).toBeDefined(); - expect(createWebhookTool.schema.headers).toBeDefined(); - expect(createWebhookTool.schema.hookEvents).toBeDefined(); - expect(createWebhookTool.schema.label).toBeDefined(); + expect(createWebhookTool.schema.shape.projectId).toBeDefined(); + expect(createWebhookTool.schema.shape.url).toBeDefined(); + expect(createWebhookTool.schema.shape.headers).toBeDefined(); + expect(createWebhookTool.schema.shape.hookEvents).toBeDefined(); + expect(createWebhookTool.schema.shape.label).toBeDefined(); }); }); @@ -326,11 +326,11 @@ describe("updateWebhookTool", () => { it("should have correct schema structure", () => { expect(updateWebhookTool.schema).toBeDefined(); - expect(updateWebhookTool.schema.hookId).toBeDefined(); - expect(updateWebhookTool.schema.url).toBeDefined(); - expect(updateWebhookTool.schema.headers).toBeDefined(); - expect(updateWebhookTool.schema.hookEvents).toBeDefined(); - expect(updateWebhookTool.schema.label).toBeDefined(); + expect(updateWebhookTool.schema.shape.hookId).toBeDefined(); + expect(updateWebhookTool.schema.shape.url).toBeDefined(); + expect(updateWebhookTool.schema.shape.headers).toBeDefined(); + expect(updateWebhookTool.schema.shape.hookEvents).toBeDefined(); + expect(updateWebhookTool.schema.shape.label).toBeDefined(); }); }); @@ -394,6 +394,6 @@ describe("deleteWebhookTool", () => { it("should have correct schema structure", () => { expect(deleteWebhookTool.schema).toBeDefined(); - expect(deleteWebhookTool.schema.hookId).toBeDefined(); + expect(deleteWebhookTool.schema.shape.hookId).toBeDefined(); }); }); diff --git a/mcp-server/test/fixtures/consumer-cjs-dynamic-import.cjs b/mcp-server/test/fixtures/consumer-cjs-dynamic-import.cjs new file mode 100644 index 0000000..1452820 --- /dev/null +++ b/mcp-server/test/fixtures/consumer-cjs-dynamic-import.cjs @@ -0,0 +1,11 @@ +const assert = require("node:assert"); + +import("../../build/api.js") + .then(({ startMcpServer }) => { + assert.strictEqual(typeof startMcpServer, "function"); + console.log("cjs-dynamic-import-ok"); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/mcp-server/test/fixtures/consumer-cjs.cjs b/mcp-server/test/fixtures/consumer-cjs.cjs new file mode 100644 index 0000000..cb48245 --- /dev/null +++ b/mcp-server/test/fixtures/consumer-cjs.cjs @@ -0,0 +1,5 @@ +const assert = require("node:assert"); +const { startMcpServer } = require("../../build/cjs/api.js"); + +assert.strictEqual(typeof startMcpServer, "function"); +console.log("cjs-require-ok"); diff --git a/mcp-server/test/fixtures/consumer-esm.mjs b/mcp-server/test/fixtures/consumer-esm.mjs new file mode 100644 index 0000000..732cc94 --- /dev/null +++ b/mcp-server/test/fixtures/consumer-esm.mjs @@ -0,0 +1,5 @@ +import assert from "node:assert"; +import { startMcpServer } from "../../build/api.js"; + +assert.strictEqual(typeof startMcpServer, "function"); +console.log("esm-ok"); diff --git a/mcp-server/test/fixtures/consumer-published-esm.mjs b/mcp-server/test/fixtures/consumer-published-esm.mjs new file mode 100644 index 0000000..3d20a7d --- /dev/null +++ b/mcp-server/test/fixtures/consumer-published-esm.mjs @@ -0,0 +1,5 @@ +import assert from "node:assert"; +import { startMcpServer } from "@currents/mcp"; + +assert.strictEqual(typeof startMcpServer, "function"); +console.log("published-esm-ok"); diff --git a/mcp-server/tsconfig.cjs.json b/mcp-server/tsconfig.cjs.json new file mode 100644 index 0000000..5b87ad0 --- /dev/null +++ b/mcp-server/tsconfig.cjs.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + /* TS5110: CommonJS + Node16/NodeNext resolution is invalid; this build targets Node 20+ runtime only. */ + "module": "CommonJS", + "moduleResolution": "Node10", + "outDir": "./build/cjs", + "declaration": false, + "declarationMap": false + }, + "include": ["src/**/*.ts"], + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.spec.ts", + "src/index.ts" + ] +} diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json index 06b8783..1f77a58 100644 --- a/mcp-server/tsconfig.json +++ b/mcp-server/tsconfig.json @@ -1,8 +1,9 @@ { "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", + "target": "ES2023", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./build", "rootDir": "./src", "strict": true,