vbhjckfd/timetable-api-node

🚆 Travel & Transportation
0 Views
0 Installs

📇 ☁️ - Real-time Lviv (Ukraine) public transport: stop arrivals, live vehicle positions, route geometry, and nearby stop discovery. No API key required.

Quick Install

One-Click IDE Configuration
claude_desktop_config.json
{
  "mcpServers": {
    "vbhjckfd-timetable-api-node": {
      "command": "npx",
      "args": [
        "-y",
        "vbhjckfd-timetable-api-node"
      ]
    }
  }
}
Or

Using an AI coding agent (Claude Code, Cursor, etc.)? Copy a ready-made prompt that tells it to fetch the setup instructions and install this server for you.

Documentation Overview

Timetable API Node

CI Node.js License: WTFPL MCP Registry

Express-based API for Lviv transport timetable data with a read-only MCP endpoint.

smithery badge vbhjckfd/timetable-api-node MCP server

timetable-api-node MCP server

Requirements

  • Node.js 26 (see .nvmrc)

Run locally

nvm use
make start

Test

nvm use && make test

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).

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;

Try the live API

MCP server card REST stops.json REST routes.json

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. The Streamable HTTP transport may require additional headers your MCP client sets automatically; for a quick manual test, follow the same sequence your MCP SDK uses (session initialize, then tools/call). Example tools/call body shape:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_stop_realtime",
    "arguments": { "stop_id": 101 }
  }
}

Successful tool responses return a natural-language text summary inside MCP content items (type: "text") — e.g. "Stop «Opera»: 3 arrivals. Next: T01 → «Rynok» in 2 min." The full structured payload is in the structuredContent field (for schema-aware clients). Each structuredContent payload follows a strict UI contract:

{
  "view": "transit_realtime",
  "data": { "...": "tool-specific source data" },
  "ui_blocks": [
    { "type": "map", "data": { "...": "map renderer input" } },
    { "type": "arrival_list", "data": { "...": "arrival list renderer input" } }
  ]
}

Consistency rule: each vehicle rendered on map must either have a matching ETA in list data or eta_status: "unassigned".

Exposed tools

  • get_stop_realtime
  • get_route_static
  • get_route_realtime
  • get_stop_geometry
  • get_stops_around_location
  • get_nearby_vehicles
  • get_vehicle_info
get_stop_realtime — input & example

Arguments (JSON):

FieldTypeRequired
stop_idpositive integer or digits-only stringyes

Example result (shape only; values from upstream):

{
  "view": "transit_realtime",
  "data": {
    "stop": { "id": "707", "name": "Стадіон Сільмаш", "lat": 49.84, "lng": 24.03 },
    "arrivals": [
      {
        "route": "T30",
        "direction": "Рясівська",
        "vehicle_type": "tram",
        "arrival_minutes": 4,
        "vehicle_id": "tram_123",
        "lat": 49.83,
        "lng": 24.02,
        "bearing": 120
      }
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": { "center": [49.84, 24.03], "vehicles": [] }
    },
    {
      "type": "arrival_list",
      "data": { "arrivals": [] }
    }
  ]
}
get_route_static — input & example

Arguments (JSON):

FieldTypeRequired
route_nameroute short name (e.g. "T30", "32A") or numeric external IDyes

Example result (shape only; stops truncated for brevity):

