idfm-mcp
Provides tools for querying real-time Paris public transport data through the official Île-de-France Mobilités PRIM API, including stop search, next departures, journey planning, and traffic disruptions.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@idfm-mcpWhen is the next RER B from Gentilly?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
idfm-mcp
An open-source MCP server that lets any AI assistant (Claude Desktop, Cursor, …) answer real-time questions about Paris public transport, using the official Île-de-France Mobilités PRIM API.
"When is the next RER B from Gentilly?" · "Are there disruptions on Metro line 7?" · "How do I get from Bastille to Orly at 18:30?"

Tools
Tool | What it does | PRIM API |
| Find stations by name, with their IDs and lines | Navitia |
| Real-time departures: line, destination, minutes to wait, delay, platform | SIRI Lite |
| Up to 3 routes with duration, transfers and steps | Navitia |
| Current and planned incidents/works, most severe first | Navitia |
Related MCP server: TFL MCP Server for Poke
Architecture
flowchart LR
U([User]) --> C["MCP client<br/>(Claude Desktop, Cursor)"]
C <-->|"MCP over stdio"| S
subgraph S["idfm-mcp"]
direction TB
SV["server.py<br/>4 tools · input resolution<br/>errors → ToolError"]
F["formatting.py<br/>short LLM-readable text"]
CL["client.py<br/>auth · timeouts · typed errors<br/>ID conversion · parsing"]
M["models.py<br/>pydantic models"]
SV --> CL
SV --> F
CL --> M
F --> M
end
CL <-->|"HTTPS + apikey header"| P[("IDFM PRIM API<br/>SIRI Lite + Navitia")]Each layer has one job: client.py is the only code that knows HTTP and raw JSON;
server.py is the only code that knows MCP; formatting.py decides what the LLM reads.
See docs/prim-api.md for the API details and gotchas.
Install
Requirements: uv and a free PRIM API key.
git clone https://github.com/Drathod-24/idfm-mcp.git
cd idfm-mcp
uv sync
cp .env.example .env # then paste your key into .envGet a key: create an account on prim.iledefrance-mobilites.fr, open Mes jetons d'authentification → API → Générer mon jeton. The token is shown only once.
Use with Claude Desktop
Open Settings → Developer → Edit Config (claude_desktop_config.json) and add:
{
"mcpServers": {
"idfm": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/idfm-mcp", "run", "idfm-mcp"],
"env": { "IDFM_API_KEY": "your-prim-api-key" }
}
}
}On Windows, use a path like "C:\\Users\\you\\idfm-mcp". Restart Claude Desktop; the
tools appear under the 🔌 icon. (env is optional if the key is already in .env.)
Cursor uses the same JSON in ~/.cursor/mcp.json.
Test with MCP Inspector
# Interactive web UI
npx @modelcontextprotocol/inspector uv --directory . run idfm-mcp
# Or scripted (CLI mode)
npx @modelcontextprotocol/inspector --cli uv --directory . run idfm-mcp --method tools/list
npx @modelcontextprotocol/inspector --cli uv --directory . run idfm-mcp \
--method tools/call --tool-name search_stop --tool-arg query=GentillyExample questions and answers
These are the tools' real output formats, produced from the test fixtures (times are illustrative):
"When is the next RER B from Gentilly?" → next_departures(stop_id="Gentilly", line="RER B")
Next departures at Gentilly (stop_area:IDFM:63244) (now 09:25):
- RER B → Aéroport CDG 2 TGV: now, 09:26
- RER B → Saint-Rémy-lès-Chevreuse: in 8 min, 09:33 (+2 min vs schedule, platform 2)"Are there disruptions on Metro line 7?" → traffic_disruptions(line="Metro 7")
1 disruption(s) for Métro 7:
- [Métro 7] Métro 7 : trafic perturbé — reduced service
En raison de travaux, le trafic est interrompu entre Place d'Italie et Villejuif. Des bus de remplacement sont mis en place.
Until 05/11 23:00
(1 elevator/escalator notices not shown.)"How do I get from Gentilly to Étoile at 9:30?" → plan_journey(from_place="Gentilly", to_place="Étoile", departure_time="09:30")
Routes from Gentilly to Étoile:
Option 1: 09:30 → 10:02 (32 min, 1 transfer)
1. Walk 4 min to Gentilly
2. RER B toward Aéroport CDG 2 TGV: Gentilly → Châtelet les Halles (09:34–09:45)
3. Change: walk 5 min to Châtelet
4. Métro 1 toward La Défense: Châtelet → Charles de Gaulle - Étoile (09:50–10:02)Errors are answers too: a missing key, a bad stop ID, a 429 quota error or a timeout
comes back as a clear MCP error (isError: true), never a crash:
IDFM API quota exceeded (default: 5 requests/second, 1000/day). Wait a bit before retrying.Development
uv run pytest # 68 tests; every PRIM call is mocked with respx, no key needed
uv run pytest -m live # optional: ~5 real calls to check fixtures match the real API
uv run ruff check . && uv run ruff format --check .CI (GitHub Actions) runs ruff, pytest and the eval dataset check on Python 3.11–3.13 on every push.
Evals
evals/questions.json has 20 natural-language questions (English
and French, plus one off-topic) with the acceptable first tool call for each. The runner
reads the tool definitions from the server itself, asks Claude which tool it would
call, and scores tool name + arguments. It tests the docstrings, which are what an LLM
actually sees.
uv run python evals/run_evals.py --offline # validate the dataset (runs in CI)
uv run --group evals python evals/run_evals.py # live run, needs ANTHROPIC_API_KEYCategory | Cases | Passed |
Next departures | 6 | pending |
Stop search | 3 | pending |
Journey planning | 5 | pending |
Disruptions | 5 | pending |
Off-topic (no tool) | 1 | pending |
Total | 20 | pending |
Full per-question results: evals/results.md. The live eval has not been run yet.
Limitations
Real-time coverage is partial: PRIM does not provide live data for every stop yet; an empty list is a normal answer.
Default quota is 1,000 requests/day per API.
next_departuresuses 1–3 calls (name lookup, line names, departures). There is no caching yet.Test fixtures are synthetic, built from the docs; run
pytest -m livewith a key to check them against the real API.Line resolution is heuristic: a bare number ("7") is assumed to be a Metro line.
plan_journeytakes the best match for free-text places and does not ask when names are ambiguous.The eval tests only the first tool call, not multi-turn conversations or answer quality.
Next steps
Cache stop/line lookups (they rarely change) to save quota
Record real API responses as fixtures
Multi-turn evals: score the final answer, not just the routing
Streamable HTTP transport for remote deployment
Accessibility: expose elevator outages as their own tool
Publish to PyPI so it runs with
uvx idfm-mcp
License
MIT. Data from Île-de-France Mobilités via PRIM, under the Mobility License / ODbL. This project is not affiliated with Île-de-France Mobilités, RATP or SNCF.
Available Tools
4 toolsnext_departuresARead-only
Get the next real-time departures at a station, with minutes until departure.
Use for questions like "When is the next RER B from Gentilly?" or "Next metro at
Bastille?". Shows line, destination, minutes to wait, delay and platform.
Args:
stop_id: Station ID from search_stop (e.g. "stop_area:IDFM:63244"). A plain
station name also works; the best match is used.
line: Optional line filter, e.g. "RER B", "Metro 7", "Tram T3a", "Bus 57".
Leave empty to get all lines at the station.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No | ||
| stop_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description goes beyond the structured fields by explicitly listing the behavioral traits of the output: 'Shows line, destination, minutes to wait, delay and platform.' This provides valuable data context missing from the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The definition is highly efficient and front-loaded, though the argument definition section is a standard listing rather than prose flow.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The definition is highly complete, detailing the expected inputs (IDs vs. names) and the specific attributes returned (line, delay, platform), making it robust for complex real-time queries.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0% schema description coverage, the description must be comprehensive. It provides critical context that the stop_id can be a formal ID (e.g. 'stop_area:IDFM:63244') or a 'plain station name,' resolving ambiguity for the agent.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The purpose is crystal clear, stating the tool gets 'next real-time departures at a station' and using specific questions (e.g., 'When is the next RER B?') to define its scope clearly against journey planning tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage patterns and examples, showing agents exactly what kind of queries this tool is intended to resolve versus general travel inquiries.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
plan_journeyARead-only
Plan a public transport journey between two places in Île-de-France.
Use for "How do I get from X to Y?" or "Fastest way to Orly from Bastille at 18:00?".
Returns up to 3 options with total duration, number of transfers and each step
(lines to take, where to change, walking time).
Args:
from_place: Origin: station name, address or landmark (e.g. "Gentilly",
"Tour Eiffel", "10 rue de Rivoli Paris") or a stop ID from search_stop.
to_place: Destination, same formats as from_place.
departure_time: Optional, Paris local time: "18:30" (today) or
"2026-11-05T18:30". Defaults to now.
| Name | Required | Description | Default |
|---|---|---|---|
| to_place | Yes | ||
| from_place | Yes | ||
| departure_time | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. Beyond that the description adds genuinely useful disclosure: at most 3 options returned, the shape of each option (duration, transfers, step list) and that departure_time defaults to now in Paris local time. It says nothing about real-time versus scheduled data, ambiguous place-name handling, or error behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with what the tool does, then when to use it, then what it returns, then the arguments — a logical order with no filler sentences. The example values inside the Args block are doing real work rather than padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a three-parameter tool with no output schema, the description covers inputs, defaults and the return shape, so an agent can call it confidently. The gap is behavioral edge cases: routing ambiguity resolution, real-time versus timetable data, and how it relates to the disruption sibling.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description carries the whole burden and does so well: from_place is defined as station name, address, landmark or a stop ID from search_stop, with examples; to_place inherits those formats; departure_time is documented as optional with both accepted formats and a 'defaults to now' rule. That fully compensates for the empty schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb and resource ('Plan a public transport journey') and pins the geographic scope to Île-de-France. That is enough to separate it from siblings like search_stop, next_departures and traffic_disruptions without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Real user phrasings ('How do I get from X to Y?', 'Fastest way to Orly from Bastille at 18:00?') make the intended trigger concrete. It stops short of naming alternatives — nothing says to use next_departures for a single stop's real-time board or traffic_disruptions for network status — so there is no explicit exclusion rule.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_stopARead-only
Find Paris / Île-de-France stations and stops by name and return their IDs.
Use this first when the user names a station (e.g. "Gentilly", "Châtelet",
"Gare de Lyon") and you need its ID for next_departures. Returns up to 5 matches
with the lines serving each one, so you can pick the right station.
Args:
query: Station or stop name, in French or English spelling. e.g. "Gentilly".
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. The description adds useful behavior beyond that — a capped result set of 'up to 5 matches with the lines serving each one' — but says nothing about behavior when zero matches are found or how ambiguous partial names are resolved, which is the main risk for a name-search tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The intent and timing are front-loaded in the first two sentences, which is the right ordering. The trailing 'Args:' block partly repeats the station examples already given in the body ('Gentilly' appears twice), which is mild waste for a one-parameter tool.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool with no output schema, the definition covers what it does, when to call it, and roughly what comes back (up to 5 matches plus their lines). The notable omission is the no-match / ambiguity case, though the small surface area limits how much damage that causes.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% — the schema only says 'Query: string' — so the description carries the burden and does add real meaning: the value is a station or stop name, accepted in French or English spelling, with worked examples ('Gentilly', 'Châtelet', 'Gare de Lyon'). It does not address partial matches or accents, but the core semantics are supplied.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Find Paris / Île-de-France stations and stops by name and return their IDs') and scopes it geographically. It also ties itself to a sibling by naming next_departures as the consumer of the returned IDs, so an agent can separate it from the other tools without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Use this first when the user names a station... and you need its ID for next_departures' gives clear triggering context and a downstream dependency. It stops short of explicit exclusions — nothing says when to skip this in favor of plan_journey or traffic_disruptions, which matters since those siblings also take station input.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
traffic_disruptionsARead-only
Get current traffic disruptions, incidents and works on the Île-de-France network.
Use for "Are there problems on Metro 7?", "Is the RER A running?" or "Any
disruptions right now?". Returns active and planned disruptions, most severe first.
Elevator/escalator outages are counted but not listed.
Args:
line: Optional line, e.g. "Metro 7", "RER B", "Tram T2", "Transilien L".
Leave empty for a network-wide summary.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnlyHint, openWorldHint), so the description only needs to add context — and it does: results include both active and planned disruptions, are ordered most-severe-first, and elevator/escalator outages are counted but not listed. That last caveat is genuinely useful because it pre-empts an agent assuming a complete outage picture.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and usage examples are front-loaded, and the args block is compact. The example questions are somewhat repetitive with the opening sentence, but each still earns its place as a concrete trigger, so there is little waste.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a one-optional-parameter read tool with no output schema, the definition covers purpose, trigger examples, identifier format, default behavior, result ordering, and a content caveat. An agent has everything it needs to decide to call it and to construct a valid argument.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the whole burden for the single optional 'line' parameter, and it does: it supplies the canonical naming convention with four examples ('Metro 7', 'RER B', 'Tram T2', 'Transilien L') and defines the null/default behavior as a network-wide summary. This removes the main failure mode of a free-text line identifier.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource — 'Get current traffic disruptions, incidents and works on the Île-de-France network' — plus a stated scope (active and planned). That is far more precise than the bare name 'traffic_disruptions', but it never names or contrasts the siblings (search_stop, next_departures, plan_journey), so an agent must infer the boundary itself.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Three concrete example questions ('Are there problems on Metro 7?', 'Is the RER A running?', 'Any disruptions right now?') give a clear trigger context, and the args block states that leaving 'line' empty yields a network-wide summary. What is missing is any when-not guidance, e.g. that live departure times belong to next_departures or routing to plan_journey.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.1.0- First observed
next_departures - First observed
plan_journey - First observed
search_stop - First observed
traffic_disruptions
TDQS
Scored across 4 tools
Each tool maps to a clearly distinct transit task: station lookup, real-time departures, journey planning, and disruption status. There is little risk of misselection, and descriptions reinforce the boundaries between tools.
Two tools follow a verb_noun pattern (search_stop, plan_journey), but next_departures is an adjective_noun phrase and traffic_disruptions is a noun_noun phrase. The names are readable and individually clear, but the set does not follow one predictable convention.
Four tools is well-scoped for a focused Île-de-France transit query server. Each tool serves a distinct core user need, and there are no redundant or filler tools.
The server covers the main transit workflows: finding stops, checking next departures, planning journeys, and viewing disruptions. Minor gaps remain, such as line-level details, fare information, or accessibility-specific queries, but agents can handle core tasks without dead ends.
Maintenance
Related MCP Connectors
Plan door-to-door trips across French trains, buses, transit and flights.
Mobility, geocoding and nearby urban services for AI agents.
Belgian trains for AI agents: live journeys, departures, delays, disruptions. Free, no key.
Read-only public transit departures, stop search, and city coverage for bus and train users.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides real-time access to Paris public transport data (métro, RER, bus, tram, Transilien) including live departure times and traffic disruptions through the IDFM PRIM API.2MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to access real-time Transport for London data, including tube/bus arrivals, line status, journey planning, and disruptions.1-
- AlicenseBqualityDmaintenanceProvides real-time Transport for London journey data, including routes, alerts, and disruptions, allowing AI assistants to search journeys and get station information.13 npm4ISC
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to query live Transport for London data, including service status, arrivals, journey planning, bike points, crowding, and fares.1MIT