TypeScript · Model Context Protocol · v0.3.0

ian-brain

A single authenticated HTTP endpoint that gives a language model structured access to a personal markdown wiki, a long-term memory service, a derived knowledge graph, a fleet of machines, and a credential vault — thirty tools, eight modules, one bearer token.

~2,940 lines of TypeScript 4 runtime dependencies 30 registered tools stateless per-request server

The system, as a graph — every node is a section

Navigation constellation of the ian-brain documentation A hub labelled "ian-brain, personal MCP gateway" sits at the centre. Six section nodes orbit it and are joined to it and to each other by hairlines: wiki substrate, retrieval router, memory and wiki, tool surface, the gateway, and write path. Each node links to its section page. ian-brain personal MCP gateway POST /mcp · bearer auth · stateless wiki substrate wiki_index · files_neighbors retrieval router files_search · wiki_read_all memory + wiki recall · retain · reflect tool surface 30 tools · 8 namespaces the gateway auth · OAuth · audit · shell write path wiki_append · dedup · git
  • storage substrate
  • read paths
  • write paths
  • transport and trust
wiki substrate
A directory of markdown files with [[wikilinks]] is treated as a graph. How pages are walked, slugs resolved, titles and snippets extracted, and neighbours reconstructed.
retrieval router
Four retrieval strategies with very different cost profiles, the decision rule embedded in every tool description, and the three-deep fallback chain when the search backend is unavailable.
write path
wiki_append — slug routing by character similarity, Jaccard de-duplication, a lock file, a git commit, and an append-only JSONL journal.
memory + wiki
Two stores with different half-lives. Memory banks for episodic recall and reflection; curated markdown for durable facts. Where the boundary is drawn and who enforces it.
tool surface
Thirty tools across eight registrars, plus the federation client that mounts entire downstream MCP servers behind a name prefix and four generic tools.
the gateway
Express, bearer authentication, a minimal OAuth authorization-code server with PKCE, a SQLite audit ledger that hashes its arguments, and the subprocess layer everything else stands on.

What this actually is

The Model Context Protocol gives a model a list of tools and a way to call them. Most MCP servers wrap one product. This one wraps a person's working context: the notes they keep, the decisions they have made, the machines they own, and the secrets those machines need. It is the difference between an assistant that can search the web and an assistant that can search your filing cabinet.

The design commitment that shapes everything else is stated in the server's own instruction block, sent to every client on connect:

It does not preload private data; call tools only when needed.

src/server.ts:79 — server instructions

Nothing is pushed into the model's context by default. There is no system-prompt dump of the wiki, no pre-warmed memory summary. Instead the server ships a router: a short block of routing rules, repeated verbatim inside five tool descriptions, that tells the agent which retrieval strategy fits which question. Context is assembled on demand, cheapest tool first. The retrieval router section takes that apart.

The shape of the process

One Express application. Two public routes that matter — POST /mcp and GET /health — plus an OAuth pair and a registry card. A fresh McpServer instance is constructed per request and torn down when the response closes, so the process holds no session state at all. Long-lived state lives in exactly two places: the SQLite audit ledger, and the in-memory map of connected downstream servers.

Anatomy of the ian-brain process A request from an MCP client enters an Express application, passes bearer authentication, and constructs a per-request McpServer. Eight registrar modules attach tools to it. Those tools reach four classes of backend: the local filesystem and git, subprocess CLIs, HTTP services on loopback, and federated downstream MCP servers. An audit module records every call to SQLite. client http layer tool registrars backends MCP client agent / IDE / chat Bearer <token> express 5 GET /health unauthenticated /oauth/authorize /oauth/token authMiddleware 401 on mismatch POST /mcp new McpServer per request StreamableHTTP sessionId: undefined registerAllTools() creds_*4 machines_*5 actions_*3 projects_*4 memory_*3 files_* / wiki_*7 wiki_append1 graph_*3 30 tools registered on every request filesystem + git wiki pages, lock file, JSONL log subprocess CLIs search index · graph builder · vault ssh · scp · git and workspace CLIs loopback HTTP memory service REST API federated MCP servers tools re-exported under a prefix catalog refreshed every 10 min audit.db — tool calls logged: name, sha256(args)[0:16], status, latency
Process anatomy. Authentication is a single equality check against one or two environment tokens; there is no user model. Everything past that boundary is trusted, which is why the audit ledger and the per-tool reason arguments exist — see the gateway.

