Skip to main content
Image de couverture pour Cartog : exposer un graph de code à un agent LLM via Model Context Protocol

Cartog : exposer un graph de code à un agent LLM via Model Context Protocol

TL;DR
Un graph de code n’est utile pour un agent que s’il sait comment l’interroger. Cartog expose 16 tools via Model Context Protocol (stdio JSON-RPC) (ex: cartog_search, cartog_outline, cartog_refs, cartog_callees, etc ). La description de chaque tool permet à l’agent de bien choisir en fonction du contexte (« use when… not for… ») . SQLite tourne en WAL pour permettre les lectures et écritures en parallèle pour plus de performance.
Pour tester et en savoir plus : documentation MCP · Cartog code source.

Les quatre articles précédents construisent un graph dirigé : symboles extraits par tree-sitter, embeddings sémantiques pour la recherche par sens, arêtes résolues plus efficacement avec LSP, réindexation pertinentes grâce au Merkle tree.

Côté CLI, ça marche : cartog impact validate --depth 3 répond en quelques millisecondes.

Mais en pratique, l’utilisateur ne tape pas cartog impact lui-même.
Il discute avec un agent (Claude Code, Cursor, etc) qui doit décider s’il faut interroger le graph, comment et dans quel ordre.

L’utilisateur demande « Quel impact de renommer authenticate ? ». Comment l’agent peut prendre la bonne décision et appeler Cartog ?

Deux questions à résoudre côté serveur :

  1. Transport : comment exposer ces fonctions de notre CLI Rust à un process LLM tournant ailleurs. Cela, sans HTTP custom et en évitant une intégration éditeur par éditeur.
  2. Lisibilité par un LLM : comment décrire chaque tool pour que l’agent choisisse le bon et ce, sans en coder la logique de routage en “dur”.

Le Model Context Protocol est une spécification ouverte publiée par Anthropic fin 2024, puis adoptée par d’autres éditeurs d’agents.

L’idée : un standard pour brancher des outils externes sur un agent (tout comme le LSP standardise le branchement des language servers sur un IDE).

En une phrase (l’idée n’est pas d’en faire un cours ici)
Un serveur MCP communique en JSON-RPC sur stdio, le client lance le binaire et échange en deux temps : une négociation (le client demande la liste des tools et leurs schémas) et des appels (l’agent invoque un tool et reçoit un résultat structuré).

