关于 Lviv Public Transport
Lviv Public Transport 是面向乘客和出行应用的利沃夫公共交通只读 API 与 MCP 服务,可查询站点、线路、时刻表和实时车辆。它结合 GTFS 时刻表数据与 GTFS-RT 实时动态,并提供路线规划和绕行信息。
主要功能
- 01搜索站点及附近站点
- 02查询实时到站与车辆位置
- 03规划直达或一次换乘路线
- 04查看线路时刻表与车辆
- 05查询停运、绕行和站点变更
- 06提供地图及到站列表数据
适用场景
- 01查找附近站点和可乘线路
- 02查看指定站点的实时到站
- 03规划两地间的公共交通路线
- 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
基于 Express 的 API,提供利沃夫交通时刻表数据,并带有只读 MCP 端点。
要求
- 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),并写入服务读取的两个存储:database/Timetable(LokiJS:站点、线路、路径、首站时刻表)和 database/gtfs(SQLite:行程和日历)。这两个目录都被 gitignore 忽略,因此新克隆的仓库在运行导入前没有数据。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 Agent 设置。 |
PULSE_WORKER_URL、PULSE_SIGNAL_SECRET | — | 每次收到 /stops/:code/timetable 请求时向 Pulse Worker 发送信号;必须同时设置这两个变量。 |
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 会强制执行这三项要求。
npm run typecheck # tsc, no emit
npm run build 只会生成 mcp/stdio.js,即发布的 bin: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 以预加载 Agent 的形式运行,因此 npm start 会携带以下标志:
node -r dotenv/config -r newrelic --import newrelic/esm-loader.mjs index.ts
先预加载 dotenv/config,确保 Agent 读取配置前已填充 .env。配置文件为 newrelic.cjs(Agent 使用 CommonJS,而本项目使用 ESM),其中不包含机密信息——密钥来自环境变量。通过 rules.ignore 将 /health 排除在事务之外。
该账户位于欧盟区域;其许可证密钥以 eu01xx 开头,Agent 会根据此前缀选择采集器。请使用 40 个字符的采集许可证密钥,而不是 NRAK-... 用户 API 密钥。
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 是公共交通网站(此代码库仍会在 HTML 站点地图和面向用户的表格中链接到该站点,而非 API 主机)。本地运行时请使用你自己的源站地址。
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 的界面(参见 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。响应以单个 SSE event: message 帧返回,其中携带 JSON-RPC 结果。
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")中返回自然语言文本摘要,例如 "站点 «Площа Ринок»:6 班车到站。下一班:Т02 → «Пасічна»,3 分钟后到达。" 完整的结构化负载位于 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 站点群内恰好换乘一次的 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 | 单辆车辆的信息:位置、线路、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" } } }
]
}
一个站名通常涵盖街道两侧的站点,但它们的 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 包含恰好换乘一次的行程,不会更多。每个选项有两段 legs(字段与直达选项相同,但不含步行信息)和 transfer_walk_meters:换乘可以在同一站点进行(0),也可以在第一段终点 300 m 范围内的任意站点进行,因此广场或路口周边的一组站点可视为一次换乘。已能直达目的地的线路绝不会作为任一段使用;排序时换乘最多相当于额外增加 5 站。没有直达路线,或换乘选项优于最佳直达选项时,才会列出换乘选项(最多 5 个)。两个列表都为空表示行程需要换乘两次或更多次。
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 会为每个方向保存一条 [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)之间的路线,可直达或换乘一次,并显示上车站的实时发车信息。 |
route-status | route_name | 按方向查询线路车辆当前的位置。route_name 支持自动补全。 |
transit-map-view | stop_id | 以地图优先的方式呈现某站点的实时车辆。 |
transit-arrival-list | stop_id | 按预计到达时间排序并按线路分组的站点到站列表。 |
transit-hybrid-view | stop_id | 先显示地图块,再显示到站列表块,并确保两者的预计到达时间一致。 |
资源和资源模板
除工具外,服务还会公开 MCP 资源,供查询无需调用工具的参考数据:
| 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 个请求(内存计数,重启后重置)。超出限制的请求会收到 HTTP 429 响应,其中包含 JSON-RPC 错误正文。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
会将存储的覆盖项应用到其 Маршрути 列——移除的线路以红色删除线显示,新增的线路以绿色显示——并将对应的 ?add=/?remove= 参数附加到该行的 SVG 和 PDF 链接上,涵盖 Табличка(站牌)和 Мапа
(网络线路图,/:code/schema)列。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
单辆车的实时位置和即将到站的到站信息。短缓存(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.uaURL 使用的拉丁字母形式);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 Agent 卡片。 |
GET /server.json、GET /smithery.json | MCP Registry 和 Smithery 清单。 |
GET /sitemap.xml | 公开 lad.lviv.ua 页面的站点地图。 |
部署
生产环境运行在 Cloud Run(timetable-api-node,europe-north1,项目
timetable-252615)上,并由 Cloudflare 提供代理。
- 镜像。 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-mcpstdio 代理。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。