Agents · 2026-05-26 · By RedDB team · 4 min read

An MCP server for RedDB — one database, every agent CLI

The Model Context Protocol lets every modern agent CLI — Claude Code, Codex, Cursor, Gemini CLI — share the same toolbelt. Wrap RedDB once and you give all of them agent memory, document storage, and semantic search through a single endpoint. Manifest, server, wiring.

Why MCP is the right seam

Earlier posts in this pillar wired RedDB into Claude Code directly: a SessionStart hook for retrieval, a skill for writes, slash commands for human-driven edits. Each of those is a Claude Code-shaped integration — a hook config, a SKILL.md, a markdown file in ~/.claude/commands/. None of it survives a port to Codex or Cursor.

The Model Context Protocol moves the seam one level down. An MCP server exposes a typed set of tools over stdio (or HTTP/SSE), and any MCP-aware host can mount it. Write the integration once, register it in every host’s config, and the same memory.write tool is available from every agent CLI you run. The host’s harness handles the rest — tool advertisement, schema validation, call routing.

For RedDB this maps cleanly. The database already speaks four data models (KV, document, vector, graph); MCP wants a flat list of tools with JSON Schema. We pick the four operations that pay rent in a daily agent loop:

MCP toolRedDB operationUsed for
memory.writeINSERT into memory table with embeddingCapturing a fact mid-turn
memory.searchSELECT … ORDER BY embedding <=> $1Pulling top-k context on demand
doc.upsertDocument store write, keyed by pathSaving a generated artifact (spec, ADR)
vector.searchRaw HNSW query against a named indexRAG over a corpus the agent doesn’t own

Everything else (transactions, graph traversals, multi-statement SQL) stays behind the server. Tools should be small.

The manifest

MCP servers are described by a tiny JSON manifest. The host reads it once and uses the tools list to populate the model’s tool registry on every turn. Schemas are JSON Schema 2020-12.

{
  "name": "reddb",
  "version": "0.1.0",
  "description": "RedDB memory and document storage for agent CLIs.",
  "tools": [
    {
      "name": "memory.write",
      "description": "Persist a short fact the agent wants to remember across sessions. Embeds the body and writes a row to the memory table.",
      "inputSchema": {
        "type": "object",
        "required": ["body"],
        "properties": {
          "body": { "type": "string", "maxLength": 2000 },
          "tags": { "type": "array", "items": { "type": "string" } },
          "project": { "type": "string" }
        }
      }
    },
    {
      "name": "memory.search",
      "description": "Semantic search over previously remembered facts. Returns top-k rows ranked by cosine similarity.",
      "inputSchema": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": { "type": "string" },
          "k": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 },
          "project": { "type": "string" }
        }
      }
    },
    {
      "name": "doc.upsert",
      "description": "Write or replace a document at the given path. Body is plain text or markdown.",
      "inputSchema": {
        "type": "object",
        "required": ["path", "body"],
        "properties": {
          "path": { "type": "string" },
          "body": { "type": "string" }
        }
      }
    },
    {
      "name": "vector.search",
      "description": "Raw similarity search against a named HNSW index. Use for RAG over corpora not owned by the agent (codebases, docs).",
      "inputSchema": {
        "type": "object",
        "required": ["index", "query"],
        "properties": {
          "index": { "type": "string" },
          "query": { "type": "string" },
          "k": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
        }
      }
    }
  ]
}

A few choices worth flagging. project is optional on memory.* because the server defaults it from an environment variable — the host sets REDDB_PROJECT=rdb-lair once and every call inherits it. The model never has to remember which project it’s in. body is capped at 2000 characters; longer artifacts go through doc.upsert. vector.search doesn’t auto-embed — the index name dictates the embedding model, and the server handles it server-side, so the model passes a plain string.

The server

A minimal implementation in Node, using the official @modelcontextprotocol/sdk and pg for RedDB’s Postgres-wire protocol. Roughly 80 lines.

import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import pg from "pg"
import { readFileSync } from "node:fs"

const manifest = JSON.parse(readFileSync(new URL("./manifest.json", import.meta.url), "utf8"))
const project = process.env.REDDB_PROJECT ?? "default"
const db = new pg.Pool({ connectionString: process.env.REDDB_URL })