sequenceDiagram
    participant A as Client / Agent
    participant S as Serveur MCP

    Note over A,S: 1. Négociation (une fois, au démarrage)
    A->>S: initialize
    S-->>A: capabilities
    A->>S: tools/list
    S-->>A: [ {name, description, inputSchema}, ... ]

    Note over A,S: 2. Appels (à chaque tour de l'agent)
    A->>S: tools/call {name, arguments}
    S-->>A: result (JSON structuré)

Pour une introduction en français au protocole lui-même (au-delà de Cartog), voir les présentations de Bertrand Nau et Horacio Gonzalez.

Cartog implémente le protocole ainsi le protocole MCP et n’importe quel client MCP en profite, sans intégration dédiée par éditeur.

La doc cartog recense aujourd’hui douze clients compatibles à date (Claude Code, Cursor, VS Code, Claude Desktop, Codex CLI, Gemini CLI, OpenCode, Windsurf, Zed, Antigravity, Kiro, Hermes Agent). La liste s’allonge au gré du protocole et nouveaux éditreurs. Rien besoin de changer côté Cartog.

Appliqué à Cartog, ce schéma générique donne un flux concret :

  • l’agent traduit une question utilisateur en tools/call
  • le serveur interroge SQLite et renvoie du JSON.
sequenceDiagram
    participant U as Utilisateur
    participant A as Agent (ex. Claude Code)
    participant M as cartog serve (MCP)
    participant DB as SQLite (.cartog/)

    U->>A: "qui appelle authenticate ?"
    A->>M: tools/call cartog_refs {name: "authenticate"}
    M->>DB: SELECT FROM edges WHERE target = ...
    DB-->>M: 12 lignes
    M-->>A: JSON {edges: [...]}
    A->>U: réponse + citations file:line

Côté implémentation, Cartog s’appuie sur la crate rmcp (l’implémentation Rust officielle de MCP).
Le binaire expose le mode serveur via cartog serve, lancé par l’éditeur au démarrage de session.

stdio uniquement ? MCP prévoit aussi un transport HTTP.
Pourquoi ne pas l’utiliser ?

Parce qu’on n’en a pas besoin pour l’instant. MCP définit deux transports :

  • stdio (le serveur tourne en local, lancé par le client)
  • HTTP (le serveur tourne ailleurs, joignable par le réseau).

Cartog ne fait que stdio et c’est un choix cohérent avec le reste de la série :

  • Local par défaut. Le serveur est un process enfant de l’éditeur, sur la même machine. Le code, le graph SQLite, les requêtes : rien ne transite par le réseau. C’est la même garantie que pour les embeddings calculés en local.
  • Zéro configuration réseau. Pas de port à ouvrir, pas de TLS à gérer, pas d’authentification à brancher. Le client lance le binaire, on parle sur stdin/stdout, fin de l’histoire.
  • Le bon périmètre. L’index vit dans le dépôt, sur le poste du développeur. Servir ce graph par HTTP supposerait un cas d’usage distant (graph partagé en CI, équipe) qui relève d’une autre brique — le sync S3 optionnel, inerte tant qu’on ne le configure pas.

Le transport HTTP n’est pas exclu pour autant. Le jour où un usage distant le justifie, il pourra être ajouté. MCP le permet sans rien changer aux 16 tools ni à leurs descriptions.

Mais tant que Cartog est un outil local, ajouter une surface réseau ajoute aussi plus de complexité.

Le serveur expose un sous-ensemble structuré de la CLI. Treize sont en lecture seule ; trois écrivent (cartog_index, cartog_rag_index reconstruisent l’index, cartog_update prépare une mise à jour différée du binaire) :

ToolRôleQuand l’agent l’appelle
cartog_searchRecherche par nom exact/partielAvant refs/callees/impact
cartog_rag_searchRecherche sémantique + BM25Entrée par défaut pour toute découverte
cartog_contextBundle one-shot : symboles + corps pertinents pour une tâchePremier appel pour cadrer une tâche en un seul aller-retour
cartog_outlineStructure d’un fichierÀ la place de Read quand on veut juste la liste des symboles
cartog_refsQui appelle/importe/hérite de X« who calls X ? »
cartog_calleesCe que X appelle« what does X call ? »
cartog_impactBlast radius transitifAvant un rename/delete/move
cartog_tracePlus court chemin d’appel entre deux symboles, corps inline« comment X arrive-t-il à appeler Y ? »
cartog_hierarchyArbre d’héritagePour class / trait
cartog_depsImports d’un fichierDépendances directes
cartog_changesSymboles touchés par les N derniers commits + working treeTriage de régression
cartog_mapFile tree + top symbolsFirst call dans un repo inconnu
cartog_statsSanté et informations de l’indexVérifier que c’est indexé
cartog_index(Re)construire l’index (écrit)1 fois par session, après gros refactor
cartog_rag_indexConstruire l’index vectoriel (écrit)Une fois par projet, optionnel
cartog_updatePréparer une mise à jour différée du binaire (écrit l’état machine, pas l’index)Sur demande explicite de mise à jour (utile si le plugin n’a pas mis à jour le binaire)

Cette liste est moins complète que les arguments disponible en CLI.
Les commandes cartog init, cartog ide, cartog doctor, cartog config restent “CLI-only” car ils écrivent des fichiers utilisateur ou diagnostiquent l’environnement (ce ne sont pas des opérations qu’un agent doit déclencher tout seul).

Le code Rust qui déclare un tool tient en deux lignes, c’est la description qui fait tout le travail.

Le champ description de chaque tool est lu par le LLM côté client pour décider quand appeler le tool. C’est l’interface utilisateur d’un agent.

Exemple concret, la description de cartog_impact :

Assess blast radius before refactoring. Shows everything that transitively depends on a symbol up to N hops.
Use when asked ‘what breaks if I change X?’, ‘is it safe to rename/delete X?’, or before any rename/extract/move/delete refactoring.
Not for: direct callers only (use cartog_refs), or what the symbol calls (use cartog_callees).
Returns: array of {edge, depth} where depth=1 is direct, depth=2 is one hop away, etc.

Quatre éléments systématiques :

  1. Description : ce que fait le tool.
  2. Use when : phrases probables de l’utilisateur (« what breaks if… », « is it safe to… »).
  3. Not for : les tools concurrents et quand préférer l’autre. C’est le routage négatif — sans ça, l’agent choisit au hasard entre refs, callees et impact.
  4. Returns : le schéma de sortie, pour que l’agent sache parser sans deviner.

Ce pattern est répété sur tous les tools. La description de cartog_rag_search mentionne explicitement « DEFAULT entry point for finding code » — c’est ce qui pousse l’agent à l’utiliser avant Grep.

Celle de cartog_search cadre l’inverse : « use ONLY to get a precise symbol name before calling cartog_refs, cartog_callees, or cartog_impact ».

flowchart TD
    Q["Question utilisateur"] --> D{"Type de question ?"}
    D -->|"concept / langue naturelle"| RAG["cartog_rag_search"]
    D -->|"nom de symbole connu"| S["cartog_search"]
    D -->|"qui utilise X ?"| R["cartog_refs"]
    D -->|"que fait X ?"| C["cartog_callees"]
    D -->|"safe de changer X ?"| I["cartog_impact"]
    S -->|"name exact"| R
    S --> C
    S --> I

    style RAG fill:#e8f5e9,stroke:#4caf50
    style I fill:#fff3e0,stroke:#ff9800

Aucune logique de routage n’est codée dans Cartog.
Tout vit dans le texte des descriptions qui est lu par le LLM à chaque tour.

Un cartog_impact authenticate --depth 5 sur un projet vaste peut remonter des centaines d’arêtes et vite dépasser ce qu’une fenêtre de contexte peut absorber utilement.

Si on renvoie toute la réponse brute, l’agent perd sa fenêtre de contexte en une seule réponse.

Cartog impose donc un plafond d’octets par réponse côté serveur (64 Ko par défaut, surchargeable via CARTOG_MCP_MAX_BYTES). Quand la sortie dépasse le seuil, le serveur tronque en préservant les caractères UTF-8 et ajoute un avis de troncature :

Response truncated: 14 832 bytes omitted to stay under the 65 536-byte cap.
Re-run with a more specific symbol name, or filter by --kind.

L’agent reçoit un résultat exploitable et sait quelle action prendre pour affiner. L’indice est propre à chaque tool : cartog_impact suggère de réduire la profondeur, cartog_refs de filtrer par --kind.

Deux choix de design derrière :

  • Tronquer côté serveur, pas côté agent : l’agent ne sait pas mesurer ses octets, le serveur si.
  • Indication d’action, pas juste un compteur : l’avis dit quoi faire, pas seulement combien il manque.

À la différence de --tokens (budget déclaré par l’appelant, pour la sortie texte humaine), le plafond MCP est fixé côté serveur, toujours actif, et s’applique au JSON renvoyé à l’agent.

Subtilité opérationnelle : pendant qu’un cartog serve est vivant côté éditeur, l’utilisateur peut très bien lancer cartog index . à la main, ou laisser tourner cartog watch qui ré-indexe en continu.
Il peut donc y avoir plusieurs writers sur le même fichier SQLite.

Trois pragmas SQLite sont activés au démarrage :

  • WAL (write-ahead log) : lectures et écritures peuvent coexister sans bloquer.
  • busy_timeout : un writer en attente patiente N ms au lieu de retourner SQLITE_BUSY immédiatement.
  • synchronous=NORMAL : compromis durabilité/perf adapté à un index reconstructible.

Résultat : l’agent peut continuer à interroger pendant qu’une indexation tourne, sans crash ni corruption.
Les nouveaux symboles deviennent visibles au prochain appel de tool, pas de redémarrage du serveur nécessaire.

Une question « est-ce safe de renommer authenticate ? » devient, côté agent :

  1. cartog_search { query: "authenticate" } → 1 symbole exact, id=42.
  2. cartog_impact { name: "authenticate", depth: 3 } → 18 dépendants transitifs (tronqué à 50 si dépassement, avec hint).
  3. Lecture ciblée des 3-4 call-sites les plus critiques via Read avec offset/limit.

Trois tool calls, aucun grep -r, aucun cat sur un fichier entier. La fenêtre de contexte sert au raisonnement, pas à digérer 4000 lignes.

La réponse finale de l’agent, après ces trois appels :

Oui, renommer `authenticate` est faisable, mais 3 points d'attention :

- middleware/auth.ts:42 — appel direct, à renommer en même temps.
- tests/auth.test.ts:18 — mock du nom exact en string littérale ('authenticate'),
  à mettre à jour manuellement.
- UserController.login() → SessionManager.start() → authenticate :
  chaîne indirecte à depth=2, couverte si on renomme via LSP.

Les 15 autres dépendants sont des imports du module `auth` —
ils ne référencent pas le nom de la fonction directement.

Plutôt que d’éditer les fichiers de config à la main, cartog ide les génère automatiquement pour les clients MCP supportés.

Lancé dans un terminal, il affiche un sélecteur interactif des clients détectés.

cartog ide

Sortie interactive :

? step 1 — which clients to configure? (space to toggle, enter to confirm) ›
✔ Claude Code      project + user available      will create
✔ Cursor           project                       will create
✔ VS Code          project + user available      will create
✔ Claude Desktop   user                          present, will merge
✔ Windsurf         user                          present, will merge
✔ OpenCode         user                          present, will merge
⬚ Zed              user                          not installed
⬚ Codex CLI        user                          not installed
✔ Gemini CLI       user                          present, will merge
⬚ Antigravity      user                          not installed
✔ Kiro             project + user available      will create
⬚ Hermes Agent     user                          not installed

Pour prévisualiser sans écrire :

cartog ide --dry-run
+ claude-code (project, .mcp.json): would create
  {
    "mcpServers": {
      "cartog": {
        "command": "cartog",
        "args": ["serve", "--watch"]
      }
    }
  }

Pour Claude Code uniquement (scope projet, .mcp.json) :

cartog ide --client claude-code

Le résultat écrit dans .mcp.json :

{
  "mcpServers": {
    "cartog": {
      "command": "cartog",
      "args": ["serve", "--watch"]
    }
  }
}

Avec --watch, le serveur surveille les fichiers et ré-indexe à chaud : les nouveaux symboles sont visibles au prochain appel de tool, sans redémarrer le serveur. Sans --watch, l’index est figé jusqu’au prochain cartog index manuel.

Chaque client a son propre format de fichier et sa propre clé JSON — cartog ide gère les différences, les fusions idempotentes, et préserve les autres serveurs MCP déjà présents.

Pour la liste complète des clients et les snippets manuels : documentation MCP.

Côté serveur, la mécanique est en place : transport stdio, 16 tools, descriptions routables, plafond d’octets, concurrence SQLite.

L’agent peut interroger le graph sans grep, sans cat, sans perdre sa fenêtre de contexte sur un fichier entier.

MCP inverse le flux : c’est l’agent qui interroge le graph au bon moment, sans que l’utilisateur n’ait à connaître les commandes. cartog ide réduit le bootstrap à une commande, et l’idempotence des configs permet de versionner .mcp.json dans le dépôt pour toute l’équipe.