MCP
In this section
Brainy speaks MCP itself. Not through a plugin, not through a wrapper process: the same binary that serves HTTP, gRPC and WebSockets answers JSON-RPC on the same port, through the same door dispatch, under the same keys. A client that speaks only MCP reaches exactly the engine an HTTP client reaches.
That matters more than a feature list. It means an agent that has never seen this deployment can connect to it and learn what it is from the server itself — what the doors are, when to reach for each one, and whatever the store's owner wanted said on arrival. None of that teaching lives in a client, a plugin or a configuration file, so none of it can drift from the engine it describes.
Connect
Over stdio — the shape most MCP clients use by default. The client starts the process; the process runs AS a principal, and the key comes from the environment rather than an argument, because an argument is visible to every process on the machine.
{
"mcpServers": {
"brainy": {
"command": "brainy",
"args": ["mcp"],
"env": { "BRAINY_KEY": "…" }
}
}
}JSONRun it in a directory holding a brain store. Nothing but JSON-RPC goes to stdout. A directory with no store is a refusal naming the one command that makes one — the server reads stores and does not yet write them, and a directory merely shaped like a store is the empty success this engine will not produce.
Over HTTP — the Streamable HTTP transport, for a server already running.
{
"mcpServers": {
"brainy": {
"type": "http",
"url": "http://127.0.0.1:8300/mcp",
"headers": { "Authorization": "Bearer …" }
}
}
}JSONThe bearer is a static key from the keys file, or a minted bk1.… token from the issuers file — the same Authorization: Bearer header either way, over stdio's $BRAINY_KEY too. See docs/serve.md's "Minted keys" section for the format, the issuers file, and the refusal classes a client can catch by name.
POST /mcp carries a JSON-RPC document and answers with one. A notification — a request with no id — is answered 202 Accepted with no body, because the server took it and has nothing to say back. GET /mcp answers 405 naming the methods that path accepts: this server has no server-initiated messages to push, so it does not offer a stream it would never write to.
The bridge
npx brainy mcp speaks two different ways, and which one answers is a fact about the RELEASE, not something you choose. Today (12.4), it is the bridge: an in-process TypeScript MCP server, over stdio only, running against the SAME public Brainy class every other TypeScript consumer of this package imports. It exists because brainy-serve's own native dispatch does not yet reach every door — the write family, transact, the accords doors — while a buyer's own agent still needs the whole surface today, not in a later release. So the bridge is not a second product: it is this package's engine, reached over MCP one release early.
Tool names, argument shapes, refusal codes and teaching text are brainy-serve's exactly. Both are generated from the same two files — docs/api-contract.json and docs/mcp-teaching.json — never a second table maintained by hand. A client written against the bridge today speaks to brainy-serve tomorrow unchanged. src/mcp/toolCatalog.test.ts is the pin: it diffs the bridge's own tool list against both files directly, so a door added to the contract without a matching teaching line, or the reverse, reds that suite by name.
The accords doors and brain.memory's doors are additionally real here, today — not disposition metadata claiming they are served, but this bridge actually calling brain.accords.<door>() / brain.memory.<door>() in-process. A tool that only this bridge answers (every write door, every accords door) is marked in its description accordingly, so a client can see which doors move to brainy-serve on its own docket and which are native today. brain.memory's doors (memory_*) have no brainy-serve counterpart at all yet and are not part of the shared conformance table. expand, expand_tree, collapse_tree, open_ref and wait_for_change are served, as memory_expand, memory_expand_rollup_tree, memory_collapse_rollup_tree, memory_open_ref and memory_wait_for_change — the naming-parity leg (BRAINY-STANDALONE-PARITY r20/r21) matched each against the user's hosted Self's own tool of the same short verb where the operation genuinely agrees, and named the two whose operation does NOT agree (a generation-stamped VFS checkout/write-back on the Self's side, this engine's episode/abstraction walk on this one's) differently on purpose — see src/mcp/memoryTools.ts's own header for the full account.
The policy_* tools (brain.policy — not to be confused with the dispatch-time policy check below, a different feature of the same name) are bridge-only too. Three of them — policy_ratify, policy_unlock and policy_freeze — rule rather than merely mutate, and want the owner's own credential; see docs/serve.md's "The owner scope". This bridge cannot yet read a per-caller scope off a bk1 token, so it refuses those three by name (PolicyOwnerCredentialUnavailableError) rather than answering from the process's own instanceOwner flag, which would let any client of an owner-run server rule as the owner; policy_check, policy_declare, policy_propose, policy_retire, policy_audit, policy_binding and policy_rules are unaffected.
Every tools/call passes one deterministic policy check before dispatch, sourced from src/accords/rules.ts's own rule table — the input-shape rules a caller's own request document can be checked against without a store read and without re-implementing the protocol a second time (id-prefix, real-calendar-day dates, a decision's exactly-one recommendation). A call the gate refuses never reaches the accords facade at all.
The bridge is stdio only. HTTP, SSE, gRPC and WebSockets stay brainy-serve's alone; nothing here binds a port. BRAINY_MCP_BRIDGE=0 forces brainy mcp back to resolving and execing brainy-serve mcp — the pre-bridge behaviour — for testing this shim's own resolution and exec path against a real binary.
The retirement rule. The day brainy-serve dispatches every door natively, brainy mcp goes back to execing brainy-serve mcp unconditionally, and the bridge's own code (src/mcp/) is deleted in that same release. Nothing about a client's experience changes on that day — the whole point of matching brainy-serve's own tool table byte for byte is that the swap is invisible from the outside.
The protocol revision
This server implements MCP revision 2025-06-18, and exactly one revision — not a range. A server that answered for a revision it had not implemented would differ from it precisely where two revisions differ, which is the only place it matters.
initialize answers with the revision this server implements, as the specification asks. When a client asked for a different one, the result also carries _meta["brainy/protocolVersion"] naming what was asked for and what is served, so a client's revision is never silently replaced.
After initialize, a request carrying an MCP-Protocol-Version header this server does not implement is refused 400 UnsupportedProtocolVersionError, naming both revisions by their specification dates. A request without the header is a client that has not negotiated yet, which is not an error.
Tools: every door, and when to use it
Every verb of the API contract is a tool. MCP is a transport here, not a curated selection, so there is no list for anyone to forget to update — the tools ARE the contract's doors, generated from it.
A tool's description has two halves, joined when the binary is built:
What it does — the contract's own summary and its exact TypeScript signature, verbatim. Not a paraphrase: the signature is where an agent reads the true shape of a call.
When to use it — one sentence about the choice. A list of accurate summaries is a reference; an agent choosing between fifty-odd doors needs advice. These lines live in
docs/mcp-teaching.json, a reviewable table diffed like any other written thing.
The join is total and enforced at build time. A contract door with no teaching line, or a teaching line naming no door, fails the build by name. Nothing is read from that file at runtime, so a running server cannot describe a door differently from the build it is.
The input schema is generated from the door's binding and the contract, and it claims only what is actually known: the path parameters the route names are required strings, and a door whose signature declares parameters also takes body. It does not INVENT the body's fields — the contract carries TypeScript signatures, not JSON Schemas, and deriving field names from a type name would be a guess wearing a specification's clothes.
Closed sets travel as enum. An option the contract declares with a fixed set of values — order is asc or desc, searchMode is one of a named few — reaches the body schema as a JSON Schema enum array, so a client validates it before sending and autocompletes it while a person types. Required options join the body's required list, and a door with a required option makes body itself required.
Every one of those values is read from the contract. None is written into the transport: a generator that filled in a value list from memory would make MCP a second authority on the engine's own vocabulary, and the first time the engine added a value, every generated client would reject it. The same lists appear on GET /contract.json under each door's options, so an HTTP or gRPC client learns them from the same place. A door whose options the contract does not declare gets no properties and no enums — an absent constraint, which a client handles, rather than a wrong one, which it cannot.
A door's refusal is a result, not a protocol error. A write that is not native yet, or a key that does not reach a brain, comes back as tool content the model reads, with isError: true and the machine-readable class in structuredContent. A protocol error is invisible to a model, and a refusal an agent cannot read is a refusal it will retry forever. Protocol errors stay what they are: a malformed request, an unknown method, a missing argument.
Every tool result carries _meta.ms — the server-side measured wall of the call that produced it, answer or refusal alike, the same clock every other transport's own framing (Server-Timing, a gRPC response's metadata, a WebSocket or SSE frame's meta) reads from.
The record profiles' doors are tools too
A record profile is a shape the engine enforces on a family of records, with its own namespaced verbs — the accords profile is the first, and its thirteen doors are thirteen more tools, listed in the same array as the engine's. Their names carry the namespace (accord_inbox, accord_read), which is not decoration: read, search and resolve exist in both families, and a tool name that dropped the namespace would be ambiguous.
A profile tool's description carries two things an engine tool's cannot, because a profile door has them: the rule ids it enforces and the refusal classes it can raise, both read from the contract. An agent can therefore know, before it writes a call, exactly what it may be told and which law it would be held to.
The five accords READS — inbox, dashboard, search, read, timeline — are served natively from the store's own records. The eight WRITES are present as tools and refuse by name with the release they arrive in, for the same reason every other not-yet-native door does: a tool that silently did nothing would be worse than one that is not there.
Instructions: what a client is told on arrival
initialize.instructions is the one place a server can teach before it is asked anything, and it has exactly two sources, in order:
The store's own document, at the fixed path
/_brainy/mcp/instructions.mdin the brain's virtual filesystem. When a store carries one it is served verbatim — not merged with, summarised from, or appended to anything. That is what makes a ceremony shippable by writing a file: fix the wording, fix every client's first contact, with no version of anything moving.A compact default generated from the contract, when the store carries no such document. It teaches the engine rather than any one deployment — how to arrive at a brain, what a refusal means, how to treat what is read, and where depth lives.
The document is read from the brain this key reaches, when it reaches exactly one. A key reaching several brains has no single ceremony to be told, and a server picking one of them would be picking for the caller; those sessions get the generated default.
The budget is measured, not estimated. Instructions are paid for on every session of every client, before any work happens, so the generated default is capped at 700 tokens and the engine's own tokenizer measures it — the same WordPiece vocabulary the engine embeds with, not a character count divided by four. A store's own document is not truncated: a host that writes its own ceremony owns its own budget, and a server that trimmed somebody else's text would be corrupting a document rather than enforcing a rule. 700 tokens is the budget to write to.
Three outcomes, never two. Served from the store; generated because the store has none; or generated after a refusal, which travels beside the text in _meta["brainy/instructions"]. A host whose ceremony failed to load is told rather than silently defaulted.
The language provider. Any tool that reads or writes with a language model (classification, summarization, extraction, judging, imagining, composing, rewriting) is only as available as the language seam brainy serve resolved at startup — see docs/language-providers.md. A store's own instructions document, or a future revision of the generated default, is where a client learns which provider is configured, or the one line to add when none is.
Resources: the store's skills
Longer guidance lives in the store as documents under /skills/, addressed as brainy://skills/<path>. resources/list names them; resources/read returns one. Both go through the same virtual-filesystem door every other transport reads files with — never through the server process's filesystem, which would answer for files the store does not hold and miss ones it does.
This is the point of serving skills from a store rather than shipping them: fixing a skill is a write, not a release. A client's depth improves the moment the document changes. No version of the server, of any plugin, or of any client moves.
An empty /skills/ directory — or no directory at all — is an empty list. A store with no skills is a store with no skills, and that is an answer. A store the server could not read is an error naming the refusal, because an empty list would be a lie about it.
Title and description come from the document
A client renders a resource index from name, title and description, so those have to be right without anyone maintaining a second table of them:
field | where it comes from |
|---|---|
| the file name, e.g. |
| the front matter's |
| the front matter's |
| the media type the store recorded, |
The three title steps always run in that order, so an author can override by writing front matter and still gets something readable having written neither. Nothing is inferred for description: one guessed from the first sentence would be wrong often enough to mislead a client choosing what to read, and MCP treats an absent description as absent rather than as an error.
Front matter is the ordinary convention — a --- fence on the first line, key: value lines, a closing --- — and only those two keys are read. A document that opens a fence and never closes it has no front matter, rather than half of one.
---
title: Arriving well
description: What to do in the first minute of a session.
---
tl;dr — ask before you assume; quote the ids you carry out.MARKDOWNAddressing is exact and traversal is refused, not normalised: a .. segment is not a typo to fix, it is a request for a file outside the namespace that resources/list advertised.
Records have addresses too, separately: resources/templates/list carries the classes an agent can name and read — a brain's census, an entity, an entity's history. Every template names the door that reads it, so reading a resource IS calling that door. A template whose read has no implementation is not offered.
What refuses today
The read path is native and serves. The write path is moving into the engine's core, and until it lands add, update, remove, transact, the filesystem writes, import and the accords profile's eight writes refuse with a typed error naming the door and the release it arrives in — never a silent no-op, and never an empty success.
The accords READS serve from the same read-only attach the file doors use, and two halves of them are named IN THE ANSWER rather than left to be discovered: a search carrying a query answers the engine's own text leg and says semantic.served: false with the reason, because a vector search needs an index an attach does not map; and inbox says presenceStamped.stamped: false, because moving a participant's last-read stamp is a write and this attach writes nothing.
Reading a file by path IS native. The server attaches the store read-only — no writer lock, nothing created, nothing repaired — and resolves a path through the store's own path posting, then reads the content-addressed blob and verifies the bytes against their own address. So resources/list, resources/read and the instructions document are all answers from the store, in this process, with no JavaScript runtime anywhere.
Two things follow that a client should know rather than discover:
One generation per attach. A document written after the server attached a brain is invisible to that brain's residency. It appears when the pool closes and reopens the brain, or when the server restarts. There is no refresh timer, because a reader that re-resolved its generation mid-session would answer two requests of one session from two states of the store and say so in neither.
A store that cannot be attached refuses by name.
StoreNotAttachableErrorcarries the attach's own reason — a writer handoff in flight, a torn SSTable, a record profile from a newer writer, or a metadata projection nothing has attested. Every cure is an action by the store's owner: a read-only attach repairs, quarantines and rebuilds nothing, because a reader that did would be deleting evidence from a store another process is writing.
A store whose projection was written by the reference engine carries no open manifest, so it is not read-only attachable at all — the owning open would build that projection from scratch, and a reader may not. Such a store refuses with no-manifest as the reason, and the cure is to open it once with the engine that attests.
A door that cannot be served is never quietly absent. Ask GET /contract.json for the whole surface, including every refusal class by name.