idf-mcp
# 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
Scored across 5 tools
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.
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.
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.
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.