Skip to content

Repository files navigation

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_PREFIXESapprovals/, 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

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.

About

Read-only MCP server exposing curated Makersmiths Forge operating state to any MCP client

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages