Skip to content
Conduit

mcp server

fermi-mcp

Fermi — a portable AI control plane: one MCP server on Cloudflare Workers with shared memory, permissions, and skills across Claude and other hosts.

abel30567/fermi-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 26, 2026

Fermi

A portable AI control plane built as a single MCP server on Cloudflare Workers. Connect it to Claude.ai, Claude Desktop, Claude Code, Cursor, or ChatGPT and they share the same memory, the same permission model, and the same skills. Unattended channels (Telegram, Discord, WhatsApp, Slack) enqueue to a D1 task queue drained by a Mac-local daemon.

One server, one memory, one permission spine — the host changes, the agent doesn't.

Full documentation site: abel30567.github.io/fermi-mcp — guide, component reference, architecture with data-flow diagrams, use cases, and the build-journey blog. Sources in docs-site/. Architecture in depth: docs/ARCHITECTURE.md. Setup and day-to-day usage: docs/USAGE.md.

What it does

  • Cross-host memory — facts, preferences, and events in D1, recalled from any connected host. A nightly job summarizes sessions, de-duplicates near-identical memories by embedding similarity, and decays stale ones.
  • Skills — procedures stored as versioned SKILL.md documents in R2 with metadata in D1. You can author them directly or promote them from a memory; a weekly job proposes draft skills from recent session summaries for you to refine.
  • Permission spine — every tool declares a scope and a risk level. High-risk tools require a two-phase approval token. Mutating tools are blocked in plan mode. Shell code is screened against a blocklist. Every call is audited to D1.
  • Hooks — declarative, glob-matched deny-gates that run before a tool, evaluated with deny > ask > allow precedence (today deny is the enforced decision).
  • Unified search — Reciprocal Rank Fusion (k=60) over capabilities, skills, memories, and message history, blending keyword, FTS5, and Vectorize semantic lanes.
  • Code mode — the execute tool runs JavaScript in an isolated Worker sandbox where capabilities are reachable as codemode.<name>(...). Outbound HTTP goes through a gateway that injects secrets and enforces per-secret host allow-lists.
  • Dual-lane browser — a headless cloud lane (Cloudflare Browser Rendering) for scraping and a headed local lane (a macOS bridge over Cloudflare Tunnel) for sites that need a real hardware fingerprint and authenticated state.
  • Channels & subagents — Telegram, Discord, WhatsApp, and Slack gateway channels drained by a local daemon; team_spawn runs role-prompted subagents.
  • Live Canvas — agent-driven UI over a Durable Object + WebSocket, persistent across turns and hosts.

