Lombardia Trains について
Lombardia Trains は、ロンバルディア州の鉄道向け MCP サーバーです。列車のリアルタイム情報と、乗り換え 1 回までの経路検索を提供します。Regione Lombardia の時刻表オープンデータと ViaggiaTreno、Trenord の公開 API を利用し、API キーは不要です。
主な機能
- 01出発・到着列車の検索
- 02遅延・番線・運休の確認
- 03列車の停車駅と運行状況の追跡
- 04直通列車の検索
- 05乗り換え 1 回までの経路計画
- 06曖昧な駅名の明示
活用シーン
- 01ミラノの駅でリアルタイムの出発情報を確認
- 02列車の位置、遅延、番線を確認
- 03ロンバルディア州内の鉄道経路を計画
MCP 対応
時刻と駅の検索、発着案内、直通列車の検索、経路計画、列車状況確認の MCP ツールを提供します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
Lombardy Trains MCP
ロンバルディア州の列車に関する質問に答える MCP サーバーです。発着案内、リアルタイムの遅延、番線、停車駅ごとの運行状況、運休、混雑状況を確認できるほか、Regione Lombardia がオープンデータとして公開している地域時刻表を使って、乗り換えを含む経路を計画します。公開されている 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 を指定できます。
呼び出し元が人ではなく言語モデルであるため、3 つの動作を備えています。いずれも、確信を持って誤った回答をするのを防ぐためのものです。
nowが用意されている理由は、モデルがローマの現在の日付を確実に把握できないためです。「今夜」「土曜日」など、相対的な表現を含む質問では、ほかのツールを呼び出す前に現在の日付を確認する必要があります。- 曖昧な駅名は、確認なしに特定されません。「Milano」は 26 駅に該当し、多くの町には国鉄の駅と、それとは別に「Nord」と名の付く駅があり、両者にはまったく異なる列車が停車します。ツールは候補一覧を返して確認を求め、最初の候補を選んで確信があるかのように答えることはありません。
- 対象外の場所については、サービスの対応範囲を示します。 Lugano について尋ねると、サービスの対応範囲を説明します。そのため、モデルはスイス行きの列車を作り上げるのではなく、回答できないと伝えられます。
find_connection が検索するのは直通列車のみで、そのことも明示します。時刻表ではなく、リアルタイムの発車案内と、候補となる各列車の停車駅一覧を参照するため、遅延や番線を反映できますが、乗り換えを組み合わせた経路は検索できません。
find_journey は乗り換えを含む経路を組み立てます。地域時刻表を使って経路を計画し、時刻表の対象外の場所についてはスイスのオープンデータに切り替えます。返されるのは遅延を反映しない予定時刻です。この 2 つのツールは同じ質問の異なる側面に答えるもので、併せて使うことを想定しています。
経路は出発時刻ではなく、到着時刻で順位付けします。行き先への行き方を尋ねる人は、できるだけ早く到着したいと考えます。出発時刻で順位付けすると、4 分早く出発して到着が 40 分遅い列車が一覧の先頭に来てしまいます。
検索する乗り換えは 1 回までです。乗り換えが 2 回になると、検索対象が大幅に増えるだけでなく、気付かないうちに誤る可能性も高まります。また、このネットワークでは、行く価値のある場所のほとんどへ 1 回の乗り換えで行けます。そのため、この上限は検索が不完全なまま隠すのではなく、明示しています。
読む価値のある部分
上流の API はどちらも公開されていますが、ドキュメントがなく、扱いにくい部分もあります。このリポジトリで大半の作業を占めるのは API の呼び出しではなく、API に対処することです。以下の各項目はコードで強制され、テストでも確認されています。
Trenord ではヘッダーが 1 つではなく 2 つ必要です
リクエストに Accept ヘッダーと User-Agent の両方が含まれていないと、Trenord は 403 Forbidden を返します。
| リクエスト | 結果 |
|---|---|
| ヘッダーなし | 403 |
Accept: */* のみ | 403 |
User-Agent のみ | 403 |
| 両方 | 200 |
これは片方だけ正しく設定してしまいがちです。curl や Python でエンドポイントを調べると、どちらも要求されなくても独自の User-Agent を送信するため、Accept だけで十分だと思われます。.NET の HttpClient は、指定されない限りどちらのヘッダーも送信しません。そのため、Accept だけを設定したクライアントは 403 を受け続け、手動で最初に試したマシンではなく、本番環境で初めてそのことに気づきます。
これが、TrenordClient のコンストラクターで両方を設定している理由です。
オプションのフィールドは null ではなく、存在しません
average_crowding、average_crowding_label、suppression_type、alerts は、報告する内容がない場合、Trenord のペイロードにまったく含まれません。null なのではなく、プロパティ自体がありません。
定期運行の列車 5 種(S5、S11、RE_5、R27)で確認したところ、4 つのフィールドはいずれにも含まれていませんでした。したがって、これらのフィールドが存在すると想定するコードは、問題のある列車では動作し、問題のない列車ではクラッシュします。これは最悪の動作です。
タイムスタンプには 3 種類の形式があります
- Trenord の
dep_time/arr_time— ローカル時刻で、"HH:MM:SS"形式です。そのまま表示できます。 - Trenord の
dep_date_time/arr_date_time—Zが付いた UTC の ISO 8601 形式です。日付の計算には便利ですが、そのまま表示するのは適切ではありません。 - ViaggiaTreno — エポックミリ秒です。必ず
Europe/Romeを明示して変換してください。マシンのローカル時刻で変換すると、イタリアのノートパソコンでは正しい結果になりますが、UTC サーバーでは誤った結果になります。
ViaggiaTreno が求めるのは JavaScript の日付形式です
ボードのエンドポイントは、Date.toString() と同じ形式のタイムスタンプを受け取ります。
Mon Aug 10 2026 20:20:00 GMT+0200
曜日名と月名は英語でなければなりません。固定配列を使っているのはそのためです。マシンのロケールを使って書式設定すると、イタリア語のシステムでは lun ago となり、エンドポイントは何も返さずに終了します。
そのほかの小さな注意点 2 つ
- ViaggiaTreno の列車検索は JSON ではなくプレーンテキストを返します。実行ごとに 1 行で、ライブ進捗エンドポイントに必要なフィールドが
|の後に続きます。 - また、報告する内容がない場合は空の配列ではなく空の本文を返すため、単純なデシリアライザーは例外をスローします。
train_operatorの区切り文字は、文字どおり$:$です。
ポータルのコピーではなく、運行事業者のファイルを読み込む
Trenord は、この時刻表を CC0 の GTFS zip として公開しており、同じポータルでは、ファイルごとにクエリ可能なテーブルに展開したものも提供しています。ページ分割された JSON でフィルターも使え、zip を解凍する必要もないため、テーブルのほうが手軽に見えますが、これは情報が失われたコピーです。元になったファイルと比較すると、次のようになります。
| zip | テーブル | |
|---|---|---|
| 停車時刻 | 90,553 | 68,952 |
| 列車便 | 8,470 | 6,265 |
時刻表の4分の1がインポート時に失われます。しかも、きれいに消えるわけではありません。列車便が途中で切り詰められます。列車11827はヴァレーゼからミラノまで走り、その先へ続きますが、テーブルではポルタ・ガリバルディで止まっています。そのため、乗り続けて行ける場所はすべて見えなくなり、ヴァレーゼからベルガモまでの所要時間は、公開されている回答より1時間も長くなりました。zip を読み込むと、分単位まで一致します。
インポートによって壊れる点は、ほかにも3つあります。どれも回避するために実際のコードが必要になりますが、ファイル自体には問題がありません。
- 時刻が仮の日付の中に収まります。 GTFS では深夜をまたぐ運行を
24:05と記述します。datetime ではこの値を扱えないため、00:05になり、列車は出発の18時間前に到着することになります。ファイルには24:05と記されています。 - 運行IDがハッシュ付きに書き換えられ、
calendar_dates内のIDと一致しなくなるため、2つのファイルを共通のキーで結合できません。ハイフンより前の番号だけにすれば再び結合できますが、列車の季節運行のすべてが1つにまとめられ、今日どの運行があるのか分からなくなります。ファイルでは、tripsとcalendar_datesの両方に1001A-2025-12-14-2026-12-12と記されています。結合は正確で、曖昧さもありません。 trip_short_nameが完全に削除されます。 これは列車番号、つまり予定された区間とリアルタイムデータを結び付ける唯一のフィールドです。
座標の小数点も失われ、route_typeにも違いがあります。テーブルではR23とRE4を列車としていますが、ファイルではバスとしています。
運行事業者に問い合わせ、そのことを明示する
このファイルも運行事業者自身による回答ではありません。その差の大きさを正確に把握しておく価値があります。同じ日の、運行事業者独自の経路検索で公開されている28件の移動と照合しました。
- ミラノ中央駅からベルガモ:フィードでは列車2217の所要時間は48分、運行事業者の回答では52分です。
- パヴィアからモルターラ:列車10668はフィードでは09:28着、運行事業者の回答では09:23着です。
- レッコからベルガモ:列車10719はフィードでは08:52にポンテ・サン・ピエトロに到着しますが、乗り継ぎのバスは08:51に出発します。フィード上では不可能な乗り換えですが、運行事業者の回答では5分の余裕をもって乗り継げます。
運行事業者は、リアルタイム情報を統合した内部時刻表を使ってHAFASを運用しています。そのGTFSエクスポートは、運行事業者の回答そのものではありません。しかし、ここで使うライブデータの提供元は運行事業者自身であり、ファイルには列車番号が記載されているため、各区間をそのまま照会して確認できます。回答が得られた場合はその時刻を表示し、時刻表の時刻は横に括弧書きで残します。ホーム、遅延、運休の情報もあわせて表示されます。
同じ28件の移動で、完全一致は22件から27件に増えました。
残る1件は、これでできることの限界を示しています。レッコからベルガモへの回答は、08:51発ではなく09:01発のバスのままです。フィードでは列車が08:52着となっていたため、確認が行われる前に検索がその乗り継ぎを除外していたからです。経路検索後に修正すれば表示内容は直せますが、誤ったデータによって除外された経路を取り戻すことはできません。ライブデータへの問い合わせは今日と明日の分だけで、それより先については問い合わせられるライブ情報がありません。
境界の向こうのプランナーは、自分が迷っていることに気づいていません
地域の時刻表が届かない場所では、経路検索はスイスのオープンデータに切り替わります。このプランナーはスイスをカバーし、国境付近ではイタリアにも及びます。しかし、国内のそれ以外の地域はカバーしていません。それなのに尋ねても、そのことを伝えません。独自のインデックスで名前を照合し、見つかったものについて回答します。
Milano Centrale から Roma Termini までの経路を尋ねたところ、自信たっぷりに、書式も正しい2時間の旅程を返しました。目的地は スイスのLaCLINIQUE、Locarno、Via Bossi 2 でした。Roma Termini から Napoli Centrale までの経路は、4回乗り換え、4時間の旅程となり、Vaud州のLucensにある番地で終わりました。
国籍を基準にしても、この問題は見つかりません。スイスのインデックスにはZurichがあり、ViaggiaTrenoにはZurich Altstettenがあるため、「両方の駅がイタリアにあるか」と尋ねると、Zurichへの実際の経路を拒否する一方で、診療所への経路は通してしまいます。有効なのは、回答が尋ねた場所についてのものかどうかを確かめる方法です。アクセント記号を同一視したうえで、意味のある単語が一つ以上共通しているかを確認します。これなら Zurich は Zürich HB に一致し、Roma Termini はLocarnoのどの場所にも一致しません。この条件を満たさない回答はそのまま渡さずに破棄し、該当する駅で実際に機能するライブツールを案内します。
精度と、その測定方法
運行会社独自のプランナーが公開している28件の経路を、このツールの結果と比較しました。同じ日、同じ時刻で、地域内の13路線を調べ、両者に違いがあればライブの列車データで確認しました。28件中27件が、分単位で一致しています。
これは同時に2つのことを測定しており、分けて考える価値があります。この規模のネットワークでは、経路検索自体が本当に難しい部分だったわけではありません。この数値が主に示しているのは、オープンな時刻表データが、運行会社の実際のダイヤをどれほど忠実に再現しているかです。答えは「かなり忠実だが、完全ではない」です。
オープンフィードはエクスポートデータです。運行会社はリアルタイム情報を織り込んだ社内システムで計画を立て、そのシステムのスナップショットを、独自のスケジュールでGTFSとして公開します。スナップショットが元の対象より遅れるのは当然です。これはスナップショットの性質であって、誰かの不具合ではありません。その遅れは、一部の列車で数分の差となって現れます。Bergamo方面では4分。Mortaraでは5分。Ponte S.Pietroでは6分です。そこでは、その差によって乗れるバスと乗れないバスが分かれます。
正しい値はデータに含まれていないため、オープンな時刻表データだけでは、こうした問題を修正できません。できるのは、時刻表を最終的な答えとして扱わないことです。各区間には列車番号があり、ライブ情報の提供元は運行会社です。そこで各区間を検索し、運行会社に確認します。これによって、28件中22件の一致が27件になりました。
28件目は、この方法の限界を正直に示しています。 LeccoからBergamoへの経路は、08:51のバスではなく、09:01のバスを案内したままです。フィードでは、列車がPonte S.Pietroに到着する時刻が08:52となっており、そのバスの出発時刻より1分遅いため、運行会社のデータと照合する前に検索で乗り継ぎが除外されていました。計画を立てた後で修正すれば表示内容は直せますが、誤ったデータによって除外された経路を復元することはできません。改善するには、修正済みの時刻で経路を計画する必要があります。つまり、回答にたまたま使われた数区間だけでなく、時刻表全体を修正することになります。これは別のプロジェクトであり、はるかに大規模です。
そのため、このアプローチでは、判明していることを正確に示し、あらゆる数値の出典を明記し、1分が重要な場合は運行会社に確認することを重視します。どの回答を疑うべきかを理解しているツールは、どんな場合も自信満々なツールより役に立ちます。
対象範囲
Trenord は、FNM を含む自社の車両を対象としています。ViaggiaTreno は RFI のネットワークを対象とし、予測不能ながら FNM の一部も対象としています。そのため、get_train はまず Trenord に問い合わせ、次に ViaggiaTreno にフォールバックします。一方、駅の発着案内は ViaggiaTreno でのみ利用できます。
地域の時刻表は国境を越える路線にも及ぶため、国境の向こうにある駅も国際移動ではなく国内移動として計画されます。リアルタイム API は国境で途切れるため、その先の区間には予定時刻のみが表示されます。
開発
dotnet build
dotnet test
3 つのレベルで、50 件のテストを実施します。
- クライアント:実際のエンドポイントを対象に、ヘッダーのルール、タイムスタンプの形式、空の配列が想定される場面での空の本文を確認します。
- ツール:モデルが呼び出す方法で実行し、回答の言い回しではなく、回答が示す内容を検証します。曖昧な名称には質問が返され、対象外の場所には対応範囲の終点が示され、予定された移動には遅延が反映されていない時刻であることが示されます。
- サーバー:プロセスとして起動し、JSON-RPC で通信します。ハンドシェイク、公開されているツールとそのスキーマ、プロトコル以外のものを標準出力に出さないルールを確認します。
出発時刻を検証するテストはありません。時刻表は毎日再公開されるため、今日の 08:24 を基準に書いたテストは来月には何の理由もなく失敗し、それを読んだ人にテストスイートを無視するよう教えることになります。検証するのは、時刻表の内容にかかわらず成り立つ特性です。時刻が前に進むこと、乗り換えに十分な時間があること、移動が指定どおりの場所で始まり、終わること、結果がツールの説明どおりに並ぶことです。実際に壊れていたのも、こうした特性です。
意図的に実際のエンドポイントを使って実行します。モックを使っても、想定にモックが一致することしか証明できません。ここで検出すべきだったバグはすべて、実際のペイロードが想定と食い違ったことに起因しています。上記の 2 つのヘッダーのルールも、元にしたドキュメントと矛盾することをテストで見つけたものです。
.NET 10 SDK が必要です。
制限事項
- 経路計画はロンバルディア州と国境を越える路線が対象です。リアルタイムデータはイタリア全土が対象です。 発着案内、到着案内、直通接続、列車追跡は Roma Termini、Napoli Centrale、Palermo、Bari で利用できますが、これらの駅間の経路計画には対応していません。その場合は、無理に結果を出さず、その旨を伝えます。
- 乗り換えは 1 回までです。 乗り換えが 2 回になると、検索空間と、誤りに気づきにくくなる可能性の両方が増します。それ以上の乗り換えが必要な場合は、結果が見つからないとだけ報告するのではなく、どの制限に達したかを回答で伝えます。
- 問い合わせ先から得られる時刻を使いますが、問い合わせできない場合は時刻表の時刻を使います。 リアルタイムの情報源に問い合わせるのは今日と明日です。それ以外の日はリアルタイム情報を取得できないため、その旨を回答で伝えます。
- 一部の列車では数分の差があります。 公開されている時刻表は、一部の列車で運行事業者の独自情報より 4〜6 分遅れています。両者に食い違いがある区間には印を付けており、4 分で乗り換える計画には使わないでください。
- 駅間の徒歩移動はモデル化されていません。 数百メートルしか離れていない 2 つの駅があり、別々の路線が乗り入れている町もあります。徒歩で駅間を移動する乗り換えを含む経路は見つかりません。
- 読み取り専用です。予約、発券、アカウントへのアクセスには対応していません。
- ViaggiaTreno はプレーン HTTP で提供されており、時折利用できなくなります。
- リアルタイム API の仕様は公開されておらず、予告なく変更されることがあります。意図的に API を対象にテストを実行しています。テストが失敗し始めたら、それが意図した警告です。
ライセンス
MIT。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。