← Retour aux projets

cortex-mcp

Un serveur MCP auto-hébergé qui donne la même mémoire à tous les assistants IA que j'utilise : un seul dossier de notes Markdown sur mon propre serveur. Claude Code sur l'ordinateur et Meta AI sur le téléphone le lisent et l'écrivent tous les jours, et je peux changer d'assistant sans toucher aux notes. Je l'ai conçu, construit en Go en écrivant les tests d'abord, et je l'exploite sur un petit VPS.

Architecture

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
Le coffre reste un simple dossier de fichiers .md. Obsidian, Syncthing, git et les sauvegardes y travaillent en dehors du serveur.

Les assistants se connectent en HTTPS à travers un proxy inverse TLS. Des applications comme claude.ai, ChatGPT et Meta AI se connectent avec OAuth 2.1, et les agents en ligne de commande utilisent un jeton Bearer. Dans le binaire, chaque requête passe par la couche HTTP (sessions, limites de débit, vérification des jetons) jusqu'à douze petits outils MCP, et un seul paquet, vault, touche au disque. L'état de connexion vit dans une base SQLite gardée hors du coffre. La synchronisation des notes vers d'autres machines est laissée à l'outil que le propriétaire utilise déjà, Syncthing dans mon cas.

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
Dans le binaire, seul le paquet vault touche aux fichiers, via os.Root de Go.

Décisions clés

Go, pour un seul petit binaire que je peux relire

J'ai comparé TypeScript, Python, Go et Rust. Go m'a donné un seul binaire statique, une image distroless minuscule, une courte liste de dépendances et des builds ARM faciles. La vitesse de Rust n'apporte rien à un serveur qui lit et écrit surtout de petits fichiers, et c'était le plus difficile des quatre à relire.

L'IA peut modifier des notes, et rien d'autre

Les outils n'ont ni shell, ni git, ni accès réseau. Une note supprimée va dans un dossier .trash au lieu de disparaître. Chaque modification doit porter la version de la note qu'elle a lue, donc une modification périmée d'un assistant échoue au lieu d'écraser ce qu'un autre vient d'écrire, et chaque écriture est journalisée avec le nom de l'application qui l'a faite.

Trois façons d'entrer, gardées à trois endroits

Le propriétaire se connecte avec une passkey, un code TOTP ou un code de récupération à usage unique, chacun gardé à un endroit différent, donc perdre un téléphone ou un gestionnaire de mots de passe ne me bloque jamais. Rien sur le web ne peut créer ou réinitialiser le propriétaire : il faut une commande sur le serveur lui-même.

Le point difficile

Le point de connexion qui pouvait remplir le disque

Chaque connexion OAuth commence par une requête vers /authorize, et le serveur la garde jusqu'à ce que je l'approuve. La revue de sécurité du travail OAuth a montré que rien ne plafonnait ces requêtes en attente, donc n'importe qui pouvait en envoyer jusqu'à remplir le disque. Une seule limite de débit globale ne suffisait pas non plus, car quelques adresses pouvaient l'épuiser et m'empêcher d'entrer sur mon propre serveur. Il a fallu trois rondes de revue pour tout fermer : les requêtes en attente sont maintenant plafonnées par source et au total, chaque source dépense son propre budget avant le budget global, et la connexion par passkey ne touche pas au budget partagé des codes, puisqu'une passkey ne se devine pas.

Sécurité et performance

  • Tout accès aux fichiers passe par os.Root de Go et reste dans le coffre. Les liens symboliques ne sont jamais suivis, et .git, .obsidian et le dossier du serveur sont interdits.
  • OAuth suit la spécification d'autorisation de MCP : PKCE S256, jetons liés à l'URL /mcp du serveur, jetons de rafraîchissement rotatifs avec détection de réutilisation, et URI de redirection limitées à une liste autorisée.
  • Les codes de récupération, les jetons de rafraîchissement et les cookies ne sont stockés que sous forme de hachages, et les écritures s'arrêtent quand il reste moins de 1 Gio libre sur le disque.
  • Chaque push lance les tests avec le détecteur de concurrence, 11 cibles de fuzzing, staticcheck, govulncheck et gosec. Les versions sont signées sans clé avec cosign et livrées avec des SBOM.

Stack

GoMCPOAuth 2.1WebAuthnSQLiteDockerGitHub Actionscosign

Liens