{
  "view": "transit_realtime",
  "data": {
    "route": {
      "name": "T30",
      "long_name": "Рясне-2 — Сихів",
      "color": "#e81717",
      "type": "tram"
    },
    "stops": [
      [
        {
          "id": "101", "name": "Головний вокзал", "lat": 49.841, "lng": 24.003,
          "departures": ["05:30", "05:52"],
          "schedule": { "workday": ["05:30", "05:52", "06:10"], "weekend": ["07:00", "07:30"] }
        },
        { "id": "707", "name": "Стадіон Сільмаш", "lat": 49.838, "lng": 24.021, "departures": [], "schedule": { "workday": [], "weekend": [] } }
      ],
      [
        { "id": "707", "name": "Стадіон Сільмаш", "lat": 49.838, "lng": 24.021, "departures": [], "schedule": { "workday": [], "weekend": [] } },
        { "id": "101", "name": "Головний вокзал", "lat": 49.841, "lng": 24.003, "departures": [], "schedule": { "workday": [], "weekend": [] } }
      ]
    ],
    "shapes": [
      [[49.841, 24.003], [49.839, 24.012], [49.838, 24.021]],
      [[49.838, 24.021], [49.839, 24.012], [49.841, 24.003]]
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": {
        "center": [49.841, 24.003],
        "zoom": 13,
        "polylines": [[[49.841, 24.003], [49.839, 24.012], [49.838, 24.021]]],
        "stops": [
          { "id": "101", "name": "Головний вокзал", "lat": 49.841, "lng": 24.003 },
          { "id": "707", "name": "Стадіон Сільмаш", "lat": 49.838, "lng": 24.021 }
        ],
        "vehicles": []
      }
    }
  ]
}

stops[0] is direction 0 (outbound), stops[1] is direction 1 (return). departures and schedule are populated only for the first stop of direction 0; all other stops have empty arrays. schedule.workday contains Monday–Friday departure times; schedule.weekend contains Saturday–Sunday departure times. departures keeps today's schedule for backward compatibility. shapes follows the same two-element order. The map block uses direction-0 polyline and all unique stops as markers.

get_route_realtime — input & example

Arguments (JSON):

FieldTypeRequired
route_nameroute short name (e.g. "T30", "32A") or numeric external IDyes

Example result:

{
  "view": "transit_realtime",
  "data": {
    "route_name": "T30",
    "vehicles": [
      {
        "id": "tram_123",
        "direction": 0,
        "lat": 49.838,
        "lng": 24.021,
        "bearing": 120,
        "lowfloor": true
      }
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": {
        "center": [49.838, 24.021],
        "zoom": 13,
        "vehicles": [
          {
            "id": "tram_123",
            "direction": 0,
            "lat": 49.838,
            "lng": 24.021,
            "bearing": 120,
            "lowfloor": true
          }
        ]
      }
    }
  ]
}

direction matches the index into get_route_static's stops array (0 = outbound, 1 = return). lowfloor: true indicates a low-floor vehicle. Returns an empty vehicles array when no vehicles are currently active on the route.

get_stop_geometry — input & example

Arguments:

FieldTypeRequired
stop_idpositive integer or digits-only stringyes

Example result:

{
  "view": "transit_realtime",
  "data": {
    "stop": { "id": "707", "name": "Стадіон Сільмаш", "lat": 49.84, "lng": 24.03 },
    "routes": [
      {
        "route": "T30",
        "polyline": [[49.84, 24.03], [49.83, 24.02]]
      }
    ]
  },
  "ui_blocks": [{ "type": "map", "data": { "routes": [] } }]
}
get_stops_around_location — input & example

Returns stops near a map point (numeric code, name, coordinates, distance). Intended for hosts that render map UI blocks (for example ChatGPT): one block with multiple stop markers and the search center. Uses the same backend as GET /closest (see below).

Arguments (JSON):

FieldTypeRequired
latitudenumber, −90…90yes
longitudenumber, −180…180yes
radius_metersinteger, 50…3000no (default 1000)

Example result (shape only):

{
  "view": "transit_realtime",
  "data": {
    "center_lat": 49.84,
    "center_lng": 24.03,
    "radius_meters": 1000,
    "stops": [
      {
        "id": "707",
        "name": "Стадіон Сільмаш",
        "lat": 49.841,
        "lng": 24.031,
        "distance_meters": 120
      }
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": {
        "center": [49.84, 24.03],
        "zoom": 15,
        "stops": [
          {
            "id": "707",
            "name": "Стадіон Сільмаш",
            "lat": 49.841,
            "lng": 24.031,
            "distance_meters": 120
          }
        ],
        "vehicles": []
      }
    }
  ]
}

