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
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.
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.