Skip to main content
Glama
nba67000

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`.