Skip to main content
Glama
README.md
# idf-mcp — « c'est quand le prochain RER ? »

Serveur MCP local qui branche Claude (Code et Desktop) sur le temps réel du RER B,
via les API publiques d'Île-de-France Mobilités (plateforme **PRIM**).

```
> c'est quand le prochain RER pour Paris, et j'arrive à quelle heure ?

La Hacquinière → Denfert-Rochereau · lun. 7 sept. 20:38
• EKLI 20:42 → Denfert-Rochereau 21:19 · 37 min · direct · terminus Aéroport Charles de Gaulle 2 · dans 4 min
• DEFI 20:57 (+3) → Denfert-Rochereau 21:34 (+3) · 37 min · direct · terminus Mitry-Claye · dans 19 min ⚠ retards annoncés
```

Avec une correspondance ou une adresse, chaque étape est détaillée :

```
> je dois être au 43 rue Saint-Dominique à 10 h

La Hacquinière → 43 Rue Saint-Dominique 75007 Paris · mar. 8 sept. 08:50
• EPAF 08:59 → 43 Rue Saint-Dominique 75007 Paris 09:48 · 49 min · 1 corresp. · dans 9 min
    ↳ marche 2 min → La Hacquinière
    ↳ RER B (EPAF) · La Hacquinière 08:59 → Saint-Michel Notre-Dame 09:32 · 12 arrêts · dir. Aéroport CDG 2
    ↳ marche 4 min → Saint-Michel Notre-Dame
    ↳ attente 2 min
    ↳ RER C (ELBA) · Saint-Michel Notre-Dame 09:38 → Invalides 09:44 · 2 arrêts · dir. Versailles Rive Gauche
    ↳ marche 4 min → 43 Rue Saint-Dominique 75007 Paris
```

## 1. Obtenir une clé PRIM (5 min)

1. Créer un compte gratuit sur <https://prim.iledefrance-mobilites.fr>.
2. Générer une clé dans **« Mes jetons »**.
3. **Souscrire aux API** — une clé ne donne accès qu'à ce à quoi on a souscrit,
   c'est la cause n°1 de `401` :
   - « Prochains passages (requête unitaire) » → `departures_board`
   - « Calculateur – accès générique v2 » → `next_trains_to_paris`, `plan_journey`
   - « Messages info trafic v2 » → `line_status`

Quota : environ 20 000 requêtes par jour et par API. Le serveur met en cache
chaque URL 25 s, donc une rafale de questions ne coûte qu'un appel.

## 2. Installer

```bash
npm install
npm run build     # optionnel : évite de dépendre de tsx au lancement
npm test
```

### Claude Code

```bash
claude mcp add --scope user idfm \
  -e IDFM_API_KEY=ta_cle \
  -e IDFM_WORK_STOP=473890 \
  -- node /chemin/vers/idf-mcp/dist/index.js
```

Sans `npm run build`, remplacer la dernière ligne par
`-- npx tsx /chemin/vers/idf-mcp/src/index.ts`.

### Claude Desktop

Dans `~/Library/Application Support/Claude/claude_desktop_config.json` :

```json
{
  "mcpServers": {
    "idfm": {
      "command": "node",
      "args": ["/chemin/vers/idf-mcp/dist/index.js"],
      "env": {
        "IDFM_API_KEY": "ta_cle",
        "IDFM_WORK_STOP": "473890"
      }
    }
  }
}
```

### Vérifier sans Claude

```bash
IDFM_API_KEY=ta_cle npm run inspector
```

## 3. Configuration

| Variable | Défaut | Rôle |
|---|---|---|
| `IDFM_API_KEY` | — | **Obligatoire.** Clé PRIM. |
| `IDFM_HOME_STOP` | `47046` (La Hacquinière) | Gare de départ par défaut. |
| `IDFM_WORK_STOP` | `473890` (Denfert-Rochereau) | Ce que « Paris » veut dire. |
| `IDFM_CACHE_TTL` | `25` | Cache par URL, en secondes. |
| `IDFM_TIMEOUT_MS` | `10000` | Timeout HTTP. |

Autres gares parisiennes de la B : `45102` Châtelet - Les Halles ·
`462394` Gare du Nord · `43833` Luxembourg · `44877` Saint-Michel Notre-Dame ·
`44500` Port Royal · `473843` Cité Universitaire.

Voir [.env.example](.env.example). Ces variables se passent au serveur via la
config MCP (`-e` ou le bloc `env`), pas par un fichier `.env` lu au démarrage.

## 4. Les tools

| Tool | Ce qu'il donne | Source |
|---|---|---|
| `next_trains_to_paris` | **Le principal.** Départ, retard, **heure d'arrivée**, durée, mission, terminus, perturbations. | Navitia `journeys` |
| `departures_board` | Tableau de gare : heure attendue, voie, train à quai, trains supprimés. Pas d'heure d'arrivée. | SIRI `stop-monitoring` |
| `line_status` | Perturbations en cours et à venir sur la B. | Navitia `line_reports`, repli SIRI `general-message` |
| `plan_journey` | Itinéraire quelconque, tous modes, avec `arrive_by`. Accepte une **adresse postale**. | Navitia `journeys` + BAN |
| `find_stop` | Recherche d'arrêt par nom → identifiant + lignes. Sans clé. | Open data IDFM |

