Skip to main content
Glama

ci-guardian

A Python MCP server plus a GitHub Actions integration that lets headless Claude review PRs and triage CI failures on a real GitHub repository, with every GitHub write action routed exclusively through the MCP server (never raw git/gh), and that boundary enforced structurally via Claude Code hooks rather than left as a prompt-level suggestion.

Status

  • Phase 0 — Scaffold and ground rules

  • Phase 1 — The ops MCP server (github plugin)

  • Phase 2 — Hooks and permissions (structural, not advisory)

  • Phase 3 — Headless Claude: PR review and CI-failure triage

  • [~] Phase 4 — GitHub Actions: the loop closes itself (deployed, live trigger pending a real CLAUDE_CODE_OAUTH_TOKEN — see below)

  • Phase 5 — Large mechanical change, proposed as a draft PR (capstone)

Related MCP server: claude-beacon

Layout

src/ci_guardian_mcp/   # the MCP server package
tests/                 # unit tests (mocked GitHub client, no live API calls)
scripts/               # headless entry points + live smoke test (Phase 1+)
.github/workflows/     # the Actions workflow that triggers headless Claude (Phase 4)
.claude/settings.json  # PreToolUse/PostToolUse hooks (Phase 2)

Local dev

python -m venv .venv
./.venv/Scripts/python.exe -m pip install -e ".[dev]"
./.venv/Scripts/python.exe -m pytest -q
./.venv/Scripts/python.exe -m ruff check .
./.venv/Scripts/python.exe -m mypy --strict src/

Headless Claude (Phase 3+)

scripts/review_pr.sh <repo> <pr_number> and scripts/triage_ci_failure.sh <repo> <run_id> each run a single claude -p "..." --mcp-config .mcp.json --permission-mode bypassPermissions --output-format json invocation, no human in the loop. Two choices worth calling out:

  • --mcp-config .mcp.json points Claude at this project's MCP server explicitly (.mcp.json in the project root) rather than relying on auto-discovery, so headless runs don't depend on whatever's configured globally.

  • --permission-mode bypassPermissions skips the interactive tool-approval prompt, which would otherwise hang forever with no human to answer it. This is safe specifically because Phase 2's hooks are a separate enforcement layer that still runs regardless of permission mode — bypassing the "ask a human" gate doesn't bypass the PreToolUse hooks that block force-pushes, gh pr merge, blind patches, etc. That's the whole point of building hooks as a structural boundary rather than a permission-prompt convention: it's what makes an unattended run safe to leave running.

Both scripts need the claude CLI on PATH, a resolvable GitHub token (same resolution as the MCP server itself: GITHUB_TOKEN/GH_TOKEN, or gh auth login already run), and Claude Code's own auth. In GitHub Actions (Phase 4) that's a CLAUDE_CODE_OAUTH_TOKEN repo secret — a long-lived token from claude setup-token (needs an interactive Claude subscription login, so it's generated on a real machine, not a runner) rather than a raw ANTHROPIC_API_KEY.

Live status: .github/workflows/ci-guardian.yml is deployed to ci-guardian-sandbox and its plumbing is proven live — checkout, pip install, and the Claude Code CLI install all succeeded on a real run (#4). The CLAUDE_CODE_OAUTH_TOKEN secret is currently a placeholder (PLACEHOLDER_SET_YOUR_REAL_CLAUDE_CODE_OAUTH_TOKEN), so the claude -p step will fail auth until it's replaced with a real token from claude setup-token (interactive, needs a Claude subscription — run it on a real machine, then gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo dishagupta2901/ci-guardian-sandbox with the printed value). Once that's set, pushing any commit to an open PR (or opening a new one) re-triggers review-pr for real, and pushing a branch with a failing test triggers triage-ci-failure once its CI run completes.

Tool transport: PyGithub, not the gh CLI

The MCP server talks to GitHub through PyGithub (dependencies in pyproject.toml), not by shelling out to gh. Reasoning:

  • Testability without a subprocess boundary. Phase 1 requires unit tests against a mocked GitHub client with zero real API calls in pytest -q. Mocking a Python object (Github(...)) with unittest.mock/pytest-mock is direct; mocking a CLI means faking subprocess exit codes and parsing its stdout/stderr, which is slower to write and easier to get subtly wrong.

  • Typed responses map cleanly onto the required Pydantic models. Every tool in Phase 1 must return a typed Pydantic model and raise a typed ToolError on failure. PyGithub gives structured objects (PullRequest, WorkflowRun, GithubException with a real status code) to build those from; gh's JSON output would need re-parsing and its error signal is a shell exit code plus stderr text, which is a worse fit for the {not_found, invalid_input, upstream_timeout, upstream_unavailable, rate_limited, conflict} taxonomy this project requires.

  • No dependency on an external binary being installed/authenticated on every host. The MCP server is meant to run inside a GitHub Actions job in Phase 4; a pure-Python HTTP client (PyGithub) only needs a token in the environment. gh would need to be installed and gh auth login'd (or GH_TOKEN wired through) in every environment the server runs in, duplicating auth plumbing the MCP server already needs for its own client.

  • Trade-off acknowledged: the gh CLI is what Phase 2's hooks are written against — hooks block the agent from calling git push --force, gh pr merge, gh pr edit --add-label auto-merge etc. directly via Bash. That's a separate concern from what the MCP server uses internally: the hooks close off the raw-shell bypass path regardless of which library sits behind the MCP tools.

PyGithub is added to pyproject.toml dependencies now; no tool implementation lands until Phase 1.

Related MCP Connectors

Related MCP Servers