GTFS MCP

May 18, 2026 · View on GitHub

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

Asking Claude about G train service alerts at Nassau Av

Arrivals example

Asking Claude about L train arrival times from Bedford Av

Quickstart

Requires Node 22+.

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

{
  "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.

{
  "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:

{
  "auth": {
    "type": "query_param",
    "param_name": "api_key",
    "key_env": "MY_API_KEY"
  }
}

Header:

{
  "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

ToolDescriptionParameters
list_systemsList all configured transit systems
search_stopsSearch stops by namesystem, query, limit?
find_nearby_stopsStops near a coordinate, ordered by distancesystem, lat, lon, radius_m?, limit?
get_stopStop details and routes serving itsystem, stop_id
get_arrivalsUpcoming arrivals with realtime delayssystem, stop_id, route_id?, limit?
list_routesRoutes in a system, with optional name filter and paginationsystem, query?, route_type?, limit?, offset?
get_routeRoute details with ordered stop listsystem, route_id, direction_id?
get_alertsActive service alertssystem, route_id?, stop_id?
get_vehiclesLive vehicle positionssystem, route_id?
get_tripTrip stop sequence with realtime delayssystem, trip_id
get_feed_healthRealtime feed diagnostics: per-URL fetch results, entity counts, oldest header agesystem

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

URIDescription
gtfs://systemsMarkdown 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

NameDescriptionArguments
transit-statusSummary 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

npm test

Evals

LLM evals use promptfoo to verify that a model selects the correct tools for natural-language transit queries. Requires ANTHROPIC_API_KEY set in .env or the environment.

npm run build
npm run eval
npm run eval:view   # web UI to inspect results

MCP inspector

Test tools interactively without a Claude client:

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.

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