Fermi
by abel30567
README.md
# Fermi
A portable AI control plane built as a single [MCP](https://modelcontextprotocol.io)
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](https://abel30567.github.io/fermi-mcp/) — guide, component reference, architecture with data-flow diagrams, use cases, and the build-journey blog. Sources in [`docs-site/`](docs-site/).
> Architecture in depth: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
> Setup and day-to-day usage: [`docs/USAGE.md`](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`](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:
| Surface | Caller | Count |
|---------|--------|-------|
| **MCP tools** | connected hosts and channels | ~60 by default, ~85 with the macOS bridge enabled |
| **Sandbox capabilities** | code passed to `execute`, as `codemode.<name>(...)` | 39 (`meta_list_capabilities`) |
MCP tools by group (host-facing):
| Group | Tools |
|-------|-------|
| Memory | `memory_recall`, `memory_write`, `memory_update`, `memory_delete`, `memory_list_recent` |
| Skills | `skill_search`, `skill_load`, `skill_set`, `skill_delete` |
| Search | `search` (unified RRF), `session_search` |
| Code mode | `execute` |
| Filesystem | `fs_read`, `fs_write`, `fs_list` (R2, scope-checked) |
| Browser (cloud) | `browser_navigate`, `browser_screenshot`, `browser_extract`, `browser_action` |
| Browser sessions | `browser_session_launch`, `browser_session_action`, `browser_session_close`, `browser_session_list`, `browser_session_request_human`, `browser_session_resume` |
| Canvas | `open_generated_ui`, `canvas_update` |
| Plan mode | `session_set_mode`, `plan_draft`, `plan_approve` |
| Team | `team_spawn` |
| Hooks | `hooks_register`, `hooks_list`, `hooks_test` |
| Secrets | `secret_set`, `secret_list`, `secret_delete`, `secret_resolve`, `secret_approve_host` |
| Meta | `meta_list_capabilities`, `usage_stats` |
| Connectors | `connector_set`, `connector_get`, `connector_list`, `connector_delete` |
| Packages | `package_set`, `package_get`, `package_list`, `package_delete` |
| Retrievers | `retriever_set`, `retriever_run`, `retriever_get`, `retriever_list`, `retriever_delete` |
| OAuth | `oauth_register_client`, `oauth_get_client`, `oauth_list_clients`, `oauth_delete_client`, `oauth_authorize_url`, `totp_setup` |
| Phone calls | `phone_call_start`, `phone_call_status`, `phone_call_hangup` (Twilio + OpenAI GPT-Live; see [USAGE §8](docs/USAGE.md#8-optional-outbound-phone-calls-twilio--gpt-live)) |
| macOS bridge (optional) | 25 `mac_*` tools, registered only when `MACOS_MCP_URL` is set |
## Quick start
```bash
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:
```bash
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`](docs/USAGE.md).
### Prerequisites
- [Bun](https://bun.sh)
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/) (`bun add -g wrangler`)
- A Cloudflare account on the Workers Paid plan ($5/mo) — required for Durable
Objects, Browser Rendering, and Vectorize
### Development
```bash
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.
```bash
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):
| Secret | Required | Purpose |
|--------|----------|---------|
| `FERMI_SECRETS_KEY` | yes | Encryption key for the secrets store |
| `FERMI_OWNER_SECRET` | when `FERMI_AUTH_ENABLED=true` | Owner password for the OAuth consent screen and `/apps/*` login |
| `FERMI_BEARER_TOKEN` | for admin HTTP endpoints | Gates `/capabilities`, `/cron/capability-reindex`, `/admin/seed-skills` |
| `ANTHROPIC_API_KEY` | for `team_spawn` | Worker-side subagent inference |
| `TELEGRAM_BOT_TOKEN` | optional | Telegram channel |
| `SLACK_BOT_TOKEN`, `SLACK_BRIDGE_SECRET` | optional | Slack Socket Mode channel (app token lives on the daemon) |
| `MACOS_MCP_URL`, `MACOS_MCP_TOKEN` | optional | Local macOS bridge (enables the `mac_*` tools) |
| `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM_NUMBER`, `OPENAI_API_KEY`, `FERMI_PUBLIC_URL` | for phone calls | Outbound 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
```json
{ "mcpServers": { "fermi": { "url": "https://<worker>/mcp" } } }
```
### Claude Code
```bash
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
</content>
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessResponsive