Live Lombardy train boards, delays, platforms, and journey planning. No API key.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent โ or use 1-click editor setup below.
๐ก Paste the JSON block into your client's configuration file under mcpServers, then restart the application.
An MCP server that answers questions about Lombardy trains: departure and arrival boards, live delays, platforms, stop-by-stop progress, cancellations and crowding โ and plans journeys with a change, from the regional timetable Regione Lombardia publishes as open data. It reads the public ViaggiaTreno (RFI/Trenitalia) and Trenord APIs. No API key, no account, no scraping.
Then register it with your MCP client. For Claude Desktop, in
claude_desktop_config.json:
For Claude Code:
| Tool | What it answers |
|---|---|
now | "what time and day is it in Italy?" |
search_station | "what is the station code for Milano Centrale?" |
get_departures | "what is leaving Milano Cadorna on Saturday morning?" |
get_arrivals | "when does the train from Varese get in?" |
find_connection | "which direct trains go from Milano Cadorna to Como?" |
find_journey | "how do I get from Castellanza to Como Lago on Monday morning?" |
get_train | "where is train 4307 right now, and how late is it?" |
Station arguments take either a code (S01700) or a name (milano centrale), and
times take either HH:mm for today or ISO 2026-08-29T09:00 for another day.
Three behaviours exist because the caller is a language model rather than a person, and each of them prevents a confident wrong answer:
now exists at all because a model has no reliable idea of the date in
Rome, and every relative question โ "tonight", "Saturday" โ needs one before
any other tool can be called.find_connection finds direct trains only, and says so. It reads live
departure boards and each candidate train's stop list rather than a timetable,
so it carries delays and platforms but cannot compose a change.
find_journey composes one. It plans from the regional timetable, falls back to
Swiss open data for anywhere that timetable does not reach, and returns
scheduled times with no delays in them โ the two tools answer different halves
of the same question and are meant to be used together.
Journeys are ranked by arrival, not departure. Someone asking how to get somewhere wants to be there soonest, and ranking by departure puts a train that leaves four minutes earlier and arrives forty minutes later at the top of the list.
Only one change is searched for. Two multiply both the search space and the ways to be quietly wrong, and on this network almost everything worth reaching is reachable with one โ so the limit is stated rather than hidden behind an incomplete search.
Both upstream APIs are public, undocumented and a little hostile. Most of the work in this repository is not calling them, it is surviving them. Each of the following is enforced in code and covered by a test.
Trenord answers 403 Forbidden unless the request carries both an Accept
header and a User-Agent:
| Request | Result |
|---|---|
| no headers | 403 |
Accept: */* only | 403 |
User-Agent only | 403 |
| both | 200 |
This is easy to get half right. Probing the endpoint with curl or Python
suggests that Accept alone is enough, because both send a User-Agent of
their own without being asked. .NET's HttpClient sends neither header unless
told to โ so a client that sets only Accept keeps getting 403, and finds out
in production rather than on the machine where the call was first tried by hand.
That is the whole reason TrenordClient sets both in its constructor.
average_crowding, average_crowding_label, suppression_type and alerts
do not appear in the Trenord payload at all when there is nothing to report.
They are not null: the properties are missing.
Verified against five regular services (S5, S11, RE_5, R27): none of them carried any of the four. Code that assumes these fields exist therefore works on the trains that have problems and crashes on the ones that do not, which is the worst possible way round.
dep_time / arr_time โ local time, "HH:MM:SS". Safe to display.dep_date_time / arr_date_time โ ISO 8601 in UTC, with the Z.
Convenient for date arithmetic, wrong if you show it as it is.Europe/Rome explicitly. Converting with the machine's local time gives the
right answer on an Italian laptop and the wrong one on a UTC server.Board endpoints take the timestamp spelled the way Date.toString() spells it:
Day and month names must be English. They are built from fixed arrays on
purpose: formatting them through the machine's culture produces lun ago on an
Italian system and the endpoint silently returns nothing.
|.train_operator uses $:$ as its separator, literally.Trenord publishes this timetable as a GTFS zip under CC0, and the same portal also serves it exploded into one queryable table per file. The tables look like the easier option โ paged JSON, filterable, no zip to unpack โ and they are a lossy copy. Measured against the file they are built from:
| the zip | the tables | |
|---|---|---|
| stop times | 90,553 | 68,952 |
| trips | 8,470 | 6,265 |
A quarter of the timetable does not survive the import, and it does not go missing tidily: trips arrive truncated. Train 11827 runs Varese to Milano and on; in the tables it stops at Porta Garibaldi, so everything reachable by staying on it is invisible, and Varese to Bergamo came back an hour worse than the published answer. Reading the zip, it matches to the minute.
Three more things the import breaks, each of which costs real code to work around and none of which is wrong in the file:
24:05; a datetime cannot hold that, so it becomes 00:05 and the train
arrives eighteen hours before it left. The file says 24:05.calendar_dates, so the two files cannot be joined on the key they share.
Cutting both back to the number before the hyphen makes them join again โ and
merges every seasonal variant of a train into one, leaving nothing to say
which of them runs today. In the file, trips and calendar_dates both write
1001A-2025-12-14-2026-12-12. The join is exact and the ambiguity does not
exist.trip_short_name is dropped entirely. That is the train number โ the one
field that connects a planned leg to the live data.The decimal point also goes missing from coordinates, and route_type differs:
the tables call R23 and RE4 trains, the file calls them buses.
The file is not the operator's own answer either, and it is worth being precise about the size of the gap. Checked against 28 journeys published by the operator's own planner, on the same day:
The operator runs HAFAS over an internal timetable with real-time folded in. A GTFS export of it is not it. But the live sources here are the operator's own, and the file carries the train number, so every leg can simply be looked up and asked. Where it answers, its times are the ones shown, the timetable's are kept in brackets beside them, and platforms, delays and cancellations come with them.
Against those same 28 journeys that takes exact agreement from 22 to 27.
The one that remains is the shape of what this cannot do. Lecco to Bergamo is still answered with the 09:01 coach rather than the 08:51 one, because the feed said the train arrived at 08:52 and the search discarded that connection before anything was checked. Correcting after the planning fixes what is shown; it does not recover what the wrong data excluded. Live data is asked for today and tomorrow only โ beyond that there is nothing live to ask.
No reviews yet โ be the first to share how this listing worked for you.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/lombardia-trains)<a href="https://allmcps.com/mcp/lombardia-trains"><img src="https://allmcps.com/api/badge/lombardia-trains?style=directory" alt="Lombardia Trains on AllMCPs" /></a>