← Volver a proyectos

cortex-mcp

Un servidor MCP autoalojado que les da la misma memoria a todos los asistentes de IA que uso: una sola carpeta de notas Markdown en mi propio servidor. Claude Code en la computadora y Meta AI en el teléfono la leen y la escriben todos los días, y puedo cambiar de asistente sin tocar las notas. Lo diseñé, lo construí en Go escribiendo primero las pruebas y lo opero en un VPS pequeño.

Arquitectura

cortex-mcp overviewAI assistants (claude.ai, ChatGPT, Meta AI, Claude Code) connect over HTTPS with OAuth 2.1 or a Bearer token to a TLS reverse proxy on the owner's server. The proxy forwards to cortex-mcp, which keeps its login state in auth.db and reads and writes a vault of Markdown files. Obsidian, Syncthing, git, and backups work on the vault outside cortex-mcp.ASSISTANTS · ANY MCP CLIENTYOUR SERVERHTTPS · OAUTH 2.1 OR BEARERFORWARDREAD / WRITESTATEYOUR CHOICE · OUTSIDE CORTEX-MCPclaude.aiOAuth 2.1ChatGPTOAuth 2.1Meta AIOAuth 2.1Claude CodeOAuth or BearerTLS proxyCaddy, nginx · :443cortex-mcpone Go binary · /mcp · 12 toolsstate_dirauth.db · writes.logYour vaulta folder of .md filesObsidianany editorSyncthinglaptop · phonegithistoryBackupsyour usual toolPROXYAPPSTATESTORE
La bóveda sigue siendo una carpeta normal de archivos .md. Obsidian, Syncthing, git y los respaldos trabajan sobre ella fuera del servidor.

Los asistentes se conectan por HTTPS a través de un proxy inverso TLS. Aplicaciones como claude.ai, ChatGPT y Meta AI inician sesión con OAuth 2.1, y los agentes de línea de comandos usan un token Bearer. Dentro del binario, cada petición pasa por la capa HTTP (sesiones, límites de tasa, verificación de tokens) hasta doce herramientas MCP pequeñas, y un solo paquete, vault, toca el disco. El estado de inicio de sesión vive en una base SQLite fuera de la bóveda. Sincronizar las notas con otras máquinas queda en manos de lo que el dueño ya usa, en mi caso Syncthing.

Inside the cortex-mcp binaryA request to /mcp enters internal/server, which handles HTTP, sessions, rate limits, and token checks. Sign-ins go to internal/oauth and token checks to internal/tokens and authdb, which keep secrets as hashes in auth.db. Allowed calls reach the twelve MCP tools in internal/tools, and only internal/vault touches the files of the vault.ONE GO BINARYPOST /mcpAUTHORIZED CALLPATH + VERSIONSIGN-INTOKEN CHECKOWNER, CLIENTSos.Rootinternal/serverHTTP · sessions · rate limits · token checksinternal/tools12 MCP tools: read, search, edit, move, trashinternal/vaultthe only code that touches filesno symlinks · protected folders · versioned writesinternal/oauthOAuth 2.1 · login page · passkeys · TOTPtokens + authdbBearer tokens · secrets stored as hashesYour vaulta folder of .md filesEDGECOREAUTH
Dentro del binario, solo el paquete vault toca archivos, mediante os.Root de Go.

Decisiones clave

Go, por un solo binario pequeño que puedo revisar

Comparé TypeScript, Python, Go y Rust. Go me dio un solo binario estático, una imagen distroless diminuta, una lista corta de dependencias y builds para ARM sin complicaciones. La velocidad de Rust no aporta nada a un servidor que sobre todo lee y escribe archivos pequeños, y era el más difícil de revisar de los cuatro.

La IA puede editar notas y nada más

Las herramientas no tienen shell, ni git, ni acceso a la red. Una nota borrada va a una carpeta .trash en vez de desaparecer. Cada edición debe llevar la versión de la nota que leyó, así que una edición desactualizada de un asistente falla en vez de sobrescribir lo que otro acaba de escribir, y cada escritura queda registrada con el nombre de la app que la hizo.

Tres formas de entrar, guardadas en tres lugares

El dueño inicia sesión con una passkey, un código TOTP o un código de recuperación de un solo uso, cada uno guardado en un lugar distinto, así que perder un teléfono o un gestor de contraseñas nunca me deja fuera. Nada en la web puede crear ni restablecer al dueño: eso requiere un comando en el propio servidor.

La parte difícil

El endpoint de inicio de sesión que podía llenar el disco

Cada inicio de sesión OAuth empieza con una petición a /authorize, y el servidor la guarda hasta que yo la apruebo. La revisión de seguridad del trabajo de OAuth encontró que nada limitaba esas peticiones pendientes, así que cualquiera podía seguir enviándolas hasta llenar el disco. Un solo límite de tasa global tampoco bastaba, porque unas pocas direcciones podían agotarlo y dejarme fuera de mi propio servidor. Hicieron falta tres rondas de revisión para cerrarlo todo: ahora las peticiones pendientes tienen tope por origen y en total, cada origen gasta su propio presupuesto antes que el global, y el inicio de sesión con passkey no toca el presupuesto compartido de códigos, porque una passkey no se puede adivinar.

Seguridad y rendimiento

  • Todo acceso a archivos pasa por os.Root de Go y se queda dentro de la bóveda. Los enlaces simbólicos nunca se siguen, y .git, .obsidian y la carpeta del propio servidor están vetados.
  • OAuth sigue la especificación de autorización de MCP: PKCE S256, tokens atados a la URL /mcp del servidor, tokens de refresco rotativos con detección de reutilización y URI de redirección limitadas a una lista permitida.
  • Los códigos de recuperación, los tokens de refresco y las cookies se guardan solo como hashes, y las escrituras se detienen cuando quedan menos de 1 GiB libres en el disco.
  • Cada push corre las pruebas con el detector de carreras, 11 objetivos de fuzzing, staticcheck, govulncheck y gosec. Los releases se firman sin llave con cosign y se publican con SBOM.

Stack

GoMCPOAuth 2.1WebAuthnSQLiteDockerGitHub Actionscosign

Enlaces