Serveur MCP
Catnip fournit un serveur MCP (Model Context Protocol) qui expose le pipeline du langage -- parsing, inspection, évaluation, debugging -- via un protocole structuré consommable par des agents.
Le serveur est implémenté en Rust pur (catnip_mcp/), utilise PurePipeline (pas de runtime Python) et communique via
stdio. SDK : rmcp.
Intégration agent
Le MCP permet à un agent de :
- parser du code à plusieurs niveaux, du tree-sitter brut (niveau 0) jusqu'à l'IR exécutable post-analyse sémantique (niveau 2) ;
- inspecter les représentations internes : AST, IR, opcodes, variables en scope ;
- piloter une session de debug avec breakpoints, stepping (into/over/out) et inspection de l'état ;
- évaluer des expressions dans le contexte courant d'une pause.
Tous les échanges passent par des appels MCP structurés (JSON in, JSON out), sans état implicite côté agent. Le serveur
spawne un thread VM par session de debug et communique via canaux mpsc -- l'agent n'a pas à gérer de concurrence.
Installation
{
"mcpServers": {
"catnip": {
"command": "/path/to/catnip/.venv/bin/catnip-mcp"
}
}
}
Build : make install-bins. Un cargo build -p catnip_mcp seul suffit à produire le binaire, mais pas à lui donner sa
bibliothèque standard : le serveur porte le moteur pur, qui charge io, sys et http comme plugins natifs depuis le
lib/ de son répertoire. Sans eux, import('io') répond « module not found ». C'est make install-mcp-bin — tirée par
install-bins — qui les construit et les dépose.
Tools
parse_catnip
Parse du code Catnip et retourne la représentation structurée.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
code |
string | requis | Code source Catnip |
level |
integer (0-2) | 1 | Niveau de parsing |
Niveaux de parsing :
- 0 : Parse tree brut (sortie texte tree-sitter)
- 1 : IR après transformation (JSON structuré)
- 2 : IR exécutable après analyse sémantique (JSON structuré)
{"ir": ["…"], "level": 1}
{"parse_tree": "…", "level": 0}
eval_catnip
Évalue du code et retourne le résultat, plus les deux flux que le programme a écrits.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
code |
string | requis | Code source |
context |
object | {} |
Variables initiales (JSON, supporte nesting) |
stdin |
string | vide | Ce que lit io.input(), ligne par ligne |
{"result_repr": "42", "result_type": "int", "stdout": "", "stderr": ""}
Ce que le programme écrit sur les descripteurs 1 et 2 est détourné pendant l'exécution : les deux reviennent dans la
réponse, en stdout et stderr, au lieu de partir dans le flux de protocole et dans les journaux du client. Sans
stdin, le descripteur 0 est vide et io.input() lève EOFError.
Le protocole lui-même n'emprunte aucun des trois : le serveur duplique ses descripteurs au démarrage, sur des numéros que le programme évalué ne nomme pas. Sinon le lecteur du transport, qui tourne sur son propre fil, prendrait l'occasion d'une lecture pendant l'évaluation pour consommer ce qui lui est destiné. S'il n'y parvient pas, il refuse de démarrer plutôt que de servir sur des descripteurs partagés. C'est aussi pourquoi le serveur se construit pour Linux et macOS : tout ce qui précède est de l'arithmétique de descripteurs POSIX.
Ce qu'une évaluation ne lit pas ne resservira pas à la suivante : io.input() lit son descripteur caractère par
caractère, sans tampon qui survivrait à l'appel. Donner "un\ndeux\n" à un programme qui ne lit qu'une ligne perd
"deux", il n'attend pas l'appel d'après.
Ce que l'évaluation accorde
Il n'y a pas de bac à sable. Le code évalué s'exécute avec les privilèges du processus serveur et dispose de toute la bibliothèque standard, ce qui lui donne, mesuré :
| Ce que le code évalué peut | Par quoi |
|---|---|
| lire l'environnement du serveur | sys.environ, ou io.open('/proc/self/environ') |
| lire un fichier que le processus peut lire | io.open(chemin) |
| écrire un fichier que le processus peut écrire | io.open(chemin, 'w') |
| terminer le processus serveur — il ne peut pas | sys.exit revient en erreur Exit |
Restreindre sys ne changerait rien : l'environnement se relit par io.open, et une garde qui se contourne en une
ligne n'en est pas une. Ce qui borne réellement l'exposition, c'est le transport : le serveur parle en stdio et n'écoute
sur aucun port, donc l'appelant est toujours un processus local que tu as lancé toi-même.
Deux conséquences pratiques. Ne mets pas dans l'environnement de ce processus un secret que tu ne donnerais pas au code qu'on lui envoie. Et souviens-toi que l'appelant est en général un agent, qui agit sur du contenu lu ailleurs : « l'appelant, c'est moi » tient jusqu'à ce que l'agent lise une page qui lui dicte quoi exécuter.
Un évaluateur qui refuserait d'évaluer serait plus sûr et strictement inutile. La question n'est pas de savoir s'il obéit, mais à qui.
check_syntax
Valide la syntaxe sans exécuter.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
code |
string | requis | Code source |
{"valid": true, "message": "Syntax is valid"}
{"valid": false, "error": "…"}
format_code
Formate du code avec style configurable.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
code |
string | requis | Code source |
indent_size |
integer | 4 | Taille d'indentation |
line_length |
integer | 120 | Longueur max de ligne |
{"formatted_code": "…"}
Un code dont la syntaxe est refusée n'est pas formaté : la réponse est marquée en erreur et porte la position fautive, plutôt qu'un source réécrit depuis un arbre incomplet.
{"error": "Unexpected token 'sé' at line 1, column 1"}
Tools de debug
Le debugger MCP fonctionne en sessions. L'agent ouvre une session, reçoit un session_id, puis pilote l'exécution via
ce handle.
Flux typique
debug_start(code, breakpoints=[5, 12])
→ status: "paused", line: 5, locals: {…}
debug_inspect(session_id)
→ locals: {x: "42", items: "[1, 2, 3]"}
debug_step(session_id, mode="over")
→ status: "paused", line: 6, locals: {…}
debug_eval(session_id, expr="x + 1")
→ result: "43"
debug_continue(session_id)
→ status: "paused", line: 12, locals: {…}
debug_continue(session_id)
→ status: "finished", result: "done"
debug_start
Démarre une session de debug. Retourne l'état à la première pause ou à la fin de l'exécution.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
code |
string | requis | Code source |
breakpoints |
array[int] | [] |
Lignes de breakpoint (1-indexed) |
debug_continue
Continue l'exécution jusqu'au prochain breakpoint ou la fin.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
session_id |
string | requis | ID de session |
debug_step
Avance d'un pas dans l'exécution.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
session_id |
string | requis | ID de session |
mode |
"into" | "over" | "out" |
"into" |
Mode de stepping |
debug_inspect
Inspecte les variables locales au point de pause courant. N'avance pas l'exécution.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
session_id |
string | requis | ID de session |
debug_eval
Évalue une expression dans le scope de la pause courante. L'évaluation se fait dans un contexte isolé : les effets de bord ne se propagent pas à la session.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
session_id |
string | requis | ID de session |
expr |
string | requis | Expression à évaluer |
debug_breakpoint
Ajoute ou retire un breakpoint pendant l'exécution.
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
session_id |
string | requis | ID de session |
line |
integer | requis | Numéro de ligne (1-indexed) |
action |
"add" | "remove" |
"add" |
Action |
Réponses debug
Toutes les commandes qui avancent l'exécution (debug_start, debug_continue, debug_step) retournent un payload
uniforme :
Pause :
{"session_id": "dbg-1", "status": "paused", "line": 5, "col": 0, "locals": {"x": "42"}, "snippet": "x = x + 1"}
Fin :
{"session_id": "dbg-1", "status": "finished", "result": "done"}
Erreur :
{"session_id": "dbg-1", "status": "error", "error": "NameError: 'y' is not defined"}
Timeout (10s sans événement) :
{"session_id": "dbg-1", "status": "timeout"}
La session est nettoyée automatiquement à la fin de l'exécution ou en cas d'erreur. Un timeout ne détruit pas la session : l'agent peut réessayer.
Ressources
Les ressources exposent de la documentation et des exemples en lecture seule.
| URI | Type | Description |
|---|---|---|
catnip://examples/{topic} |
JSON | Exemples par thème (basics, functions, broadcast, cfg, …) |
catnip://codex/{category}/{module} |
text | Exemples d'intégration Python (web, data-analytics, …) |
catnip://docs/{section} |
JSON | Liste des topics disponibles dans une section |
catnip://docs/{section}/{topic} |
markdown | Page de documentation (sections : lang, tuto, user) |
La documentation est servie directement depuis les fichiers
docs/. Ce serveur ne génère rien, il transmet. Un proxy sans opinion