Map zoom is 15 for radius ≤ 1500 m and 14 for larger radii (up to 3000 m).

get_nearby_vehicles — input & example

Returns live positions for all transit vehicles within 1 km of given coordinates. Wraps the same backend as GET /transport.

Arguments (JSON):

FieldTypeRequired
latitudenumber, −90…90yes
longitudenumber, −180…180yes

Example result (shape only):

{
  "view": "transit_realtime",
  "data": {
    "center_lat": 49.84,
    "center_lng": 24.03,
    "vehicles": [
      {
        "id": "tram_123",
        "route": "T01",
        "vehicle_type": "tram",
        "lat": 49.841,
        "lng": 24.031,
        "bearing": 90,
        "lowfloor": true
      }
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": {
        "center": [49.84, 24.03],
        "zoom": 14,
        "vehicles": [{ "id": "tram_123", "route": "T01", "lat": 49.841, "lng": 24.031, "bearing": 90, "eta_status": "unassigned" }]
      }
    }
  ]
}
get_vehicle_info — input & example

Full details for one vehicle by its ID: position, route, license plate, direction, and upcoming stop arrival times. Vehicle IDs come from get_route_realtime, get_nearby_vehicles, or get_stop_realtime.

Arguments (JSON):

FieldTypeRequired
vehicle_idstringyes

Example result (shape only):

{
  "view": "transit_realtime",
  "data": {
    "vehicle_id": "tram_123",
    "route": "route-ext-1",
    "license_plate": "BC-1234-AB",
    "lat": 49.841,
    "lng": 24.031,
    "bearing": 90,
    "direction": 0,
    "upcoming_stops": [
      { "code": 707, "arrival": "2026-01-23T12:05:00Z", "departure": null },
      { "code": 708, "arrival": "2026-01-23T12:08:00Z", "departure": null }
    ],
    "updated_at": "2026-01-23T12:00:00Z"
  },
  "ui_blocks": [
    {
      "type": "map",
      "data": { "center": [49.841, 24.031], "zoom": 15, "vehicles": [{ "id": "tram_123", "eta_status": "unassigned" }] }
    }
  ]
}

Resources and resource templates

In addition to tools, the server exposes MCP resources for reference data that doesn't require a tool call:

URIDescription
timetable://aboutScope, usage, and data caveats for this server (Markdown)
timetable://reference/toolsTools reference table (Markdown)
timetable://reference/promptsPrompt templates catalog (Markdown)
timetable://stop/{code}Static info for a stop by numeric code — name, coordinates, serving routes (JSON)
timetable://route/{name}Static metadata for a route by short name — color, type, stop counts (JSON)

Security model

  • Public read-only (no authentication).
  • No mutating tools are exposed.
  • POST /mcp is rate-limited to 60 requests/min per IP (in-memory, resets on restart). Excess requests receive HTTP 429 with a JSON-RPC error body.
  • robots.txt is only a best-effort discovery hint and not a protocol contract.

REST API

All endpoints return JSON. :code is a numeric stop code; :name is a route short name (e.g. T1, 32A) or numeric external ID.

Stops

GET /stops.json

All stops as a JSON array, sorted by code.

  • Response: array of { code, name, eng_name, location: [lat, lng], routes, sign, sign_pdf }.

(GET /stops returns an HTML table instead.)

GET /stops/:code

Single stop with live realtime timetable. Short-cached (5–10 s).

  • Optional: skipTimetableData=1 — omit live arrivals (long-cached response).
  • Response: { code, name, eng_name, latitude, longitude, transfers, timetable }.

GET /stops/:code/timetable

Live timetable only for a stop. Short-cached (5–10 s).

  • Response: array of timetable items.

GET /stops/:code/static

Static stop info without live data. Long-cached (30 days).

  • Response: { code, name, eng_name, latitude, longitude, transfers }.

GET /closest?latitude={lat}&longitude={lng}

