Server data from the Official MCP Registry
Lviv public transport MCP: stops, timetables, routes, and live vehicle positions. No API key.
Lviv public transport MCP: stops, timetables, routes, and live vehicle positions. No API key.
Remote endpoints: streamable-http: https://api.lad.lviv.ua/mcp
This is a read-only MCP server for public transit data in Lviv, Ukraine with no authentication required (by design). The codebase is well-structured with proper input validation, safe tool implementations, and appropriate permissions for its purpose. Minor code quality observations exist around error handling and logging, but no security vulnerabilities were identified. The server correctly implements the MCP protocol and exposes only read-only operations on public data. Supply chain analysis found 9 known vulnerabilities in dependencies (0 critical, 6 high severity).
5 files analyzed · 13 issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
This plugin requests these system permissions. Most are normal for its category.
Available as Local & Remote
This plugin can run on your machine or connect to a hosted endpoint. during install.
From the project's GitHub README.
Express-based API for Lviv transport timetable data with a read-only MCP endpoint.
.nvmrc)nvm use
make start
nvm use && make test
This service exposes a public read-only MCP endpoint over Streamable HTTP.
/mcp/.well-known/mcp/server-card.json/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.
/mcp flowAn 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;
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
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 stringified JSON inside MCP content items (type: "text"), and each 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".
get_stop_realtimeget_vehicles_by_stopget_stop_geometryget_stops_around_locationArguments (JSON):
| Field | Type | Required |
|---|---|---|
stop_id | positive integer or digits-only string | yes |
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": [] }
}
]
}
Arguments:
| Field | Type | Required |
|---|---|---|
stop_ids | array of positive integers and/or digits-only strings | yes |
Example result:
{
"view": "transit_realtime",
"data": {
"stop_ids": ["707"],
"stops": [{ "id": "707", "name": "Стадіон Сільмаш", "lat": 49.84, "lng": 24.03 }],
"vehicles": [
{
"id": "tram_123",
"route": "T30",
"lat": 49.83,
"lng": 24.02,
"bearing": 120,
"next_stop_id": "707",
"eta_minutes": 4,
"eta_status": "assigned"
}
]
},
"ui_blocks": [{ "type": "map", "data": { "vehicles": [] } }]
}
Arguments:
| Field | Type | Required |
|---|---|---|
stop_id | positive integer or digits-only string | yes |
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": [] } }]
}
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):
| Field | Type | Required |
|---|---|---|
latitude | number, −90…90 | yes |
longitude | number, −180…180 | yes |
radius_meters | integer, 50…3000 | no (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 /closest)Same stop search as get_stops_around_location, for non-MCP clients:
GET /closest?latitude={lat}&longitude={lng}radius — meters, clamped between 50 and 3000 (default 1000).{ code, name, latitude, longitude, distance_meters } (sorted by distance).robots.txt is only a best-effort discovery hint and not a protocol contract.Be the first to review this server!
by Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
by Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
by mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.