mana-mcp:
- Policy-gate section: POLICY_MODE semantics, the four decision
rules, where to find soak metrics during log-only burn-in.
- /metrics section pointing at the Prometheus job.
mana-ai:
- New v0.8 status block: reminderChannel wiring, the two live
producers (tokenBudgetReminder active, retryLoopReminder dormant
pending LoopState extension), why POLICY_MODE here is limited to
freetext inspection, why parallel-reads have no effect until the
tool-registry absorbs the full AI_TOOL_CATALOG (M4 of personas).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.7 KiB
mana-mcp
MCP (Model Context Protocol) gateway for Mana. External agents — Claude Desktop, Claude Code, the persona-runner — connect here to drive Mana modules over a single, JWT-authed protocol.
Plan: docs/plans/mana-mcp-and-personas.md
Tech Stack
| Layer | Technology |
|---|---|
| Runtime | Bun |
| Framework | Hono |
| Transport | MCP Streamable HTTP (@modelcontextprotocol/sdk) |
| Auth | JWT verify via JWKS from mana-auth (no service-key path) |
| Tools | @mana/tool-registry (shared SSOT — also consumed by mana-ai) |
Port: 3069
Quick Start
# Requires: mana-auth (3001) and mana-sync (3050) running
pnpm --filter @mana/mcp-service dev
Health check: curl localhost:3069/health
Architecture
External Agent (Claude Desktop, persona-runner, …)
│
│ POST /mcp Authorization: Bearer <jwt>
│ GET /mcp X-Mana-Space: <spaceId>
│ DELETE /mcp Mcp-Session-Id: <session>
▼
┌─────────────────────────────────┐
│ src/index.ts (Hono :3069) │
│ ├── CORS │
│ ├── /health, /metrics │
│ └── /mcp │
│ │ │
│ ▼ authenticateRequest │
│ │ src/auth.ts │
│ │ (verify JWT via JWKS, │
│ │ pull X-Mana-Space) │
│ ▼ │
│ handleMcpRequest │
│ src/transport.ts │
│ (per-user MCP session, │
│ scoped, no cross-user) │
│ │ │
│ ▼ createMcpServerForUser │
│ src/mcp-adapter.ts │
│ (registry → MCP tools) │
└────────────────┬────────────────┘
│
▼
@mana/tool-registry handlers
│
┌───────────┼────────────┐
▼ ▼ ▼
mana-sync mana-auth (other services
(push/pull) (org list) via tool handlers)
Auth model
Every MCP request must carry:
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <jwt> |
yes | EdDSA JWT issued by mana-auth, verified via JWKS |
X-Mana-Space: <spaceId> |
yes | Active Space — every tool write lands here |
Mcp-Session-Id: <id> |
after init | Session tracking; absent on first POST /mcp |
No service-key path. Personas, the persona-runner, and any future agent client all hold real user JWTs. There is no admin bypass — admin-scoped tools (scope: 'admin') are silently filtered out before being registered with the MCP server.
Per-user session isolation. A session is created against a specific user. If a request later arrives with a session ID that belongs to a different user, the gateway returns 403. This is defense-in-depth against session-id collisions or a leaked session header.
Adding a tool
Tools are defined in packages/mana-tool-registry/src/modules/<module>.ts, never in this service. Steps:
- Open the relevant module file (or create a new one).
- Define the
ToolSpec: name (module.verb), zod input/output schemas,scope,policyHint, handler. - Add it to the module's
register<Module>Tools()function. - If new module: extend
ModuleIdinpackages/mana-tool-registry/src/types.tsand call the new register function frommodules/index.ts.
The MCP server picks up the tool on next restart — no service code change needed.
Policy gating reminder: scope: 'admin' tools never reach MCP clients. policyHint: 'destructive' tools are exposed but should be rare; prefer policyHint: 'write' with a soft-delete semantic.
Policy gate
Every tool call is evaluated by evaluatePolicy() from @mana/tool-registry before reaching the handler (see docs/plans/agent-loop-improvements-m1.md for the design, and docs/reports/claude-code-architecture.md for the Claude-Code UH1 precedent).
POLICY_MODE |
Behaviour |
|---|---|
off |
Gate disabled. Legacy path. |
log-only |
Default. Evaluates, increments metrics, never blocks. Used during soak. |
enforce |
Deny decisions abort the call with the reminder payload attached to the MCP error. |
Decisions are emitted as mana_mcp_policy_decisions_total{decision, reason, mode} on /metrics. During log-only soak, watch decision="deny" — those are the calls that WOULD have been blocked. If false-positive rate stays below ~1 % over a week, flip to enforce.
What the gate decides today
scope: 'admin'→ deny outright (defense-in-depth;isExposablealready filters these, but mana-ai consuming the registry doesn't).policyHint: 'destructive'not in user'sallowDestructivelist → deny. Today the list is hard-coded to[]insettingsFor(); next PR sources it from the user profile.- Rolling 30 calls / 60 s per tool per user → deny. Ring buffer in
invocation-log.ts. - Prompt-injection markers in freetext args → allow with
decision=flagged. Non-blocking; signal only.
Metrics
GET /metrics exposes the mana_mcp_ registry (prom-client default metrics + policy + tool counters). Scraped by Prometheus at 30 s via the mana-mcp job in docker/prometheus/prometheus.yml.
Local smoke test (M1 exit gate)
Manual end-to-end check that proves: external client → MCP → mana-sync → Postgres.
# 1. Start the stack
pnpm docker:up # Postgres, Redis, MinIO
pnpm dev:auth # mana-auth on 3001
pnpm dev:sync # mana-sync on 3050
pnpm --filter @mana/mcp-service dev # mana-mcp on 3069
# 2. Get a dev-user JWT
pnpm setup:dev-user # creates dev@mana.test
# Then login to get a JWT — easiest path is via the web app dev-tools
# panel, or use a curl against /api/v1/auth/sign-in/email.
# 3. Fetch the user's active Space ID
curl -H "Authorization: Bearer $JWT" \
http://localhost:3001/api/auth/organization/list
# 4. Configure Claude Code (.mcp.json in repo root or ~/.claude.json)
{
"mcpServers": {
"mana": {
"type": "http",
"url": "http://localhost:3069/mcp",
"headers": {
"Authorization": "Bearer <JWT>",
"X-Mana-Space": "<SPACE_ID>"
}
}
}
}
# 5. In Claude Code, ask: "List my mana habits, then create one called 'Spazieren'"
# Verify a row appears in mana_sync.sync_changes for table='habits'.
Environment Variables
PORT=3069
MANA_AUTH_URL=http://localhost:3001
MANA_SYNC_URL=http://localhost:3050
JWT_AUDIENCE=mana
CORS_ORIGINS=http://localhost:5173
No provider keys, no DB connection — this service is stateless and forwards everything through tool handlers to other services.
Why a separate service (not folded into apps/api)
apps/api/src/mcp/server.ts already exists and exposes AI_TOOL_CATALOG over MCP. Per the plan (M4), that file and AI_TOOL_CATALOG get deleted once the new @mana/tool-registry covers all 67+ tools currently in mana-ai. Until then this service is the new path; the old endpoint stays for compatibility but is not extended.
Keeping mana-mcp standalone lets it be deployed independently, scaled separately (sessions are stateful in memory), and reasoned about as the single agent-facing entrypoint.