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, tables DB2, analyse d'impact.

Le LLM de la demo est **Mistral** (La Plateforme). Le serveur, lui, est
agnostique : n'importe quel client MCP peut le consommer.

> Base de demonstration : [AWS CardDemo](https://github.com/aws-samples/aws-mainframe-modernization-carddemo),
> une application CICS / DB2 / VSAM de gestion de cartes de credit publiee par
> AWS. Aucun code client n'est utilise.

## Le probleme

Une base COBOL de 20 ans, c'est quelques centaines de programmes, des
copybooks partages par tout le monde, des `XCTL` CICS qui sautent de programme
en programme, et personne qui sait plus repondre a *« qu'est-ce qui casse si je
rallonge ce champ ? »*.

Un LLM seul ne peut pas repondre : la base ne tient pas dans son contexte, et
il ne l'a jamais vue. Donner au LLM des **outils** pour interroger la base —
plutot que d'esperer qu'il la memorise — c'est exactement ce que resout MCP.

## 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. Zero dependance LLM.
3. **Client LLM** — Mistral 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.

## Outils MCP

| 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 ? |

## Installation

Prerequis : Python ≥ 3.10, git, et une cle API Mistral
([console.mistral.ai](https://console.mistral.ai/api-keys)).

```bash
bash setup.sh        # venv + dependances + clone de CardDemo + .env
```

Puis renseigne ta cle dans `.env` :

```ini
MISTRAL_API_KEY=...
```

`.env` est gitignore — la cle ne quitte jamais ta machine.

## Utilisation

```bash
# 1. Indexer la base COBOL (a refaire si le code source change)
.venv/Scripts/python.exe -m parser.build_index

# 2. Poser une question en langage naturel
.venv/Scripts/python.exe demo/ask.py "Que fait le programme COACTVWC ?"
```

*(Sous Linux/macOS : `.venv/bin/python` au lieu de `.venv/Scripts/python.exe`.)*

### Questions de demonstration

```bash
demo/ask.py "Que fait le programme COACTVWC et quelles tables DB2 utilise-t-il ?"
demo/ask.py "Qu'est-ce qui casse si je modifie le copybook CVACT01Y ?"
demo/ask.py "Montre-moi la chaine d'appels depuis le menu principal."
```

## Etat d'avancement

- [x] **Etape 1** — fondation : arborescence, setup, gestion des secrets
- [x] **Etape 2** — parseur COBOL fixed-format + index inverse
- [x] **Etape 2b** — graphe de flot interne + outil `explain_program`
- [x] **Etape 3** — serveur MCP : 5 outils, autotest 32/32
- [x] **Etape 4** — integration Mistral : wrapper + pont de function calling
- [ ] **Etape 5** — demo de bout en bout + doc

La chaine complete est validee de bout en bout avec `mistral-medium-latest` :
trois questions, dont une resolue en 5 appels d'outils sur 6 tours.

> Si `python -m demo.ask` repond `429` 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 (alors que
> `/v1/models` repond 200 — lister et appeler relevent de droits differents).
> `python -m mistral.client` le diagnostique en une seconde. En attendant,
> `--simulate` exerce tout le pont sans API.

## Ce que le parseur extrait

Sur les 44 programmes de CardDemo (22 893 lignes de code) :

| | |
|---|---|
| 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 |
| Donnees | 8 fichiers VSAM, 2 tables DB2, 2 segments IMS |
| Ecrans | transaction CICS et map BMS resolues par programme |

### Le point dur : les appels indirects

CardDemo n'ecrit jamais le nom du programme cible dans un `XCTL` :

```cobol
MOVE LIT-MENUPGM TO CDEMO-TO-PROGRAM        *> LIT-MENUPGM VALUE 'COMEN01C'
EXEC CICS XCTL PROGRAM(CDEMO-TO-PROGRAM) END-EXEC
```

Un `grep XCTL` ne remonte donc **aucun** appel exploitable. 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 |

Aucune arete ne reste non resolue. Et surtout : 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.

### Essayer un outil sans serveur ni cle API

La logique des outils (`server/tools.py`) est separee de la plomberie MCP.
On peut donc interroger l'index directement :

```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 produit une section **« A savoir avant de conclure »** : ce qu'il
ne sait pas. C'est elle qui empeche le LLM d'affirmer ce que l'analyse
statique n'etablit pas.

## Le serveur MCP

```bash
.venv/Scripts/python.exe -m server.selftest     # verifie les 5 outils
.venv/Scripts/python.exe server/mcp_server.py   # lance le serveur
```

Le serveur dialogue en JSON-RPC sur stdin/stdout : lance seul, il n'affiche
rien. C'est normal. L'autotest, lui, 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 -> COACTUPC
- 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 differentes.

### Brancher un autre client MCP

Le serveur ne contient aucune reference a un modele particulier. N'importe quel
client MCP le consomme avec une configuration de cette forme :

```json
{
  "mcpServers": {
    "cobol-legacy": {
      "command": "/chemin/vers/mcp_cobol/.venv/Scripts/python.exe",
      "args": ["/chemin/vers/mcp_cobol/server/mcp_server.py"]
    }
  }
}
```

## L'integration Mistral

```bash
python -m mistral.client                          # pre-vol : config, modeles, appel test
python -m demo.ask "Que fait le programme COACTVWC ?"
python -m demo.ask --simulate "..."               # le pont, sans appeler l'API
```

### 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**, en JSON-RPC sur
stdin/stdout. Il ignore l'existence 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.

### Verifier le pont sans quota API

```bash
python -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 : 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.

## 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. C'est une **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=`.
- Le parseur cible le **fixed-format** (colonnes 7-72), le 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**.