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
querystringWhat to search for. Max. 2,000 characters.
limitinteger10Max. results (1100).
scopestringNarrows by scope: global or project:<name>.
projectstringNarrows to a project (equivalent to scope: project:<name>).
typestringA 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
titlestringShort, descriptive title. Max. 300 characters.
bodystringContent in markdown. Max. 100,000 characters.
typestringnotegotcha | pattern | decision | diary | guide | note.
tagsstring[]Tags to filter/retrieve. Max. 32.
projectstringProject (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✅ · reasonNever deletes: moves the .md to .archive/ with the reason in the frontmatter and removes it from the index.
brain_similarentry_idSemantic 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.