Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
/hooks_receiver/ @hamir-suspect @DamjanBecirovic @forestileao
/keycloak/ @skipi @hamir-suspect
/loghub2/ @lucaspin @hamir-suspect @DamjanBecirovic
/mcp_server/ @DamjanBecirovic @hamir-suspect @dexyk
/notifications/ @hamir-suspect @dexyk @DamjanBecirovic
/periodic_scheduler/ @DamjanBecirovic @dexyk @skipi
/plumber/ @DamjanBecirovic @dexyk @skipi
Expand Down
71 changes: 71 additions & 0 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2626,6 +2626,77 @@ blocks:
- name: "Lint"
commands:
- make lint


# MCP Server
- name: "MCP Server: Provision Prod Image"
dependencies: []
run:
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
task:
env_vars:
- name: DOCKER_BUILDKIT
value: "1"
- name: APP_ENV
value: prod
prologue:
commands:
- checkout && cd mcp_server
jobs:
- name: "Build prod image"
commands:
- make pull
- make build
- make push
- name: "MCP Server: Deployment Preconditions"
dependencies: ["MCP Server: Provision Prod Image"]
run:
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
task:
env_vars:
- name: DOCKER_BUILDKIT
value: "1"
- name: APP_ENV
value: prod
- name: SERVICE_NAME
value: "MCP Server"
prologue:
commands:
- checkout && cd mcp_server
- make pull
jobs:
- name: "Check code"
commands:
- make check.go.code
- name: "Check dependencies"
commands:
- make check.go.deps
- name: "Check docker"
commands:
- make build
- make check.docker
- name: "MCP Server: QA"
dependencies: ["MCP Server: Provision Prod Image"]
run:
when: "change_in('/mcp_server', {pipeline_file: 'ignore', default_branch: 'main'})"
task:
env_vars:
- name: DOCKER_BUILDKIT
value: "1"
- name: APP_ENV
value: prod
prologue:
commands:
- checkout && cd mcp_server
- make build
jobs:
- name: "Test"
commands:
- make test.setup
- make test
- name: "Lint"
commands:
- make lint
# Encryptor
- name: "Encryptor: Provision Prod Image"
dependencies: []
Expand Down
6 changes: 6 additions & 0 deletions .semaphore/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@
"component": "loghub2"
}
],
"MCP Server": [
{
"path": "mcp_server",
"component": "mcp-server"
}
],
"Zebra": [
{
"path": "zebra",
Expand Down
12 changes: 12 additions & 0 deletions mcp_server/.air.dev.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = "."
tmp_dir = "tmp/dev"

[build]
cmd = "go build -o ./tmp/dev/mcp_server ./cmd/mcp_server"
bin = "./tmp/dev/mcp_server"
args = ["-http", ":3001"]
include_ext = ["go"]
exclude_dir = ["tmp", "vendor"]

[env]
MCP_USE_STUBS = "true"
19 changes: 19 additions & 0 deletions mcp_server/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Repository Guidelines

## Project Structure & Module Organization
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/`.

## Build, Test, and Development Commands
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.

## Coding Style & Naming Conventions
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.

## Testing Guidelines
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.

## Commit & Pull Request Guidelines
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.

## Security & Configuration Tips
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.
42 changes: 42 additions & 0 deletions mcp_server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
ARG GO_VERSION=1.25.2
ARG ALPINE_VERSION=3.22

FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
ARG APP_NAME
ARG BUILD_ENV
WORKDIR /app

ENV CGO_ENABLED=0 \
GO111MODULE=on \
GOOS=linux \
GOARCH=amd64

COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download

COPY cmd ./cmd
COPY pkg ./pkg
COPY test ./test

FROM base AS dev
RUN --mount=type=cache,target=/root/.cache/go-build \
go build ./cmd/mcp_server
CMD ["sh", "-c", "while sleep 1000; do :; done"]

FROM base AS builder
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o /tmp/mcp_server ./cmd/mcp_server

FROM alpine:${ALPINE_VERSION} AS runner
RUN adduser -D -H -s /sbin/nologin appuser
USER appuser
WORKDIR /app

COPY --from=builder /tmp/mcp_server /usr/local/bin/mcp_server

EXPOSE 3001

ENTRYPOINT ["/usr/local/bin/mcp_server"]
CMD ["-http", ":3001"]
47 changes: 47 additions & 0 deletions mcp_server/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
include ../Makefile

APP_NAME=mcp_server
APP_ENV=prod

INTERNAL_API_BRANCH?=master
TMP_REPO_DIR ?= /tmp/internal_api
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
PROTOC_IMAGE?=golang:1.24-alpine

.PHONY: tidy test test.setup lint pb.gen dev.run

tidy:
go mod tidy

test.setup:
@true

test:
go test ./...

lint:
go vet ./...

pb.gen:
rm -rf $(TMP_REPO_DIR)
mkdir -p $(TMP_REPO_DIR)
git clone git@github.com:renderedtext/internal_api.git $(TMP_REPO_DIR) && (cd $(TMP_REPO_DIR) && git checkout $(INTERNAL_API_BRANCH) && cd -)
docker run --rm \
-v $(PWD):/app \
-v $(TMP_REPO_DIR):/tmp/internal_api \
-w /app \
$(PROTOC_IMAGE) \
sh -c 'apk add --no-cache bash protobuf && \
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.2 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 && \
bash script/internal_api/gen.sh "$(INTERNAL_API_MODULES)" $(INTERNAL_API_BRANCH) /tmp/internal_api'
rm -rf $(TMP_REPO_DIR)

dev.run:
@if command -v air >/dev/null 2>&1; then \
echo "Starting MCP server with air (hot reload)"; \
air -c .air.dev.toml; \
else \
echo "air not found, falling back to go run"; \
MCP_USE_STUBS=true go run ./cmd/mcp_server -http :3001; \
fi
129 changes: 129 additions & 0 deletions mcp_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# mcp_server

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.

## Configuration:

### Claude Code:

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:

claude mcp add semaphore https://mcp.semaphoreci.com/mcp \
--scope user --transport http \
--header "Authorization: Bearer $MY_MCP_TOKEN"

Example prompt: "Help me figure out why have my test failed on Semaphore"

### Codex:

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)

[mcp_servers.semaphore]
url = "https://mcp.semaphoreci.com/mcp"
bearer_token_env_var = "MY_MCP_TOKEN"
startup_timeout_sec = 30
tool_timeout_sec = 300

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.

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.

_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.

## Contributor Guide

Refer to [`AGENTS.md`](AGENTS.md) for repository guidelines, project structure, and development workflows.

## Exposed tools

| Tool | Description |
| ---- | ----------- |
| `echo` | Returns the provided `message` verbatim (handy for smoke tests). |
| `organizations_list` | Lists organizations that the user can access. |
| `projects_list` | List projects that belong to a specific organization. |
| `projects_search` | Search projects inside an organization by project name, repository URL, or description. |
| `workflows_search` | Search recent workflows for a project (most recent first). |
| `pipelines_list` | List pipelines associated with a workflow (most recent first). |
| `pipeline_jobs` | List jobs belonging to a specific pipeline. |
| `jobs_describe` | Describes a job, surfacing agent details and lifecycle timestamps. |
| `jobs_logs` | Fetches job logs. Hosted jobs stream loghub events; self-hosted jobs return a URL to fetch logs. |

## Requirements

- Go 1.25 (toolchain `go1.25.2` is configured in `go.mod` and `Dockerfile`).
- SSH access to `renderedtext/internal_api` for protobuf generation.

## Generating protobuf stubs

The server consumes internal gRPC definitions. Generate (or refresh) the Go descriptors whenever the protos change:

```bash
cd mcp_server
make pb.gen INTERNAL_API_BRANCH=master
```

`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.

## Configuration

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.

| Purpose | Environment variables (first non-empty wins) |
| ------- | -------------------------------------------- |
| Workflow gRPC endpoint | `INTERNAL_API_URL_PLUMBER`, `MCP_WORKFLOW_GRPC_ENDPOINT`, `WF_GRPC_URL` |
| Pipeline gRPC endpoint | `INTERNAL_API_URL_PLUMBER`, `MCP_PIPELINE_GRPC_ENDPOINT`, `PPL_GRPC_URL` |
| Job gRPC endpoint | `INTERNAL_API_URL_JOB`, `MCP_JOB_GRPC_ENDPOINT`, `JOBS_API_URL` |
| Loghub gRPC endpoint (hosted logs) | `INTERNAL_API_URL_LOGHUB`, `MCP_LOGHUB_GRPC_ENDPOINT`, `LOGHUB_API_URL` |
| Loghub2 gRPC endpoint (self-hosted logs) | `INTERNAL_API_URL_LOGHUB2`, `MCP_LOGHUB2_GRPC_ENDPOINT`, `LOGHUB2_API_URL` |
| RBAC gRPC endpoint | `INTERNAL_API_URL_RBAC`, `MCP_RBAC_GRPC_ENDPOINT` |
| Users gRPC endpoint | `INTERNAL_API_URL_USER`, `MCP_USER_GRPC_ENDPOINT` |
| Featurehub gRPC endpoint | `INTERNAL_API_URL_FEATURE`, `MCP_FEATURE_GRPC_ENDPOINT` |
| Dial timeout | `MCP_GRPC_DIAL_TIMEOUT` (default `5s`) |
| Call timeout | `MCP_GRPC_CALL_TIMEOUT` (default `15s`) |

Hosted jobs require `loghub` to be reachable. Self-hosted jobs require `loghub2`. Missing endpoints yield structured MCP errors from the relevant tools.

## Running locally

```bash
cd mcp_server
make pb.gen # only needed after proto updates
go run ./cmd/mcp_server -http :3001
# or: make dev.run # launches with stubbed responses on :3001
```

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.

### Development stubs

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.

```bash
export MCP_USE_STUBS=true
go run ./cmd/mcp_server
# or: make dev.run
```

Disable the variable (or set it to anything other than `true`) to talk to real internal APIs again.

> 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`.

## Docker

Build the container image:

```bash
cd mcp_server
docker build -t semaphore-mcp-server .
```

Run it locally (listening on port 3001):

```bash
docker run --rm -p 3001:3001 \
-e INTERNAL_API_URL_PLUMBER=ppl:50053 \
-e INTERNAL_API_URL_JOB=semaphore-job-api:50051 \
-e INTERNAL_API_URL_LOGHUB=loghub:50051 \
-e INTERNAL_API_URL_LOGHUB2=loghub2-internal-api:50051 \
semaphore-mcp-server
```
Loading