Codex · Infrastructure
Un playbook complet : exposer un serveur MCP auto-hébergé — avec des outils d'écriture — à Claude, de façon privée et non-interactive, via un tunnel et un portail MCP Cloudflare. Plus le patron d'architecture « un prompt, un outil, un résultat déterministe » qu'il dessert.
Trois lieux, un seul point de vérité. On édite le code à un endroit, GitHub arbitre, le serveur exécute. Le poste de dev n'est jamais sollicité au runtime.
Poste de dev
Édite le code. Pousse vers GitHub. Hors du chemin d'exécution.
GitHub
Vérité du modèle (ontologie, shapes, catalogue). Le serveur tire et pousse ici.
Serveur (conteneurs)
Clone git monté en /repo · base graphe · données · classeurs générés.
Le serveur fait tourner deux conteneurs à partir d'une image unique :
Idée-clé : le dépôt git est bind-monté en lecture-écriture dans les conteneurs sous /repo. Un git commit fait dans le conteneur modifie le vrai dépôt, et le push part vers GitHub avec un jeton stocké dans .env (lu par un credential-helper, jamais écrit en clair). Le serveur est ainsi autonome : il lit, écrit, commit et pousse sans le poste de dev.
Objectif : un prompt en langage naturel → Claude choisit le bon outil → le résultat est reproductible (graphe + fichiers + git). Chaque outil fait une chose, de façon vérifiable.
| Famille | Outils (exemples) | Rôle |
|---|---|---|
| lecture | vues · vue · sparql · decrire · existant · modele | Interroger le graphe et le modèle sans rien écrire. |
| lecture | corpus · corpus_texte · journal · snapshots · valider | Corpus documentaire, historique, snapshots, validation SHACL. |
| réconcil. | comparer · exporter · recharger · git | Comparer graphe ↔ git, résoudre une divergence dans un sens ou l'autre, état du dépôt. |
| écriture | vue_creer · vue_enrichir · vue_supprimer · catalogue | CRUD complet des vues + régénération des classeurs. Chaque écriture suit une séquence fixe. |
| écriture | proposer · appliquer · restaurer | Écriture des données d'instance : proposer un lot → appliquer après accord → restaurer un snapshot. |
La discipline : les lectures sont libres, les écritures sont encadrées. Une écriture de vue enchaîne toujours la même séquence (section 7), et une écriture de données passe par un proposer → montrer → appliquer validé.
Règle centrale : ne jamais écraser un côté sans savoir ce qu'il contient. Le graphe en mémoire et le TTL de git peuvent diverger — quelqu'un a pu éditer le graphe à la main.
Avant toute modification du modèle, l'outil compare le graphe à son TTL de git. Trois états :
exporter (garder l'état du graphe, qui sera commité) ou recharger (garder git).La partie qui coûte le plus de temps. Un serveur MCP auto-hébergé n'est pas trivial à exposer à Claude de façon sécurisée — voici précisément pourquoi.
Le connecteur distant de Claude attend que l'endpoint MCP soit un serveur d'autorisation OAuth : il tente de s'enregistrer comme client (identité publiée / enregistrement dynamique). Or :
302 vers sa page de login — ce n'est pas un serveur OAuth pour le client MCP. Résultat concret à l'ajout du connecteur : « Impossible de s'inscrire auprès du service de connexion ».Cinq pièces à assembler, dans cet ordre. Chacune a une décision non-évidente qui fait échouer tout le reste si on la rate.
Claude
Connecteur personnalisé. OAuth + login Access.
MCP Portal
portal.example.com · fournit l'OAuth · policy e-mail.
Backend (Access)
mcp.example.com · joignable par service token seulement.
Tunnel → conteneur
cloudflared → serveur-mcp:8003/<path>.
cloudflared tourne comme conteneur dans le même réseau Docker que le serveur MCP, lancé avec un token de tunnel (connexion sortante seule, aucun port entrant à ouvrir). Dans le dashboard :
Zero Trust → Networks → Tunnels → ton tunnel → onglet « Public Hostname » → Add
mcpexample.comhttp://serveur-mcp:8003 (le nom du service Docker, pas une IP)Ainsi https://mcp.example.com/<path> atteint http://serveur-mcp:8003/<path>.
C'est l'auth machine que le portail présentera au backend (non-interactive, aucun navigateur).
Zero Trust → Access → Service credentials → Create Service Token → nomme-le → copie le Client ID et le Client Secret (le secret n'est affiché qu'une fois).
Le hostname du backend (mcp.example.com) reçoit une application Access « self-hosted », avec une policy dédiée :
Résultat : le backend n'est joignable que par ce token — plus rien de public.
Zero Trust → Access → MCP Portals → Create portal.
mon-portailportal (pas mcp) · Cloudflare crée le DNS tout seulmcp), les deux se disputent le DNS. Le portail doit vivre sur un autre nom (portal).Dans le portail, « Add a server » :
https://mcp.example.com/<path> — URL complète, avec le chemin ET le bon TLD (attention aux fautes de frappe : .com oublié = « introuvable »)CF-Access-Client-Id et CF-Access-Client-Secret = ton service tokenSave and connect server → le statut passe à Ready et Cloudflare introspecte les outils exposés (preuve que le token joint bien le backend).
| Couche | Qui / quoi | Autorisé par |
|---|---|---|
| Portail portal.example.com | point d'entrée client (OAuth) | policy e-mail (toi) |
| Serveur dans le portail | droit d'utiliser ses outils | policy e-mail (toi) |
| Backend mcp.example.com | le vrai serveur MCP | service token (Service Auth) |
Côté client, tu connectes Claude à l'URL du portail (pas au backend). C'est le portail qui fournit l'OAuth.
Ajouter un connecteur personnalisé
Réglages → Connecteurs → Ajouter. URL = l'URL du portail (https://portal.example.com/…). L'app exige du https — une IP de LAN est refusée ici, d'où le besoin du tunnel.
Choisir l'authentification
« Se connecter maintenant » (flux OAuth). Client OAuth : « Utiliser l'identité publiée de Claude » (CIMD).
Passer la vérification
La sonde peut afficher un 302 / « impossible de vérifier » : c'est la redirection Access. Choisis « Continuer quand même ».
S'authentifier une fois
Claude ouvre le login Access → PIN e-mail ou SSO (un fournisseur d'identité comme Microsoft Entra rend ça « 1 clic »). Les outils apparaissent ensuite dans la conversation.
Exemple : « trouve une propriété des objets X absente de la vue, et mets la vue à jour ». Entièrement exécuté par l'assistant, sans commande manuelle.
Explorer
Lister les propriétés portées par les instances dans le graphe, et repérer celle qui manque à la vue mais existe déjà dans le modèle.
Réconcilier
comparer sur le graphe des vues → identique. Si « diffère », l'opération s'arrête et montre le diff (section 3).
Éditer le catalogue (git)
Remplacer la requête de la vue dans le TTL versionné pour ajouter la colonne.
Recharger
Recharger le graphe des vues depuis le TTL édité.
Régénérer les classeurs
Réécrire les classeurs côté serveur, dans le dossier de données, avec l'URL de la base graphe joignable depuis le client gravée dans la requête.
Commit + push
Committer le catalogue et le pousser vers GitHub. État final : graphe, fichiers et git alignés.
Boucle de dev pour le code ; opérations à chaud via le connecteur.
# 1. Poste de dev : éditer, puis
git add -A && git commit -m "..." && git push
# 2. Serveur : tirer et reconstruire l'image
git pull
docker compose up -d --build
Une fois le connecteur en place, les opérations (créer/enrichir/supprimer une vue, réconcilier, régénérer) ne nécessitent aucun déploiement : l'assistant appelle les outils directement.
.env structurantes| Variable | Rôle |
|---|---|
| REPO_DIR | Clone git de l'hôte, bind-monté en /repo dans les conteneurs. |
| MODEL_TTL_DIR | TTL du modèle lus/écrits dans le dépôt (/repo/...). |
| GIT_TOKEN | Commit + push automatiques (portée contents:write, lu par un credential-helper). |
| GRAPH_URL_CLIENT | URL de la base graphe vue depuis le client, gravée dans les classeurs (≠ l'adresse interne du conteneur). |
| DATA_DIR | Données d'instance, corpus, classeurs générés (hors git). |
La liste des aller-retours réels. Chacun a une cause non-évidente.
Un entrypoint.sh récupéré sous Windows a des fins de ligne CRLF ; le noyau cherche l'interpréteur /bin/sh\r et renvoie exec: no such file or directory. Fix : sed -i 's/\r$//' /entrypoint.sh dans le Dockerfile, + un .gitattributes forçant * text=auto eol=lf (et *.sh text eol=lf).
Un gabarit binaire (.zip) exclu par .gitignore est absent de l'image → la génération plante à l'exécution. Fix : exception explicite dans .gitignore (!chemin/vers/gabarit.zip) pour le suivre.
Le conteneur joint sa base via host.docker.internal — injoignable depuis un client externe. Les fichiers générés côté serveur doivent graver l'URL cliente (variable dédiée), pas l'adresse interne.
Un classeur ouvert dans un tableur n'est pas réécrit (verrou). C'est voulu (protection) : ferme-le et relance la régénération.
Connecteur Claude + app Access simple = échec d'inscription OAuth. Il faut un MCP Portal (fournit l'OAuth) + un service token en Service Auth pour le backend. Détails en sections 4–5.
.com.En ligne de commande, si le connecteur n'est pas disponible.
# État git du dépôt monté dans le conteneur
docker compose exec mcp sh -c "git -C /repo rev-parse --short HEAD && ls /repo/modele/ttl"
# Tester l'authentification du token (push/pull)
docker compose exec mcp sh -c "cd /repo && git fetch && echo AUTH-OK"
# Appeler une fonction d'outil directement (ex. réconciliation)
docker compose exec mcp python -c "import json; from app import tools as T; print(json.dumps(T.compare('views'), ensure_ascii=False, indent=1))"
# Logs du serveur MCP
docker compose logs mcp --tail 50