关于 Lombardia Trains
Lombardia Trains 是面向伦巴第地区铁路出行的 MCP 服务,可查询实时车次信息并规划带一次换乘的行程。它结合 Regione Lombardia 时刻表开放数据与 ViaggiaTreno、Trenord 的公开接口,无需 API 密钥。
主要功能
- 01查询出发与到达车次
- 02查看延误、站台与取消
- 03查询列车沿途进度
- 04搜索直达车次
- 05规划最多一次换乘行程
- 06识别含糊的车站名称
适用场景
- 01查询米兰车站的实时出发信息
- 02确认列车位置、延误和站台
- 03规划伦巴第地区的铁路行程
MCP 相关
它通过 MCP 工具向客户端提供时钟、车站搜索、到发查询、直达车次搜索、行程规划和列车状态查询。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
Lombardia Trains MCP
一个 MCP 服务,可回答有关伦巴第大区列车的问题:出发和到达信息、实时晚点、站台、逐站运行情况、取消和拥挤情况;还可根据伦巴第大区政府以开放数据形式发布的区域时刻表,规划需要换乘的行程。它读取公开的 ViaggiaTreno(RFI/Trenitalia)和 Trenord API。无需 API 密钥、无需账号,也不进行抓取。
> is the next train to Malpensa on time?
Departures — MILANO CADORNA (S01066), 21:29
21:23 REG787 SEVESO +2' platform 9
21:26 REG387 MALPENSA AEROPORTO TERMINA -2' platform 1
21:32 REG887 SARONNO 0' platform 6
安装
dotnet tool install -g LombardiaTrains.Mcp
然后在 MCP 客户端中注册。对于 Claude Desktop,在
claude_desktop_config.json 中:
{
"mcpServers": {
"lombardia-trains": {
"command": "lombardia-trains-mcp"
}
}
}
对于 Claude Code:
claude mcp add lombardia-trains -- lombardia-trains-mcp
工具
| 工具 | 回答的问题 |
|---|---|
now | “意大利现在是几点,星期几?” |
search_station | “Milano Centrale 的车站代码是什么?” |
get_departures | “周六早上有哪些列车从 Milano Cadorna 出发?” |
get_arrivals | “从 Varese 出发的列车什么时候到?” |
find_connection | “有哪些直达列车从 Milano Cadorna 开往 Como?” |
find_journey | “周一早上怎么从 Castellanza 到 Como Lago?” |
get_train | “4307 次列车现在在哪里,晚点多久?” |
车站参数可以是代码(S01700)或名称(milano centrale);时间参数可以是表示今天的 HH:mm,也可以是表示其他日期的 ISO 时间 2026-08-29T09:00。
由于调用者是大模型而非人类,因此存在以下三种行为,每一种都能避免给出看似肯定、实则错误的答案:
now的存在本身就有必要,因为模型无法可靠判断罗马的日期,而每个相对时间问题——“今晚”“周六”——都需要先确定日期,才能调用其他工具。- 有歧义的名称绝不会被悄悄确定为某个地点。“Milano”匹配 26 个车站,许多城镇既有国家铁路车站,也有另一个独立的“Nord”车站,两者由完全不同的列车服务。工具会返回列表并询问用户,而不是擅自选择第一个结果并表现得很确定。
- **对于未知地点,会说明服务覆盖范围的边界。**询问 Lugano 时,工具会说明服务覆盖哪些区域,让模型可以拒绝回答,而不是编造一趟开往瑞士的列车。
find_connection 只查找直达列车,并会明确说明这一点。它读取实时出发信息和每趟候选列车的停站列表,而不是时刻表,因此会提供晚点和站台信息,但无法规划换乘。
find_journey 可以规划换乘。它根据区域时刻表规划行程;若时刻表未覆盖某处,则回退使用瑞士开放数据;返回的时间为计划时间,不包含晚点信息——这两个工具回答同一问题的不同部分,应配合使用。
行程按到达时间排序,而不是按出发时间排序。询问如何前往某地的人,想要的是尽快到达;如果按出发时间排序,那么一趟早出发 4 分钟、晚到 40 分钟的列车就会排在列表顶部。
只搜索一次换乘。两次换乘会让搜索空间和悄无声息出错的可能性都成倍增加,而且在这条铁路网上,几乎所有值得前往的地方都能通过一次换乘到达——因此我们明确说明这个限制,而不把不完整的搜索隐藏起来。
值得一读的部分
这两个上游 API 都是公开的、没有文档,而且不太好用。本仓库的大部分工作不是调用它们,而是应对它们带来的问题。以下每一项都有代码强制实施,并由测试覆盖。
Trenord 需要两个标头,而不是一个
除非请求同时带有 Accept 和 User-Agent 两个标头,否则 Trenord 会返回 403 Forbidden:
| 请求 | 结果 |
|---|---|
| 不带标头 | 403 |
仅带 Accept: */* | 403 |
仅带 User-Agent | 403 |
| 两者都带 | 200 |
这很容易只做对一半。用 curl 或 Python 探测端点时,会让人以为只需设置 Accept,因为即使没有要求,它们也会各自发送一个 User-Agent。除非明确设置,否则 .NET 的 HttpClient 两个标头都不会发送。因此,只设置 Accept 的客户端会一直收到 403,而且要等到生产环境中才会发现问题,而不是在最初手动尝试调用的那台机器上发现。
这正是 TrenordClient 在构造函数中设置这两个标头的全部原因。
可选字段是缺失,而不是 null
如果没有内容需要报告,Trenord 载荷中根本不会出现 average_crowding、average_crowding_label、suppression_type 和 alerts。它们不是 null:这些属性是缺失的。
已针对五趟常规列车(S5、S11、RE_5、R27)进行验证:它们都不包含这四个字段。因此,假设这些字段必定存在的代码,在有问题的列车上能正常工作,却会在没有问题的列车上崩溃——这恰恰是最糟糕的情况。
时间戳有三种不同格式
- Trenord 的
dep_time/arr_time——本地时间,格式为"HH:MM:SS"。可以安全显示。 - Trenord 的
dep_date_time/arr_date_time——采用带Z的 ISO 8601 格式,时间为 UTC。方便进行日期运算,但直接显示会出错。 - ViaggiaTreno——纪元毫秒数,必须明确指定
Europe/Rome进行转换。使用机器的本地时间转换,在意大利笔记本电脑上结果正确,在 UTC 服务器上则不正确。
ViaggiaTreno 需要 JavaScript 对日期的格式
列车板端点要求时间戳采用 Date.toString() 生成的格式:
Mon Aug 10 2026 20:20:00 GMT+0200
日期和月份名称必须使用英文。它们是通过固定数组生成的,这是刻意为之:如果使用机器的区域设置来格式化,在意大利系统上就会得到 lun ago,而端点会静默地返回空结果。
另外还有两个小问题
- ViaggiaTreno 的列车查询返回的是纯文本,不是 JSON——每趟列车运行一行,
|后面是实时进度端点所需的字段。 - 如果没有内容需要报告,它还会返回空响应体,而不是空数组,这会导致朴素的反序列化器抛出异常。
train_operator使用$:$作为分隔符,且是字面值。
读取运营方的文件,而不是门户网站上的副本
Trenord 以 CC0 许可发布了这份时刻表的 GTFS 压缩包,同一门户网站也提供了展开后的版本,每个文件对应一张可查询的表格。表格看起来更方便——分页 JSON、可筛选,无需解压压缩包——但它们是有损副本。与原始文件相比:
| 压缩包 | 表格 | |
|---|---|---|
| 停靠时间 | 90,553 | 68,952 |
| 列车班次 | 8,470 | 6,265 |
四分之一的时刻表在导入时丢失了,而且丢失的方式并不规整:列车班次会被截断。11827 次列车从 Varese 开往 Milano,并继续前行;在表格中,它到 Porta Garibaldi 就停了,因此所有可以继续乘坐它抵达的地点都不可见,而 Varese 到 Bergamo 的行程比已发布的答案多花了一个小时。读取压缩包后,时间精确到分钟,与答案一致。
导入过程还造成了另外三处问题,每一处都需要编写实际代码来绕过,而原始文件中没有任何错误:
- 时间被置于占位日期中。 GTFS 会把午夜后的服务写作
24:05;datetime 无法表示这个时间,因此它会变成00:05,导致列车到达时间比出发时间早了 18 个小时。文件中写的是24:05。 - 服务 ID 被改写并附加了哈希,不再与
calendar_dates中的 ID 匹配,因此两个文件无法通过共有的键连接起来。把两者都截取到连字符前的数字,就能再次连接——但这样会把列车的所有季节性变体合并为一个,无法判断当天运行的是哪一个。原始文件中,trips和calendar_dates都写着1001A-2025-12-14-2026-12-12。连接完全匹配,不存在歧义。 trip_short_name被完全丢弃。 这个字段就是列车编号——它是连接计划行程与实时数据的关键字段。
坐标的小数点也会丢失,route_type 也不一致:表格把 R23 和 RE4 称为列车,而文件中称它们为巴士。
先询问运营方,再如实说明
这份文件也并非运营方自己的答案,而且有必要准确说明差距有多大。我们在同一天,将 28 段行程与运营方自有规划器发布的结果进行了核对:
- Milano Centrale 到 Bergamo:数据源显示乘坐 2217 次列车需要 48 分钟,运营方的答案是 52 分钟。
- Pavia 到 Mortara:数据源显示 10668 次列车于 09:28 到达,答案是 09:23。
- Lecco 到 Bergamo:数据源显示 10719 次列车于 08:52 到达 Ponte S.Pietro,而接驳巴士于 08:51 发车——数据源本身给出的换乘不可能实现,但运营方显示有 5 分钟的换乘时间。
运营方使用 HAFAS 处理融合了实时信息的内部时刻表。它的 GTFS 导出文件并不等同于内部时刻表。不过,这里的实时数据来源是运营方自己的系统,而且文件中带有列车编号,因此可以直接查询每一段行程。运营方有结果时,会显示其时间,并在旁边括号中保留时刻表时间,同时附上站台、延误和取消信息。
针对同样的 28 段行程,这让完全一致的结果从 22 段提升到了 27 段。
剩下的那一段体现了这种方法的局限。Lecco 到 Bergamo 的结果仍然是 09:01 的巴士,而不是 08:51 的那班,因为数据源称列车于 08:52 到达,搜索在进行任何核对之前就排除了这次换乘。规划完成后的修正能改正显示内容,却无法找回被错误数据排除的方案。实时数据只会查询今天和明天——再往后,就没有实时数据可查了。
边境另一侧的规划器并不知道自己迷路了
区域时刻表覆盖不到的地方,行程规划就会退回到瑞士开放数据。这个规划器覆盖瑞士,并延伸至边境附近的意大利。它不覆盖该国其他地区——但问到这些地方时,它并不会说明这一点。它会在自己的索引中匹配名称,然后针对找到的内容作答。
当被要求规划 Milano Centrale 到 Roma Termini 的行程时,它自信地给出了一份格式正确、耗时两小时的行程,目的地却是瑞士洛迦诺 Via Bossi 2 的 LaCLINIQUE。Roma Termini 到 Napoli Centrale 的行程则变成了一趟需要换乘四次、耗时四小时的旅程,终点是沃州 Lucens 的一个街道地址。
国籍不是能发现问题的判断标准:瑞士索引包含 Zurich,而 ViaggiaTreno 包含 Zurich Altstetten,因此,“这两个车站是否都在意大利”这一判断会拒绝真正前往 Zurich 的行程,却放过那家诊所。有效的判断标准是答案是否在说用户询问的地点——至少有一个有意义的共同词,忽略重音符号,因此 Zurich 能匹配 Zürich HB,而 Roma Termini 在 Locarno 找不到任何匹配。未通过这一判断的答案会被丢弃,而不会直接传递;回复会列出那些确实能处理这些车站的实时工具。
准确度如何,以及如何测量
我们将运营方自己的规划器公布的二十八条行程,与本工具返回的结果进行了比较——同一天、同一时间、覆盖该地区的十三条路线;对于两者不一致的情况,则查询实时列车数据来裁定。二十八条中有二十七条精确到分钟。
这同时衡量了两件事,值得将它们区分开来。在这样规模的网络中,路线规划本来就不是真正的难点:这个数字主要衡量的是开放时刻表数据还原运营方实际运行时刻表的忠实程度。答案是:非常接近,但并非完全一致。
开放数据源是一次导出。运营方使用内部系统进行规划,并将实时信息纳入其中,然后按自己的计划将该系统的快照以 GTFS 格式发布。快照总是滞后于它所记录的对象——这就是快照的本质,并非任何一方的缺陷——这种滞后会在少数列车上造成几分钟的误差。前往 Bergamo 的列车晚了四分钟。前往 Mortara 的晚了五分钟。前往 Ponte S.Pietro 的晚了六分钟,在那里,这几分钟决定了能不能赶上接驳巴士。
这些问题无法通过开放时刻表数据修复,因为其中没有正确的数据。我们能做的是不再把时刻表当作最终依据:每一段行程都包含列车编号,而实时数据源属于运营方,因此我们会逐段查询,并向运营方核实。正是这一步,让匹配数从二十二条上升到了二十七条。
第二十八条行程正是这种方法坦诚面对的局限。 Lecco 到 Bergamo 的行程仍显示乘坐 09:01 的接驳巴士,而不是 08:51 的那班。数据源显示进站列车于 08:52 抵达 Ponte S.Pietro,比接驳巴士发车晚了一分钟,因此搜索在依据运营方数据核实之前就排除了这次换乘。规划完成后再校正,只能修正展示的信息;错误数据已经排除的选项,无法因此恢复。要做得更好,就必须基于校正后的时间进行规划,也就是说要校正整份时刻表,而不只是答案恰好用到的几段行程——这会是另一个项目,而且规模要大得多。
因此,这种方法的立场是:准确说明已知信息,标明每个数字的来源,并在分秒必争时去向运营方核实。知道哪些答案不可信的工具,比处处都自信的工具更有用。
覆盖范围
Trenord 覆盖自家车队,包括 FNM。ViaggiaTreno 覆盖 RFI 网络,并且时有时无地覆盖 FNM 的一部分。因此,get_train 会先查询 Trenord,失败后再查询 ViaggiaTreno;而车站看板仅由 ViaggiaTreno 提供。
区域时刻表沿跨境线路延伸,因此边境另一侧的车站会被视为国内行程规划,而非国际行程。实时 API 的数据在边境处截止,因此跨境路段只有计划时刻。
开发
dotnet build
dotnet test
五十项测试,分为三个层级:
- 客户端,针对实时端点测试——请求头规则、时间戳格式,以及预期为空数组时却返回空响应体的情况;
- 工具,以模型调用它们的方式进行调用,断言回答所表达的内容,而非措辞方式:遇到有歧义的名称时会反问,遇到未知地点时会说明覆盖范围的终点,遇到计划行程时会说明其时刻不包含延误;
- 服务端,作为进程启动,并通过 JSON-RPC 通信——包括握手、公布的工具及其架构,以及除协议内容外不得有任何内容输出到 stdout 的规则。
没有任何测试会断言某个出发时间。时刻表每天都会重新发布,围绕今天 08:24 编写的测试下个月就会无缘无故失败,让读到它的人学会忽略整个测试套件。测试断言的是无论时刻表如何变化都成立的属性:时间向前推进,换乘时间足够,行程从指定的起点出发并抵达指定终点,结果按工具所声称的方式排序。这些也是实际出过问题的地方。
这些测试有意针对实时端点运行。模拟测试只能证明模拟结果符合预期,而这里所有值得捕获的 bug 都源于实际载荷与预期不符——包括上文的双请求头规则,它就是通过一项与其依据的文档相矛盾的测试发现的。
需要 .NET 10 SDK。
限制
- 行程规划覆盖 Lombardy 和跨境线路;实时数据覆盖意大利全境。 出发看板、到达信息、直达连接和列车追踪可用于 Roma Termini、Napoli Centrale、Palermo 和 Bari;但无法规划这些地点之间的路线,系统会明确说明,而不会擅自编造。
- 最多换乘一次。 两次换乘会同时扩大搜索空间和悄然出错的可能性。若行程需要更多次换乘,回答会说明触及了哪项限制,而不是报告未找到结果。
- 查询日期对应的运营商时刻与未查询日期对应的时刻表时刻。 实时数据源仅查询今天和明天;其他日期没有可查询的实时数据,回答会说明这一点。
- 部分列车相差几分钟。 某些服务公布的时刻表比运营商自己的时刻表慢四到六分钟。两者不一致的路段会标明,不宜据此安排只有四分钟的换乘。
- 不对车站之间的步行路程建模。 有些城镇的两个车站相距几百米,却由不同线路服务;需要步行换乘这两个车站的行程不会被找到。
- 只读。不提供预订、售票或账户访问功能。
- ViaggiaTreno 通过纯 HTTP 提供服务,且偶尔不可用。
- 实时 API 没有文档,且可能随时变更。测试有意针对这些 API 运行:如果有测试开始失败,这正是预期的警报。
许可证
MIT。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。