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