mcpField
L
Lviv Public Transport

Lviv Public Transport

远程服务
lad.lviv.ua地图与出行32 天前更新

关于 Lviv Public Transport

Lviv Public Transport 是面向乘客和出行应用的利沃夫公共交通只读 API 与 MCP 服务,可查询站点、线路、时刻表和实时车辆。它结合 GTFS 时刻表数据与 GTFS-RT 实时动态,并提供路线规划和绕行信息。

主要功能

  1. 01搜索站点及附近站点
  2. 02查询实时到站与车辆位置
  3. 03规划直达或一次换乘路线
  4. 04查看线路时刻表与车辆
  5. 05查询停运、绕行和站点变更
  6. 06提供地图及到站列表数据

适用场景

  1. 01查找附近站点和可乘线路
  2. 02查看指定站点的实时到站
  3. 03规划两地间的公共交通路线
  4. 04确认线路的实时车辆与绕行

MCP 相关

通过 Streamable HTTP 暴露只读 MCP 服务,提供查询站点、实时到站、线路、车辆、路线和交通中断的工具。工具结果同时包含文本摘要与结构化数据,供 MCP 客户端使用。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://api.lad.lviv.ua/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

8
参数类型必填说明
stop_idinteger是市政站点代码,显示在站牌上(例如,707)。正整数或仅含数字的等效字符串均可。

详细介绍

Timetable API Node

CI Node.js License: WTFPL MCP Registry

基于 Express 的 API,提供利沃夫交通时刻表数据,并带有只读 MCP 端点。

smithery badge vbhjckfd/timetable-api-node MCP server

timetable-api-node MCP server

要求

  • 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。

变量默认值用途
PORT8080HTTP 端口。
VEHICLES_LOCATION_URLhttps://track.ua-gis.com/gtfs/lviv/vehicle_positionGTFS-RT 车辆位置数据源。
TRIP_UDPDATES_URLhttps://track.ua-gis.com/gtfs/lviv/trip_updatesGTFS-RT 行程更新数据源(拼写错误是实际名称)。
MCP_PUBLIC_BASE_URLhttps://api.lad.lviv.uaMCP 元数据(图标 URL)中使用的基础 URL。
NEW_RELIC_LICENSE_KEY—启用 New Relic APM(参见监控)。
NEW_RELIC_APP_NAME、NEW_RELIC_LOG_LEVELtimetable-api-node、infoNew Relic Agent 设置。
PULSE_WORKER_URL、PULSE_SIGNAL_SECRET—每次收到 /stops/:code/timetable 请求时向 Pulse Worker 发送信号;必须同时设置这两个变量。
MCP_URLhttps://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 server card REST stops.json REST routes.json

**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_stopsquery(字符串,≥ 2 个字符),limit(1–25,默认 10)乌克兰语或英语名称中包含查询词的站点(支持词形变化匹配:"опера" 可找到 «Театр опери та балету»;忽略标点和形似拉丁字母的字符,因此 "пл. ринок" 和 "Cихiв" 均可匹配),并包含 routes。
get_stops_around_locationlatitude、longitude、radius_meters(50–3000,默认 1000)点附近的站点,按距离由近到远排列,并包含 distance_meters 和 routes。
get_stop_realtimestop_id、lowfloor_only(默认 false)实时到站信息:route、direction(目的地)、vehicle_type、arrival_minutes、车辆位置、lowfloor;scheduled: true 表示在没有预测到车辆时显示的时刻表条目。
find_routes_betweenfrom_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_staticroute_name、include_shapes(默认 false)名称、类型、颜色、两个方向的站点列表、首站时刻表;仅在请求时返回路线折线。
get_route_realtimeroute_name、direction(0 或 1,可选)线路上的车辆,包含 destination 和 next_stop(id、name、ISO arrival);每个方向的 headways:车辆数量、从前到后的实时 gaps_minutes、scheduled_headway_minutes。
get_nearby_vehicleslatitude、longitude、radius_meters(100–1000,默认 500)、route、limit(1–50,默认 15)、lowfloor_only(默认 false)实时车辆,按距离由近到远排列,包含 destination 和 distance_meters;total 统计范围内的所有车辆。
get_vehicle_infovehicle_id单辆车辆的信息:位置、线路、destination、车牌、尚未经过的后续站点(id、name、ISO 时间)。
get_disruptionsroute、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" } } }
  ]
}
每个端点都会覆盖步行 300 m 范围内的所有站点(`stop_ids`):线路的两个方向通常会在街道两侧的不同站名处停靠,例如这里,Т01 开往车站时从 «Руська» 发车,而不是 «Площа Ринок»。选项按站点数加步行距离排序(步行 150 m 折算为 1 站),每条线路、每个方向各列一个。方向的最后一站只计作下车地点,不计作上车地点。

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-tripfrom, to查询两个具名地点(或站点 ID)之间的路线,可直达或换乘一次,并显示上车站的实时发车信息。
route-statusroute_name按方向查询线路车辆当前的位置。route_name 支持自动补全。
transit-map-viewstop_id以地图优先的方式呈现某站点的实时车辆。
transit-arrival-liststop_id按预计到达时间排序并按线路分组的站点到站列表。
transit-hybrid-viewstop_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.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.jsonOpenAI 插件清单和 A2A Agent 卡片。
GET /server.json、GET /smithery.jsonMCP 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-mcp stdio 代理。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。