Lviv Public Transport について
Lviv Public Transport は、リヴィウの交通を調べる乗客や旅行アプリ向けの読み取り専用 API・MCP サーバーです。GTFS の時刻表と GTFS-RT の更新情報を組み合わせ、車両のリアルタイム情報や経路、運行変更を提供します。
主な機能
- 01停留所と周辺停留所の検索
- 02リアルタイム到着と車両位置の確認
- 03直通または1回乗り換えの経路検索
- 04路線時刻表と車両の確認
- 05運休、迂回、停留所変更の確認
- 06地図と到着リストのデータ提供
活用シーン
- 01近くの停留所と利用可能な路線の検索
- 02停留所のリアルタイム到着確認
- 032地点間の公共交通経路の計画
- 04車両の運行状況と迂回の確認
MCP 対応
Streamable HTTP 経由で読み取り専用の MCP ツールを公開し、停留所、到着情報、路線、車両、経路、運行変更を検索できます。MCP クライアント向けにテキスト要約と構造化データを返します。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://api.lad.lviv.ua/mcpclaude mcp add --transport http vbhjckfd-timetable-api-node https://api.lad.lviv.ua/mcp{"mcpServers":{"vbhjckfd-timetable-api-node":{"type":"http","url":"https://api.lad.lviv.ua/mcp"}}}Cursor で開く{"servers":{"vbhjckfd-timetable-api-node":{"type":"http","url":"https://api.lad.lviv.ua/mcp"}}}VS Code で開く{"mcpServers":{"vbhjckfd-timetable-api-node":{"type":"http","url":"https://api.lad.lviv.ua/mcp"}}}{"mcpServers":{"vbhjckfd-timetable-api-node":{"serverUrl":"https://api.lad.lviv.ua/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
8| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| stop_id | integer | はい | 停留所標識に表示される市営停留所コード(例:707)。正の整数、または数字のみの同等の文字列を指定できます。 |
詳細
Timetable API Node
リヴィウの交通機関の時刻表データを提供する、読み取り専用 MCP エンドポイント付きの Express ベース API です。
要件
- Node.js 26(
.nvmrcを参照)
ローカルで実行
nvm use
npm install
cp .env.dist .env
make import # first run, and whenever the GTFS feed changes
make start
make importは静的GTFSフィード(gtfs-import-config.json)をダウンロードし、サーバーが読み込む2つのストアを書き込みます。database/Timetable(LokiJS:停留所、路線、形状、始発停留所の時刻表)とdatabase/gtfs(SQLite:便とカレンダー)です。どちらもGitの追跡対象外のため、新規にクローンした直後は実行するまでデータがありません。
make import-slimはSQLiteの軽量テーブルのみを再インポートし、database/Timetableには変更を加えません。
設定
すべての変数は省略可能です。.env.distを参照してください。
| 変数 | デフォルト値 | 用途 |
|---|---|---|
PORT | 8080 | HTTPポートです。 |
VEHICLES_LOCATION_URL | https://track.ua-gis.com/gtfs/lviv/vehicle_position | GTFS-RT車両位置フィードです。 |
TRIP_UDPDATES_URL | https://track.ua-gis.com/gtfs/lviv/trip_updates | GTFS-RT便更新フィードです(このスペルミスが実際の名前です)。 |
MCP_PUBLIC_BASE_URL | https://api.lad.lviv.ua | MCPメタデータ(アイコンURL)で使用するベースURLです。 |
NEW_RELIC_LICENSE_KEY | — | New Relic APMを有効にします(モニタリングを参照)。 |
NEW_RELIC_APP_NAME, NEW_RELIC_LOG_LEVEL | timetable-api-node, info | New Relicエージェントの設定です。 |
PULSE_WORKER_URL, PULSE_SIGNAL_SECRET | — | /stops/:code/timetableリクエストごとにPulseワーカーへシグナルを送信します。両方とも設定する必要があります。 |
MCP_URL | https://api.lad.lviv.ua/mcp | timetable-mcpのstdioプロキシ専用です。転送先のリモートエンドポイントです。 |
テスト
nvm use && make test
TypeScript
コードはTypeScriptで書かれており、Node組み込みの型ストリッピング機能を使って.tsソースから直接実行されます。サーバー、GTFSインポート、スクリプトのいずれにもビルド手順はありません。そのため、消去可能な構文のみを使用し(enum、namespace、パラメータープロパティは不可)、型のみのインポートにはimport typeを使い、相対インポートでは.tsファイルを指定します。tsconfig.jsonがこれら3つすべてを適用します。
npm run typecheck # tsc, no emit
npm run buildは、公開対象のbinであるmcp/stdio.jsだけを出力します。Nodeはnode_modules内の型をストリッピングしないため、npmパッケージにはJavaScriptを含める必要があります。この処理はnpm pack/npm publishで実行され、出力ファイルはコミットされません。
public/stopOverrides.jsはブラウザーにそのまま配信されるため、JavaScriptのままにして、JSDocを使って型チェックします。
監視
New Relic APM は、NEW_RELIC_LICENSE_KEY が設定されていない場合は無効です。エラーと、utils/metrics.ts を通じて記録されるカスタムメトリクスも送信します。
New Relic はプリロードされたエージェントとして実行されるため、npm start には次のフラグが含まれます。
node -r dotenv/config -r newrelic --import newrelic/esm-loader.mjs index.ts
エージェントが設定を読み込む前に .env を読み込めるよう、dotenv/config を最初にプリロードします。設定ファイルは newrelic.cjs です(エージェントは CommonJS ですが、このプロジェクトは ESM です)。このファイルにシークレットは含まれず、キーは環境変数から取得します。rules.ignore により、/health はトランザクションから除外されます。
アカウントは EU リージョンにあります。ライセンスキーは eu01xx で始まり、エージェントはこのプレフィックスからコレクターを選択します。NRAK-... ユーザー API キーではなく、40 文字の取り込み用ライセンスキーを使用してください。
Cloud Run は Secret Manager からキーを読み込みます。
gcloud run services update timetable-api-node --region=europe-north1 --project=timetable-252615 --set-secrets=NEW_RELIC_LICENSE_KEY=new-relic-license-key:latest
MCP サーバー
このサービスは、Streamable HTTP 経由で読み取り専用の公開 MCP エンドポイントを提供します。
- MCP エンドポイント:
/mcp - サーバーカード:
/.well-known/mcp/server-card.json - 検出ヒント:
/robots.txt(非標準のコメントヒント)
本番環境へのデプロイ(Cloud Run については cloudbuild.yaml を参照)では、api.lad.lviv.ua から REST と MCP を提供します。メインサイトの lad.lviv.ua は公共交通機関のウェブサイトです(このリポジトリでは、API ホスト用ではなく人向けに、HTML サイトマップとテーブルから引き続きリンクしています)。ローカルで実行する場合は、独自のオリジンを使用してください。
LLM と /mcp のフロー
MCP クライアント(Claude、Cursor、または MCP SDK)は、Streamable HTTP 経由で POST /mcp に JSON-RPC リクエストを送信します。ツールハンドラーは、REST API と同じ Express アクションを再利用し、LokiJS の時刻表データ、GTFS SQLite(gtfs 経由)、および ライブ GTFS-RT フィード(例:track.ua-gis.com)を利用します。
graph LR;
Client[LLM or MCP client] -->|JSON-RPC Streamable HTTP| Mcp["POST /mcp"];
Mcp --> Tools[Tool handlers];
Tools --> Actions[Express actions];
Actions --> Loki[(LokiJS)];
Actions --> Gtfs[(GTFS SQLite)];
Actions --> Rt[GTFS-RT upstream];
Loki --> Actions;
Gtfs --> Actions;
Rt --> Actions;
Actions --> Tools;
Tools --> Mcp;
Mcp -->|MCP tool result| Client;
ライブ API を試す
MCP Inspector(ローカル): npx @modelcontextprotocol/inspector を実行し、トランスポートとサーバー URL があらかじめ入力された UI を開きます(Inspector README より)。
http://localhost:6274/?transport=streamable-http&serverUrl=https%3A%2F%2Fapi.lad.lviv.ua%2Fmcp
Postman / curl:本番環境でツールを呼び出す
Content-Type: application/json および Accept: application/json, text/event-stream を指定して POST https://api.lad.lviv.ua/mcp にリクエストします。両方を受け入れないリクエストは、Streamable HTTP トランスポートによって拒否されます。サーバーはステートレスです。セッションはないため、initialize を先に実行しなくても、tools/call を単独で実行できます。レスポンスは、JSON-RPC の結果を含む単一の SSE event: message フレームとして届きます。
curl -s https://api.lad.lviv.ua/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_stop_realtime","arguments":{"stop_id":101}}}'
tools/call のリクエストボディの例:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_stop_realtime",
"arguments": { "stop_id": 101 }
}
}
ツールが正常に応答すると、MCP の content アイテム(type: "text")内に自然言語のテキスト要約が返されます。例:"Stop «Площа Ринок»: 6 arrivals. Next: Т02 → «Пасічна» in 3 min." 完全な構造化ペイロードは structuredContent フィールドにあります(スキーマ対応クライアント向け)。各 structuredContent ペイロードは、次の契約に従います。
{
"view": "transit_realtime",
"data": { "...": "tool-specific payload" },
"ui_blocks": [
{ "type": "map", "data": { "center": [49.84, 24.03], "zoom": 14, "layers": { "stops": "data.stop", "vehicles": "data.arrivals" } } },
{ "type": "arrival_list", "data": { "source": "data.arrivals" } }
]
}
ui_blocks はデータを重複させず、data 内のデータを参照します。map ブロックには中心座標とズームが含まれ、layers は stops、vehicles、polylines を結果内のドットパスに対応付けます。そこに含まれる各停留所と車両には lat/lng があり、車両にはさらに bearing があります。arrival_list ブロックは、描画する到着情報の配列を指定します。この配列はすでに arrival_minutes 順にソートされています(null = 到着予定時刻なし)。
公開されているツール
すべてのツールは読み取り専用です。停留所 ID は停留所標識に記載された数値コードで、文字列("707")として返されます。路線名は車両に表示された短縮名("Т30"、"А1")です。ラテン文字の "T30"/"A01" も使えます。数字だけを指定すると、内部路線 ID として扱われます。不明な路線または車両を指定すると、代わりに何を渡すべきかを示すヒントとともに isError: true が返されます。
| ツール | 引数 | 戻り値 |
|---|---|---|
search_stops | query(文字列、2 文字以上)、limit(1~25、デフォルト 10) | ウクライナ語名または英語名に、検索語がすべて含まれる停留所(語形変化を許容します。「опера」なら「Театр опери та балету」が見つかります。句読点と見た目の似たラテン文字は無視されるため、「пл. ринок」や「Cихiв」も一致します)。routes も含まれます。 |
get_stops_around_location | latitude、longitude、radius_meters(50~3000、デフォルト 1000) | 指定地点に近い停留所を、近い順に返します。distance_meters と routes も含まれます。 |
get_stop_realtime | stop_id、lowfloor_only(デフォルト false) | ライブ到着情報:route、direction(行き先)、vehicle_type、arrival_minutes、車両位置、lowfloor。車両の到着が予測されない場合に表示される時刻表の項目には、scheduled: true が設定されます。 |
find_routes_between | from_stop_id、または from_lat + from_lng;to_stop_id、または to_lat + to_lng;depart_at または arrive_by(リヴィウ時間の "HH:MM" または ISO 8601、デフォルトは現在時刻) | 各停留所の端点から徒歩 300 m 以内(地点の場合は 600 m 以内)のルートを、最適な順に返します。直通の options(board_stop、alight_stop、destination、stops_count、各端点での徒歩距離)と、300 m 以内の停留所クラスター内で乗り換えが 1 回だけの transfer_options が含まれます。各オプションには schedule(時刻表に基づく推定値:leave_at、区間ごとの board_at/alight_at、arrive_at、total_minutes)が含まれます。現在時刻を基準に計画する場合は、乗車する停留所からのライブ情報 next_departures も含まれます。運休中の路線と迂回によって通過しない停留所は除外されます。端点間が短い徒歩距離の場合は walk_meters が含まれます。 |
get_route_static | route_name、include_shapes(デフォルト false) | 路線名、種類、色、両方向の停留所一覧、始発停留所の時刻表を返します。ポリラインはリクエストした場合のみ含まれます。 |
get_route_realtime | route_name、direction(0 または 1、省略可能) | 路線上の車両を、destination と next_stop(id、name、ISO 形式の arrival)とともに返します。方向ごとの headways には、車両数、先頭から順のライブ gaps_minutes、時刻表上の scheduled_headway_minutes が含まれます。 |
get_nearby_vehicles | latitude、longitude、radius_meters(100~1000、デフォルト 500)、route、limit(1~50、デフォルト 15)、lowfloor_only(デフォルト false) | ライブ車両を近い順に返します。destination と distance_meters も含まれます。total は範囲内の全車両数です。 |
get_vehicle_info | vehicle_id | 1 台の車両について、位置、路線、destination、ナンバープレート、通過前の次の停留所(id、name、ISO 形式の時刻)を返します。 |
get_disruptions | route、stop_id(どちらも省略可能) | 本日有効な、発表済みの運休、迂回(detour.via の道路)、移設された停留所を返します。get_stop_realtime、get_route_static、get_route_realtime、find_routes_between も、対象の路線または停留所に関係する運行情報がある場合は、disruptions 配列を追加します。 |
get_stop_realtime — 例
{
"view": "transit_realtime",
"data": {
"stop": { "id": "61", "name": "Площа Ринок", "lat": 49.84146, "lng": 24.03227 },
"arrivals": [
{
"route": "Т02",
"direction": "Пасічна",
"vehicle_type": "tram",
"arrival_minutes": 3,
"vehicle_id": "2393",
"lat": 49.83461,
"lng": 24.01672,
"bearing": 60
}
],
"updated_at": "2026-09-23T09:01:21Z"
},
"ui_blocks": [
{
"type": "map",
"data": { "center": [49.84146, 24.03227], "zoom": 14, "layers": { "stops": "data.stop", "vehicles": "data.arrivals" } }
},
{ "type": "arrival_list", "data": { "source": "data.arrivals" } }
]
}
search_stops — 例
{
"view": "transit_realtime",
"data": {
"query": "rynok",
"stops": [
{
"id": "61",
"name": "Площа Ринок",
"lat": 49.84146,
"lng": 24.03227,
"eng_name": "Rynok square",
"routes": ["Т01", "Т02"]
}
],
"updated_at": "2026-09-23T09:36:53Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.84146, 24.03227], "zoom": 14, "layers": { "stops": "data.stops" } } }
]
}
通常、1 つの停留所名は道路の両側を別々の ID で表します。それぞれが返され、routes で区別できます。get_stops_around_location は同じ停留所オブジェクトに加えて、distance_meters を返します。
find_routes_between — 例
テキストの要約:「『Площа Ринок』から『Залізничний вокзал』まで直通ルートが 3 本あります。最適なルート:Т01、『Залізничний вокзал』方面、8 停留所、『Руська』から乗車(徒歩 137 m)。」
{
"view": "transit_realtime",
"data": {
"from": { "id": "61", "name": "Площа Ринок", "stop_ids": ["10", "57", "58", "59", "61", "63", "855"] },
"to": { "id": "118", "name": "Залізничний вокзал", "stop_ids": ["117", "118", "188", "189", "190", "191"] },
"options": [
{
"route": "Т01",
"vehicle_type": "tram",
"direction": 0,
"destination": "Залізничний вокзал",
"board_stop": { "id": "58", "name": "Руська", "lat": 49.84186, "lng": 24.03408 },
"alight_stop": { "id": "118", "name": "Залізничний вокзал", "lat": 49.839, "lng": 23.99677 },
"stops_count": 8,
"walk_to_board_meters": 137,
"walk_from_alight_meters": 0
}
],
"transfer_options": [],
"map_stops": [
{ "id": "58", "name": "Руська", "lat": 49.84186, "lng": 24.03408, "role": "board" },
{ "id": "118", "name": "Залізничний вокзал", "lat": 49.839, "lng": 23.99677, "role": "alight" }
],
"updated_at": "2026-09-23T09:36:53Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.84043, 24.01543], "zoom": 13, "layers": { "stops": "data.map_stops" } } }
]
}
transfer_optionsには、乗り換えがちょうど1回の経路が含まれます。2回以上の乗り換えはありません。各候補は2つのlegs(直通候補と同じフィールドですが、徒歩距離を除きます)とtransfer_walk_metersを持ちます。乗り換えは同じ停留所(0 m)でも、第1区間の終点から300 m以内にある任意の停留所でも可能です。そのため、広場や交差点周辺に停留所が集まっている場合、それらを1か所の乗り換え地点として利用できます。目的地まで直通する路線は、どちらの区間にも使われません。また、順位付けでは乗り換え1回につき停留所5つ分までの追加コストがかかります。直通経路がない場合、または直通経路の最良候補より優れている場合に、乗り換え候補を(最大5件)表示します。両方のリストが空の場合、移動には2回以上の乗り換えが必要です。
map_stopsには、最良の経路の停留所が乗車順に並びます(乗り換え候補が表示される場合は、その経路の停留所です。直通経路のどの候補よりも優れている場合にのみ表示されるためです)。各停留所にはroleがあり、board、transfer(同じ停留所での乗り換え)、transfer_alight + transfer_board(徒歩での乗り換え)、alightのいずれかです。mapブロックではこれらの停留所をプロットし、すべての停留所(乗り換え地点を含む)を囲む範囲を中心に表示します。範囲の大きさに応じてズームレベルは14~11です。
get_route_static — 使用例
{
"view": "transit_realtime",
"data": {
"route": { "name": "Т30", "long_name": "Університет - Городоцька - вул. Ряшівська", "color": "#EF88AA", "type": "trolleybus" },
"stops": [
[
{
"id": "101", "name": "Університет", "lat": 49.841, "lng": 24.003,
"departures": ["05:30", "05:52"],
"schedule": { "workday": ["05:30", "05:52", "06:10"], "saturday": ["07:00", "07:30"], "sunday": ["07:00"], "weekend": ["07:00", "07:30"] }
},
{ "id": "707", "name": "Стадіон Сільмаш", "lat": 49.838, "lng": 24.021, "departures": [], "schedule": { "workday": [], "saturday": [], "sunday": [], "weekend": [] } }
],
[
{ "id": "707", "name": "Стадіон Сільмаш", "lat": 49.838, "lng": 24.021, "departures": ["06:00"], "schedule": { "workday": ["06:00"], "saturday": [], "sunday": [], "weekend": [] } }
]
],
"updated_at": "2026-09-23T09:01:12Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.841, 24.003], "zoom": 13, "layers": { "stops": "data.stops" } } }
]
}
stops[0]は方向0(往路)、stops[1]は方向1(復路)です。departuresとscheduleは、各方向の最初の停留所に設定されます。他の停留所の配列は空です。schedule.workdayは月曜日から金曜日、schedule.saturday / schedule.sundayは週末の各日を表します(異なる場合があります)。schedule.weekendは両方をまとめたもので、後方互換性のために保持されています。departuresには、後方互換性のために今日の時刻表が保持されています。include_shapes: trueの場合、data.shapesには方向ごとに1つの[lat, lng]ポリラインが格納され、mapブロックには"polylines": "data.shapes"が追加されます。
get_route_realtime — 使用例
{
"view": "transit_realtime",
"data": {
"route_name": "Т01",
"destinations": ["Залізничний вокзал", "Погулянка"],
"vehicles": [
{
"id": "5907",
"direction": 0,
"destination": "Залізничний вокзал",
"next_stop": { "id": "118", "name": "Залізничний вокзал", "arrival": "2026-09-23T09:35:00.000Z" },
"lat": 49.83947,
"lng": 23.99566,
"bearing": 126,
"lowfloor": true
}
],
"updated_at": "2026-09-23T09:32:10Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.83947, 23.99566], "zoom": 13, "layers": { "vehicles": "data.vehicles" } } }
]
}
route_nameは、渡された形式にかかわらず、標準の短縮名です。directionは、get_route_staticのstops(0 = 往路、1 = 復路)およびdestinationsのインデックスです。フィードに車両の運行更新情報がない場合、next_stopはnullです。
get_nearby_vehicles — 使用例
{
"view": "transit_realtime",
"data": {
"center_lat": 49.8419,
"center_lng": 24.0316,
"radius_meters": 500,
"total": 18,
"vehicles": [
{
"id": "3422",
"route": "Т01",
"vehicle_type": "tram",
"direction": 0,
"destination": "Залізничний вокзал",
"lat": 49.84154,
"lng": 24.03364,
"bearing": 258,
"lowfloor": false,
"distance_meters": 152
}
],
"updated_at": "2026-09-23T09:32:10Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.8419, 24.0316], "zoom": 15, "layers": { "vehicles": "data.vehicles" } } }
]
}
get_vehicle_info — 使用例
{
"view": "transit_realtime",
"data": {
"vehicle_id": "5907",
"route": "Т01",
"license_plate": "1238",
"lat": 49.83947,
"lng": 23.99566,
"bearing": 126,
"direction": 0,
"destination": "Залізничний вокзал",
"upcoming_stops": [
{ "id": "118", "name": "Залізничний вокзал", "arrival": null, "departure": "2026-09-23T09:35:00.000Z" },
{ "id": "188", "name": "Приміський вокзал", "arrival": "2026-09-23T09:35:49.000Z", "departure": null }
],
"updated_at": "2026-09-23T09:32:10Z"
},
"ui_blocks": [
{ "type": "map", "data": { "center": [49.83947, 23.99566], "zoom": 15, "layers": { "vehicles": "data" } } }
]
}
routeは路線の短縮名です。ローカルデータに路線がない場合に限り、代わりに不透明なGTFS路線IDが使われます。どちらの値も、get_route_staticとget_route_realtimeのroute_nameとして使用できます。フィードにナンバープレートがない場合、license_plateはnullです。
プロンプト
再利用可能な指示テンプレートです。stop_idは正の整数、または数字のみの文字列です。
| プロンプト | 引数 | 用途 |
|---|---|---|
plan-trip | from, to | 名前付きの場所(または停留所ID)間の経路を、直通または乗り換え1回で案内し、乗車停留所からのリアルタイム発車時刻を表示します。 |
route-status | route_name | 路線の車両が方向別に現在どこにいるかを表示します。route_nameは補完に対応しています。 |
transit-map-view | stop_id | 停留所のリアルタイム車両情報を、地図を優先して表示します。 |
transit-arrival-list | stop_id | 停留所の到着一覧を、ETA順に並べ、路線ごとにまとめて表示します。 |
transit-hybrid-view | stop_id | 地図ブロックを先に、到着一覧ブロックを後に表示し、両方のETA値を一致させます。 |
リソースとリソーステンプレート
ツールに加えて、サーバーはツール呼び出しを必要としない参照データを リソース として公開しています。
| URI | 説明 |
|---|---|
timetable://about | このサーバーの対象範囲、使用方法、データに関する注意事項(Markdown) |
timetable://reference/tools | ツールの参照表(Markdown) |
timetable://reference/prompts | プロンプトテンプレートのカタログ(Markdown) |
timetable://stop/{code} | 数値コードで指定した停留所の静的情報 — 名称、座標、運行路線(JSON) |
timetable://route/{name} | 短縮名で指定した路線の静的メタデータ — 色、種別、停留所数(JSON) |
timetable://route/{name} の {name} は completion/complete に対応しています(「t3」→「Т30」、「a1」→「А01」)。不明な停留所または路線を指定すると、JSON-RPC エラー -32002(リソースが見つかりません)が返されます。
セキュリティモデル
- 認証なしで公開読み取り専用です。
- データを変更するツールは公開されていません。
POST /mcpは IP あたり毎分 60 リクエスト に制限されています(メモリ内で管理され、再起動時にリセットされます)。制限を超えたリクエストには、JSON-RPC エラーボディを伴う HTTP 429 が返されます。robots.txtはあくまで参考情報であり、プロトコル上の契約ではありません。
REST API
すべてのエンドポイントは JSON を返します。:code は数値の停留所コード、:name は路線の短縮名(例:T1、32A)または数値の外部 ID です。
停留所
GET /stops.json
すべての停留所をコード順に並べた JSON 配列です。
- レスポンス:
{ code, name, eng_name, location: [lat, lng], routes, sign, sign_pdf }の配列です。
(GET /stops は代わりに HTML テーブルを返します。)
停留所ごとの路線の上書き
停留所の上流データに含まれる路線一覧が、実際の状況と異なることがあります。GET /stops は、保存済みの上書きを Маршрути 列に適用します。削除された路線は赤色の取り消し線付きで、追加された路線は緑色で表示されます。また、その行の Табличка(停留所標識)列と Мапа(路線図ポスター、/:code/schema)列にある SVG および PDF のリンクには、対応する ?add=/?remove= が付加されます。offline.lad.lviv.ua と pdf.lad.lviv.ua はどちらもこの指定に対応しています。
路線列は常にクリックできます。路線をクリックすると削除または復元でき、「+」ボックスに路線を入力すると追加できます。
上書きはブラウザー自身の localStorage(public/stopOverrides.js を参照)に保存され、サーバーには保存されません。そのため、編集用アカウントもキャッシュの消去も必要なく、編集内容はただちに反映されます。ただし、その上書きが表示されるのは変更を行ったブラウザーだけであり、ほかの人が /stops を開いても表示されません。
/stops.json では、上書きを適用せずに sign と sign_pdf を返します。
GET /stops/:code
リアルタイムの時刻表を含む、単一の停留所の情報です。短時間キャッシュされます(5~10 秒)。
- 省略可能:
skipTimetableData=1— リアルタイムの到着情報を省略します(長時間キャッシュされるレスポンス)。 - レスポンス:
{ code, name, eng_name, latitude, longitude, transfers, timetable }です。
GET /stops/:code/timetable
停留所のリアルタイム時刻表のみを返します。短時間キャッシュされます(5~10 秒)。
- レスポンス: 時刻表アイテムの配列です。
GET /stops/:code/static
リアルタイムデータを含まない、停留所の静的情報です。長期間キャッシュされます(30 日)。
- レスポンス:
{ code, name, eng_name, latitude, longitude, transfers }です。
GET /closest?latitude={lat}&longitude={lng}
近隣の停留所を返します。MCP 以外のクライアント向けに、get_stops_around_location と同じ検索を行います。
- 省略可能:
radius— メートル単位の半径です。50~3000 の範囲に制限されます(デフォルトは 1000)。 - レスポンス:
{ code, name, latitude, longitude, distance_meters }の JSON 配列です(距離順)。
路線
GET /routes.json
すべての路線を短縮名順に並べた JSON 配列です。
- 省略可能:
full=1— リスト項目の代わりに、時刻表ストア内の生の路線オブジェクト(運行データのマップ、経路形状、発車時刻のマップ。約 9 MB)を返します。 - レスポンス:
{ external_id, short_name, long_name, color, type, stops_by_shape }の配列です(約 20 KB)。stops_by_shapeには、方向ごとの停留所コードが"0"と"1"をキーとして格納されています。
(GET /routes は HTML テーブルを返します。)
GET /routes/static/:name
路線の形状、停留所一覧、メタデータです。30日間の長期キャッシュが適用されます。
- レスポンス:
{ id, color, type, route_short_name, route_long_name, stops: [[dir0…], [dir1…]], shapes }。 - 各停留所オブジェクト:
{ code, name, loc, transfers, departures, schedule }。departures— 本日の出発時刻(HH:MM)です。方向 0 の最初の停留所にのみ設定されます。後方互換性のために保持されています。schedule— 日の種別ごとの出発時刻{ workday: string[], saturday: string[], sunday: string[], weekend: string[] }です(weekend= 土曜日 ∪ 日曜日、非推奨)。方向 0 の最初の停留所にのみ設定されます。
GET /routes/dynamic/:name
路線のリアルタイム車両位置です。10秒間の短期キャッシュが適用されます。
- レスポンス:
{ id, direction, location: [lat, lng], bearing, speed, lowfloor }の配列です。speedは GPS ユニットから取得した m/s 単位の速度で、報告されていない場合はnullです。
車両
GET /vehicle/:vehicleId
車両1台のリアルタイム位置と、次の停留所への到着情報です。5秒間の短期キャッシュが適用されます。
- レスポンス:
{ location: [lat, lng], routeId, bearing, speed, direction, licensePlate, arrivals }。speedは GPS ユニットから取得した m/s 単位の速度で、報告されていない場合はnullです。
GET /vehicle-by-plate/:plate
ナンバープレートから車両 ID を検索します。5秒間の短期キャッシュが適用されます。
- ナンバープレートの照合では大文字と小文字を区別せず、スペースとハイフンは無視されます(
BC-1234-AA、bc 1234 aa、bc1234aaはすべて同じものとして扱われます)。 - レスポンス:
{ vehicleId }— 返された ID をGET /vehicle/:vehicleIdで使用します。
GET /transport?latitude={lat}&longitude={lng}
指定地点から1 km以内の車両です。10秒間の短期キャッシュが適用されます。
- レスポンス:
{ id, route, routeId, direction, vehicle_type, color, location: [lat, lng], bearing, speed, lowfloor }の配列です。routeIdは/routes/static/:nameの:nameとして使用できます。directionはstops/shapesのインデックスに対応し(0 = 往路、1 = 復路、不明の場合は null)、speedは m/s 単位で、不明の場合はnullです。
GET /disruptions?route={name}&stop={code}
市が発表した、本日有効な運休、迂回、停留所移設です。GTFS フィードには含まれていません。CDN で5分間キャッシュされます。
data/disruptions.jsonで手動管理され、起動時とテスト時にスキーマとの照合が行われます。エントリの追加や編集はここで行い、デプロイしてください。エントリはsinceの日からuntilの日まで(キーウ時間)配信されるため、期限切れのエントリは自動的に非表示になります。route(キリル文字またはラテン文字の路線略称)とstop(数値のコード)は任意のフィルターです。いずれかを指定すると、それに該当するエントリのみが一覧表示されます。- レスポンス:
{ disruptions, updated_at }。各項目:{ id, kind, reason, since, until, until_text, routes, stops, alternatives, detour, note, source, url }。kindはsuspended、detour、stop_moved、stop_closedのいずれかです。routesとalternativesは{ name, id }(nameはТ33のような値、idは lad.lviv.ua の URL で使用されるラテン文字表記)です。detourは{ via: [streets], skipped_stops: [codes] }またはnullです。
サービスとディスカバリー
| エンドポイント | 返される内容 |
|---|---|
GET /health | { status: "ok" }。時刻表ストアが読み込まれていない間は 503 を返します。上流フィードの確認は行いません。 |
GET /ping | {} |
GET /openapi.yaml(および /.well-known/openapi.yaml) | REST API の OpenAPI 3.0 記述です。 |
GET /llms.txt、GET /INTEGRATION.md | LLM 向けの概要と統合ガイドです。 |
GET /.well-known/ai-plugin.json、GET /.well-known/agent.json | OpenAI プラグインマニフェストと A2A エージェントカードです。 |
GET /server.json、GET /smithery.json | MCP Registry と Smithery のマニフェストです。 |
GET /sitemap.xml | 公開されている lad.lviv.ua ページのサイトマップです。 |
デプロイ
本番環境は Cloudflare の背後にある Cloud Run(timetable-api-node、europe-north1、プロジェクト
timetable-252615)で稼働しています。
- イメージ。 Dockerfile はビルド時に GTFS フィードをインポートします
(
gtfs-import.tsの後にscripts/slim-gtfs-db.tsが、サーバーが読み取らない SQLite テーブルを空にします)。そのため、各イメージには独自のデータスナップショットが含まれます。last-modified.txtには、フィードのLast-Modifiedヘッダーが記録されます。 - Cloud Build。
cloudbuild.yamlがイメージをビルド、プッシュ、デプロイします。 毎日のスケジュール実行では_DATA_REFRESH=1を設定し、上流のstatic.zipがデプロイ済みリビジョン以降変更されていない場合は、ビルド全体をスキップします。 - 手動。
make deployはlinux/amd64イメージをビルドしてプッシュし、デプロイします。make smokeは本番環境のエンドポイントをいくつか確認します。make clear-short-cache、clear-long-cache、clear-all-cacheは、キャッシュタグを使って Cloudflare のキャッシュを消去します。 - npm パッケージ。
make publishはパッチバージョンを上げ(server.jsonとopenapi.yamlも同期します)、タグをプッシュします。そのタグによって.github/workflows/publish.ymlが実行され、npm と MCP Registry に公開されます。 パッケージにはtimetable-mcpの stdio プロキシのみが含まれます。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。