# 09-mcp-github.md

kstack:
  book: Centerpoint Home Lab
  chapter: AI & Automation
  page: MCP GitHub Server
  tags: [mcp, github, oauth2, ai, claude]
---

## Overview

The MCP GitHub Server exposes GitHub's Model Context Protocol (MCP) server over
HTTPS with OAuth 2.1 authentication, making it accessible to Claude.ai and other
MCP-compatible AI clients from anywhere on the internet.

The stack consists of two containers:

- **`mcp-github-proxy`** — a custom Node.js OAuth 2.1 proxy (locally built image)
  that validates JWTs from Authentik before forwarding requests to the MCP server.
- **`github-mcp-server`** — the official GitHub MCP server running in HTTP mode,
  internal-only, with access scoped to repos and issues.

## Access

| Type      | URL                                       | Notes                                             |
|-----------|-------------------------------------------|---------------------------------------------------|
| Internal  | `https://mcp-github.home.local`           | LAN access via Step-CA TLS                        |
| External  | `https://mcp-github.jeeves5454.ddns.net`  | OAuth 2.1 authenticated — no Authentik ForwardAuth |

The external route uses **OAuth 2.1** (not Authentik ForwardAuth) as the auth layer.
GeoBlock and CrowdSec are still applied. The OAuth 2.1 issuer is Authentik.

## Containers in This Stack

| Container            | Image                                       | Role                                     |
|----------------------|---------------------------------------------|------------------------------------------|
| `mcp-github-proxy`   | `mcp-github-proxy:latest` (local build)     | OAuth 2.1 proxy + MCP request forwarder  |
| `github-mcp-server`  | `ghcr.io/github/github-mcp-server:latest`   | GitHub MCP server (HTTP mode, internal)  |

## Configuration

**Compose project:** `mcp-github`
**Compose file:** `/home/jeeves/docker/mcp-github/proxy/docker-compose.yml`

### `mcp-github-proxy` Environment Variables

| Variable                | Value / Notes                                                   |
|-------------------------|-----------------------------------------------------------------|
| `PORT`                  | `3000`                                                          |
| `GITHUB_MCP_URL`        | `http://github-mcp-server:8080`                                 |
| `GITHUB_PERSONAL_ACCESS_TOKEN` | **REDACTED** — PAT with read-only scopes (Contents, Issues, Metadata) |
| `AUTHENTIK_ISSUER`      | `https://auth.jeevesconsults.ca/application/o/mcp-github/`     |
| `AUTHENTIK_JWKS_URL`    | `https://auth.jeevesconsults.ca/application/o/mcp-github/jwks/` |
| `SERVER_URL`            | `https://mcp-github.jeeves5454.ddns.net`                        |

### `github-mcp-server` Environment Variables

| Variable                          | Value / Notes                                         |
|-----------------------------------|-------------------------------------------------------|
| `GITHUB_PERSONAL_ACCESS_TOKEN`    | **REDACTED** — same read-only PAT as proxy            |
| `GITHUB_TOOLSETS`                 | `repos,issues` — restricts available tools            |
| `GITHUB_READ_ONLY`                | `1` — belt-and-suspenders read-only flag              |