Prompt `/prochain-rer` : déclenche `next_trains_to_paris` avec les valeurs par défaut.

Les gares s'écrivent par identifiant (`473890`), par nom (`Denfert-Rochereau`),
sans accent (`hacquiniere`) ou par alias (`denfert`, `chatelet`, `cdg`, `st remy`).
`plan_journey` accepte en plus une **adresse postale** (`43 rue Saint-Dominique, Paris`)
et des coordonnées (`2.317444;48.859821`) — inutile de chercher la station la plus
proche à la main. Les horaires acceptent `18:30`, `18h30`, `demain`, `demain 08:15`
ou une date ISO.

### Exemples de questions

- « c'est quand le prochain RER pour Paris ? »
- « je pars dans 20 min, j'arrive à quelle heure à Châtelet ? »
- « il y a des retards sur la B ? »
- « le train est à quai ? sur quelle voie ? »
- « pour être à Denfert à 9 h, je pars à quelle heure ? » → `plan_journey` avec `arrive_by`
- « je dois être au 43 rue Saint-Dominique demain à 9 h » → `plan_journey`, adresse telle quelle
- « le dernier train pour Saint-Rémy ce soir ? » → `departures_board` sens `saint-remy`

## 5. Ce que fait le code

```
src/
  index.ts          bootstrap MCP (stdio), les 5 tools et le prompt
  config.ts         lecture de l'environnement
  time.ts           Europe/Paris : Navitia rend du local sans offset, SIRI de l'ISO
  format.ts         rendu texte compact (« EKLI 20:42 (+2) → … · dans 4 min »)
  idfm/client.ts    fetch + apikey + cache TTL + erreurs traduites (401/404/429/5xx/timeout)
  idfm/siri.ts      stop-monitoring, general-message
  idfm/navitia.ts   journeys, line_reports
  idfm/stops.ts     les 47 gares de la B en dur (ids + coordonnées) + ordre de la ligne
  idfm/geocode.ts   adresse → coordonnées (Base Adresse Nationale, sans clé)
tests/              99 tests, dont un test d'intégration qui démarre le serveur
scripts/            capture-fixtures.sh
```

Quelques décisions qui méritent une phrase :

- **L'heure d'arrivée vient de Navitia, pas de SIRI.** SIRI ne sait dire que
  « ce train part d'ici à telle heure » ; toutes les missions de la B ne
  desservent pas les mêmes gares, donc SIRI seul ne permet pas de conclure.
- **Le sens « vers Paris » est calculé, pas deviné.** Les 47 gares portent une
  position le long de la ligne (branches comprises), et Paris intra-muros est un
  intervalle sur le tronc commun. Depuis La Hacquinière « vers Paris » exclut
  Saint-Rémy ; depuis Le Bourget il exclut CDG et Mitry. Depuis une gare
  parisienne, la question n'a pas de sens et le tool le dit au lieu de filtrer.
- **Les trains supprimés sont affichés**, marqués `⛔ SUPPRIMÉ`. Les masquer
  serait le plus sûr moyen de rater une info importante.
- **L'heure d'arrivée est celle à destination**, marche finale comprise, pas
  celle du dernier train : sur « je dois y être à 10 h », c'est l'écart qui
  compte. Le détail des étapes donne l'heure exacte de chaque train.
- **Le détail n'apparaît que s'il sert** : correspondance à faire, ou marche
  d'au moins trois minutes. Un RER direct reste sur une seule ligne.
- **Une réponse vide n'est pas une erreur** : la nuit, il n'y a simplement plus
  de train, et le message le dit.
