forge-mcp
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.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues