Serveur MCP (Model Context Protocol) pour l'API Docs de La Suite numérique.
Il expose 25 tools et 2 resources permettant à un agent IA d'interagir avec les documents Docs : lire, créer, mettre à jour le contenu (markdown), gérer les accès et les invitations, organiser l'arborescence, utiliser les fonctions IA natives de Docs (résumé, correction, traduction), et plus encore.
- Prérequis
- Installation
- Configuration
- Utilisation avec Claude Desktop
- Tools disponibles
- Exemples de workflows
- Validation de la configuration
- Sécurité
- Développement
- Architecture
- Python 3.12+
- uv (gestionnaire de paquets Python)
- Un compte sur une instance Docs (ex. https://docs.numerique.gouv.fr)
git clone <repo-url>
cd mcp-docs
uv syncLe serveur est configuré par variables d'environnement, préfixées DOCS_.
| Variable | Description | Défaut |
|---|---|---|
DOCS_BASE_URL |
URL de base de l'API Docs | https://docs.numerique.gouv.fr |
DOCS_AUTH_MODE |
session (cookie) ou oidc (Bearer token) |
session |
DOCS_SESSION_COOKIE |
Cookie docs_sessionid (mode session, explicite) |
— |
DOCS_SESSION_FILE |
Chemin JSON contenant le cookie (mode session, alternative) | $XDG_STATE_HOME/mcp-docs/session.json |
DOCS_OIDC_TOKEN |
Bearer token OIDC (mode oidc) | — |
DOCS_MAX_RETRIES |
Nombre de retries sur erreurs transitoires | 3 |
DOCS_MAX_CONCURRENT |
Limite de requêtes concurrentes | 5 |
Le mode session est le mode par défaut. Il utilise le cookie docs_sessionid obtenu après connexion ProConnect. Deux options :
Un CLI dédié lance un navigateur, attend que vous complétiez la connexion ProConnect, puis écrit le cookie dans un fichier local lu automatiquement par le MCP :
# Installation (une seule fois)
uv sync --extra browser
uv run playwright install chromium
# Rafraîchir le cookie (aucun argument requis)
uv run mcp-docs-refresh-sessionAu premier lancement, un Chromium s'ouvre — terminez la connexion ProConnect dans la fenêtre. Le cookie est écrit dans ~/.local/state/mcp-docs/session.json (permissions 0600) et le MCP le lit au démarrage.
Les lancements suivants vérifient d'abord si le cookie existant est encore valide (≈ 200 ms, aucun navigateur lancé). Si oui, on sort immédiatement. Sinon, le flow Playwright redémarre — et comme un profil navigateur persistant est conservé sous ~/.local/share/mcp-docs/browser-profile/, la session ProConnect est souvent déjà connue, donc la récupération est quasi-instantanée.
Override du chemin : DOCS_SESSION_FILE=/autre/chemin.json uv run mcp-docs-refresh-session.
Timeout configurable : uv run mcp-docs-refresh-session --timeout 600.
Pour ne plus jamais avoir à lancer la commande manuellement, planifier un run horaire en arrière-plan avec launchd. Le flag --headless fait tourner Chromium sans fenêtre ; si la session ProConnect IdP est encore vivante dans le profil persistant, le refresh est invisible. Si elle a expiré, le job échoue et une notification macOS s'affiche pour te signaler qu'il faut relancer en interactif.
Récupérer le chemin absolu de uv :
which uv # ex: /opt/homebrew/bin/uvCréer ~/Library/LaunchAgents/mcp-docs.refresh.plist (adapter PATH_TO_UV et PATH_TO_REPO) :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcp-docs.refresh</string>
<key>ProgramArguments</key>
<array>
<string>PATH_TO_UV</string>
<string>run</string>
<string>--directory</string>
<string>PATH_TO_REPO</string>
<string>mcp-docs-refresh-session</string>
<string>--headless</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>UV_PROJECT_ENVIRONMENT</key>
<string>/tmp/venv-mcp-docs</string>
<key>UV_LINK_MODE</key>
<string>copy</string>
</dict>
<key>StartInterval</key>
<integer>3600</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/mcp-docs-refresh.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/mcp-docs-refresh.err.log</string>
</dict>
</plist>Charger (ou recharger après une modification du plist) :
launchctl bootout gui/$(id -u)/mcp-docs.refresh 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/mcp-docs.refresh.plist
launchctl kickstart -k gui/$(id -u)/mcp-docs.refresh # déclenche immédiatement pour testerVérifier :
launchctl print gui/$(id -u)/mcp-docs.refresh | head
tail -f /tmp/mcp-docs-refresh.err.logDésactiver :
launchctl bootout gui/$(id -u)/mcp-docs.refresh
rm ~/Library/LaunchAgents/mcp-docs.refresh.plistSi vous préférez ne pas installer Playwright :
- Se connecter à https://docs.numerique.gouv.fr (ou votre instance) via ProConnect
- Ouvrir les DevTools du navigateur (F12) → onglet Application → Cookies
- Copier la valeur du cookie
docs_sessionidet la placer dansDOCS_SESSION_COOKIE
⚠️ Ce cookie a une durée de vie limitée. Voir Sécurité pour la rotation.
Précédence : DOCS_SESSION_COOKIE (env) > DOCS_SESSION_FILE > chemin par défaut.
Le mode oidc est prévu pour la migration vers /external_api/v1.0/ avec Bearer token ProConnect. Pas encore opérationnel en production.
Config à ajouter dans ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) :
{
"mcpServers": {
"docs": {
"command": "/ABSOLUTE/PATH/TO/uv",
"args": ["run", "--directory", "/ABSOLUTE/PATH/TO/mcp-docs", "mcp-docs"],
"env": {
"DOCS_BASE_URL": "https://docs.numerique.gouv.fr",
"DOCS_AUTH_MODE": "session",
"UV_PROJECT_ENVIRONMENT": "/tmp/venv-mcp-docs",
"UV_LINK_MODE": "copy"
}
}
}
}Pourquoi pas "command": "uv" : Claude Desktop est une GUI app macOS, elle n'hérite pas du PATH du shell (contrairement à Claude Code lancé depuis un terminal). uv doit être référencé par chemin absolu. Récupère-le via which uv (souvent /opt/homebrew/bin/uv, /Users/<toi>/.local/bin/uv, ou /usr/local/bin/uv).
Pourquoi les variables UV : si le repo est dans iCloud Drive (~/Dev en symlink), un .venv local peut partir en deadlock sur hardlinks. UV_PROJECT_ENVIRONMENT=/tmp/venv-mcp-docs déplace le venv hors iCloud, UV_LINK_MODE=copy évite les hardlinks. Si ton repo n'est pas dans iCloud, ces deux variables peuvent être retirées.
Pas de DOCS_SESSION_COOKIE : le cookie est lu depuis ~/.local/state/mcp-docs/session.json, alimenté par mcp-docs-refresh-session (voir Configuration). Si tu préfères malgré tout l'injecter en env, ajoute "DOCS_SESSION_COOKIE": "..." (mais tu perds la rotation automatique et le cookie finit écrit en clair dans ce fichier de config).
Merge propre si tu as déjà d'autres MCP servers configurés (préserve les autres entrées de mcpServers) :
CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
mkdir -p "$(dirname "$CONFIG")"
[ -f "$CONFIG" ] || echo '{}' > "$CONFIG"
jq --arg uv "$(which uv)" --arg dir "$(pwd)" '.mcpServers.docs = {
command: $uv,
args: ["run", "--directory", $dir, "mcp-docs"],
env: {
DOCS_BASE_URL: "https://docs.numerique.gouv.fr",
DOCS_AUTH_MODE: "session",
UV_PROJECT_ENVIRONMENT: "/tmp/venv-mcp-docs",
UV_LINK_MODE: "copy"
}
}' "$CONFIG" > "$CONFIG.new" && mv "$CONFIG.new" "$CONFIG"
python3 -m json.tool "$CONFIG" > /dev/null && echo "JSON OK"Puis redémarrer Claude Desktop proprement (pas seulement fermer la fenêtre) :
osascript -e 'quit app "Claude"' && sleep 2 && open -a ClaudeClaude Desktop expose alors les 25 tools MCP. En cas d'échec : tail -n 50 ~/Library/Logs/Claude/mcp-server-docs.log.
| Tool | Description |
|---|---|
docs_list_documents |
Lister les documents (paginé, trié par date de mise à jour) |
docs_get_document_content |
Récupérer le contenu d'un document (markdown/html/json) |
docs_search_documents |
Rechercher par titre ou contenu |
docs_list_children |
Lister les sous-documents d'un document parent |
docs_get_me |
Infos de l'utilisateur connecté |
docs_list_accesses |
Lister les utilisateurs ayant accès à un document |
docs_list_invitations |
Lister les invitations en attente sur un document |
| Tool | Description |
|---|---|
docs_create_document |
Créer un document depuis du markdown |
docs_update_document_title |
Renommer un document |
docs_update_document_content |
Remplacer le contenu d'un document avec du markdown (formatage préservé) |
docs_grant_access |
Donner un accès à un utilisateur (par user_id) |
docs_create_invitation |
Inviter un utilisateur par email |
| Tool | Description |
|---|---|
docs_delete_document |
Supprimer (corbeille) un document |
docs_update_access |
Modifier le rôle d'un accès existant |
docs_revoke_access |
Retirer un accès |
| Tool | Description |
|---|---|
docs_ai_transform |
Transformer du texte : correct, rephrase, summarize, beautify, emojify, prompt |
docs_ai_translate |
Traduire du texte (code ISO : en, fr, es, de, …) |
| Tool | Description |
|---|---|
docs_update_link_configuration |
Configurer le partage par lien : restricted / authenticated / public + rôle |
| Tool | Description |
|---|---|
docs_list_favorites |
Lister les documents favoris |
docs_add_favorite |
Ajouter un document aux favoris |
docs_remove_favorite |
Retirer un document des favoris |
| Tool | Description |
|---|---|
docs_move_document |
Déplacer dans la hiérarchie (first-child, last-child, left, right, siblings) |
docs_duplicate_document |
Dupliquer (avec ou sans sous-documents, avec ou sans permissions) |
docs_list_trashbin |
Lister les documents supprimés |
docs_restore_document |
Restaurer un document depuis la corbeille |
| URI | Description |
|---|---|
docs://user |
Profil de l'utilisateur authentifié |
docs://documents |
10 documents les plus récemment mis à jour |
« Crée un document "Note de synthèse" avec le contenu markdown suivant : [...]. Demande à l'IA un résumé en une phrase, puis remplace le contenu par ce résumé. »
Outils : docs_create_document → docs_get_document_content → docs_ai_transform (action=summarize) → docs_update_document_content.
« Crée un doc avec un message en français, traduis-le en anglais, crée un nouveau doc avec la traduction, et rends-le public en lecture. »
Outils : docs_create_document → docs_ai_translate → docs_create_document → docs_update_link_configuration (public, reader).
« Crée un projet "ABC" et ses 3 phases. Déplace chaque phase en enfant du projet. »
Outils : docs_create_document (×4) → docs_move_document (×3, last-child).
« Supprime ce doc, vérifie qu'il est dans la corbeille, puis restaure-le. »
Outils : docs_delete_document → docs_list_trashbin → docs_restore_document.
Plus de scénarios : voir tasks/ dans le repo.
Pour vérifier rapidement que la config est correcte avant de lancer le serveur MCP :
uv run mcp-docs --config-checkSortie attendue :
Config OK: base_url=https://docs.numerique.gouv.fr, auth_mode=session
Auth OK: connected as <Votre Nom> (<votre.email@gouv.fr>)
En cas d'erreur (cookie expiré, URL incorrecte), le code de sortie est 1 et le message d'erreur est imprimé sur stderr.
- Aucun secret dans le code. Les cookies et tokens sont chargés depuis l'environnement
- Validation stricte de toutes les entrées (Pydantic + whitelists pour les rôles/positions)
- Messages d'erreur génériques sans détails internes
- Les tokens ne sont jamais loggés
- Scan de secrets via
gitleaksen pre-commit
Le cookie docs_sessionid a une durée de vie limitée (expiration Django / ProConnect). En mode fichier — recommandé — la rotation est automatique via mcp-docs-refresh-session (fast-path s'il est encore valide, sinon Playwright extrait un nouveau cookie du navigateur). Avec le plist launchd ci-dessus, c'est totalement transparent tant que la session ProConnect IdP tient.
En cas de compromission avérée d'un cookie :
- Invalider côté serveur : se déconnecter de https://docs.numerique.gouv.fr depuis le navigateur où le cookie est vivant (logout UI → destruction de la session Django correspondante).
- Nuker le profil Playwright pour forcer une re-saisie ProConnect :
rm -rf ~/.local/share/mcp-docs/browser-profile rm ~/.local/state/mcp-docs/session.json
- Re-générer :
uv run mcp-docs-refresh-session(login ProConnect interactif, nouveau cookie écrit). - Nettoyer : retirer tout
DOCS_SESSION_COOKIErésiduel de~/.claude.json,.mcp.json,claude_desktop_config.json.
Pour le mode OIDC : révoquer le token auprès du fournisseur d'identité et en regénérer un.
Un pre-commit hook gitleaks empêche de committer des secrets accidentellement :
uv run pre-commit installEn cas d'incident de sécurité (fuite de secret, accès non autorisé) :
- Révoquer immédiatement les accès compromis (étapes ci-dessus)
- Contacter le CERT ministériel (DINUM)
- Documenter l'incident (date, périmètre, actions prises)
uv run pytest # Lancer tous les tests
uv run pytest --cov # Tests + rapport de couverture
uv run ruff check . # Lint
uv run pyright # Type check
uv run pre-commit run --all # Hooks pre-commit (gitleaks, etc.)Après modification du code, pour que Claude Desktop (ou une autre session Claude Code) prenne en compte les changements :
rm -rf .venv && uv venv && uv sync # si besoin, recrée le venv
# Puis redémarrer le client MCPmcp-docs/
├── src/mcp_docs/
│ ├── app.py # FastMCP + lifespan
│ ├── client.py # Client HTTP asynchrone (httpx)
│ ├── config.py # Config Pydantic Settings
│ ├── exceptions.py # Hiérarchie d'exceptions typées
│ ├── models.py # Modèles Pydantic des réponses API
│ ├── resources.py # MCP resources (docs://user, docs://documents)
│ ├── server.py # Entry point + CLI --config-check
│ ├── tools.py # Tools CRUD principaux
│ ├── tools_access.py # Tools accès + invitations
│ ├── tools_ai.py # Tools IA (transform + translate)
│ ├── tools_sharing.py # Tools link config + favoris
│ └── tools_organize.py # Tools move/duplicate/trashbin
└── tests/ # 160+ tests (respx pour les mocks HTTP)
- Client HTTP :
httpx.AsyncClientavec retrytenacity(exponential backoff sur 429/502/503/timeouts) et rate limiting par sémaphore - Auth duale : mode
session(cookie Django + CSRF token généré client-side) ouoidc(Bearer token) - Conversion markdown → Yjs : pour mettre à jour le contenu, un document temporaire est créé et son Yjs pré-converti est transplanté sur le doc cible (contournement élégant pour supporter le markdown formaté sans client Yjs)
- Erreurs typées :
DocsAPIError+ sous-classes (DocsAuthError,DocsNotFoundError, …) avec mapping HTTP auto
- Documentation Docs (La Suite numérique)
- Repo upstream Docs
- Spec MCP (Model Context Protocol)
- SDK Python MCP
MIT