diff --git a/.circleci/config.yml b/.circleci/config.yml index cb1dc69..0614af8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,6 +64,27 @@ jobs: - run: name: Install dependencies command: npm ci + - run: + # Bakes the release version into the User-Agent header (src/tools/api.ts's + # USER_AGENT constant, compiled into dist/ below) and into server.json's + # version fields (MCP registry manifest — not read by this pipeline, but + # kept in sync with the release for whatever reads it out of the tagged + # checkout). Both stay "SNAPSHOT" on branch/PR builds. CIRCLE_TAG is + # validated against a strict vX.Y.Z pattern first: git ref names may + # contain shell metacharacters like $() or backticks, and CIRCLE_TAG is + # interpolated into a shell command below, so an unvalidated tag would be + # a command-injection vector. + name: Bake release version into User-Agent and server.json + command: | + if [ -n "${CIRCLE_TAG:-}" ]; then + if ! echo "${CIRCLE_TAG}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "CIRCLE_TAG '${CIRCLE_TAG}' does not match expected vX.Y.Z pattern — failing." >&2 + exit 1 + fi + VERSION="${CIRCLE_TAG#v}" + sed -i "s/rundeck-mcp\/SNAPSHOT/rundeck-mcp\/${VERSION}/" src/tools/api.ts + sed -i "s/\"SNAPSHOT\"/\"${VERSION}\"/" server.json + fi - run: name: Build command: npm run build diff --git a/package.json b/package.json index 8b63143..a39774c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rundeck/mcp", - "version": "0.0.1", + "version": "SNAPSHOT", "description": "MCP server for Rundeck documentation", "type": "module", "main": "dist/index.js", diff --git a/server.json b/server.json index ba279e3..1df9421 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.rundeckpro/rundeck-mcp", "description": "Rundeck MCP server — exposes Rundeck documentation, API access, and job/runner management tools to AI assistants.", - "version": "1.0.0", + "version": "SNAPSHOT", "repository": { "url": "https://github.com/rundeckpro/rundeck_mcp", "source": "github" @@ -11,7 +11,7 @@ { "registryType": "npm", "identifier": "@rundeck/mcp", - "version": "1.0.0", + "version": "SNAPSHOT", "transport": { "type": "stdio" }, diff --git a/src/__tests__/tools/api.test.ts b/src/__tests__/tools/api.test.ts index f6e958a..6cd4b5c 100644 --- a/src/__tests__/tools/api.test.ts +++ b/src/__tests__/tools/api.test.ts @@ -7,6 +7,7 @@ import { rundeckSetupToken, rundeckListEndpoints, isRunnerCredentialRegenerationEndpoint, + USER_AGENT, } from "../../tools/api.js"; import { configManager } from "../../config.js"; @@ -137,11 +138,12 @@ describe("API Tools", () => { // The endpoint "/projects" gets prepended with the API base URL const callArgs = mockFetch.mock.calls[0]; expect(callArgs[0]).toContain("/api/59/projects"); - expect(callArgs[1]).toMatchObject({ - method: "GET", - headers: expect.objectContaining({ - "X-Rundeck-Auth-Token": "test-token", - }), + expect(callArgs[1]).toMatchObject({ method: "GET" }); + expect((callArgs[1] as RequestInit).headers).toEqual({ + "X-Rundeck-Auth-Token": "test-token", + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": USER_AGENT, }); }); diff --git a/src/tools/api.ts b/src/tools/api.ts index a2e4cda..132b921 100644 --- a/src/tools/api.ts +++ b/src/tools/api.ts @@ -7,6 +7,10 @@ import { configManager } from "../config.js"; import { listApiEndpoints } from "../resources/api.js"; import { loadOpenApiDocument, validateOpenApiRequest } from "../utils/openapi-validate.js"; +// Replaced with the release tag's version by the CI pipeline's tagged builds (see +// .circleci/config.yml's "build" job); stays "SNAPSHOT" on branch/PR builds. +export const USER_AGENT = "rundeck-mcp/SNAPSHOT"; + /** * Node's `fetch` (undici) collapses every network-level failure into a generic * `TypeError: fetch failed`, with the actual reason nested one level down in @@ -123,6 +127,7 @@ export async function rundeckApiCall(params: { "X-Rundeck-Auth-Token": config.apiToken, "Accept": "application/json", "Content-Type": params.content_type || "application/json", + "User-Agent": USER_AGENT, }; const options: RequestInit = {