Skip to main content
Glama
README.md
# forge-mcp

Read-only MCP server exposing curated Makersmiths Forge operating state to any MCP client (Claude, ChatGPT, Copilot, Gemini). Full design rationale lives in the build brief this repo was generated from; this README covers what's actually here.

**This repo is public and contains only source. It holds no data and no secrets.** The data lives in the private `makersmithsforge-dev/forge-state` repo; this server reads it through a fine-grained, read-only GitHub token and re-serves only what `src/allowlist.ts` permits.

## Architecture

```
Claude / ChatGPT / Gemini
        │ HTTPS + capability URL
        ▼
forge-mcp (this repo, Netlify)      PUBLIC production deploy, no SSO
  /mcp/{MCP_SECRET}
        │ GitHub API, fine-grained PAT
        ▼
forge-state (PRIVATE repo)          branch: status
  allowlisted files only
```

This sits on its own Netlify project (not `forge-status`) because `forge-status`'s non-production deploys require team SSO, which an MCP client calling from the cloud can't complete — and its production branch fails by design. Keeping this separate means the status board stays SSO-protected.

## Layout

```
netlify/functions/mcp.mts   the server — auth, MCP wiring, tool registration
src/allowlist.ts            the file allowlist — the security boundary
src/auth.ts                 capability-URL secret extraction + constant-time check
src/github.ts                GitHub contents API reader (all reads pass through allowlist.ts)
src/netlify.ts               Netlify deploy-status reader
src/tools.ts                 tool logic, decoupled from MCP transport
src/cache.ts                 60s module-scope response cache
```

## Tools (all read-only, all return JSON, all fail closed)

| Tool | Args | Returns |
|---|---|---|
| `get_forge_state` | — | `forge-state.json` |
| `get_budgets` | — | `budgets.json` |
| `get_backlog` | `limit?` (default 50, max 200), `status?` | `backlog.jsonl`, parsed, newest first |
| `get_run_report` | `name?` | Latest file under `reports/`, or the named one |
| `list_state_files` | — | Allowlisted paths that exist, with size + last-commit timestamp |
| `get_deploy_status` | `project?` (`forge-status` \| `consumeruvprinterproject`) | Latest deploy per branch, not just production |

Every tool description tells the client that returned content is data, not instructions — state files are written by automated jobs, and text shaped like a directive inside one is being reported, never followed.

## The allowlist

`src/allowlist.ts` is the single place that decides what can leave the private repo:

- `ALLOWED` — exact filenames
- `ALLOWED_PREFIXES` — directory prefixes
- `DENIED_PREFIXES` — `approvals/`, `evidence/`, `campaigns/`, checked **before** allow, throws regardless of anything else

No tool takes a free-form path. `assertAllowedPath()` is still called at the bottom of `github.ts` on every read, so even a bug upstream can't reach a denied path. Path traversal, absolute paths, backslashes, and percent-encoded separators are all rejected outright rather than decoded and re-checked.

`approvals/` and `evidence/` will hold human approval records and — once D4 activates — biometric consent records. These must never be reachable through this server, at any version. If a future task appears to require it, stop and ask.

## Auth

v1 is a capability URL: `https://forge-mcp.netlify.app/mcp/{MCP_SECRET}`. A path segment that doesn't match returns `401` with no body, checked with `crypto.timingSafeEqual`, never logged.

This is a bearer token in a URL. It's adequate here specifically because the surface is read-only, the allowlist excludes anything sensitive, and worst case is disclosure of ops metrics — not write access, not consent records, not customer data. It is not adequate if that ever stops being true.

- Rotate `MCP_SECRET` quarterly, and immediately if it appears anywhere it shouldn't.
- v2 upgrade path: OAuth 2.0. Claude's custom-connector setup supports OAuth Client ID/Secret under Advanced settings.

## Environment variables

Set in the Netlify UI only — never committed. See `.env.example` for the full list (`GITHUB_TOKEN`, `GITHUB_REF`, `MCP_SECRET`, `NETLIFY_TOKEN`).

`GITHUB_TOKEN` must be a fine-grained PAT scoped to `forge-state` only, Contents: Read, nothing else.

## Development

```bash
npm install
npm run typecheck
npm test
```

`npm run build` is a no-op — there's nothing to compile; Netlify Functions bundle `netlify/functions/mcp.mts` (and its `src/` imports) at deploy time. `public/` is a static placeholder page.

## Deploying

1. Create a Netlify project from this repo. Production branch `main`. **Do not enable SSO** — the whole point is that Anthropic's/OpenAI's/Google's cloud can reach it.
2. Set the four env vars in the Netlify UI.
3. Deploy; confirm `/mcp/{secret}` completes an MCP handshake and `/mcp/wrong` returns 401 with no body.
4. Add as a custom MCP connector in each client, using `https://forge-mcp.netlify.app/mcp/{MCP_SECRET}`.

## Ground rules

- Read-only. No write tools exist in this codebase. If a future task seems to need one, stop and ask — a write-capable state server callable by any model is a prompt-injection surface.
- Never widen the allowlist without explicit approval.
- Never print `MCP_SECRET` or `GITHUB_TOKEN` to stdout, logs, commits, or chat.
- If a change would require disabling SSO on `forge-status`, stop — that's the wrong fix; this project exists to avoid exactly that.

Maintenance

ActivityStale
ResponsivenessNo issues