{
  "markdown": "# r2mcp — Persistent Memory for Claude Code\n\nPersistent, semantic, tiered memory layer for Claude Code sessions.\n\n**The problem:** Every Claude Code session starts fresh. Context is lost. You repeat yourself.\n\n**The fix:** r2mcp gives Claude a structured, searchable memory that survives session boundaries — stored in PostgreSQL with pgvector semantic search.\n\n## What you get\n\n- **11 MCP tools:** `remember`, `recall`, `search`, `meditate`, `reject`, `stats`, `compile`, `lint`, `classify`, `dump_edges_sidecar`, `extract_entities`\n- **3-tier memory:** `preferences` (decisions, style) → `project-context` (architecture, state) → `conversations` (relationship, history)\n- **Semantic search:** Progressive tier search with MMR diversity reranking and relevance floor filtering (Recall v2)\n- **Typed memory edges:** `contradicts`, `supersedes`, `supports`, `evolved_into`, `depends_on`, `related_to` — surfaced as signals on `recall()`\n- **Wiki compile:** Regenerable browsable views — `compile()` synthesizes `memory/compiled/` from pgvector\n- **Lint as a first-class op:** SQL-only structural feedback — contradictions, stale, orphans, drift, superseded_unflagged\n- **Multi-provider LLM layer:** Classifier and compile work on a Max plan ($0/call), Anthropic API, or OpenRouter — picked per-invocation\n- **Bundled `/remember` skill:** Client-side judgment pipeline — classify → conflict-check → store\n\n## Setup\n\n**Prerequisites:** Node.js 20+. An OpenRouter API key is strongly recommended — it powers semantic-search embeddings. Without one, r2mcp still works but degrades to full-text search (and tells you so via a startup warning and `warnings[]` on tool responses). Docker is optional (Option B only).\n\nr2mcp works with any PostgreSQL + pgvector backend. The fastest path is Supabase (free tier, no Docker required).\n\n### Option A: Supabase (no Docker required)\n\n#### 1. Create a Supabase project\n\nCreate a free project at [supabase.com](https://supabase.com). Once created, click **Connect** (top of the dashboard) and copy the **Session pooler** connection string — port `5432`, host like `aws-0-<region>.pooler.supabase.com`, username `postgres.<project-ref>`.\n\n> **Why the Session pooler?** The Direct connection (`db.<ref>.supabase.co:5432`) resolves to an IPv6 address, and IPv4 for direct connections is a paid add-on — on an IPv4-only network it fails with `connect ENETUNREACH`. The Session pooler is IPv4-compatible on every tier and fully supports schema setup. (Do **not** use the Transaction pooler on port `6543` — it can't run DDL; setup will refuse it.) If your network has IPv6, the Direct connection works too.\n\n#### 2. Clone and configure\n\n```bash\ngit clone https://github.com/DMokong/r2mcp.git && cd r2mcp && npm install\ncp .env.example .env\n# Set R2MCP_DATABASE_URL to your Session pooler URL (port 5432, not 6543)\n# Set R2MCP_OPENROUTER_API_KEY to your OpenRouter key (enables semantic search)\n```\n\n#### 3. Provision schema and build\n\n```bash\nnpm run setup && npm run build\n```\n\nThis creates the `memories` table, pgvector indexes, and full-text search index. **Safe to re-run.**\n\n#### 4. Register in Claude Code\n\nAdd to your project's `.mcp.json`. Use `${VAR}` expansion so credentials stay in your environment instead of the file — **`.mcp.json` is typically committed, so never paste real credentials into it**:\n\n```json\n{\n  \"mcpServers\": {\n    \"memory\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/r2mcp/dist/index.js\"],\n      \"env\": {\n        \"R2MCP_DATABASE_URL\": \"${R2MCP_DATABASE_URL}\",\n        \"R2MCP_OPENROUTER_API_KEY\": \"${R2MCP_OPENROUTER_API_KEY}\"\n      }\n    }\n  }\n}\n```\n\nClaude Code expands `${VAR}` (and `${VAR:-default}`) from your environment at launch. Inline literal values are fine only for throwaway local experiments — if you go that route, gitignore `.mcp.json` and treat any committed credential as compromised.\n\nRestart Claude Code, then see [After setup](#after-setup-both-options).\n\n---\n\n### Option B: Docker (local dev)\n\nFor local development or air-gapped environments.\n\n#### 1. Clone\n\n```bash\ngit clone https://github.com/DMokong/r2mcp.git\ncd r2mcp\nnpm install\n```\n\n#### 2. Configure\n\n```bash\ncp .env.example .env\n# Edit .env — set R2MCP_DATABASE_URL and R2MCP_OPENROUTER_API_KEY\n```\n\n#### 3. Start Postgres\n\n```bash\ndocker compose up -d\n# Wait ~10s for healthy status\n```\n\n#### 4. Provision schema\n\n```bash\nnpm run setup\n```\n\nThis creates the `memories` table, pgvector indexes, and full-text search index. **Safe to re-run.**\n\n#### 5. Build\n\n```bash\nnpm run build\n```\n\n#### 6. Register in Claude Code\n\nAdd to your project's `.mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"memory\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/r2mcp/dist/index.js\"],\n      \"env\": {\n        \"R2MCP_DATABASE_URL\": \"postgresql://r2mcp:r2mcp@localhost:5432/r2mcp\",\n        \"R2MCP_OPENROUTER_API_KEY\": \"${R2MCP_OPENROUTER_API_KEY}\"\n      }\n    }\n  }\n}\n```\n\n(The local Docker DB URL contains no real secret; the OpenRouter key does — keep it in your environment via `${VAR}` expansion.)\n\nRestart Claude Code, then see [After setup](#after-setup-both-options).\n\n## After setup (both options)\n\nYou now have `mcp__memory__remember`, `mcp__memory__recall`, etc. available. Two optional steps make memory actually get used:\n\n### Install the /remember skill (recommended)\n\nThe bundled skill gives Claude a judgment pipeline for memory writes — classify → conflict-check → store. Copy it into your **consuming project** (the one whose `.mcp.json` registers r2mcp):\n\n```bash\nmkdir -p .claude/skills && cp -r /path/to/r2mcp/skills/remember .claude/skills/\n```\n\nClaude Code auto-discovers project skills from `.claude/skills/<name>/SKILL.md`. (For all your projects at once, use `~/.claude/skills/` instead.) Then `/remember <note>` persists memories through the full pipeline.\n\n### Teach your agent the session loop (recommended)\n\nr2mcp ships MCP server instructions that Claude Code loads automatically, so the agent knows the basics. For stronger habits, add a short protocol to your project's `CLAUDE.md`:\n\n```markdown\n## Memory\n\nThis project has persistent memory via the `memory` MCP server.\n- At session start, `recall` context relevant to the task at hand.\n- When a durable decision, preference, or correction surfaces, `remember` it\n  (tier: preferences = decisions/style, project-context = architecture/state,\n  conversations = session continuity).\n- Run `/remember` before ending a work session to persist anything unsaved.\n```\n\n**First session on an empty database:** `recall` returning zero results is expected — start `remember`-ing as decisions come up and recall pays off within a session or two.\n\n## Configuration\n\nr2mcp reads its configuration from the MCP transport's environment — for\nconsumers, **the `.mcp.json` `env` block is the primary config surface**\n(use `${VAR}` expansion for secrets):\n\n```json\n{\n  \"mcpServers\": {\n    \"memory\": {\n      \"command\": \"node\",\n      \"args\": [\"./node_modules/r2mcp/dist/index.js\"],\n      \"env\": {\n        \"R2MCP_DATABASE_URL\": \"${R2MCP_DATABASE_URL}\",\n        \"R2MCP_OPENROUTER_API_KEY\": \"${R2MCP_OPENROUTER_API_KEY}\",\n        \"R2MCP_CLASSIFIER_PROVIDER\": \"claude-code\",\n        \"R2MCP_EDGE_MAX_USD\": \"1.00\",\n        \"R2MCP_COMPILE_MAX_USD\": \"1.00\"\n      }\n    }\n  }\n}\n```\n\n| Variable | Required | What it does |\n|----------|----------|--------------|\n| `R2MCP_DATABASE_URL` | **Yes** | PostgreSQL + pgvector connection string. The server **fails fast at startup** if unset — it never guesses a database. |\n| `R2MCP_OPENROUTER_API_KEY` | Recommended | Enables semantic-search embeddings. When unset, the server logs a startup warning and `remember`/`recall` responses carry a `warnings[]` field — everything still works full-text. |\n| `R2MCP_SCOPE` | Optional | Project namespace (default `global`). Set this per-project so multiple projects can share one database without their memories colliding — see [Cross-Project Memory](#cross-project-memory). |\n| `R2MCP_CLAUDE_BIN` | Sometimes | Absolute path to the `claude` binary for the $0 Max-plan provider. Needed when the spawning process's PATH doesn't include it — common under launchd jobs and some MCP hosts (e.g. `~/.local/bin/claude`). The spawn error names this variable when it's the fix. |\n| `ANTHROPIC_API_KEY` | Optional | Only for `--provider=anthropic` on classifier/compile runs. |\n| `R2MCP_CLASSIFIER_PROVIDER` | Optional | Pin a provider (`claude-code` \\| `anthropic` \\| `openrouter`) instead of auto-fallback. |\n| `R2MCP_EDGE_MAX_USD` / `R2MCP_COMPILE_MAX_USD` / `R2MCP_ENTITY_MAX_USD` | Optional | Cost caps for the batch jobs (defaults `$1.00`). |\n| `R2MCP_MODEL_TIER` | Optional | Global model tier (`haiku` \\| `sonnet` \\| `opus`) for every LLM call site — see [Model Tiers](#model-tiers). |\n| `R2MCP_COMPILE_WIKI_MODEL` / `R2MCP_CLASSIFY_EDGES_STAGE1_MODEL` / `R2MCP_CLASSIFY_EDGES_STAGE2_MODEL` / `R2MCP_EXTRACT_ENTITIES_MODEL` | Optional | Per-call-site tier overrides. Each beats `R2MCP_MODEL_TIER` for its own call site. |\n\nThe server also loads a `.env` file from its working directory at startup\n(non-clobbering — real environment variables always win). A `.env` at the\nr2mcp source root is the normal path for `npm run` scripts when working from\na checkout; consumers configuring via `.mcp.json env` don't need one.\n\n## Troubleshooting\n\n| Symptom | Cause & fix |\n|---------|-------------|\n| `connect ENETUNREACH 2406:...` during setup | Supabase Direct connection is IPv6-only (IPv4 is a paid add-on) and your network is IPv4-only. Use the **Session pooler** string instead: Dashboard → Connect → Session pooler (port 5432). Setup classifies this error and says the same. |\n| `Transaction-pooler URL detected (port 6543)` | The transaction pooler can't run DDL or prepared statements. Use the Session pooler (port 5432). |\n| `R2MCP_DATABASE_URL is not set` | Deliberate fail-fast — set it in `.mcp.json env` or `.env`. The error lists both surfaces and the Docker default URL. |\n| `embeddings disabled` warning at startup or in `warnings[]` | `R2MCP_OPENROUTER_API_KEY` is unset (or the embed call failed — the message distinguishes the two). Full-text search still works; set the key to enable semantic search. |\n| `could not spawn 'claude' (ENOENT)` on classifier/compile runs | The claude CLI isn't on the spawning process's PATH. Set `R2MCP_CLAUDE_BIN` to its absolute path. |\n| Fresh credentials rejected right after a Supabase password reset | The pooler caches auth-rejection state for 30–60s. Wait a minute and retry before assuming the rotation failed. |\n\n## Running tests\n\nThe suite mixes pure-unit tests with DB-integration tests that need a local PostgreSQL + pgvector. CI runs the same flow on every push/PR (`.github/workflows/ci.yml`).\n\n```bash\ndocker compose up -d        # starts Postgres + provisions r2mcp_test with pgvector\nnpm test                    # vitest — schema is applied automatically per run\n```\n\n- **Test database:** the suite runs against `r2mcp_test`, which `docker compose up` provisions on first init (`docker/init-test-db.sql`). If you already have a Postgres volume, create it once: `createdb r2mcp_test && psql -d r2mcp_test -c \"CREATE EXTENSION vector\"`.\n- **Isolation guard:** a vitest `setupFile` forces `R2MCP_DATABASE_URL` to a safe local test DB before every test module, so the suite can **never** touch a production database — even if your shell or `.env` points at one (it warns and overrides). To point tests at a specific DB, set `R2MCP_TEST_DATABASE_URL`; a *remote* test DB additionally requires `R2MCP_ALLOW_REMOTE_TEST_DB=1`.\n- **Live-LLM tests self-skip:** tests that need real embeddings or an LLM provider are skipped unless `R2MCP_OPENROUTER_API_KEY` (or a provider) is set, so the default run is offline and deterministic.\n- **Gates:** `npm run lint` (eslint, zero warnings), `npm run format:check` (prettier), `npm run build` (tsc + schema copy).\n\n## Schema migrations\n\nThe schema is applied via **numbered migrations** (`src/migrations/NNN_name.sql`, contiguous from `001_baseline.sql`), tracked in a `schema_migrations` table.\n\n- **`npm run setup` applies** pending migrations (advisory-locked, each in its own transaction, recorded per version). Safe to re-run.\n- **Boot only verifies.** The MCP server and every CLI check the schema version at startup and fail fast with `run: npm run setup` when the database is behind — no DDL executes at runtime, so the runtime role no longer needs owner privileges, and non-additive changes are now expressible.\n- **Upgrading an existing deployment:** the first `npm run setup` after this change adopts your database — `001_baseline` is the former idempotent `schema.sql`, so it no-ops through existing objects and records version 1. Run setup once before restarting servers onto the new build.\n- **Adding a migration:** create `src/migrations/002_short_name.sql` (next number, no gaps), then `npm run setup`. Prefer idempotent DDL where possible; each file runs in a transaction.\n\n## Operations — backup & restore\n\nYour memories are the whole point of this server — treat the database like it can vanish, because on some hosting tiers it can. **Supabase's free tier has no PITR** (point-in-time recovery) and its automated backups are limited; a `DELETE` executed against the wrong database is unrecoverable without your own dumps.\n\n### JSONL export / import (built in)\n\n```bash\n# Full logical backup — all scopes, all four tables, embeddings included\nnpm run db:export -- --out=backup-$(date +%Y-%m-%d).jsonl\n\n# One scope only\nnpm run db:export -- --scope=myproject --out=myproject.jsonl\n\n# Restore (idempotent — already-present rows are skipped, so re-runs are safe)\nnpm run db:import -- backup-2026-07-02.jsonl\n\n# Validate a backup file without writing anything\nnpm run db:import -- backup-2026-07-02.jsonl --dry-run\n```\n\nThe export is a single JSONL file: a header line (version, timestamp, per-table counts) followed by one row per line across `memories`, `entities`, `memory_edges`, `memory_entities` in FK-safe order. UUIDs and embeddings are preserved verbatim, so a restore into an empty database reproduces the full memory graph. Import conflicts (same PK, same `(project_scope, fingerprint)`, same edge triple) are skipped, and row-level failures are reported without aborting the run — the exit code is non-zero if any row failed, so scripts notice partial restores.\n\n### pg_dump (belt and braces)\n\nThe JSONL export is portable and diffable; `pg_dump` captures everything else (indexes, constraints, roles):\n\n```bash\npg_dump \"$R2MCP_DATABASE_URL\" --no-owner --no-privileges -f r2mcp-$(date +%Y-%m-%d).sql\n```\n\n### Restore drill\n\nDo this once now, not during an incident:\n\n1. `npm run db:export -- --out=drill.jsonl`\n2. Point `R2MCP_DATABASE_URL` at a scratch database (e.g. the docker-compose Postgres) and run `npm run setup`\n3. `npm run db:import -- drill.jsonl` — expect `errors: []` and inserted counts matching the export header\n4. Spot-check: `recall` a memory you know, verify an edge survived\n\n### Suggested cadence\n\n- **Daily** `db:export` via cron/launchd to a dated file (they're small — a few MB even with embeddings)\n- **Before any prod mutation** (migrations, scope re-stamps, bulk cleanups): take a fresh export first\n- Keep at least a week of dailies; prune older ones\n\n## Memory Tiers\n\n| Tier | What goes here | Auto-archived after |\n|------|---------------|---------------------|\n| `preferences` | Decisions, coding style, tool choices | Never |\n| `project-context` | Architecture, system state, what's built | 180 days |\n| `conversations` | Relationship continuity, session history | 90 days |\n\n## Tools Reference\n\n| Tool | Description |\n|------|-------------|\n| `remember` | Store/update/archive a memory with tier + metadata |\n| `recall` | Semantic + full-text search with progressive tier search; emits `signals[]` from typed edges |\n| `search` | Filter by type, tier, topics, date range |\n| `meditate` | Archive stale entries, find duplicates; pass `include_lint: true` to fold lint findings in |\n| `reject` | Mark a memory as rejected (excluded from future recall) |\n| `stats` | Health check — counts, staleness, embedding status |\n| `compile` | Regenerate browsable wiki views under `memory/compiled/` (SPEC-044, see below) |\n| `classify` | Classify candidate memory pairs into typed edges (supports, contradicts, supersedes, evolved_into, depends_on, related_to). Subprocess-spawned (SPEC-044 invariant). |\n| `dump_edges_sidecar` | In-process JSON dump of memory_edges + memories to a caller-supplied directory. Used by downstream consumers like Memory Explorer. |\n| `lint` | Surface structural feedback: contradictions, stale, orphans, drift, superseded_unflagged (SPEC-044, see below) |\n| `extract_entities` | Extract structured entities (project / person / tool / decision) from memories. Spawns the entity extractor driver via the shared `resolveCliCommand` helper. Inherits cost cap (`R2MCP_ENTITY_MAX_USD`, default $1.00) and resumability from SPEC-043. Top-N known entities (`R2MCP_ENTITY_CONTEXT_TOP_N`, default 100) seed the LLM context. (SPEC-046, see below) |\n| `recall` (extended) | Accepts an optional `entity` parameter that narrows results to memories linked to a named entity (matched by canonical name or any alias). When `entity` is set, `query` is optional. Response gains `entity_resolved: boolean`, optional `entity_id`, and per-result `entity_links[]`. (SPEC-046) |\n\n## Recall v2 — semantic + budget-aware retrieval\n\n`recall()` is the workhorse retrieval tool. v2 (xMemory-inspired, 2026-04) layers four retrieval shapes on top of the underlying hybrid semantic + full-text search:\n\n### 1. Relevance floor — `min_score`\n\nFilter out low-quality matches before they're returned. Without this, semantic search dumps a long tail of weakly-related results.\n\n```ts\nrecall({ query: \"edge classifier cost cap\", min_score: 0.3 })\n```\n\nSuggested defaults: `0.3` for semantic queries, `0.1` for keyword-driven ones.\n\n### 2. MMR diversity — `diversity` (lambda 0.0–1.0)\n\nMaximal Marginal Relevance reranks results to balance relevance against redundancy. `1.0` is pure relevance (may return three near-duplicates of the top hit); `0.0` is pure diversity (spreads coverage); the default `0.7` favors relevance with mild diversification.\n\n```ts\nrecall({ query: \"memory architecture\", diversity: 0.5, top_k: 8 })\n```\n\nUse lower values when you want broad coverage of a topic, higher when you want the single best answer plus close runners-up.\n\n### 3. Context budget — `max_tokens`\n\nToken-budget retrieval: walks MMR-reranked results in score order and stops when adding the next result would exceed the budget. Returns `tokens_used` in the response so you know how much you actually pulled.\n\n```ts\nrecall({ query: \"what we learned about classifiers\", max_tokens: 4000 })\n// → up to N results, summing to ≤4000 tokens, prioritized by relevance × diversity\n```\n\nThis is the right call when you're stuffing recall results into a downstream prompt and have a hard context limit. `top_k` is ignored when `max_tokens` is set — the budget decides the cut.\n\n### 4. Progressive tier search — `progressive` + `confidence_threshold`\n\nTop-down retrieval through the tier hierarchy (`preferences` → `project-context` → `conversations`). High-confidence matches in `preferences` short-circuit the search before lower tiers are consulted, mimicking the xMemory observation that decisions/preferences usually answer questions before context/history needs to.\n\n```ts\nrecall({ query: \"do we use bun or npm\", progressive: true, confidence_threshold: 0.82 })\n// → returns immediately if a preferences-tier match scores ≥0.82, else widens to project-context, then conversations\n```\n\nDefault behavior — turn off with `progressive: false` to force a full sweep across tiers, or pin a single tier with `tier: 'preferences'`.\n\n### Composing them\n\nThe four parameters compose:\n\n```ts\nrecall({\n  query: \"spec-bench cleanup conventions\",\n  min_score: 0.3,           // drop weak matches\n  diversity: 0.6,           // some diversification\n  max_tokens: 3000,         // fit in context\n  progressive: true,        // early-stop on prefs hits\n  confidence_threshold: 0.82,\n})\n```\n\nPlus `signals[]` on the response surfaces typed memory edges (`contradicts`, `superseded_by`) on the returned memories so callers can flag conflicts inline.\n\n## Cross-Project Memory\n\nMultiple projects can share one database while keeping their memories separate, via the `R2MCP_SCOPE` namespace.\n\n**Set `R2MCP_SCOPE` per-project** in each project's `.mcp.json` `env` block:\n\n```json\n\"env\": {\n  \"R2MCP_DATABASE_URL\": \"${R2MCP_DATABASE_URL}\",\n  \"R2MCP_SCOPE\": \"my-project\"\n}\n```\n\nHow scoping behaves:\n\n- **Writes** (`remember`, `reject`) land in the current scope.\n- **Reads** (`recall`, `search`) default to the **current scope + `global`**. So each project sees its own memories plus anything you deliberately put in the shared `global` scope. Pass `all_scopes: true` to read across every scope.\n- **Destructive maintenance** (`meditate`, `lint --fix`, `compile`, the edge classifier) is confined to the current scope unconditionally — a cleanup in one project can never archive or rewrite another's data.\n- **Entities** are scoped too; `global`-scoped entities resolve from any project, so you can share a common vocabulary deliberately.\n\n**Default behavior (no `R2MCP_SCOPE` set):** everything reads and writes the `global` scope — a single shared pool, identical to pre-scope behavior. Existing memories from before you upgraded are backfilled to `global`, so nothing is lost.\n\n**Sharing knowledge across projects:** write memories you want everywhere into the `global` scope (run that session with `R2MCP_SCOPE=global` or unset), or query with `all_scopes: true` when you explicitly want the whole store.\n\n> **Note for scheduled jobs:** set `R2MCP_SCOPE` in the launchd/cron job's environment too, not just `.mcp.json` — a background classifier or compile run uses its own environment, and an unset scope there would operate on `global` instead of your project.\n\n## OpenTelemetry (optional)\n\nEnable OTel tracing and metrics:\n\n```bash\nOTEL_ENABLED=true\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n```\n\nMetrics use the `r2mcp.memory.*` namespace.\n\n## Prior Art & Acknowledgements\n\nr2mcp stands on the shoulders of two projects:\n\n**[Open Brain](https://github.com/NateBJones-Projects/OB1) by [Nate B. Jones](https://natesnewsletter.substack.com/)**\nThe core architectural insight — \"one database, any AI plugs in\" — comes from Open Brain. The idea that your knowledge layer should be sovereign and portable (not locked inside a specific tool) is the founding premise of r2mcp. Open Brain proved the PostgreSQL + pgvector substrate works for personal AI memory at minimal cost ($0.10–0.30/month). r2mcp narrows the scope to Claude Code's MCP protocol and adds a more opinionated retrieval layer on top of that foundation.\n\n**[xMemory](https://arxiv.org/abs/2602.02007) — \"Beyond RAG for Agent Memory: Retrieval by Decoupling and Aggregation\"**\nHu et al. (2026) established the hierarchical tier approach and showed that progressive top-down retrieval with coverage maximization + redundancy minimization cuts token usage ~50% vs. flat RAG while improving accuracy. r2mcp's 3-tier memory (preferences → project-context → conversations) is a hand-crafted simplification of their 4-level hierarchy (messages → episodes → semantics → themes). The MMR diversity reranking in `recall()` directly implements their redundancy minimization insight.\n\n## Migrating from ClaudeClaw\n\nIf you're moving from the ClaudeClaw-internal `memory-mcp-server`:\n\n```bash\nR2MCP_DATABASE_URL=<your-new-url> npx tsx src/cli/migrate.ts /path/to/your/memory/\n```\n\nThe migration script reads `preferences.md`, `project-context.md`, and `conversations.md` from the specified directory and imports them. It's idempotent — safe to re-run.\n\n## Memory edges (SPEC-043)\n\nr2mcp supports a typed-relation table (`memory_edges`) that captures structural\nrelations between memories — `contradicts`, `supersedes`, `supports`,\n`evolved_into`, `depends_on`, `related_to`. The `recall()` MCP tool surfaces\n`contradicts` / `superseded_by` relations as an optional `signals[]` field on\nthe response (additive — existing clients work unchanged).\n\n### Running the classifier\n\nThe classifier is a manual batch process — it is NOT invoked from the MCP server\nhot path. Provider selection follows the SPEC-044 precedence (see below); on a\nMax plan, no API key is required.\n\n```bash\n# Estimate cost without making API calls or writing edges\nnpm run edges:classify -- --dry-run\n\n# Auto-fallback: prefers claude-code (Max plan, $0/call)\nnpm run edges:classify -- --max-cost=1.00\n\n# Force a specific provider\nnpm run edges:classify -- --provider=anthropic --max-cost=1.00\nnpm run edges:classify -- --provider=openrouter --max-cost=1.00\n\n# Incremental run on memories from the last 7 days\nnpm run edges:classify -- --since=7d --max-cost=0.25\n\n# Resume a prior run that hit its cap (the run_id is printed at exit and stored in\n# data/edges-state.last-run)\nnpm run edges:classify -- --resume=<run_id>\n```\n\nState and run summaries are written under `data/edges-state.*` (JSONL append-log,\nlast-run sidecar, per-run JSON summary at `data/edges-state.runs/<run_id>.json`).\n\n## LLM provider abstraction (SPEC-044)\n\nThe classifier and wiki compiler share a small `LLMProvider` abstraction with\nthree adapters. Providers run from standalone Node processes only — the MCP\nserver itself never makes LLM calls.\n\n| Adapter | Auth | Cost per call | Concurrency cap |\n|---------|------|---------------|------------------|\n| `claude-code` | Claude Code OAuth (Max plan) | **$0** (strict equality) | 2 (subprocess overhead) |\n| `anthropic` | `ANTHROPIC_API_KEY` | Per-token (list price) | 10 |\n| `openrouter` | `R2MCP_OPENROUTER_API_KEY` | Per-token (list price) | 10 |\n\n### Selection precedence\n\n1. `--provider=<name>` CLI flag — highest priority\n2. `R2MCP_CLASSIFIER_PROVIDER` environment variable\n3. Auto-fallback: `claude-code` if logged in → `anthropic` if API key set →\n   `openrouter` if API key set → fatal error naming all three remediation paths\n\nThe fallback prefers `claude-code` so a Max-plan user pays nothing by default.\n\nOpenRouter's primary role remains text→vector embeddings. Its classifier /\ncompile use is opt-in per invocation, never auto-routed for embeddings.\n\n## Model tiers\n\nThe provider decides *where* a call goes; the **tier** decides *which* model\nanswers it. Tiers are logical (`haiku` / `sonnet` / `opus`) — each provider maps\nthem to a concrete model id in its own `MODEL_IDS` table, so retuning a tier\nnever means hardcoding a vendor model string at a call site.\n\nEvery call site resolves its tier from the environment at call time, so you can\nretune a scheduled job by editing its launchd/cron env — no republish, no\nre-vendor.\n\n| Call site | Default | Purpose-specific override |\n|-----------|---------|---------------------------|\n| Wiki compile (tier + topic pages) | `sonnet` | `R2MCP_COMPILE_WIKI_MODEL` |\n| Edge classify — stage 1 (filter) | `sonnet` | `R2MCP_CLASSIFY_EDGES_STAGE1_MODEL` |\n| Edge classify — stage 2 (adjudicate) | `opus` | `R2MCP_CLASSIFY_EDGES_STAGE2_MODEL` |\n| Entity extraction | `sonnet` | `R2MCP_EXTRACT_ENTITIES_MODEL` |\n\n**Resolution order** (first hit wins): the purpose-specific variable →\n`R2MCP_MODEL_TIER` → the built-in default.\n\n```bash\n# everything on opus for one expensive backfill\nR2MCP_MODEL_TIER=opus npm run edges:classify\n\n# global sonnet, but adjudicate on opus\nR2MCP_MODEL_TIER=sonnet R2MCP_CLASSIFY_EDGES_STAGE2_MODEL=opus npm run edges:classify\n```\n\nTwo deliberate choices worth knowing:\n\n- **Edge classification stays a cascade.** Stage 1 is a cheap high-recall filter;\n  stage 2 only sees what survived. If you set `R2MCP_MODEL_TIER` alone, both\n  stages land on the same tier and the cascade stops saving anything — set the\n  stage-2 override too if you care about that.\n- **Invalid values fail open, loudly.** A typo (`R2MCP_MODEL_TIER=sonnnet`)\n  falls back to the next source and writes one warning to stderr naming the\n  variable and value — deduped per variable, since stage 1 resolves once per\n  candidate pair. These call sites run inside scheduled jobs, where taking down\n  the nightly pipeline over a typo is worse than using a working default.\n\n## Wiki compile (SPEC-044)\n\n`compile()` regenerates browsable markdown views of the memory store from\npgvector. Output goes to `memory/compiled/` (gitignored, regenerable).\n\n```bash\n# Compile all three tier files (preferences.md, project-context.md, conversations.md)\nnpm run compile-wiki -- --all\n\n# Compile a single tier\nnpm run compile-wiki -- --tier=preferences\n\n# Compile a per-topic page\nnpm run compile-wiki -- --topic=wiki-mode\n\n# Preview without writing\nnpm run compile-wiki -- --all --dry-run\n\n# Force a provider (otherwise uses auto-fallback)\nnpm run compile-wiki -- --all --provider=claude-code\n```\n\n### Output shape\n\nEvery compiled file carries YAML frontmatter recording `generated_at`,\n`compile_run_id`, `source_count`, `source_memory_ids`, `provider`,\n`source_git_sha`, and `tier` or `topic`. The body is structured prose with\ninline `<m:id>` citations and a `Sources:` line per cluster.\n\n### Structural stability\n\nCompile is treated as a regenerable view: across two runs against the same\ninput, the set of `## H2` / `### H3` headers and the set of cited memory IDs\nare bit-identical. Prose-level variance is bounded at 5% (Levenshtein ratio\n≥ 0.95) — the only LLM nondeterminism allowance. The compiler controls\nheaders and citations; only the prose paragraphs come from the LLM.\n\n### Cost cap\n\n`R2MCP_COMPILE_MAX_USD` (default `$1.00`) — when exceeded mid-run, compile\nexits cleanly with `hit_cost_cap: true` and partial files. Same shape as the\nclassifier cap-hit behavior.\n\n### What compile never does\n\n- Modifies `memory/MEMORY.md` — the human-curated hub stays invariant\n- Writes outside `memory/compiled/`\n- Touches the live `memories` or `memory_edges` tables — read-only at the DB layer\n- Uses any direct Anthropic SDK call — every synthesis routes through `LLMProvider`\n\n## Lint (SPEC-044)\n\n`lint()` surfaces five structural checks on the memory store. SQL-only — no\nLLM calls, no cost cap.\n\n```bash\n# Run all checks against the live DB and produce a human-readable report\nnpm run lint:memory\n\n# Run a single check\nnpm run lint:memory -- --check=stale\n\n# Apply auto-fixes for high-confidence findings\nnpm run lint:memory -- --fix\n```\n\n| Check | What it surfaces |\n|-------|-------------------|\n| `contradictions` | Edges where `relation='contradicts'` between two unarchived memories |\n| `stale` | Memories older than 90d with zero incoming edges, tier ≠ preferences |\n| `orphans` | Memories with zero edges in either direction, older than 30d |\n| `drift` | Pairs sharing ≥2 topics with no edge yet — classifier hasn't run on this pair |\n| `superseded_unflagged` | `contradicts` edge where the temporal pattern says it should be `supersedes` |\n\n### `--fix` semantics\n\n`lint --fix` only acts on findings with `confidence ≥ 0.9`:\n\n- `stale` → memory is archived (`type='archived'`)\n- `superseded_unflagged` → edge type is rewritten from `contradicts` to `supersedes`\n\nLower-confidence findings are returned as suggestions only, never auto-acted.\n\n### `meditate` integration\n\n`meditate({include_lint: true})` runs lint first and surfaces findings as a\n`lint_findings` field on the response. The default invocation\n(`meditate({mode: 'full', dry_run: false})`) returns the byte-identical\npre-spec response shape — backward compatibility for direct callers is\npreserved.\n\n## Entity extraction (SPEC-046)\n\nLight entity extraction over the memory store — pulls structured `project` /\n`person` / `tool` / `decision` entities out of memories, persists them to two\nnew tables (`entities` for canonical names + aliases, `memory_entities` for the\nM:N link to `memories`), and lets `recall()` filter on entity name or alias.\n\nThe extractor is a subprocess-driven batch process — the MCP server itself\nnever makes LLM calls. Provider selection follows the SPEC-044 precedence; on a\nMax plan, no API key is required.\n\n```bash\n# One-shot batch extraction over the last week, capped at $0.50\nnpm run entities:extract -- --since-days=7 --max-cost=0.5\n\n# Or via MCP tool from any client\n# mcp.callTool('extract_entities', { since_days: 7, max_cost_usd: 0.5 })\n\n# Then ask for Speculator-scoped recall\n# mcp.callTool('recall', { entity: 'Speculator', query: 'compaction' })\n```\n\n### Env vars\n\n| Variable | Default | What it controls |\n|----------|---------|------------------|\n| `R2MCP_ENTITY_MAX_USD` | `1.00` | Cost cap for a single extraction run. On overrun, the run exits cleanly with `hit_cost_cap: true` (same shape as the classifier and compile caps). |\n| `R2MCP_ENTITY_CONTEXT_TOP_N` | `100` | Number of known entities seeded into the LLM context to bias toward canonical names + alias merging. |\n\n### Scoped recall\n\nWhen `recall()` is called with `entity` set:\n\n- `query` is optional — entity-only recall returns all memories linked to the entity (matched by canonical name or any alias).\n- The response carries `entity_resolved: boolean` and, when resolved, `entity_id`.\n- Each result carries an `entity_links[]` array describing how that memory connects to the named entity.\n\n## License\n\n[MIT](LICENSE)\n",
  "bytes": 33374,
  "sha": "b4c913ef656ec63c54bf10b5d6f1ef25358a7254e6a874bea3b893f03633f904",
  "repo_slug": "dmokong/r2mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_dmokong_r2mcp_docs_asbuilt_index_md_2ac66a33/readme"
}