Timetable API Node
以下摘录自该仓库的 README(片段) · 在 GitHub 读完整 README →
Express-based API for Lviv transport timetable data with a read-only MCP endpoint.
Requirements
- Node.js 26 (see .nvmrc)
Run locally
make import downloads the static GTFS feed (gtfs-import-config.json) and
writes the two stores the server reads: database/Timetable (LokiJS: stops,
routes, shapes, first-stop schedules) and database/gtfs (SQLite: trips and
calendars). Both are gitignored, so a fresh clone has no data until it runs.
make import-slim re-imports only the light tables into SQLite and leaves
database/Timetable alone.
Configuration
All variables are optional; see .env.dist.
| Variable | Default | Purpose |
|----------|---------|---------|
| PORT | 8080 | HTTP port. |
| VEHICLES_LOCATION_URL | https://track.ua-gis.com/gtfs/lviv/vehicle_position | GTFS-RT vehicle positions feed. |
| TRIP_UDPDATES_URL | https://track.ua-gis.com/gtfs/lviv/trip_updates | GTFS-RT trip updates feed (the misspelling is the real name). |
| MCP_PUBLIC_BASE_URL | https://api.lad.lviv.ua | Base URL used in MCP metadata (icon URL). |
| NEW_RELIC_LICENSE_KEY | — | Enables New Relic APM (see Monitoring). |
| NEW_RELIC_APP_NAME, NEW_RELIC_LOG_LEVEL | timetable-api-node, info | New Relic agent settings. |
| PULSE_WORKER_URL, PULSE_SIGNAL_SECRET | — | Signal a Pulse worker on each /stops/:code/timetable request; both must be set. |
| MCP_URL | https://api.lad.lviv.ua/mcp | Only for the timetable-mcp stdio proxy: the remote endpoint it forwards to. |
Test
TypeScript
The code is TypeScript and runs straight from the .ts sources under Node's
built-in type stripping — there is no build step for the server, the GTFS
import or the scripts. That means only erasable syntax (no enum,
namespace or parameter properties), import type for type-only imports, and
relative imports that name the .ts file; tsconfig.json enforces all three.
npm run build emits just mcp/stdio.js, the published bin: Node does not
strip types inside node_modules, so the npm package has to ship JavaScript.
It runs on npm pack/npm publish; the output is not committed.
public/stopOverrides.js is served to browsers as-is, so it stays JavaScript
and is type-checked from its JSDoc.
Monitoring
New Relic APM, off unless NEW_RELIC_LICENSE_KEY is set. It also carries
errors and the custom metrics recorded through utils/metrics.ts.
New Relic runs as a preloaded agent, so npm start carries the flags:
dotenv/config is preloaded first so .env is populated before the agent
reads its configuration. The config file is newrelic.cjs (the agent is
CommonJS and this project is ESM) and holds no secrets — the key comes from the
environment. /health is excluded from transactions via rules.ignore.
The account is in the **EU** region; its license key starts with eu01xx and
the agent picks the collector from that prefix. Use the 40-character ingest
license key, not an NRAK-... user API key.
Cloud Run reads the key from Secret Manager:
MCP Server
This service exposes a public read-only MCP endpoint over Streamable HTTP.
- MCP endpoint: /mcp
- Server card: /.well-known/mcp/server-card.json
- Discovery hint: /robots.txt (non-standard comment hint)
Production deployment (see cloudbuild.yaml for Cloud Run) serves **REST and MCP** from **api.lad.lviv.ua**. The main site **lad.lviv.ua** is the public transport website (this repo still links there in HTML sitemap and tables for people, not for the API host). Use your own origin when running locally.
LLM and /mcp flow
An MCP client (Claude, Cursor, or the MCP SDK) talks JSON-RPC over **Streamable HTTP** to POST /mcp. Tool handlers reuse the same Express actions as the REST API, backed by **LokiJS** timetable data, **GTFS** SQLite (via gtfs), and **live GTFS-RT** feeds (for example track.ua-gis.com).
Try the live API
**MCP Inspector (local):** run npx @modelcontextprotocol/inspector, then open the UI with transport and server URL prefilled (from the inspector README):
http://localhost:6274/?transport=streamable-http&serverUrl=https%3A%2F%2Fapi.lad.lviv.ua%2Fmcp
Postman / curl: call a tool on production
POST https://api.lad.lviv.ua/mcp with Content-Type: application/json **and** Accept: application/json, text/event-stream — the Streamable HTTP transport rejects a request that does not accept both. The server is stateless: there is no session, so tools/call works on its own without an initialize first. The response arrives as a single SSE event: message frame carrying the JSON-RPC result.