mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
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>
171 lines
7.7 KiB
Markdown
171 lines
7.7 KiB
Markdown
# 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`](../../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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
1. Open the relevant module file (or create a new one).
|
|
2. Define the `ToolSpec`: name (`module.verb`), zod input/output schemas, `scope`, `policyHint`, handler.
|
|
3. Add it to the module's `register<Module>Tools()` function.
|
|
4. If new module: extend `ModuleId` in `packages/mana-tool-registry/src/types.ts` and call the new register function from `modules/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`](../../docs/plans/agent-loop-improvements-m1.md) for the design, and [`docs/reports/claude-code-architecture.md`](../../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
|
|
|
|
1. `scope: 'admin'` → deny outright (defense-in-depth; `isExposable` already filters these, but mana-ai consuming the registry doesn't).
|
|
2. `policyHint: 'destructive'` not in user's `allowDestructive` list → deny. Today the list is hard-coded to `[]` in `settingsFor()`; next PR sources it from the user profile.
|
|
3. Rolling 30 calls / 60 s per tool per user → deny. Ring buffer in `invocation-log.ts`.
|
|
4. 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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```env
|
|
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.
|