Architecture at a glance

 MCP Hosts ─┐
 Channels ──┼──► Cloudflare Worker ──► D1 · R2 · KV · Workers AI · Vectorize
 Cron ──────┘     (FermiMCP + 3 DOs)
  • Worker (src/index.ts) routes MCP (/mcp, /sse), channels, OAuth, the /apps/* host, the canvas WebSocket, and cron dispatch. When FERMI_AUTH_ENABLED is true, an OAuth provider gates the MCP transports; otherwise they are open.
  • Durable Objects: FermiMCP (the agent, SQLite-backed), LiveCanvasDO, SandboxStorageDO, BrowserSessionDO.
  • Storage: D1 (memory, sessions/messages + FTS5, audit, hooks, skills metadata, secrets metadata, oauth, approval tokens), R2 (skills, files, apps), KV (config), Workers AI (embeddings + summaries), Vectorize (semantic index).

See docs/ARCHITECTURE.md for the full diagram and data flow.

Two tool surfaces

Fermi exposes tools at two layers, and they are deliberately not identical:

SurfaceCallerCount
MCP toolsconnected hosts and channels~60 by default, ~85 with the macOS bridge enabled
Sandbox capabilitiescode passed to execute, as codemode.<name>(...)39 (meta_list_capabilities)

MCP tools by group (host-facing):

GroupTools
Memorymemory_recall, memory_write, memory_update, memory_delete, memory_list_recent
Skillsskill_search, skill_load, skill_set, skill_delete
Searchsearch (unified RRF), session_search
Code modeexecute
Filesystemfs_read, fs_write, fs_list (R2, scope-checked)
Browser (cloud)browser_navigate, browser_screenshot, browser_extract, browser_action
Browser sessionsbrowser_session_launch, browser_session_action, browser_session_close, browser_session_list, browser_session_request_human, browser_session_resume
Canvasopen_generated_ui, canvas_update
Plan modesession_set_mode, plan_draft, plan_approve
Teamteam_spawn
Hookshooks_register, hooks_list, hooks_test
Secretssecret_set, secret_list, secret_delete, secret_resolve, secret_approve_host
Metameta_list_capabilities, usage_stats
Connectorsconnector_set, connector_get, connector_list, connector_delete
Packagespackage_set, package_get, package_list, package_delete
Retrieversretriever_set, retriever_run, retriever_get, retriever_list, retriever_delete
OAuthoauth_register_client, oauth_get_client, oauth_list_clients, oauth_delete_client, oauth_authorize_url, totp_setup
Phone callsphone_call_start, phone_call_status, phone_call_hangup (Twilio + OpenAI GPT-Live; see USAGE §8)
macOS bridge (optional)25 mac_* tools, registered only when MACOS_MCP_URL is set

Quick start

git clone <repo-url> fermi && cd fermi
bun install
wrangler login

chmod +x bootstrap.sh
./bootstrap.sh my-instance-name        # provisions D1, R2, KV, Vectorize; writes wrangler.jsonc

cd packages/worker
wrangler secret put FERMI_SECRETS_KEY   # required: encrypts stored secrets
wrangler secret put FERMI_OWNER_SECRET  # owner password for the OAuth consent screen
wrangler secret put FERMI_BEARER_TOKEN  # gates the admin HTTP endpoints
bun run migrate:remote
wrangler deploy --var FERMI_AUTH_ENABLED:true

⚠️ Deploy with FERMI_AUTH_ENABLED:true (or set it in wrangler.jsonc vars). Without it the MCP transports are completely unauthenticated on a public workers.dev URL — anyone who finds it can read your memories, resolve your stored secrets, and run code. Open mode is for wrangler dev only.

Deploying from GitHub Actions. .github/workflows/deploy.yml deploys master after CI passes. It renders wrangler.jsonc with scripts/render-wrangler.mjs from the production environment (secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, D1_DATABASE_ID, KV_FERMI_ID, KV_OAUTH_ID; vars: WORKER_NAME, D1_DATABASE_NAME, R2_BUCKET_NAME, VECTORIZE_INDEX_NAME, FERMI_PUBLIC_URL), applies D1 migrations, deploys with FERMI_AUTH_ENABLED:true, then smoke-tests /health, /mcp and the OAuth host. Without CLOUDFLARE_API_TOKEN the deploy job is skipped. Check your values locally with node scripts/render-wrangler.mjs --check.

Agent evals on worker PRs. .github/workflows/evals.yml runs fermi-daemon's PR smoke (abel30567/fermi-daemon@main, scripts/evals-ci.sh with evals/smoke.json, k=3, the baseline harness) against the PR's head as the eval worker. It runs on ready_for_review and every later push of a non-draft, same-repo PR that touches packages/worker/src/** or packages/worker/seeds/**; a newer push cancels the older run. It posts the summary and a per-scenario pass table (scripts/evals-scenarios.mjs; never transcripts) as a PR comment and sets the commit status evals/smoke. The token is the evals environment's CLAUDE_CODE_OAUTH_TOKEN secret, passed on a file descriptor as in fermi-daemon; without it the run is skipped.

Then seed the bundled skills and confirm the capability registry:

curl -X POST https://<worker>/admin/seed-skills -H "Authorization: Bearer $FERMI_BEARER_TOKEN"
curl https://<worker>/capabilities             -H "Authorization: Bearer $FERMI_BEARER_TOKEN"

Full instructions, including auth modes and channel setup, are in docs/USAGE.md.

Prerequisites

  • Bun
  • Wrangler (bun add -g wrangler)
  • A Cloudflare account on the Workers Paid plan ($5/mo) — required for Durable Objects, Browser Rendering, and Vectorize

Development

bun run dev            # wrangler dev --local
bun run check          # Biome lint + format check
bun run format         # auto-format
bun run migrate:local  # apply D1 migrations locally
bun run test           # private scan, script tests, vitest (worker): unit + MCP contract

bun run test starts with scripts/private-scan.mjs, which fails on any git-tracked E.164 or formatted phone number, WhatsApp JID or LID, or phone-looking chat_id/sender that is not a placeholder (5555550xxx, 1555555xxxx, 10000000000000, 20000000000000). It also applies one regex per line from the untracked ~/.config/fermi/private-patterns (or $FERMI_PRIVATE_PATTERNS) when that file exists, and names only file, line and kind.

Running the eval target

packages/worker/wrangler.eval.jsonc runs the real worker fully locally for the live harness evals: no Cloudflare login, no remote ids, auth off. Outbound APIs are pointed at local fakes with SLACK_API_BASE, TELEGRAM_API_BASE, DISCORD_API_BASE, TWILIO_API_BASE and ANTHROPIC_API_BASE (unset means the real service). MACOS_MCP_URL and any tokens come from the same env file.

cd packages/worker
cat > /tmp/fermi-eval.env <<EOF
SLACK_API_BASE=http://127.0.0.1:9101
SLACK_BOT_TOKEN=xoxb-fake
MACOS_MCP_URL=http://127.0.0.1:9102
EOF
STATE=/tmp/fermi-eval/state
wrangler dev --local --config wrangler.eval.jsonc --port 8787 --persist-to $STATE \
  --env-file /tmp/fermi-eval.env &
node scripts/eval-seed.mjs test/fixtures/eval/sample.json --persist-to $STATE \
  --url http://127.0.0.1:8787
# ... run the eval against http://127.0.0.1:8787/mcp ...
node scripts/eval-dump.mjs --persist-to $STATE > dump.json

eval-seed applies the D1 migrations, then replaces the profile, memories, allowlist and tasks with the fixture's. A conversation block ({ channel, chat_id, sender?, history: [{ role, text, minutes_ago }] }, see test/fixtures/eval/history.json) replaces that chat's transcript with the same sessions/messages rows the channel gateways write, each turn dated minutes_ago before seed time, so context_bootstrap, conversation_history and session_search return it. With sender, user turns get the group attribution prefix ([sender] text). It writes each fixture skill ({ slug, body, metadata? }) by calling the real skill_set tool on the running worker's /mcp (auth is off there). Channel bindings, such as the Slack finance channel mapping to household-finance, are daemon prompt rules rather than worker state, so a fixture only seeds the skill and the allowlist.

eval-dump prints profile, memories, allowlist, tasks, conversation messages (with their chat host), outbox, schedules, skill metadata and audit as JSON. It reads D1 only. Phone calls are kept in each PhoneCallDO, not in D1, so evals assert on them from the fake Twilio log (via TWILIO_API_BASE) and the harness trace.

Configuration

Secrets are set with wrangler secret put (values entered interactively):

SecretRequiredPurpose
FERMI_SECRETS_KEYyesEncryption key for the secrets store
FERMI_OWNER_SECRETwhen FERMI_AUTH_ENABLED=trueOwner password for the OAuth consent screen and /apps/* login
FERMI_BEARER_TOKENfor admin HTTP endpointsGates /capabilities, /cron/capability-reindex, /admin/seed-skills
ANTHROPIC_API_KEYfor team_spawnWorker-side subagent inference
TELEGRAM_BOT_TOKENoptionalTelegram channel
SLACK_BOT_TOKEN, SLACK_BRIDGE_SECREToptionalSlack Socket Mode channel (app token lives on the daemon)
MACOS_MCP_URL, MACOS_MCP_TOKENoptionalLocal macOS bridge (enables the mac_* tools)
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, OPENAI_API_KEY, FERMI_PUBLIC_URLfor phone callsOutbound calls. Read from the Fermi secrets store (secret_set, scope app) first, then from Worker vars/secrets of the same name

Auth mode is controlled by the FERMI_AUTH_ENABLED var. Set it to true to put the MCP transports behind OAuth — recommended for every deployed instance. Leaving it unset serves /mcp and /sse with no authentication, which is only appropriate for local wrangler dev.

In OAuth mode, connecting hosts self-register and land on Fermi's consent screen, which validates your owner secret and (once enrolled via totp_setup) a TOTP code:

Connecting a host

Claude.ai (web)

Settings → Connectors → add a custom connector, URL https://<worker>/sse.

Claude Desktop / Cursor / VS Code

{ "mcpServers": { "fermi": { "url": "https://<worker>/mcp" } } }

Claude Code

claude mcp add fermi --transport http https://<worker>/mcp

Cost

Single-user, expect ~$8–20/month: the Workers Paid plan ($5) plus variable Browser Rendering and Anthropic API usage. D1/R2/KV/Vectorize stay near the free tier at this volume. MCP hosts pay for their own inference; Fermi only spends on model calls in unattended channels and team_spawn.

License

MIT

↑↓ navigate openesc close