Booting it

Four runtime dependencies: the MCP SDK, Express 5, better-sqlite3 for the audit ledger, and Zod for input schemas. The build is tsc with no bundler. The server refuses to start without a token — the check happens before the HTTP listener is created.

bash — gateway host
$ npm install
$ npm run build

> ian-brain@0.3.0 build
> tsc

$ node dist/index.js
FATAL: IAN_BRAIN_TOKEN env var is required
# src/server.ts:32 — checked before app.listen(), exit code 1

$ IAN_BRAIN_TOKEN="$(cat ~/.config/ian-brain/token)" node dist/index.js
[federation] no downstreams config at ./config/downstreams.json, federation disabled
[ian-brain v0.3.0] listening on 127.0.0.1:5050

The default bind address is 127.0.0.1 (src/server.ts:28). Public exposure is somebody else's job — a tunnel or reverse proxy in front, never the Node process itself.

Reading the repository

Eight modules under src/tools/ each export a single register*(server) function; src/tools/index.ts calls all eight in order. There is no plugin discovery, no decorator magic, no dependency injection. Adding a tool means writing a Zod schema and a handler and adding one line to a barrel file.

Source layout — line counts as measured
FileLinesResponsibility
src/index.ts6Entry point. Calls startServer(), exits non-zero on a rejected promise.
src/server.ts155Express app, routes, the client instruction block, graceful shutdown on SIGTERM/SIGINT.
src/auth.ts28Bearer check against a set of one or two valid tokens.
src/oauth.ts169Authorization-code flow with PKCE, in-memory code store, redirect allowlist.
src/audit.ts44SQLite ledger in WAL mode; arguments stored only as a truncated hash.
src/federation/client.ts211Downstream MCP clients, cached tool catalogs, lazy reconnect.
src/tools/files.ts487Wiki walking, search, index, neighbours, whole-wiki read, file read/write, ingest.
src/tools/machines.ts555Host inventory, five-step command routing, file copy, log tail, access guide.
src/tools/wiki_append.ts207The guarded wiki write path: routing, dedup, lock, commit, journal.
src/tools/creds.ts179Vault reads and writes through the op CLI, keyed by alias.
src/tools/projects.ts166Generic list / search / schema / call over federated namespaces.
src/tools/memory.ts181Recall, retain and reflect against a memory service over HTTP.
src/tools/graph.ts124Query, path and explain over a pre-built graph JSON.
src/tools/actions.ts113GitHub CLI, Workspace CLI, and a raw local shell.
src/tools/shell.ts80The spawn wrapper: timeout, output cap, SIGKILL, result formatting.

Three observations from the source

Advertised but not registered

The registry card served at /.well-known/registry-card names discover_namespaces as the introspection tool (src/server.ts:62-65). No tool by that name is registered by registerAllTools. The working equivalent is projects_list. The federation client's error text points at a discover_tools that is likewise absent (src/federation/client.ts:167).

The wiki write path was retrofitted

patch.cjs at the repository root is a 32-line one-shot script that inserts the wiki_append registration into the tool barrel and splices five lines into the server's instruction block. It writes a .bak-wikiappend-* backup and is idempotent by string match. Both edits are already present in the committed source — the script is a fossil of how the write path was added.

No test suite

There is no test directory, no test runner in devDependencies, and no test script in package.json. The correctness argument rests on TypeScript strict mode, Zod validation at the tool boundary, and the audit ledger as a post-hoc record. Every claim on this site is drawn from reading the source, not from running it.

Documentation compiled by reading the source of a private repository. Tool names, schemas, defaults, limits and control flow are quoted from that source with file and line references. Wiki contents, memory contents, credential aliases, machine hostnames, account identities and service endpoints have been deliberately omitted; where an example needs a value, a placeholder such as <slug> or <host> appears instead.