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).
| Argument | Type | Req. | Default | What it does |
|---|---|---|---|---|
query | string | ✅ | — | What to search for. Max. 2,000 characters. |
limit | integer | 10 | Max. results (1–100). | |
scope | string | — | Narrows by scope: global or project:<name>. | |
project | string | — | Narrows to a project (equivalent to scope: project:<name>). | |
type | string | — | A single type: gotcha | pattern | decision | diary | guide | note. | |
tags | string[] | — | 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.
| Argument | Type | Req. | Default | What it does |
|---|---|---|---|---|
title | string | ✅ | — | Short, descriptive title. Max. 300 characters. |
body | string | ✅ | — | Content in markdown. Max. 100,000 characters. |
type | string | note | gotcha | pattern | decision | diary | guide | note. | |
tags | string[] | — | Tags to filter/retrieve. Max. 32. | |
project | string | — | 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 acontent: [{ "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.
| Tool | Arguments | What it does |
|---|---|---|
brain_feedback | entry_id✅ · useful✅ (bool) · note | Marks whether the entry's latest retrieval helped or was noise — feeds dead-knowledge pruning. |
brain_update | entry_id✅ · title · body · type · tags | Fixes 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_delete | entry_id✅ · reason✅ | Never deletes: moves the .md to .archive/ with the reason in the frontmatter and removes it from the index. |
brain_similar | entry_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.
| Variable | Default | What it does |
|---|---|---|
PINKY_DB | brain.db | Path 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_DIR | documentation | Folder 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.sock | Socket 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_LOG | warn | Logging 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
Option A — pinky init (recommended)
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: MCPpinky-mcp(this, for the agent), CLIpinky(CLI.md, for you/scripts) and hookspinky-hooks(Claude Code). Full overview in HOW-IT-WORKS.md.