Codex · Infrastructure

Piloter un serveur MCP privé avec Claude

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.

Pile Docker · tunnel Cloudflare · MCP Portal · Claude Niveau intermédiaire → avancé Format mémo technique réutilisable

01 Architecture & principes

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.

Séparation des données. Seuls les TTL du modèle (ontologie, shapes, catalogue des vues) sont versionnés. Les données d'instance et les classeurs générés vivent dans un dossier monté hors git — rien de confidentiel n'est poussé.

02 La boîte à outils déterministe

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.

FamilleOutils (exemples)Rôle
lecturevues · vue · sparql · decrire · existant · modeleInterroger le graphe et le modèle sans rien écrire.
lecturecorpus · corpus_texte · journal · snapshots · validerCorpus documentaire, historique, snapshots, validation SHACL.
réconcil.comparer · exporter · recharger · gitComparer graphe ↔ git, résoudre une divergence dans un sens ou l'autre, état du dépôt.
écriturevue_creer · vue_enrichir · vue_supprimer · catalogueCRUD complet des vues + régénération des classeurs. Chaque écriture suit une séquence fixe.
écritureproposer · 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é.

03 Réconciliation avant écriture

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 :

Pourquoi ne pas automatiser le sens. Un « diffère » ne dit pas quel côté est le plus récent. Choisir automatiquement risquerait d'écraser soit une amélioration faite dans git, soit une correction manuelle faite dans le graphe. Le diff est montré, le choix est explicite. Après écriture, les deux côtés reflètent de nouveau le même état.

04 Le mur : pourquoi Access seul échoue

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 :

Conclusion. Il faut une couche qui parle OAuth à Claude et protège le backend. C'est exactement le rôle du MCP Portal de Cloudflare (section 5). On ne peut pas s'en passer avec une simple app Access.

05 Montage Cloudflare, pas à pas

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

5.1 Le tunnel cloudflared

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

Subdomain
mcp
Domain
example.com
Service
HTTP · http://serveur-mcp:8003 (le nom du service Docker, pas une IP)
Path
vide — le tunnel transmet le chemin tel quel au conteneur

Ainsi https://mcp.example.com/<path> atteint http://serveur-mcp:8003/<path>.

5.2 Le service token

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

5.3 Protéger le backend (Service Auth)

Le hostname du backend (mcp.example.com) reçoit une application Access « self-hosted », avec une policy dédiée :

Action
Service Auth (surtout pas « Allow »)
Include
Service Token = ton token
Le piège le plus subtil. Un token placé dans une policy « Allow » ne fonctionne pas : « Allow » attend une session d'identité (login navigateur). Seule l'action « Service Auth » laisse passer un token sans login. Et garde-la dans sa propre policy — ne la mélange pas avec la policy e-mail humaine (sinon le token ouvre aussi tes autres apps).

Résultat : le backend n'est joignable que par ce token — plus rien de public.

5.4 Créer le MCP Portal

Zero Trust → Access → MCP Portals → Create portal.

Portal name
ex. mon-portail
Custom domain
subdomain différent du backend — ex. portal (pas mcp) · Cloudflare crée le DNS tout seul
Access
attache une policy e-mail / Allow (ton identité) — c'est elle qui t'autorise à entrer
Cloudflare Gateway
Off (le backend est joint par son hostname public)
Code mode
Off
Conflit de sous-domaine. Si tu donnes au portail le même sous-domaine que le backend (mcp), les deux se disputent le DNS. Le portail doit vivre sur un autre nom (portal).

5.5 Ajouter le serveur au portail

Dans le portail, « Add a server » :

HTTP URL
https://mcp.example.com/<path> — URL complète, avec le chemin ET le bon TLD (attention aux fautes de frappe : .com oublié = « introuvable »)
Authentication type
Custom headers — pas « OAuth » (le backend n'est pas un serveur OAuth), pas « None » (il est protégé par token)
Headers
CF-Access-Client-Id et CF-Access-Client-Secret = ton service token
Access policies
attache ta policy e-mail ici aussi (sinon : « No allowed servers available »)

Save and connect server → le statut passe à Ready et Cloudflare introspecte les outils exposés (preuve que le token joint bien le backend).

5.6 Récapitulatif : trois couches alignées

CoucheQui / quoiAutorisé par
Portail portal.example.compoint d'entrée client (OAuth)policy e-mail (toi)
Serveur dans le portaildroit d'utiliser ses outilspolicy e-mail (toi)
Backend mcp.example.comle vrai serveur MCPservice token (Service Auth)
Durée de session. Règle la session Access à 1 mois (réglages de l'app / policy) : tu t'authentifies une fois, Claude rafraîchit le jeton dans cette fenêtre.

06 Connecter Claude au portail

Côté client, tu connectes Claude à l'URL du portail (pas au backend). C'est le portail qui fournit l'OAuth.

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

  2. Choisir l'authentification

    « Se connecter maintenant » (flux OAuth). Client OAuth : « Utiliser l'identité publiée de Claude » (CIMD).

  3. Passer la vérification

    La sonde peut afficher un 302 / « impossible de vérifier » : c'est la redirection Access. Choisis « Continuer quand même ».

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

Service token vs OAuth. Le service token (section 5.2) ne sert qu'au lien portail → backend. Toi, humain, tu passes par l'OAuth du portail. Les deux coexistent : l'un est machine-à-machine, l'autre interactif.

07 Le flux d'une opération

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.

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

  2. Réconcilier

    comparer sur le graphe des vues → identique. Si « diffère », l'opération s'arrête et montre le diff (section 3).

  3. Éditer le catalogue (git)

    Remplacer la requête de la vue dans le TTL versionné pour ajouter la colonne.

  4. Recharger

    Recharger le graphe des vues depuis le TTL édité.

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

  6. Commit + push

    Committer le catalogue et le pousser vers GitHub. État final : graphe, fichiers et git alignés.

08 Déploiement & configuration

Boucle de dev pour le code ; opérations à chaud via le connecteur.

Boucle dev (changement de code)

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

Clés .env structurantes

VariableRôle
REPO_DIRClone git de l'hôte, bind-monté en /repo dans les conteneurs.
MODEL_TTL_DIRTTL du modèle lus/écrits dans le dépôt (/repo/...).
GIT_TOKENCommit + push automatiques (portée contents:write, lu par un credential-helper).
GRAPH_URL_CLIENTURL de la base graphe vue depuis le client, gravée dans les classeurs (≠ l'adresse interne du conteneur).
DATA_DIRDonnées d'instance, corpus, classeurs générés (hors git).

09 Pièges rencontrés & solutions

La liste des aller-retours réels. Chacun a une cause non-évidente.

Fins de ligne CRLF dans les scripts

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

Fichier binaire exclu de l'image

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.

Mauvaise URL de service dans les fichiers générés

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.

Fichier ouvert = non régénéré

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.

OAuth MCP (le gros morceau)

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.

Les petites fautes qui bloquent tout

  • URL du connecteur sans le chemin MCP, ou sans le .com.
  • Sous-domaine du portail identique à celui du backend (conflit DNS).
  • Service token mis en Allow au lieu de Service Auth.
  • Policy e-mail oubliée sur le serveur-dans-le-portail → « No allowed servers available ».

10 Commandes de secours

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