mcp_cobol
by nba67000
README.md
# mcp_cobol — comprendre une base COBOL legacy avec un LLM
Un serveur **MCP** (Model Context Protocol) qui donne a un LLM les moyens
d'explorer une base COBOL mainframe qu'il n'a jamais vue : graphe d'appels,
usage des copybooks, acces aux donnees, analyse d'impact.
Le LLM de la demo est **Mistral** (La Plateforme). Le serveur, lui, est
agnostique du modele : n'importe quel client MCP peut le consommer.
> Base de demonstration : [AWS CardDemo](https://github.com/aws-samples/aws-mainframe-modernization-carddemo),
> application CICS / DB2 / VSAM / IMS de gestion de cartes de credit publiee par
> AWS. 44 programmes, 62 copybooks, 22 893 lignes. Aucun code client n'est
> utilise.
**Pour faire la demonstration : [DEMO.md](DEMO.md)** — deroule minute par
minute, commandes dans l'ordre, et plan B si le reseau lache.
---
## Le probleme, en une commande
Sur une base COBOL reelle, `grep` ne suffit pas a savoir qui appelle qui :
```bash
grep -rhoE "PROGRAM *\([A-Z0-9()-]+\)" vendor/carddemo/app --include=*.cbl | sort -u
```
*(Pour la rejouer : `bash setup.sh` d'abord — voir [Demarrage](#demarrage) —
c'est lui qui clone CardDemo dans `vendor/`.)*
```
PROGRAM (CCARD-NEXT-PROG)
PROGRAM (CDEMO-TO-PROGRAM)
PROGRAM (LIT-ADDTPGM)
PROGRAM (LIT-MENUPGM)
PROGRAM(CDEMO-ADMIN-OPT-PGMNAME(WS-OPTION))
PROGRAM(CDEMO-MENU-OPT-PGMNAME(WS-OPTION))
PROGRAM(CDEMO-TO-PROGRAM)
PROGRAM(WS-PGM-AUTH-FRAUD)
```
Huit operandes pour tous les transferts CICS de l'application, et **aucun n'est
un nom de programme** : ce sont des variables, remplies quelques lignes plus
haut.
```cobol
MOVE LIT-MENUPGM TO CDEMO-TO-PROGRAM *> LIT-MENUPGM VALUE 'COMEN01C'
EXEC CICS XCTL PROGRAM(CDEMO-TO-PROGRAM) END-EXEC
```
Un LLM seul ne peut pas davantage repondre : la base ne tient pas dans son
contexte, et il ne l'a jamais vue. Lui donner des **outils** qui interrogent la
base — plutot qu'esperer qu'il la memorise — c'est ce que resout MCP.
## Demarrage
Prerequis : Python ≥ 3.10 et git.
```bash
bash setup.sh # venv + deps + clone CardDemo
.venv/Scripts/python.exe -m parser.build_index # construit data/index.json
```
*Sous Linux/macOS : `.venv/bin/python` au lieu de `.venv/Scripts/python.exe`.*
**Tout ce qui precede fonctionne sans cle API** — et la suite aussi :
```bash
.venv/Scripts/python.exe -m server.selftest # 32/32
.venv/Scripts/python.exe -m server.tools impact CVACT01Y
.venv/Scripts/python.exe -m demo.ask --simulate "Que fait le programme COACTVWC ?"
```
Seule la demo avec le LLM demande une cle
([console.mistral.ai](https://console.mistral.ai/api-keys)), a placer dans
`.env` (gitignore, la cle ne quitte jamais la machine) :
```ini
MISTRAL_API_KEY=...
```
```bash
.venv/Scripts/python.exe -m mistral.client # verifie config, modeles, appel reel
.venv/Scripts/python.exe -m demo.ask "Qu'est-ce qui casse si je modifie le copybook CVACT01Y ?"
```
### Les trois questions de demonstration
Validees de bout en bout avec `mistral-medium-latest` :
```bash
.venv/Scripts/python.exe -m demo.ask "Que fait le programme COACTVWC et quelles donnees manipule-t-il ?"
.venv/Scripts/python.exe -m demo.ask "Qu'est-ce qui casse si je modifie le copybook CVACT01Y ?"
.venv/Scripts/python.exe -m demo.ask "Montre-moi la chaine d'appels depuis l'ecran de connexion, et dis-moi quels programmes sont des points d'entree batch."
```
La troisieme se resout en **5 appels d'outils sur 6 tours**, avec une
auto-correction : le modele cherche d'abord « connexion » puis « login », obtient
zero resultat, liste les programmes CICS et identifie `COSGN00C` lui-meme.
## Architecture
```
vendor/carddemo parser/ data/index.json
(COBOL brut) ──▶ cobol_parser.py ──▶ faits extraits
build_index.py + index inverse
│
▼
demo/ask.py ◀── mistral/client.py ◀── server/mcp_server.py
(question NL) (function calling) (outils MCP)
```
Trois etages, volontairement decouples :
1. **Parsing** — lent, heuristique, sans reseau. Produit un JSON.
2. **Serveur MCP** — lit le JSON, expose 5 outils. Aucun `import` de Mistral.
3. **Client LLM** — le modele decide *quels* outils appeler et dans quel ordre.
Le serveur ne relit jamais le COBOL : chaque appel d'outil est quasi instantane,
ce qui rend la demo fluide en live.
Dans `server/`, la logique (`tools.py`) est separee de la plomberie du protocole
(`mcp_server.py`). On peut donc tester un outil en ligne de commande, et le jour
ou `mcp` cassera son API — il est en majeure 2.x — seul le fichier de plomberie
bougera.
## Les cinq outils
| Outil | Question a laquelle il repond |
|---|---|
| `list_programs` | Qu'est-ce qu'il y a dans cette base ? |
| `explain_program` | Que fait ce programme, avec quoi travaille-t-il ? |
| `call_graph` | Qui appelle qui ? |
| `who_uses_copybook` | Qui depend de cette structure de donnees ? |
| `impact_analysis` | Qu'est-ce qui casse si je modifie ca ? |
Chacun est interrogeable directement, sans serveur ni cle API :
```bash
.venv/Scripts/python.exe -m server.tools list --role batch
.venv/Scripts/python.exe -m server.tools explain COACTVWC
.venv/Scripts/python.exe -m server.tools graph COMEN01C --depth 2
.venv/Scripts/python.exe -m server.tools copybook CVACT01Y
.venv/Scripts/python.exe -m server.tools impact CVACT01Y
```
```
# COACTVWC — Accept and process Account View request
- Role : transactionnel-cics
- Source : app/cbl/COACTVWC.cbl (703 lignes de code, 35 paragraphes)
- Transaction CICS : CAVW
- Ecran BMS : CACTVWA (mapset COACTVW)
## Donnees manipulees
- VSAM ACCTDAT — lecture (READ)
- VSAM CUSTDAT — lecture (READ)
- VSAM CXACAIX — lecture (READ)
## Navigation
- Appele par : COMEN01C
- Appelle : COMEN01C via XCTL (l. 349) — probable
## Flot interne
Point d'entree : 0000-MAIN — 17 PERFORM, 9 GO TO, 1 gestionnaire CICS
358 1000-SEND-MAP THRU 1000-SEND-MAP-EXIT
362 2000-PROCESS-INPUTS THRU 2000-PROCESS-INPUTS-EXIT
369 9000-READ-ACCT THRU 9000-READ-ACCT-EXIT
## A savoir avant de conclure
- 2 copybooks absents du depot (DFHAID, DFHBMSCA) : dependances CICS
- Ecran designe par une variable non resolue (CCARD-NEXT-MAP) : valeur
transmise par l'appelant via la COMMAREA
- 3 paragraphes cible d'aucun PERFORM/GO TO/LABEL
```
Chaque outil termine par **« A savoir avant de conclure »** : ce qu'il ne sait
pas. C'est cette section qui empeche le LLM d'affirmer ce que l'analyse statique
n'etablit pas — et le modele la relaie effectivement dans ses reponses.
## Ce que le parseur extrait
| | |
|---|---|
| Programmes | 25 transactionnels CICS, 17 batch, 2 sous-programmes |
| Copybooks | 62 presents + 8 externes identifies (MQ, CICS systeme) |
| Paragraphes | 870, relies par 1 163 `PERFORM` et 185 `GO TO` |
| Graphe d'appels | 92 aretes, **0 non resolue** |
| Donnees | 8 fichiers VSAM, 2 tables DB2, 2 segments IMS |
| Ecrans | transaction CICS et map BMS resolues par programme |
### Resoudre les appels indirects
Le parseur remonte la chaine (clauses `VALUE`, `MOVE` successifs, tables
`OCCURS` declarees dans les copybooks) et **etiquette chaque arete** par sa
methode de resolution :
| Methode | Aretes | Confiance |
|---|---|---|
| nom litteral dans l'instruction | 64 | certain |
| clause `VALUE` / `MOVE` de litteral | 17 | certain |
| chaine de deux `MOVE` | 11 | probable |
| table de programmes d'un copybook | 3 | heuristique |
Une arete heuristique est **presentee comme telle**, avec sa provenance
(« table declaree dans COMEN02Y »). Un outil d'analyse d'impact qui affirme
sans nuancer est pire qu'inutile.
### Deterministe
Deux constructions de l'index dans des processus distincts produisent
exactement le meme contenu (hors horodatage), et un outil appele quatre fois
rend une sortie identique au caractere. Ce n'est pas gratuit : un parcours de
`set` rendait le chemin d'appel affiche dependant de la randomisation du hachage
de Python, donc variable d'une execution a l'autre. Un outil d'analyse dont la
sortie bouge sans que l'entree bouge interdit toute comparaison et ruine la
confiance.
## Le serveur MCP
```bash
.venv/Scripts/python.exe -m server.selftest # 32/32, sans cle API
.venv/Scripts/python.exe server/mcp_server.py # lance le serveur
```
Lance seul, le serveur n'affiche rien : il dialogue en JSON-RPC sur
stdin/stdout. L'autotest le verifie de deux facons — branche en memoire sur
l'objet serveur, puis lance comme un **vrai sous-processus**. Le second mode est
le seul qui detecte une ecriture parasite sur stdout, laquelle corrompt le
protocole de maniere tres difficile a diagnostiquer.
### Exemple : « qu'est-ce qui casse si je modifie le copybook CVACT01Y ? »
```
# Impact d'une modification de copybook CVACT01Y
23 programme(s) concerne(s) : 14 a recompiler, 9 a retester.
Repartition : 7 batch, 16 transactionnel-cics
## A recompiler — embarquent la modification
- CBACT01C (batch) — inclut CVACT01Y directement
- COACTVWC (transactionnel-cics) — inclut CVACT01Y directement
... 12 autres
## A retester — appellent un programme modifie
- COMEN01C (transactionnel-cics) — chaine : COMEN01C -> COBIL00C
- COPAUS1C (transactionnel-cics) — chaine : COPAUS1C -> COPAUS0C
... 7 autres
## Perimetre de recette
- Transactions CICS : CAUP, CAVW, CB00, CC00, CCDL, CCLI, CCUP, CM00,
CPVD, CPVS, CR00, CT00, CT01, CT02
- Fichiers VSAM : ACCTDAT, CARDAIX, CARDDAT, CCXREF, CUSTDAT, CXACAIX,
TRANSACT, USRSEC
- Segments IMS : PAUTDTL1, PAUTSUM0
```
La separation **a recompiler** / **a retester** n'est pas cosmetique : inclure
un copybook modifie impose une recompilation, appeler un programme modifie
n'impose qu'une recette. Ce sont deux charges de travail, souvent deux equipes.
### Brancher un autre client MCP
```json
{
"mcpServers": {
"cobol-legacy": {
"command": "/chemin/vers/mcp_cobol/.venv/Scripts/python.exe",
"args": ["/chemin/vers/mcp_cobol/server/mcp_server.py"]
}
}
}
```
## Mistral ne parle pas MCP : le pont
Il n'existe aucun lien direct entre les deux protocoles. `demo/ask.py` fait
trois traductions :
| | MCP | Mistral |
|---|---|---|
| Declaration | `{name, description, input_schema}` | `{"type":"function","function":{name, description, parameters}}` |
| Appel | `mcp.call_tool(nom, args)` | `tool_calls[].function.arguments` (JSON **texte**) |
| Resultat | blocs de contenu | message `{"role":"tool", "tool_call_id", "content"}` |
Puis on boucle : tant que le modele demande des outils, on execute et on
renvoie. Le `tool_call_id` est ce qui relie une reponse a sa question quand le
modele appelle plusieurs outils d'un seul coup.
Le serveur MCP tourne dans un **vrai sous-processus** et n'importe rien de
Mistral. Changer de fournisseur ne touche que `demo/ask.py` et
`mistral/client.py`.
### Souverainete
Le SDK expose trois endpoints : `api.mistral.ai`, **`api.eu.mistral.ai`**,
`api.us.mistral.ai`. Mettre `MISTRAL_SERVER=eu` dans `.env` route tout le trafic
par l'Union europeenne. De meme, passer du modele generaliste a Codestral est une
variable (`MISTRAL_MODEL`), pas une modification de code.
### Verifier le pont sans cle API
```bash
.venv/Scripts/python.exe -m demo.ask --simulate "Qu'est-ce qui casse si je modifie le copybook CVACT01Y ?"
```
```
[demo] 5 outils MCP disponibles : list_programs, explain_program, call_graph,
who_uses_copybook, impact_analysis
[tour 1] impact_analysis(target='CVACT01Y')
-> 2726 caracteres | # Impact d'une modification de copybook CVACT01Y
[demo] termine en 2 tour(s)
```
`--simulate` remplace le modele par une doublure scriptee et exerce la **meme**
boucle sur les **vrais** outils MCP, a travers le vrai sous-processus : les
trois traductions sont donc reellement testees. La doublure annonce
explicitement qu'aucun modele n'a repondu — elle ne peut pas etre confondue avec
une reponse du LLM.
## Depannage
**`CERTIFICATE_VERIFY_FAILED` au premier appel API.** Un antivirus ou une
appliance reseau inspecte le trafic HTTPS et reemet les certificats. L'autorite
est dans le magasin du systeme mais inconnue de `certifi`, que Python utilise
par defaut. `MISTRAL_USE_SYSTEM_CERTS=true` (defaut) fait utiliser le magasin de
l'OS via `truststore`. La verification TLS reste **active** — ne jamais
« corriger » cela en la desactivant : ce code transmet une cle API.
**`429 Rate limit exceeded` avec `x-ratelimit-limit-req-minute: 0`.** Ce n'est
pas une limitation passagere : le compte n'a aucun droit d'appel sur les
completions. A noter que `/v1/models` repond `200` dans ce cas — lister et
appeler relevent de droits differents, donc un diagnostic qui s'arrete a la
liste des modeles conclut a tort. `python -m mistral.client` tranche en une
seconde.
**`ImportError: cannot import name 'Mistral' from 'mistralai'`.** En 2.x,
`mistralai` est un package d'espace de noms : `from mistralai.client import
Mistral`. Les exemples qui ecrivent `from mistralai import Mistral` sont du 1.x.
**Nom de modele refuse.** `mistral-large-latest` et les `devstral-*` ne sont
plus servis. `python -m mistral.client` liste ceux qui le sont reellement pour
votre cle.
## Limites assumees
- Analyse **statique et non sensible au flot** : la resolution est faite par
variable, pas par chemin d'execution. Si un programme charge deux valeurs
differentes dans la meme variable a deux endroits, les deux cibles sont
listees. **Sur-approximation volontaire** : en analyse d'impact, mieux vaut un
candidat de trop qu'un oubli.
- Les **47 JCL ne sont pas analyses**. Un programme classe `batch` l'est parce
qu'aucun COBOL ne l'appelle, pas parce qu'on a lu sa carte `EXEC PGM=`.
- `flow.never_targeted` signifie « cible d'aucun `PERFORM`, `GO TO` ni `LABEL` »,
et **non** « code mort » : en COBOL un paragraphe s'execute aussi par
enchainement sequentiel.
- Le parseur cible le **fixed-format** (colonnes 7-72), format historique
mainframe. Le free-format n'est pas l'objectif.
- Pas de vocation a remplacer un outil commercial d'analyse d'impact : la valeur
demontree ici est le **couplage base legacy ↔ LLM**.
## Etat
| Etape | |
|---|---|
| 1 — fondation : arborescence, setup, secrets | fait |
| 2 — parseur COBOL fixed-format + index inverse | fait |
| 2b — graphe de flot interne + `explain_program` | fait |
| 3 — serveur MCP : 5 outils, autotest 32/32 | fait |
| 4 — integration Mistral : wrapper + pont function calling | fait |
| 5 — demo de bout en bout + documentation | fait |
~3 450 lignes de Python, validees depuis un clone vierge : `setup.sh`, index
reproduit a l'identique, autotest 32/32 sans cle API, et les trois questions de
demonstration repondues par `mistral-medium-latest`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues