forge-mcp
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 onlyThis 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 cacheTools (all read-only, all return JSON, all fail closed)
Tool | Args | Returns |
| — |
|
| — |
|
|
|
|
|
| Latest file under |
| — | Allowlisted paths that exist, with size + last-commit timestamp |
|
| 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 filenamesALLOWED_PREFIXES— directory prefixesDENIED_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_SECRETquarterly, 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
npm install
npm run typecheck
npm testnpm 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
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.Set the four env vars in the Netlify UI.
Deploy; confirm
/mcp/{secret}completes an MCP handshake and/mcp/wrongreturns 401 with no body.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_SECRETorGITHUB_TOKENto 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.