The pinky-mcp MCP server

pinky-mcp is how an agent uses the brain: it exposes memory as tools over MCP via stdio. The agent searches and saves knowledge without grepping or reading files by hand; it shares the same index (brain.db) as the pinky CLI.

  • It is not a daemon and does not listen on a port: it is a stdio process that the MCP client (Claude Code or another) launches and talks to over stdin/stdout.
  • Protocol: JSON-RPC 2.0, JSON messages delimited by line. Protocol version 2024-11-05. Methods: initialize, tools/list, tools/call, ping.
  • All logging goes to stderr (PINKY_LOG/RUST_LOG), never to stdout, so as not to pollute the JSON-RPC channel.

The tools

brain_search — search knowledge

Hybrid search (BM25 full-text + semantic vector, RRF fusion) over the brain. Records usage telemetry (what gets retrieved).

ArgumentTypeReq.DefaultWhat it does
querystring✅—What to search for. Max. 2,000 characters.
limitinteger10Max. results (1–100).
scopestring—Narrows by scope: global or project:<name>.
projectstring—Narrows to a project (equivalent to scope: project:<name>).
typestring—A single type: gotcha | pattern | decision | diary | guide | note.
tagsstring[]—Only entries that have all these tags.
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": { "name": "brain_search",
    "arguments": { "query": "timeout closing the postgres pool",
                   "type": "gotcha", "project": "sgsvp", "limit": 5 } } }

brain_save — save new knowledge

Writes the source-of-truth .md (in PINKY_SAVE_DIR) and indexes it on the fly. Use it when the agent discovers something reusable worth remembering.

ArgumentTypeReq.DefaultWhat it does
titlestring✅—Short, descriptive title. Max. 300 characters.
bodystring✅—Content in markdown. Max. 100,000 characters.
typestringnotegotcha | pattern | decision | diary | guide | note.
tagsstring[]—Tags to filter/retrieve. Max. 32.
projectstring—Project (scope) of the entry.

The scope of the saved entry comes from the project argument or, if omitted, from the PINKY_PROJECT env var; with neither, it defaults to global.

Credentials are redacted before anything is written: a title, body or tags carrying an API key, a GitHub token, an AWS key id, a JWT, a private key or a scheme://user:pass@host password lands in the .md and in the index as …REDACTED. It is a backstop, not permission — an agent should describe the credential, never paste it. Same for brain_update. See pinky redact in CLI.md to audit knowledge saved before this existed.

{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": { "name": "brain_save",
    "arguments": { "type": "gotcha", "title": "El pool de PG se cuelga al apagar",
                   "body": "Usar pool.close(timeout=…) antes de salir…",
                   "tags": ["postgres", "pool"] } } }

brain_stats — index size

No arguments. Returns the number of indexed entries and chunks.

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
  "params": { "name": "brain_stats", "arguments": {} } }
Each tool responds with a content: [{ "type": "text", "text": … }]. On an error (invalid input, missing index) it responds with the same shape plus "isError": true — it does not drop the connection.

Curation: brain_feedback · brain_update · brain_delete · brain_similar

They close the agent loop (search → use → fix/prune). All of them reference entries by the id returned by brain_search and brain_save.

ToolArgumentsWhat it does
brain_feedbackentry_id✅ · useful✅ (bool) · noteMarks whether the entry's latest retrieval helped or was noise — feeds dead-knowledge pruning.
brain_updateentry_id✅ · title · body · type · tagsFixes the source-of-truth .md and reindexes. The previous version is archived under .archive/ (reversible); rejected if the file changed since the last reindex. created is preserved, last_verified becomes today.
brain_deleteentry_id✅ · reason✅Never deletes: moves the .md to .archive/ with the reason in the frontmatter and removes it from the index.
brain_similarentry_id✅Semantic neighbours of an entry (similarity ≥ 0.85, top 3) — to spot duplicates and merge candidates.

Defensive cap: at most 50 mutations (update + delete) per server session.

brain_context — what the brain knows about a code file

Opt-in: not in the default PINKY_MCP_TOOLS set (search,save,stats), so it costs no tool-list tokens unless you ask for it (PINKY_MCP_TOOLS=search,save,stats,context).

{ "name": "brain_context", "arguments": { "file": "src/db.rs", "limit": 3 } }

Resolves the // Brain: <slug> breadcrumbs inside that file to entries, and spends any leftover room on one wikilink hop from them. No query and no embeddings — it answers with the shared model asleep and in airgapped installs. Each hit says where it came from (↳ via db.rs (breadcrumb)).

The same path runs automatically in the pre-read/pre-write hooks, which is where it matters most: they used to turn auth/login_controller.rs into the words "auth login controller" and hope BM25 found something.

Configuration (environment variables)

pinky-mcp takes no flags: it is configured through the environment. See also CONFIGURATION.md.

VariableDefaultWhat it does
PINKY_DBbrain.dbPath to the SQLite index. Set it the same as in the CLI so that pinky reindex and the agent see the same database.
PINKY_SAVE_DIRdocumentationFolder where brain_save writes the .md files.
PINKY_PROJECT(global)If set, saved entries land in the project:<value> scope.
PINKY_HASH_EMBED(unset)Uses the deterministic embedder (without downloading the ONNX model). Ideal for airgapped/CI.
PINKY_EMBED_SOCKET~/.pinky/embed.sockSocket of the shared embeddings daemon.
PINKY_EMBED_INPROC(unset)Loads the model inside this server instead of using the daemon: ~1.2 GB per chat.
PINKY_LOG / RUST_LOGwarnLogging level (to stderr).

Memory: one model for every chat

The client starts one pinky-mcp per chat, so anything this process loads is multiplied by the number of open chats. It therefore loads nothing: the embeddings model lives in a shared daemon (pinky embed-daemon) that the first search auto-starts and that exits on its own when idle.

  • a chat that never searches the brain: ~10 MB, no daemon
  • N chats searching: N × ~10 MB + one daemon (~1.2 GB), instead of N × 1.2 GB

Check it with pinky embed-daemon --status or pinky doctor.


Registering it in Claude Code

Prepares the project and writes .mcp.json with relative paths (relocatable across machines/clones), preserving other servers already defined:

pinky init

It leaves something like this:

{
  "mcpServers": {
    "pinky": {
      "command": "pinky-mcp",
      "env": {
        "PINKY_DB": "brain.db",
        "PINKY_SAVE_DIR": "documentation"
      }
    }
  }
}

PINKY_DB=brain.db matches the CLI default (pinky --db brain.db), so pinky reindex documentation and the agent share an index. Reopen the project in Claude Code and the pinky MCP is available.

Option B — claude mcp add (machine-global registration)

claude mcp add pinky --env PINKY_DB="$PWD/brain.db" -- pinky-mcp
# or, from the pinky_brain repo:
make mcp-register

Other MCP clients

Any client that speaks MCP over stdio works: point it at the pinky-mcp binary (on the PATH) as command, with the env vars above. The core is agnostic — only the hooks are specific to Claude Code.


Testing it by hand (debug)

Since it speaks JSON-RPC line by line, you can exercise it without a client:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"brain_stats","arguments":{}}}' \
  | PINKY_DB=brain.db pinky-mcp

You should see the serverInfo ("name":"pinky-brain"), the listing of the three tools and the index stats.


The three pieces share the same index: MCP pinky-mcp (this, for the agent), CLI pinky (CLI.md, for you/scripts) and hooks pinky-hooks (Claude Code). Full overview in HOW-IT-WORKS.md.