- **Les identifiants et coordonnées des gares sont en dur** (vérifiés contre
  l'open data IDFM le 2026-09-07) : pas d'appel réseau pour résoudre « denfert ».
- **Une station, une entrée.** Le référentiel expose un identifiant par quai et
  par ligne : « Invalides » y figure 24 fois. Les résultats sont regroupés par
  nom et commune, ce qui évite de déclarer ambiguë chaque gare parisienne.
- **On tranche au lieu d'échouer.** Quand plusieurs arrêts collent encore, le
  mieux classé est retenu (nom exact, puis mode lourd, puis nombre de lignes) et
  les autres sont affichés — refuser de choisir rendait le calcul inutilisable.

## 6. Développement

```bash
npm test              # suite complète
npm run test:watch
npm run typecheck
npm run dev           # lance le serveur en stdio (pour l'inspecteur)
```

**Les fixtures livrées sont synthétiques** : elles reproduisent la forme
documentée des API, pas une capture réelle. Avec une clé en main :

```bash
IDFM_API_KEY=ta_cle ./scripts/capture-fixtures.sh
npm test
```

Le script écrase `tests/fixtures/*.json` avec de vraies réponses. Si un test
casse à ce moment-là, c'est une bonne nouvelle : il a trouvé un écart entre la
documentation PRIM et la réalité, et l'endroit exact à corriger.

Si le calculateur refuse les itinéraires :

```bash
IDFM_API_KEY=ta_cle ./scripts/diagnose.sh
```

Il teste une par une les causes possibles (filtre de ligne, identifiants
d'arrêt, chemin appelé, souscription) et dit laquelle est la bonne.

## 7. Pièges connus

- **`401` sur Navitia alors que SIRI marche** → l'API « Calculateur » n'est pas
  souscrite sur cette clé. Le message d'erreur du serveur le dit explicitement.
- **`allowed_id[]` est une liste blanche, pas un filtre.** Tout ce qui n'y
  figure pas est interdit — y compris l'origine et la destination du trajet, ce
  qui fait répondre `no_origin_nor_destination`. Les deux gares sont donc
  toujours ajoutées à la liste ; et si le calculateur refuse malgré tout le
  filtre, il est abandonné et la réponse le signale plutôt que de ne rien rendre.
- **Le calculateur veut des coordonnées, pas des identifiants d'arrêt.** PRIM
  n'expose que l'endpoint global de Navitia (`/v2/navitia/journeys`), et celui-ci
  ne sait résoudre que des `lon;lat` : lui passer un `stop_area:IDFM:47046`
  répond invariablement `no_origin_nor_destination`, quel que soit le couple de
  gares. Les trajets sont donc demandés en coordonnées — embarquées pour les
  47 gares de la B, cherchées dans l'open data sinon — tandis que les
  identifiants restent utilisés dans `allowed_id[]`, où ils sont indispensables.
  En repli, le serveur retente en identifiants, puis sur
  `/v2/navitia/coverage/{coverage}/journeys` si cette coverage existe
  (`/v2/navitia/coverage` répond 404 sur au moins une clé PRIM, et l'ancienne
  `fr-idf` a été décommissionnée : elle est donc demandée, jamais devinée).
  La combinaison qui marche est mémorisée — les appels suivants coûtent une
  seule requête.
- **La Hacquinière a 3 points d'arrêt (ZDE) pour 1 zone d'arrêt (ZDA)** — on
  travaille toujours au niveau ZDA (`47046`). Si SIRI ne renvoie rien pour une
  ZDA, `departures_board` bascule automatiquement sur les quais.
- **Changement d'heure** : Navitia renvoie de l'heure locale sans offset. La
  conversion est testée sur la nuit du 25 octobre.
- **Un identifiant de quai n'est pas une zone d'arrêt.** `IDFM:22193` est un
  quai, `IDFM:monomodalStopPlace:470540` une station : seul le second existe
  comme `stop_area` côté calculateur. Le premier donnait un
  `no_origin_nor_destination` très trompeur.
- **Le géocodage est bridé à l'Île-de-France.** « Denfert » seul renvoie une
  avenue à La Rochelle avec un score honorable ; hors des départements 75 à 95,
  le résultat est rejeté.
- **Ne pas boucler** depuis un hook : le cache de 25 s protège le quota d'une
  rafale, pas d'une boucle.

## 8. Sources

- [Identification des objets (formats `MonitoringRef`)](https://prim.iledefrance-mobilites.fr/en/aide-et-contact/documentation/prise-en-main-des-api/prise-en-main-des-api-prochains-passages/identification-des-objets)
- [API Prochains passages](https://prim.iledefrance-mobilites.fr/en/apis/idfm-ivtr-requete_unitaire)
- [API Calculateur Navitia v2](https://prim.iledefrance-mobilites.fr/en/apis/idfm-navitia-general-v2)
- [API Info trafic v2](https://prim.iledefrance-mobilites.fr/en/apis/idfm-navitia-line_reports-v2)
- [Documentation Navitia](https://doc.navitia.io/)
- [Référentiel arrêts / lignes (open data)](https://data.iledefrance-mobilites.fr/explore/dataset/arrets-lignes/)
- [Base Adresse Nationale](https://adresse.data.gouv.fr/api-doc/adresse) — géocodage des adresses

TDQS

A4.1/5.0

Scored across 5 tools

Disambiguation4/5

Most tools have clearly distinct jobs: departures_board handles live platform/departure info, line_status handles disruptions, find_stop handles stop lookup, and plan_journey handles multimodal itineraries. next_trains_to_paris overlaps somewhat with plan_journey for the habitual commute, but the descriptions explicitly explain when to prefer each.

Naming Consistency3/5

All names are readable and use snake_case, but they mix noun-phrase resource names (departures_board, line_status, next_trains_to_paris) with verb-first command names (plan_journey, find_stop). There is no consistent verb_noun pattern across the set.

Tool Count5/5

Five tools is a well-scoped size for a transit information server. Each tool covers a distinct layer of functionality—next trains, live departures, line status, journey planning, and stop lookup—without unnecessary duplication.

Completeness4/5

The set covers the core transit domain well: departures, arrivals via next_trains_to_paris, disruptions, stop search, and full journey planning. Minor gaps exist, such as no dedicated tool for return-trip RER B status or line status for non-B lines, but plan_journey and line_status can work around these.

Maintenance

ActivityMaintained
ResponsivenessNo issues