{"slug": "gocd-mcp-server", "title": "GoCD MCP Server", "summary": "GoCD MCP Server is a production-grade Model Context Protocol server that exposes GoCD pipelines over Streamable HTTP with per-user authentication via GoCD Personal Access Tokens, enabling MCP-capable agents to inspect and operate pipelines while enforcing GoCD's own RBAC. The server offers 17 tools across three risk tiers, including read-only, safe actions, and config editing, gated by a single TOOLSET switch, and includes an audit log, health probes, and typed compact outputs.", "body_md": "A production-grade [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server\nthat exposes [GoCD](https://www.gocd.org/) over **Streamable HTTP**, authenticated **per-user**\nwith GoCD Personal Access Tokens (PAT).\n\nIt lets MCP-capable agents (Claude Code, Claude Desktop, IDE extensions, …) inspect and operate\nGoCD pipelines — list and trigger pipelines, read statuses/history/console logs, pause/cancel,\nand (optionally) edit pipeline configuration — while **GoCD's own RBAC is enforced** for every\ncall, because the server acts strictly as the authenticated user.\n\n**Protocol:** MCP revision`2025-11-25`\n\n, Streamable HTTP transport**SDK:** official`modelcontextprotocol/go-sdk`\n\n**Language:** Go (1.25+)**Target:** a single GoCD instance (verified against GoCD`25.4.0`\n\n)\n\n[Features](#features)[Tools & resources](#tools--resources)[Requirements](#requirements)[Installation](#installation)[Configuration](#configuration)[Authentication & authorization](#authentication--authorization)[Running the server](#running-the-server)[Connecting an MCP client](#connecting-an-mcp-client)[Toolsets (risk tiers)](#toolsets-risk-tiers)[Architecture](#architecture)[Security](#security)[Logging & observability](#logging--observability)[Development](#development)[Troubleshooting](#troubleshooting)[Contributing](#contributing)[License](#license)\n\n- 🔐\n**Per-user identity**— the MCP client presents a GoCD PAT as a bearer token; the server validates it and acts as that user, so GoCD RBAC applies automatically. No shared service account. - 🧰\n**17 tools across three risk tiers**— read-only, safe actions, and config editing — gated by a single`TOOLSET`\n\nswitch. - 🧱\n**Typed, compact outputs**— focused projections of GoCD's responses to keep token usage low. - 📓\n**Audit log**— every mutating operation is logged (login, action, target); tokens are never logged. - 🩺\n**Health probes**—`/healthz`\n\n(liveness) and`/readyz`\n\n(checks GoCD reachability). - 🧪\n**Well-tested**— unit (domain), contract (GoCD client via`httptest`\n\n), and end-to-end (MCP over the real SDK client) tests.\n\nMutating tools carry MCP annotations (`destructiveHint`\n\n/ `idempotentHint`\n\n) so the host can ask\nfor user confirmation before running them.\n\n| Tool | Toolset | Kind | Description |\n|---|---|---|---|\n`whoami` |\nall | read | Authenticated GoCD login (verifies the token) |\n`list_pipelines` |\nall | read | Dashboard pipelines with latest run status, pause/lock state (optional group filter) |\n`get_pipeline_status` |\nall | read | Pause / lock / schedulable state of a pipeline |\n`get_pipeline_history` |\nall | read | Past runs (paginated via `offset` ) with stage statuses |\n`get_pipeline_instance` |\nall | read | One run's detail incl. per-stage and per-job state/result |\n`get_job_console_log` |\nall | read | Console log of a job run (last `tail_lines` , default 200) |\n`list_agents` |\nall | read | Build agents and their config/runtime state |\n`get_pipeline_config` |\nall | read | Full pipeline config + ETag (needed to update) |\n`trigger_pipeline` |\nactions, full | action | Schedule a pipeline run |\n`pause_pipeline` |\nactions, full | action | Pause a pipeline (reason required) |\n`unpause_pipeline` |\nactions, full | action | Resume a paused pipeline |\n`cancel_stage` |\nactions, full | action | Cancel a running stage |\n`comment_on_pipeline` |\nactions, full | action | Comment on a pipeline instance |\n`update_pipeline_config` |\nfull | destructive |\nReplace a pipeline config (optimistic locking via ETag/If-Match) |\n`create_pipeline` |\nfull | destructive |\nCreate a new pipeline in a group |\n`update_agent` |\nfull | destructive |\nPatch an agent (enable/disable, resources, environments) |\n`delete_pipeline` |\nfull | destructive |\nDelete a pipeline (irreversible) |\n\nRead-only mirrors of dashboard data, addressable by URI for a user/app to attach as context:\n\n`gocd://dashboard`\n\n— all pipelines and latest status`gocd://agents`\n\n— all build agents`gocd://pipeline/{name}/config`\n\n— a pipeline's configuration`gocd://pipeline/{name}/history`\n\n— a pipeline's recent runs\n\n- Go\n**1.25+**(to build), or Docker - Network access to a GoCD server, and its URL (\n`GOCD_BASE_URL`\n\n—**required**, no default) - A GoCD\n**Personal Access Token** per user (see below) - TLS certificate/key for production (or a TLS-terminating proxy)\n\n``` php\ngo install github.com/ivinco/gocd-mcp/cmd/gocd-mcp@latest   # -> $GOBIN/gocd-mcp\ngit clone https://github.com/ivinco/gocd-mcp.git\ncd gocd-mcp\ngo build -o gocd-mcp ./cmd/gocd-mcp\n```\n\nThe image is multi-stage and runs on `distroless/static`\n\nas a non-root user (`nonroot`\n\n,\nuid 65532). It carries no configuration of its own: **mount** the config file (and TLS\ncertificates), and pass any **overrides via environment variables**.\n\n```\ndocker build -t gocd-mcp .\n```\n\n**Recommended: config file + certs mounted, secrets/overrides via env.**\n\nPrepare a `config.yaml`\n\n(see [ config.example.yaml](/Ivinco/gocd-mcp/blob/master/config.example.yaml)) and your TLS\nmaterial, then mount them read-only and point\n\n`CONFIG_FILE`\n\nat the mounted path:\n\n```\ndocker run --rm -p 8443:8443 \\\n  -v /etc/gocd-mcp/config.yaml:/etc/gocd-mcp/config.yaml:ro \\\n  -v /etc/gocd-mcp/certs:/etc/gocd-mcp/certs:ro \\\n  -e CONFIG_FILE=/etc/gocd-mcp/config.yaml \\\n  gocd-mcp\n```\n\nBecause the file wins over the environment, you can ship a baseline `config.yaml`\n\nand still\noverride a single value at runtime only for keys the file omits — keep environment-specific\nor sensitive values out of the file and inject them with `-e`\n\n(e.g. from your secret store):\n\n```\ndocker run --rm -p 8443:8443 \\\n  -v /etc/gocd-mcp/config.yaml:/etc/gocd-mcp/config.yaml:ro \\\n  -v /etc/gocd-mcp/certs:/etc/gocd-mcp/certs:ro \\\n  -e CONFIG_FILE=/etc/gocd-mcp/config.yaml \\\n  -e LOG_LEVEL=debug \\\n  gocd-mcp\n```\n\nNote: the file takes priority, so a key set in\n\n`config.yaml`\n\ncannot be overridden by`-e`\n\n. Put values you want to override at runtime in the environment and omit them from the file.\n\n**Env-only** (no file) — omit `CONFIG_FILE`\n\nand pass everything as environment variables:\n\n```\ndocker run --rm -p 8443:8443 \\\n  -e GOCD_BASE_URL=https://gocd.example.com \\\n  -e TLS_CERT_FILE=/certs/tls.crt -e TLS_KEY_FILE=/certs/tls.key \\\n  -e TOOLSET=readonly \\\n  -v /etc/gocd-mcp/certs:/certs:ro \\\n  gocd-mcp\n```\n\nEach MCP user still authenticates with their own GoCD PAT via the `Authorization: Bearer`\n\nheader (see [Authentication](#authentication--authorization)); the container holds no GoCD\ncredential of its own.\n\nThe server can be configured from **environment variables**, from a **YAML config file**,\nor both. Precedence is:\n\n```\nbuilt-in default  <  environment variable  <  YAML file   (the file wins)\n```\n\nA key present in the file overrides the matching environment variable; a key omitted from\nthe file falls back to the env var, then to the default. The file is loaded only when\n`CONFIG_FILE`\n\npoints at it (otherwise configuration is env-only, as before).\n\n| Variable | YAML key | Description | Default |\n|---|---|---|---|\n`CONFIG_FILE` |\n— | Path to a YAML config file (enables file-based config) | — |\n`GOCD_BASE_URL` |\n`gocd_base_url` |\nGoCD server root URL — required |\n— |\n`LISTEN_ADDR` |\n`listen_addr` |\nAddress to listen on | `:8443` |\n`TLS_CERT_FILE` |\n`tls_cert_file` |\nTLS certificate (set together with key) | — |\n`TLS_KEY_FILE` |\n`tls_key_file` |\nTLS key (set together with cert) | — |\n`MCP_ENDPOINT_PATH` |\n`mcp_endpoint_path` |\nPath of the MCP endpoint | `/mcp` |\n`GOCD_REQUEST_TIMEOUT` |\n`gocd_request_timeout` |\nPer-request timeout to GoCD | `30s` |\n`TOKEN_CACHE_TTL` |\n`token_cache_ttl` |\nHow long a validated PAT is cached | `60s` |\n`TOOLSET` |\n`toolset` |\n`readonly` | `actions` | `full` |\n`full` |\n`LOG_LEVEL` |\n`log_level` |\n`debug` | `info` | `warn` | `error` |\n`info` |\n`LOG_FILE` |\n`log_file` |\nLog destination; empty → stderr, else append JSON to this file | — |\n\n`GOCD_BASE_URL`\n\nis the only required setting — the server refuses to start without it,\nfrom either source. Everything else has a working default.\n\nSee [ config.example.yaml](/Ivinco/gocd-mcp/blob/master/config.example.yaml) for a fully annotated sample. Durations\nuse Go syntax (\n\n`30s`\n\n, `2m`\n\n, `1h`\n\n).The MCP client sends the user's GoCD PAT as a bearer token on **every** request:\n\n```\nAuthorization: Bearer <gocd-personal-access-token>\n```\n\nThe server validates the token against GoCD's `current_user`\n\nendpoint (caching the result for\n`TOKEN_CACHE_TTL`\n\n), then builds a **per-request GoCD client bound to that token**. As a result,\nevery GoCD call is made *as that user* and is subject to GoCD's role-based access control. The\nserver stores no credentials of its own.\n\nDesign note.This is a deliberate, documented deviation from the OAuth 2.1 section of the MCP spec: GoCD itself is the identity provider and the PAT is the credential (conceptually the stdio \"credentials from the environment\" model, lifted onto HTTP). A migration path to full OAuth 2.1 is kept open by isolating token verification behind a single interface.\n\n⚠️ TLS is required in production.The PAT travels in the`Authorization`\n\nheader. Run with`TLS_CERT_FILE`\n\n/`TLS_KEY_FILE`\n\n, or terminate TLS at a trusted proxy. The PAT is never logged.\n\nIn GoCD, open your user menu → **Personal Access Tokens** → create a token. Each user uses their\nown; the token's GoCD permissions define what that user can do through this server.\n\n```\n# Read-only, TLS-enabled\nTLS_CERT_FILE=server.crt TLS_KEY_FILE=server.key \\\nGOCD_BASE_URL=https://gocd.example.com \\\nTOOLSET=readonly \\\n./gocd-mcp\n```\n\nHealth checks (unauthenticated):\n\n``` php\ncurl -fsS https://localhost:8443/healthz   # -> ok\ncurl -fsS https://localhost:8443/readyz    # -> ready (200) if GoCD is reachable\n```\n\nUnauthenticated requests to the MCP endpoint return `401 Unauthorized`\n\n.\n\nThe binary is static and configuration-free by itself, so a unit is all it takes. Install\nit to `/usr/local/bin/gocd-mcp`\n\n, put a `config.yaml`\n\nnext to your TLS material, and point\n`CONFIG_FILE`\n\nat it:\n\n```\n[Unit]\nDescription=GoCD MCP server\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nEnvironment=CONFIG_FILE=/etc/gocd-mcp/config.yaml\nExecStart=/usr/local/bin/gocd-mcp\nRestart=always\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n```\n\nSet `log_file`\n\nin the config to write JSON logs to a path (rotate it with `logrotate`\n\n,\nusing `copytruncate`\n\n— the server keeps the file open), or leave it unset to log to\nstderr and let journald collect it.\n\nPoint the client at the Streamable HTTP endpoint and supply the GoCD PAT as a bearer token.\n\n**Claude Code** (`.mcp.json`\n\nin your project, or via `claude mcp add`\n\n):\n\n```\n{\n  \"mcpServers\": {\n    \"gocd\": {\n      \"type\": \"http\",\n      \"url\": \"https://your-host:8443/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${GOCD_PAT}\" }\n    }\n  }\n}\n```\n\n**Claude Desktop** (`claude_desktop_config.json`\n\n): use the same `mcpServers`\n\nblock with an\n`http`\n\ntransport and the `Authorization`\n\nheader.\n\nSet `GOCD_PAT`\n\nin your environment to your GoCD Personal Access Token.\n\n`TOOLSET`\n\ngates which tools are registered, so you can deploy the least-privileged surface\nappropriate to a use case:\n\n`TOOLSET` |\nIncludes |\n|---|---|\n`readonly` |\nQueries only — no state changes |\n`actions` |\n`readonly` + trigger / pause / unpause / cancel / comment |\n`full` |\n`actions` + config editing (update / create / delete pipeline, update agent) |\n\nA read-only deployment is useful for broad, low-risk access; `full`\n\nshould be reserved for\ntrusted operators (GoCD RBAC still applies on top).\n\nThe protocol layer is intentionally thin; all logic lives in a transport-agnostic domain layer, so the GoCD integration is independently testable.\n\n```\nMCP client ──Streamable HTTP (Bearer PAT)──▶ httpx (TLS, middleware, /healthz /readyz)\n                                                └─ bearer-token auth (validate PAT vs GoCD)\n                                                    └─ MCP server (tools / resources / prompts)\n                                                        └─ domain (validation, orchestration)\n                                                            └─ gocd (typed REST client, acts as user)\n```\n\n| Package | Responsibility |\n|---|---|\n`cmd/gocd-mcp` |\nEntry point: config → wiring → graceful shutdown |\n`internal/config` |\nEnv-based configuration + validation |\n`internal/httpx` |\nnet/http server, TLS, middleware (recovery / request-id / access log), health probes |\n`internal/auth` |\nBearer-token verification (PAT → GoCD), per-request principal, validation cache |\n`internal/mcpsrv` |\nMCP tools/resources, argument schemas, audit logging, error mapping |\n`internal/domain` |\nInput validation and orchestration of GoCD calls (no MCP, no net/http) |\n`internal/gocd` |\nTyped GoCD REST client (pinned API media-type versions, ETag/If-Match, error mapping) |\n`internal/obs` |\nStructured logging (slog) |\n\nGoCD's API uses per-endpoint versioned media types (`application/vnd.go.cd.vN+json`\n\n) and\noptimistic locking (ETag + `If-Match`\n\n) for config edits; both are handled in `internal/gocd`\n\n.\n\n**TLS required** in production; the PAT is a bearer credential.**Tokens are never logged**— access and audit logs contain the GoCD login only.** Authorization is delegated to GoCD**— the server never widens a user's permissions.** Audit trail**— every mutating action logs`action`\n\n,`login`\n\n, and`target`\n\n.**Least privilege**— run with`TOOLSET=readonly`\n\n(or`actions`\n\n) where full access isn't needed.**Input validation**— pipeline/agent identifiers are validated before building request paths.- Destructive tools are annotated so hosts can require explicit user confirmation.\n\nStructured JSON logs (`slog`\n\n) at `LOG_LEVEL`\n\n. Destination is `LOG_FILE`\n\n— unset means\nstderr (captured by Docker/journald); a path appends JSON there (rotate externally, e.g.\nlogrotate). **Tokens are never logged.** Three event types:\n\n`msg` |\nWhen | Key fields |\n|---|---|---|\n`request` |\nevery HTTP request | `method` , `path` , `status` , `duration_ms` , `request_id` , `login` (for authenticated `/mcp` calls) |\n`tool_call` |\nevery tool invocation (reads and writes) | `tool` , `login` , `ok` , `duration_ms` , `request_id` |\n`audit` |\nevery mutating tool | `action` , `login` , `target` |\n\n`request_id`\n\ncorrelates the `request`\n\nand `tool_call`\n\nlines of the same call. Tool\n*arguments* are not logged (to avoid leaking config bodies); the `audit`\n\nline carries the\nmutation target.\n\n```\ngo test ./...     # unit (domain), contract (gocd via httptest), e2e (MCP via SDK client)\ngo vet ./...\ngofmt -l .        # should print nothing\ngo build ./...\n```\n\nProject layout follows the package table in [Architecture](#architecture). Tests run fully\noffline (a fake GoCD via `httptest`\n\n); no live GoCD instance is required.\n\n| Symptom | Likely cause / fix |\n|---|---|\n`401 Unauthorized` on `/mcp` |\nMissing/invalid `Authorization: Bearer <PAT>` , or GoCD rejected the token |\n| Tool error \"your GoCD user lacks permission\" | The user's GoCD RBAC does not allow the operation |\n| Tool error \"version conflict (ETag mismatch)\" | Config changed since you read it — re-run `get_pipeline_config` and retry |\n`/readyz` returns 503 |\nGoCD is unreachable from the server (check `GOCD_BASE_URL` / network) |\n| A tool isn't listed | It's gated by `TOOLSET` — raise the tier (`actions` / `full` ) |\n`GOCD_BASE_URL is required` on startup |\nThe base URL has no default — set it in the config file or the environment |\n\nIssues and pull requests are welcome — see [CONTRIBUTING.md](/Ivinco/gocd-mcp/blob/master/CONTRIBUTING.md) for the\nbuild/test loop and the layering rules the codebase keeps to.\n\nFound a security problem? Please report it privately: see [SECURITY.md](/Ivinco/gocd-mcp/blob/master/SECURITY.md).\n\n[MIT](/Ivinco/gocd-mcp/blob/master/LICENSE) © Ivinco", "url": "https://wpnews.pro/news/gocd-mcp-server", "canonical_source": "https://github.com/Ivinco/gocd-mcp", "published_at": "2026-07-20 14:11:26+00:00", "updated_at": "2026-07-20 14:25:47.797121+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools", "artificial-intelligence"], "entities": ["GoCD", "Model Context Protocol", "Claude Code", "Claude Desktop"], "alternates": {"html": "https://wpnews.pro/news/gocd-mcp-server", "markdown": "https://wpnews.pro/news/gocd-mcp-server.md", "text": "https://wpnews.pro/news/gocd-mcp-server.txt", "jsonld": "https://wpnews.pro/news/gocd-mcp-server.jsonld"}}