cortex-mcp
A self-hosted MCP server that gives every AI assistant I use the same memory: one folder of Markdown notes on my own server. Claude Code on the desktop and Meta AI on the phone read and write it every day, and I can swap the assistant without touching the notes. I designed it, built it test first in Go, and run it on a small VPS.
Architecture
Assistants connect over HTTPS through a TLS reverse proxy. Apps like claude.ai, ChatGPT, and Meta AI sign in with OAuth 2.1, and CLI agents use a Bearer token. Inside the binary, each request goes through the HTTP layer (sessions, rate limits, token checks) to twelve small MCP tools, and only one package, the vault, ever touches the disk. Login state lives in a SQLite database kept outside the vault. Syncing the notes to other machines is left to whatever the owner already uses, in my case Syncthing.
Key decisions
Go, for one small binary I can review
I compared TypeScript, Python, Go, and Rust. Go gave me a single static binary, a tiny distroless image, a short dependency list, and easy ARM builds. Rust's speed buys nothing for a server that mostly reads and writes small files, and it was the hardest of the four to review.
The AI can edit notes and nothing else
The tools have no shell, no git, and no network access. A deleted note goes to a .trash folder instead of disappearing. Every edit must carry the version of the note it read, so a stale edit from one assistant fails instead of overwriting what another one just wrote, and every write is logged with the name of the app that made it.
Three ways in, kept in three places
The owner signs in with a passkey, a TOTP code, or a single-use recovery code, each kept somewhere different, so losing a phone or a password manager never locks me out. Nothing on the web can create or reset the owner: that takes a command on the server itself.
The hard part
The sign-in endpoint that could fill the disk
Every OAuth sign-in starts with a request to /authorize, and the server stores it until I approve it. The security review of the OAuth work found that nothing capped those pending requests, so anyone could keep sending them until the disk was full. A single global rate limit was not enough either, because a few addresses could spend it and keep me out of my own server. It took three review rounds to close it all: pending requests are now capped per source and overall, each source spends its own budget before the global one, and passkey logins skip the shared code budget, since a passkey cannot be guessed.
Security and performance
- All file access goes through Go's os.Root and stays inside the vault. Symbolic links are never followed, and .git, .obsidian, and the server's own folder are off limits.
- OAuth follows the MCP authorization spec: PKCE S256, tokens bound to the server's /mcp URL, rotating refresh tokens with reuse detection, and redirect URIs limited to an allowlist.
- Recovery codes, refresh tokens, and cookies are stored only as hashes, and writes stop when the disk has less than 1 GiB free.
- Every push runs the tests with the race detector, 11 fuzz targets, staticcheck, govulncheck, and gosec. Releases are signed keylessly with cosign and ship with SBOMs.