async function embed(text: string): Promise<number[]> {
  const r = await fetch("https://api.voyageai.com/v1/embeddings", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${process.env.VOYAGE_API_KEY}`,
    },
    body: JSON.stringify({ model: "voyage-3", input: text }),
  })
  const json = await r.json()
  return json.data[0].embedding
}

const handlers: Record<string, (args: any) => Promise<unknown>> = {
  "memory.write": async ({ body, tags = [], project: p = project }) => {
    const v = await embed(body)
    const { rows } = await db.query(
      "INSERT INTO memory (project, body, tags, embedding) VALUES ($1,$2,$3,$4) RETURNING id, created_at",
      [p, body, tags, JSON.stringify(v)],
    )
    return rows[0]
  },
  "memory.search": async ({ query, k = 5, project: p = project }) => {
    const v = await embed(query)
    const { rows } = await db.query(
      `SELECT id, body, tags, created_at, 1 - (embedding <=> $1) AS score
       FROM memory
       WHERE project = $2 AND deleted_at IS NULL
       ORDER BY embedding <=> $1
       LIMIT $3`,
      [JSON.stringify(v), p, k],
    )
    return rows
  },
  "doc.upsert": async ({ path, body }) => {
    const { rows } = await db.query(
      `INSERT INTO doc (path, body, project) VALUES ($1,$2,$3)
       ON CONFLICT (project, path) DO UPDATE SET body = EXCLUDED.body, updated_at = now()
       RETURNING path, updated_at`,
      [path, body, project],
    )
    return rows[0]
  },
  "vector.search": async ({ index, query, k = 10 }) => {
    const { rows } = await db.query(
      "SELECT * FROM reddb.vector_search($1, $2, $3)",
      [index, query, k],
    )
    return rows
  },
}

const server = new Server({ name: manifest.name, version: manifest.version }, { capabilities: { tools: {} } })

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: manifest.tools }))

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const fn = handlers[req.params.name]
  if (!fn) throw new Error(`unknown tool: ${req.params.name}`)
  const result = await fn(req.params.arguments ?? {})
  return { content: [{ type: "text", text: JSON.stringify(result) }] }
})

await server.connect(new StdioServerTransport())

Things to notice:

  • Schema lives in manifest.json, not in code. The ListTools handler ships the manifest verbatim. One file is the source of truth; hosts and humans read the same thing.
  • embed is one HTTP call. No batching, no retries. The model only calls these tools when it has decided a fact is worth a round-trip; latency budget is generous.
  • Errors propagate. If the database rejects the insert, the SDK turns the thrown Error into a tool error the model sees on the next turn. No silent failures.
  • vector.search defers to a SQL function. The server doesn’t pick an embedding model; the function reddb.vector_search looks up the index’s configured model and does the right thing. Keeps the tool surface tiny.

Run it once locally with node --env-file=.env server.js. Wrong env var names will surface as a connect-time error; nothing about an MCP server is mysterious once you’ve started it.

Wiring into Claude Code

Claude Code reads MCP servers from ~/.claude/mcp.json (global) or .mcp.json at the repo root (per-project). A project-scoped config keeps secrets pinned to one tree:

{
  "mcpServers": {
    "reddb": {
      "command": "node",
      "args": ["./tools/reddb-mcp/server.js"],
      "env": {
        "REDDB_URL": "postgres://reddb@localhost:5432/lair",
        "REDDB_PROJECT": "rdb-lair",
        "VOYAGE_API_KEY": "${env:VOYAGE_API_KEY}"
      }
    }
  }
}

${env:…} references inherit from the user’s shell, so the file is checkable into the repo without leaking secrets. After saving, restart the harness and /mcp lists the server with its four tools. The model can now call memory.write directly — no skill, no slash command, no hook.

The same config syntax works for Codex (~/.codex/mcp.json) and Cursor (settings UI, JSON-equivalent). Gemini CLI uses a slightly different shape but the same command/args/env triplet. One server, four CLIs.

What you gave up, and what you bought

You gave up Claude Code-specific affordances. A skill can include a long SKILL.md describing when to use it; an MCP tool gets one description line. A slash command can be invoked deterministically by the user; an MCP tool is only ever called by the model. If your write path needs human-in-the-loop confirmation, the slash command from the D4 post is still the right tool.

What you bought is portability and a single audit point. Every read and every write goes through one process; logging it is a console.error away. Every CLI on the team can share one memory store without each engineer maintaining four parallel hook configs. And the next agent CLI someone in the org wants to try — the one that hasn’t been built yet — gets RedDB for free, the day it ships MCP support.

That is the whole pitch: the database is the integration, the protocol is the bus, the CLIs are interchangeable.

Next

  • Multi-agent shared memory — the same MCP server, but with several agents writing into it concurrently and a conflict-resolution doc keeping them honest.
  • Agent observability — wrap the MCP server with a tool-call audit log and build the “what did Claude Code cost me last sprint” query.

© 2026 RedDB.io. AGPL-3.0 self-host · Managed Cloud.