Content
# GTFS MCP
This is an MCP server for querying public transit info from GTFS schedules and GTFS-RT realtime feeds. It works with any GTFS-compatible transit system, and includes a ready-to-use config for NYC Subway (MTA).
Ask things like *"When's the next L from Bedford Av?"*, *"Any service alerts on the G?"*, or *"What stops does the 7 train serve?"*.
**Service alerts example**

**Arrivals example**

## Quickstart
Requires Node 22+.
```bash
git clone https://github.com/jdamcd/gtfs-mcp.git && cd gtfs-mcp
npm install
npm run build
```
Add to your Claude Desktop config (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"gtfs": {
"command": "node",
"args": ["/absolute/path/to/gtfs-mcp/dist/index.js"],
"env": {
"GTFS_MCP_CONFIG": "/absolute/path/to/gtfs-mcp/config.mta.json"
}
}
}
}
```
Restart Claude Desktop and ask an NYC transit question. The first query triggers a GTFS schedule download (~30s for MTA). Subsequent queries are fast.
### Adding other transit systems
The server reads a JSON config defining one or more transit systems, pointed to by `GTFS_MCP_CONFIG`. Find feeds for your local agency in the [Mobility Database](https://mobilitydatabase.org/).
```json
{
"systems": [
{
"id": "mbta",
"name": "MBTA Boston",
"schedule_url": "https://cdn.mbta.com/MBTA_GTFS.zip",
"timezone": "America/New_York",
"realtime": {
"trip_updates": ["https://cdn.mbta.com/realtime/TripUpdates.pb"],
"vehicle_positions": ["https://cdn.mbta.com/realtime/VehiclePositions.pb"],
"alerts": ["https://cdn.mbta.com/realtime/Alerts.pb"]
},
"auth": null
}
],
"data_dir": "~/.gtfs-mcp/data",
"schedule_refresh_hours": 24
}
```
Each realtime feed type accepts multiple URLs (e.g. MTA splits trip updates across 8 feeds). Set any feed type to `[]` if the system doesn't provide it.
`timezone` is the agency's IANA timezone (e.g. `"Europe/London"`). It's used to compare scheduled stop times to "now" and render times in responses regardless of server timezone.
### Authenticated feeds
Some transit APIs require an API key. Keys are read from environment variables at runtime. Set them in the Claude Desktop `env` block alongside `GTFS_MCP_CONFIG`.
**Query parameter**:
```json
{
"auth": {
"type": "query_param",
"param_name": "api_key",
"key_env": "MY_API_KEY"
}
}
```
**Header:**
```json
{
"auth": {
"type": "header",
"header_name": "X-Api-Key",
"key_env": "MY_API_KEY"
}
}
```
Set `auth` to `null` for systems that don't require authentication.
## Tools
| Tool | Description | Parameters |
|------|-------------|------------|
| `list_systems` | List all configured transit systems | — |
| `search_stops` | Search stops by name | `system`, `query`, `limit?` |
| `find_nearby_stops` | Stops near a coordinate, ordered by distance | `system`, `lat`, `lon`, `radius_m?`, `limit?` |
| `get_stop` | Stop details and routes serving it | `system`, `stop_id` |
| `get_arrivals` | Upcoming arrivals with realtime delays | `system`, `stop_id`, `route_id?`, `limit?` |
| `list_routes` | Routes in a system, with optional name filter and pagination | `system`, `query?`, `route_type?`, `limit?`, `offset?` |
| `get_route` | Route details with ordered stop list | `system`, `route_id`, `direction_id?` |
| `get_alerts` | Active service alerts | `system`, `route_id?`, `stop_id?` |
| `get_vehicles` | Live vehicle positions | `system`, `route_id?` |
| `get_trip` | Trip stop sequence with realtime delays | `system`, `trip_id` |
| `get_feed_health` | Realtime feed diagnostics: per-URL fetch results, entity counts, oldest header age | `system` |
The `system` parameter is the system ID from your config (e.g. `"mta-subway"`). IDs flow between tools — e.g. `search_stops` returns a `stop_id` that `get_arrivals` consumes.
## Resources
| URI | Description |
|-----|-------------|
| `gtfs://systems` | Markdown overview of every configured system: id, name, timezone, and how many realtime feed URLs are wired up per feed type. |
Clients that support resources (Claude Desktop, MCP Inspector) can attach this as context — handy for grounding a conversation in which systems the server actually knows about, without invoking `list_systems`.
## Prompts
| Name | Description | Arguments |
|------|-------------|-----------|
| `transit-status` | Summary of currently-active alerts across configured systems. In Claude Desktop this appears in the attachment ("+") menu. | `system?` — optional system ID to scope to one system. |
The prompt returns a steering message that instructs the model to call `get_alerts` for the relevant system(s) and produce a tight summary. The model still does the work via tool calls — the prompt is just the framing.
## Development
```bash
npm test
```
### Evals
LLM evals use [promptfoo](https://promptfoo.dev/) to verify that a model selects the correct tools for natural-language transit queries. Requires `ANTHROPIC_API_KEY` set in `.env` or the environment.
```bash
npm run build
npm run eval
npm run eval:view # web UI to inspect results
```
### MCP inspector
Test tools interactively without a Claude client:
```bash
GTFS_MCP_CONFIG=./config.mta.json npx @modelcontextprotocol/inspector node dist/index.js
```
### Invariant testing
`scripts/campaign.ts` exercises the full tool surface against real GTFS feeds across the systems in `config.testing.json`, checks the responses for invariant violations, and writes per-call JSON under `campaign-results/`.
> ⚠️ This downloads static GTFS data for every configured system into `./data-testing/` — up to ~10GB.
```bash
GTFS_MCP_CONFIG=./config.testing.json npx tsx scripts/campaign.ts --smoke # quick pass across all systems
GTFS_MCP_CONFIG=./config.testing.json npx tsx scripts/campaign.ts --deep mbta,vbb # full phases on selected systems
```