Skip to content

Commit defded4

Browse files
feat(mcp_server): Add read-only MCP server implementation (#699)
Adds a read-only MCP server implementation with an initial set of tools for listing and describing workflows, pipelines, and jobs. These tools allow agents to check the status of the pipeline and fetch the logs for failed jobs, which allows agents to identify and fix the issue. --------- Co-authored-by: Amir Hasanbasic <43892661+hamir-suspect@users.noreply.github.com> Co-authored-by: hamir-suspect <ahasanbasic@semaphore.io>
1 parent 89557f8 commit defded4

File tree

75 files changed

+51960
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+51960
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
/hooks_receiver/ @hamir-suspect @DamjanBecirovic @forestileao
2424
/keycloak/ @skipi @hamir-suspect
2525
/loghub2/ @lucaspin @hamir-suspect @DamjanBecirovic
26+
/mcp_server/ @DamjanBecirovic @hamir-suspect @dexyk
2627
/notifications/ @hamir-suspect @dexyk @DamjanBecirovic
2728
/periodic_scheduler/ @DamjanBecirovic @dexyk @skipi
2829
/plumber/ @DamjanBecirovic @dexyk @skipi

.semaphore/semaphore.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,77 @@ blocks:
26262626
- name: "Lint"
26272627
commands:
26282628
- make lint
2629+
2630+
2631+
# MCP Server
2632+
- name: "MCP Server: Provision Prod Image"
2633+
dependencies: []
2634+
run:
2635+
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
2636+
task:
2637+
env_vars:
2638+
- name: DOCKER_BUILDKIT
2639+
value: "1"
2640+
- name: APP_ENV
2641+
value: prod
2642+
prologue:
2643+
commands:
2644+
- checkout && cd mcp_server
2645+
jobs:
2646+
- name: "Build prod image"
2647+
commands:
2648+
- make pull
2649+
- make build
2650+
- make push
2651+
- name: "MCP Server: Deployment Preconditions"
2652+
dependencies: ["MCP Server: Provision Prod Image"]
2653+
run:
2654+
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
2655+
task:
2656+
env_vars:
2657+
- name: DOCKER_BUILDKIT
2658+
value: "1"
2659+
- name: APP_ENV
2660+
value: prod
2661+
- name: SERVICE_NAME
2662+
value: "MCP Server"
2663+
prologue:
2664+
commands:
2665+
- checkout && cd mcp_server
2666+
- make pull
2667+
jobs:
2668+
- name: "Check code"
2669+
commands:
2670+
- make check.go.code
2671+
- name: "Check dependencies"
2672+
commands:
2673+
- make check.go.deps
2674+
- name: "Check docker"
2675+
commands:
2676+
- make build
2677+
- make check.docker
2678+
- name: "MCP Server: QA"
2679+
dependencies: ["MCP Server: Provision Prod Image"]
2680+
run:
2681+
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
2682+
task:
2683+
env_vars:
2684+
- name: DOCKER_BUILDKIT
2685+
value: "1"
2686+
- name: APP_ENV
2687+
value: prod
2688+
prologue:
2689+
commands:
2690+
- checkout && cd mcp_server
2691+
- make build
2692+
jobs:
2693+
- name: "Test"
2694+
commands:
2695+
- make test.setup
2696+
- make test
2697+
- name: "Lint"
2698+
commands:
2699+
- make lint
26292700
# Encryptor
26302701
- name: "Encryptor: Provision Prod Image"
26312702
dependencies: []

.semaphore/services.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@
145145
"component": "loghub2"
146146
}
147147
],
148+
"MCP Server": [
149+
{
150+
"path": "mcp_server",
151+
"component": "mcp-server"
152+
}
153+
],
148154
"Zebra": [
149155
{
150156
"path": "zebra",

mcp_server/.air.dev.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
root = "."
2+
tmp_dir = "tmp/dev"
3+
4+
[build]
5+
cmd = "go build -o ./tmp/dev/mcp_server ./cmd/mcp_server"
6+
bin = "./tmp/dev/mcp_server"
7+
args = ["-http", ":3001"]
8+
include_ext = ["go"]
9+
exclude_dir = ["tmp", "vendor"]
10+
11+
[env]
12+
MCP_USE_STUBS = "true"

mcp_server/AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
Semaphore is a polyglot monorepo. Core Elixir services (`auth/`, `guard/`, `projecthub/`) keep runtime code in `lib/` with ExUnit suites in `test/`. Go utilities such as `bootstrapper/`, `repohub/`, and this `mcp_server/` follow the `cmd/` (entrypoints) and `pkg/` (libraries, generated protobufs) layout. The Phoenix/React frontend lives in `front/` with assets under `front/assets/`. Shared documentation resides in `docs/` and `rfcs/`, and enterprise-only modules live in `ee/`.
5+
6+
## Build, Test, and Development Commands
7+
Use `make build` at the repo root to produce Docker images. Run Elixir suites with `make test.ex` or target a file via `make test.ex TEST_FILE=test/<path>.exs`. Go packages (including `mcp_server`) rely on `make test` (`go test ./...`) and `make lint` for `go vet` plus static checks. Frontend bundles run through `make test.js`. For a local UI, start `make dev.server`; `LOCAL-DEVELOPMENT.md` covers Minikube and dev-container workflows.
8+
9+
## Coding Style & Naming Conventions
10+
Elixir modules use PascalCase with snake_case filenames; tests end in `_test.exs`. Go packages stay lowercase, with table-driven `_test.go` suites. React components prefer PascalCase filenames. Formatters and linters are mandatory before commits: `make format.ex` for Elixir, `make lint` for Go, and `make lint.js` for frontend assets. Share helpers through `test/support/` instead of duplicating utilities.
11+
12+
## Testing Guidelines
13+
Elixir services use ExUnit with focused `describe` blocks; add `--only integration` for slower suites. Go code should include regression cases when bugfixing; run `go test ./...` (and `go test -race ./...` when touching concurrency paths). Frontend updates require `make test.js`. Align Phoenix endpoint changes with their paired ExUnit suites.
14+
15+
## Commit & Pull Request Guidelines
16+
Follow Conventional Commits (e.g., `feat(auth):`, `fix(front):`, `docs:`) and keep scopes tight. Summaries must state rationale and risk. Before opening a PR, ensure formatters, linters, and relevant `make test*` targets pass. Link issues where available and attach screenshots or logs for UI or automation changes.
17+
18+
## Security & Configuration Tips
19+
Surface dependency issues early with `make check.ex.deps`, `make check.go.deps`, and `make check.docker`. Store secrets in local `.env` files; never commit credentials. Runtime configuration reads internal gRPC endpoints from `INTERNAL_API_URL_PLUMBER`, `INTERNAL_API_URL_JOB`, `INTERNAL_API_URL_LOGHUB`, and `INTERNAL_API_URL_LOGHUB2`, falling back to legacy `MCP_*` variables. Export `DOCKER_BUILDKIT=1` to mirror CI Docker builds.

mcp_server/Dockerfile

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
ARG GO_VERSION=1.25.2
2+
ARG ALPINE_VERSION=3.22
3+
4+
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
5+
ARG APP_NAME
6+
ARG BUILD_ENV
7+
WORKDIR /app
8+
9+
ENV CGO_ENABLED=0 \
10+
GO111MODULE=on \
11+
GOOS=linux \
12+
GOARCH=amd64
13+
14+
COPY go.mod go.sum ./
15+
RUN --mount=type=cache,target=/go/pkg/mod \
16+
--mount=type=cache,target=/root/.cache/go-build \
17+
go mod download
18+
19+
COPY cmd ./cmd
20+
COPY pkg ./pkg
21+
COPY test ./test
22+
23+
FROM base AS dev
24+
RUN --mount=type=cache,target=/root/.cache/go-build \
25+
go build ./cmd/mcp_server
26+
CMD ["sh", "-c", "while sleep 1000; do :; done"]
27+
28+
FROM base AS builder
29+
RUN --mount=type=cache,target=/root/.cache/go-build \
30+
go build -o /tmp/mcp_server ./cmd/mcp_server
31+
32+
FROM alpine:${ALPINE_VERSION} AS runner
33+
RUN adduser -D -H -s /sbin/nologin appuser
34+
USER appuser
35+
WORKDIR /app
36+
37+
COPY --from=builder /tmp/mcp_server /usr/local/bin/mcp_server
38+
39+
EXPOSE 3001
40+
41+
ENTRYPOINT ["/usr/local/bin/mcp_server"]
42+
CMD ["-http", ":3001"]

mcp_server/Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
include ../Makefile
2+
3+
APP_NAME=mcp_server
4+
APP_ENV=prod
5+
6+
INTERNAL_API_BRANCH?=master
7+
TMP_REPO_DIR ?= /tmp/internal_api
8+
INTERNAL_API_MODULES?=include/internal_api/status,include/internal_api/response_status,plumber_w_f.workflow,plumber.pipeline,server_farm.job,loghub,loghub2,user,repository_integrator,rbac,organization,projecthub,feature
9+
PROTOC_IMAGE?=golang:1.24-alpine
10+
11+
.PHONY: tidy test test.setup lint pb.gen dev.run
12+
13+
tidy:
14+
go mod tidy
15+
16+
test.setup:
17+
@true
18+
19+
test:
20+
go test ./...
21+
22+
lint:
23+
go vet ./...
24+
25+
pb.gen:
26+
rm -rf $(TMP_REPO_DIR)
27+
mkdir -p $(TMP_REPO_DIR)
28+
git clone git@github.com:renderedtext/internal_api.git $(TMP_REPO_DIR) && (cd $(TMP_REPO_DIR) && git checkout $(INTERNAL_API_BRANCH) && cd -)
29+
docker run --rm \
30+
-v $(PWD):/app \
31+
-v $(TMP_REPO_DIR):/tmp/internal_api \
32+
-w /app \
33+
$(PROTOC_IMAGE) \
34+
sh -c 'apk add --no-cache bash protobuf && \
35+
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.2 && \
36+
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 && \
37+
bash script/internal_api/gen.sh "$(INTERNAL_API_MODULES)" $(INTERNAL_API_BRANCH) /tmp/internal_api'
38+
rm -rf $(TMP_REPO_DIR)
39+
40+
dev.run:
41+
@if command -v air >/dev/null 2>&1; then \
42+
echo "Starting MCP server with air (hot reload)"; \
43+
air -c .air.dev.toml; \
44+
else \
45+
echo "air not found, falling back to go run"; \
46+
MCP_USE_STUBS=true go run ./cmd/mcp_server -http :3001; \
47+
fi

mcp_server/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# mcp_server
2+
3+
The `mcp_server` service is a Model Context Protocol (MCP) server implemented with [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go). It exposes Semaphore workflow, pipeline, and job data to MCP-compatible clients.
4+
5+
## Configuration:
6+
7+
### Claude Code:
8+
9+
In terminal export the env var called MY_MCP_TOKEN with the value of the API token that should be used to connect to Semaphore MCP server. run the following command:
10+
11+
claude mcp add semaphore https://mcp.semaphoreci.com/mcp \
12+
--scope user --transport http \
13+
--header "Authorization: Bearer $MY_MCP_TOKEN"
14+
15+
Example prompt: "Help me figure out why have my test failed on Semaphore"
16+
17+
### Codex:
18+
19+
Open your ~/.codex/config.toml (if you’re using the CLI) or via the Codex IDE Extension in VS Code (Gear icon → MCP settings → Open config.toml)
20+
21+
[mcp_servers.semaphore]
22+
url = "https://mcp.semaphoreci.com/mcp"
23+
bearer_token_env_var = "MY_MCP_TOKEN"
24+
startup_timeout_sec = 30
25+
tool_timeout_sec = 300
26+
27+
In terminal export the env var called MY_MCP_TOKEN with the value of the API token that should be used to connect to Semaphore MCP server.
28+
29+
You can then use Semaphore MCP in codex CLI by starting it in that same terminal session, or in VS Code codex extension by starting the VS Code from that terminal session with `code <path-to-working-directory>` command.
30+
31+
_Note_: Due to current limitations of Codex extension for VS Code, if you start VS Code in any other way except from the terminal session where MY_MCP_TOKEN env var has correct value, the Semaphore MCP server will not work.
32+
33+
## Contributor Guide
34+
35+
Refer to [`AGENTS.md`](AGENTS.md) for repository guidelines, project structure, and development workflows.
36+
37+
## Exposed tools
38+
39+
| Tool | Description |
40+
| ---- | ----------- |
41+
| `echo` | Returns the provided `message` verbatim (handy for smoke tests). |
42+
| `organizations_list` | Lists organizations that the user can access. |
43+
| `projects_list` | List projects that belong to a specific organization. |
44+
| `projects_search` | Search projects inside an organization by project name, repository URL, or description. |
45+
| `workflows_search` | Search recent workflows for a project (most recent first). |
46+
| `pipelines_list` | List pipelines associated with a workflow (most recent first). |
47+
| `pipeline_jobs` | List jobs belonging to a specific pipeline. |
48+
| `jobs_describe` | Describes a job, surfacing agent details and lifecycle timestamps. |
49+
| `jobs_logs` | Fetches job logs. Hosted jobs stream loghub events; self-hosted jobs return a URL to fetch logs. |
50+
51+
## Requirements
52+
53+
- Go 1.25 (toolchain `go1.25.2` is configured in `go.mod` and `Dockerfile`).
54+
- SSH access to `renderedtext/internal_api` for protobuf generation.
55+
56+
## Generating protobuf stubs
57+
58+
The server consumes internal gRPC definitions. Generate (or refresh) the Go descriptors whenever the protos change:
59+
60+
```bash
61+
cd mcp_server
62+
make pb.gen INTERNAL_API_BRANCH=master
63+
```
64+
65+
`make pb.gen` clones `renderedtext/internal_api` and emits Go code under `pkg/internal_api/`. The generated files are required for builds—remember to commit them after regeneration.
66+
67+
## Configuration
68+
69+
The server dials internal gRPC services based on environment variables. Deployment defaults come from the `INTERNAL_API_URL_*` ConfigMap entries; legacy `MCP_*` variables and historical endpoints remain as fallbacks.
70+
71+
| Purpose | Environment variables (first non-empty wins) |
72+
| ------- | -------------------------------------------- |
73+
| Workflow gRPC endpoint | `INTERNAL_API_URL_PLUMBER`, `MCP_WORKFLOW_GRPC_ENDPOINT`, `WF_GRPC_URL` |
74+
| Pipeline gRPC endpoint | `INTERNAL_API_URL_PLUMBER`, `MCP_PIPELINE_GRPC_ENDPOINT`, `PPL_GRPC_URL` |
75+
| Job gRPC endpoint | `INTERNAL_API_URL_JOB`, `MCP_JOB_GRPC_ENDPOINT`, `JOBS_API_URL` |
76+
| Loghub gRPC endpoint (hosted logs) | `INTERNAL_API_URL_LOGHUB`, `MCP_LOGHUB_GRPC_ENDPOINT`, `LOGHUB_API_URL` |
77+
| Loghub2 gRPC endpoint (self-hosted logs) | `INTERNAL_API_URL_LOGHUB2`, `MCP_LOGHUB2_GRPC_ENDPOINT`, `LOGHUB2_API_URL` |
78+
| RBAC gRPC endpoint | `INTERNAL_API_URL_RBAC`, `MCP_RBAC_GRPC_ENDPOINT` |
79+
| Users gRPC endpoint | `INTERNAL_API_URL_USER`, `MCP_USER_GRPC_ENDPOINT` |
80+
| Featurehub gRPC endpoint | `INTERNAL_API_URL_FEATURE`, `MCP_FEATURE_GRPC_ENDPOINT` |
81+
| Dial timeout | `MCP_GRPC_DIAL_TIMEOUT` (default `5s`) |
82+
| Call timeout | `MCP_GRPC_CALL_TIMEOUT` (default `15s`) |
83+
84+
Hosted jobs require `loghub` to be reachable. Self-hosted jobs require `loghub2`. Missing endpoints yield structured MCP errors from the relevant tools.
85+
86+
## Running locally
87+
88+
```bash
89+
cd mcp_server
90+
make pb.gen # only needed after proto updates
91+
go run ./cmd/mcp_server -http :3001
92+
# or: make dev.run # launches with stubbed responses on :3001
93+
```
94+
95+
The server advertises itself as `semaphore-echo` and serves the MCP Streamable HTTP transport on `:3001`. Health probes remain on `GET /readyz` and `GET /healthz`. Use `-version` to print the binary version, `-name` to override the advertised implementation identifier, or `-http` to change the listening address.
96+
97+
### Development stubs
98+
99+
When you just want to exercise the MCP tools without wiring real services, export `MCP_USE_STUBS=true` before starting the server. The process will skip gRPC dialing and respond with deterministic in-memory data for workflows, pipelines, jobs, and logs.
100+
101+
```bash
102+
export MCP_USE_STUBS=true
103+
go run ./cmd/mcp_server
104+
# or: make dev.run
105+
```
106+
107+
Disable the variable (or set it to anything other than `true`) to talk to real internal APIs again.
108+
109+
> Tip: when [`air`](https://github.com/cosmtrek/air) is installed, `make dev.run` automatically enables hot reloading using `.air.dev.toml`; otherwise it falls back to `go run`.
110+
111+
## Docker
112+
113+
Build the container image:
114+
115+
```bash
116+
cd mcp_server
117+
docker build -t semaphore-mcp-server .
118+
```
119+
120+
Run it locally (listening on port 3001):
121+
122+
```bash
123+
docker run --rm -p 3001:3001 \
124+
-e INTERNAL_API_URL_PLUMBER=ppl:50053 \
125+
-e INTERNAL_API_URL_JOB=semaphore-job-api:50051 \
126+
-e INTERNAL_API_URL_LOGHUB=loghub:50051 \
127+
-e INTERNAL_API_URL_LOGHUB2=loghub2-internal-api:50051 \
128+
semaphore-mcp-server
129+
```

0 commit comments

Comments
 (0)