Nearby stops — same search as get_stops_around_location, for non-MCP clients.

  • Optional: radius — meters, clamped between 50 and 3000 (default 1000).
  • Response: JSON array of { code, name, latitude, longitude, distance_meters } (sorted by distance).

Routes

GET /routes.json

All routes as a JSON array, sorted by short name.

  • Response: raw route objects from the timetable store.

(GET /routes returns an HTML table.)

GET /routes/static/:name

Route shape, stop list, and metadata. Long-cached (30 days).

  • Response: { id, color, type, route_short_name, route_long_name, stops: [[dir0…], [dir1…]], shapes }.
  • Each stop object: { code, name, loc, transfers, departures, schedule }.
    • departures — today's departure times (HH:MM), populated only for direction 0 first stop. Kept for backward compatibility.
    • schedule{ workday: string[], weekend: string[] } departure times by day type, populated only for direction 0 first stop.

GET /routes/dynamic/:name

Live vehicle positions for a route. Short-cached (10 s).

  • Response: array of { id, direction, location: [lat, lng], bearing, speed, lowfloor }. speed is m/s from the GPS unit, or null when not reported.

Vehicles

GET /vehicle/:vehicleId

Live position and upcoming stop arrivals for one vehicle. Short-cached (5 s).

  • Response: { location: [lat, lng], routeId, bearing, speed, direction, licensePlate, arrivals }. speed is m/s from the GPS unit, or null when not reported.

GET /vehicle-by-plate/:plate

Look up a vehicle ID by its license plate. Short-cached (5 s).

  • The plate is matched case-insensitively with spaces and dashes ignored (BC-1234-AA, bc 1234 aa, and bc1234aa are all equivalent).
  • Response: { vehicleId } — use the returned ID with GET /vehicle/:vehicleId.

GET /transport?latitude={lat}&longitude={lng}

Vehicles within 1 km of a point. Short-cached (10 s).

  • Response: array of { id, route, routeId, direction, vehicle_type, color, location: [lat, lng], bearing, speed, lowfloor }. routeId is usable as :name in /routes/static/:name; direction matches the index into stops/shapes (0 = outbound, 1 = return, null if unknown). speed is m/s or null.

Related MCP Servers

alcylu/nightlife-mcp

📇 ☁️ - MCP server for Tokyo nightlife event discovery, venue search, performer info, AI recommendations, and VIP table booking.

🚆 Travel & Transportation0 views
amirdaraee/luxembourg-mcp

🎖️ 🐍 ☁️ 🏠 - Keyless access to official Luxembourg public data: laws, statistics, live parking and traffic, weather alerts, environment, parliament, geodata, GTFS stops, and the national open-data catalogue. 21 read-only tools, zero dependencies; hosted endpoint at mcp.luxembourg-mcp.com or local Python server. No API keys.

🚆 Travel & Transportation0 views
Autonomad1/autonomad-travel

📇 ☁️ - AI travel agent over MCP — live flights, hotels, activities, and events worldwide, then completes the booking on autonomad.ai (free signup at checkout, first month of Premium included).

🚆 Travel & Transportation0 views
campertunity/mcp-server

🎖️ 📇 🏠 - Search campgrounds around the world on campertunity, check availability, and provide booking links

🚆 Travel & Transportation0 views

Engagement

Views
0
Installs
0
Upvotes
0

Views and upvotes are unique per visitor network (hashed IP). Installs count copy actions.

Status

Health: Not checked yet

We have not completed a health check for this listing yet.

No check timestamp yet.

Unclaimed listing (imported or pending owner verification). Claim it →
★ Spotlight Slot

Feature Your MCP Server

Get maximum visibility for your server across our directory, search results, and detail pages.

Spotlight Your Server

Own this project?

This directory is pre-filled from public sources. Claim via GitHub README, site badge, or DNS TXT to get the verified badge and attach your website.

Claim this listing

Promote this listing

Optional paid placement. Free listings stay free forever.

Share & Embed

Add our SVG badge (dark/light directory styles) or embeddable widget to your site.