> **Security note:** `GITHUB_READ_ONLY=1` has a known bug in HTTP mode
> (github/github-mcp-server #2156) — toolset flags may not fully enforce read-only
> behaviour. Primary read-only enforcement is the PAT scopes themselves
> (Contents/Issues/Metadata: read only).

### Traefik Labels

```yaml
# External route — GeoBlock + CrowdSec only (OAuth 2.1 handles auth)
traefik.http.routers.mcp-github-ext.rule: Host(`mcp-github.jeeves5454.ddns.net`)
traefik.http.routers.mcp-github-ext.entrypoints: websecure
traefik.http.routers.mcp-github-ext.tls.certresolver: letsencrypt
traefik.http.routers.mcp-github-ext.middlewares: plex-geoblock@file,crowdsec-bouncer@file
traefik.http.routers.mcp-github-ext.service: mcp-github-proxy-svc

# Internal route
traefik.http.routers.mcp-github-int.rule: Host(`mcp-github.home.local`)
traefik.http.routers.mcp-github-int.entrypoints: websecure
traefik.http.routers.mcp-github-int.tls.certresolver: step-ca
traefik.http.routers.mcp-github-int.service: mcp-github-proxy-svc

traefik.http.services.mcp-github-proxy-svc.loadbalancer.server.port: 3000
```

Note: Authentik ForwardAuth (`authentik-auth@docker`) is **not** used here.
OAuth 2.1 is the authentication mechanism — the proxy validates the `Authorization`
header JWT against Authentik's JWKS endpoint directly.

### Sub-section: GitHub MCP Server

`github-mcp-server` runs the official GitHub MCP server in HTTP mode on port `8080`,
internal-only. It is never directly exposed to Traefik or the host — all requests
arrive through the `mcp-github-proxy` over the `mcp-github-internal` network.

The server is restricted to `repos` and `issues` toolsets, providing read-only
access to repository content, metadata, and issues. Write operations are not
available via the PAT scopes.

### Proxy Image Build

The `mcp-github-proxy` image is built locally on Centerpoint before deployment:

```bash
cd /home/jeeves/docker/mcp-github/proxy
docker build -t mcp-github-proxy:latest .
```

Source files: `/home/jeeves/docker/mcp-github/proxy/{Dockerfile,package.json,src/}`

## Volumes / Bind Mounts

No persistent volumes required. Both containers are stateless — the proxy holds no
state, and the MCP server reads from GitHub's API on every request.

## Networks

| Network                       | Purpose                                             |
|-------------------------------|-----------------------------------------------------|
| `traefik-net`                 | Exposes `mcp-github-proxy` via Traefik              |
| `mcp-github_mcp-github-internal` | `mcp-github-proxy` ↔ `github-mcp-server` communication |

`github-mcp-server` is on the internal network only — never on `traefik-net`.

## Healthchecks

Both containers have healthchecks defined:

```yaml
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:<port>/health"]
  interval: 30s
  timeout: 5s
  retries: 3
```

- Proxy healthcheck: `http://localhost:3000/health`
- MCP server healthcheck: `http://localhost:8080/health`

## Authentik OAuth 2.1 Provider Setup

In Authentik, an OAuth2/OIDC provider is configured for MCP GitHub with:

| Setting              | Value                                                   |
|----------------------|---------------------------------------------------------|
| Redirect URI         | `https://claude.ai/api/mcp/auth_callback`               |
| Issuer               | `https://auth.jeevesconsults.ca/application/o/mcp-github/` |
| JWKS URL             | `<issuer>/jwks/`                                        |

## Dependencies

- Authentik (`auth.jeevesconsults.ca`) — proxy validates JWTs against Authentik JWKS
- GitHub API (internet access required for the MCP server to function)
- Traefik on `traefik-net` for both routes

## Notes / Gotchas

- The GitHub PAT must be rotated before expiry. If it expires, all GitHub MCP tool
  calls will fail with 401 errors. Update in both container environment variables.
- The proxy image must be rebuilt after any source code changes:
  ```bash
  cd /home/jeeves/docker/mcp-github/proxy
  docker build -t mcp-github-proxy:latest .
  docker compose up -d --force-recreate mcp-github-proxy
  ```
- Claude.ai connects to this server via the external URL. The redirect URI
  (`https://claude.ai/api/mcp/auth_callback`) must be registered in the Authentik
  OAuth2 provider — adding any other redirect URI will cause the OAuth flow to fail.
- GeoBlock allows CA, US, and IN. Claude.ai's servers may originate from other
  regions — if MCP calls fail, check Traefik logs for GeoBlock rejections and
  adjust the country allowlist accordingly.

---
*Last Updated: 2026-06-16*