septa-mcp
This MCP server supports SEPTA Regional Rail station lookup, schedules, live train status, departure boards, and service alerts.
Resolve vague or ambiguous Regional Rail station names and see their serving lines.
Get scheduled trains between stations, including one-transfer itineraries and time/date filters.
Check next trains right now with live delay status, falling back to schedule data if needed.
View live station departure boards with destinations, delays, tracks, and optional direction filtering.
Track a specific train by number for current position, lateness, and stop-by-stop estimates.
Fetch current Regional Rail alerts and advisories, optionally filtered by line.
Uses a Nominatim (OpenStreetMap) geocoder to resolve landmarks, addresses, neighborhoods and towns within SEPTA's region into coordinates, powering the place lookup, stops-near, stops-on-a-street and transit-in-area tools. Requests are rate-limited and cached, the geocoder endpoint is configurable, and results credit OpenStreetMap.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@septa-mcp When's the next train from 30th Street to Paoli, and is it late?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
SEPTA Regional Rail, Metro and bus connector for Claude
Public MCP URL: https://septa-mcp-60753635813.us-east4.run.app/mcp
An MCP server that lets Claude answer questions like:
"What times do trains leave Jefferson for Swarthmore on Saturday morning?"
"When's the next train from 30th Street to Paoli, and is it late?"
"Where is train 5305 right now?"
"How do I get from Erie to 2nd Street on the subway?"
"When's the next T3 trolley from 30th Street, and is it running late?"
"When does the 42 leave Broad and Walnut, and where is it now?"
"What's the closest stop to Reading Terminal Market?" / "What buses run through Fishtown?"
"Any problems on the Media/Wawa line, the El, or the 42 bus today?"
It covers Regional Rail, buses and the SEPTA Metro: the subway (L1 Market-Frankford Line, B1/B2 Broad Street Line, B3 Broad-Ridge Spur), the trolleys (T1–T5, G1, D1, D2) and the M1 Norristown High Speed Line. It combines SEPTA's published GTFS timetables (for any date in the current schedule) with SEPTA's real-time API (live delays, tracks, train and trolley and bus positions, and alerts for every mode).
Tools
Tool | Source | Use it for |
| GTFS | Timetable questions for any day. Falls back to one-transfer itineraries when there's no direct train. |
| Live (NextToArrive) | "Next train" right now, with delay status, including late trains the live board dropped. Falls back to the timetable if the live API is down. |
| Live (Arrivals) | A station's departure board: final stops, delays, track numbers, including late trains the board dropped. |
| Live (TrainView + RRSchedules) | Where a train is, how late, stop-by-stop estimates. |
| GTFS | Resolving vague or ambiguous station names. |
| GTFS | Subway and trolley timetables for any day, with one-transfer itineraries (including short walks between stations) when there's no direct trip. |
| GTFS + live (TransitView) | Next departures from a Metro station or trolley stop, with live delays for trolleys and the M1. |
| Live (TransitView) | Where a line's trolleys are, their next stops and how late. |
| GTFS | Resolving Metro station and trolley stop names. |
| GTFS | Buses that go directly between two stops, for any day. |
| GTFS + live (TransitViewAll) | Next buses from a stop, with live delays and which corner each leaves from. |
| Live (TransitView) | Where a route's buses are and how late. |
| GTFS | Bus stops by cross streets or stop number. |
| Live (Alerts + GTFS-realtime) | Advisories and detours for Regional Rail, Metro and bus routes, including closed stations, shuttle buses and platform changes. |
| Geocoder + GTFS | A landmark, address, neighborhood or town → coordinates, plus SEPTA stops named like it. |
| GTFS (+ geocoder) | Stops of every mode around a point, with walking minutes; each nearby bus route's closest stop; the nearest rail and Metro stations. |
| GTFS (+ geocoder) | Stops along a street in order, by cross street, one stretch per town or segment. |
| Geocoder + GTFS | What serves a neighborhood or town: stations, trolley lines, bus routes and how many stops each has there. |
The place tools are meant to be combined: Claude can look up a landmark, list the stops
near it, then ask a mode's own tools about one of them (every result carries the
stop_id those tools accept).
The Regional Rail tools and the Metro tools are separate: none plans a trip that mixes the two, so Claude plans each part and changes at a shared hub (Suburban ↔ 15th St/City Hall, Gray 30th St ↔ Drexel Station at 30th St, Jefferson ↔ 13th St or 8th-Market, Fern Rock and Norristown Transit Centers).
Station names are matched loosely: "Jefferson", "Market East", "30th Street", "Temple", "Mt Airy" all work. Ambiguous names ("Chestnut Hill", "Airport", "Norristown") return the candidates so Claude can ask which one you mean. Metro lines go by their current names or the ones riders still use: "MFL" or "the El" is the L1, "BSL" is B1–B3, "Route 10" is the T1, "Route 15" the G1, "Route 101" the D1, "NHSL" the M1.
Example
train_schedule("Jefferson", "Swarthmore", "saturday", "05:00", "12:00") against the
v202609271 feed returns trains 5301, 5305, 5309, 5313, 5317 and 5321, leaving Jefferson at
:38 past each hour from 6:38 AM and arriving at Swarthmore 35 minutes later.
When no train reaches the destination (e.g. buses replace trains on the last stretch of a
line), results list the alerts for the line plus partial_service: the trains to the
closest station on the way that trains still reach, where riders change to the shuttle.
Metro results (metro_schedule, metro_departures) always carry the current alerts for
the lines they use (service_alerts), since the timetable doesn't show a station that
trains bypass or a stretch run by shuttle buses. When a rider names a stop that isn't in
the schedule (11th St on the L1 is closed and bypassed until August 2027), the closest name
is used, and station_note says so and points to SEPTA's alert about the stop, if any.
find_metro_stop returns those alerts too.
Related MCP server: mcp-bart
SEPTA quirks it handles
Through-routed trains. Most Regional Rail trains change line name in Center City (a Lansdale/Doylestown train becomes a Media/Wawa train at Suburban Station). GTFS models this as two trips joined by an in-seat transfer, so a naive "trips serving both stations" query finds only a handful of the Jefferson → Swarthmore trains. This server stitches those trips back together, so you see every train.
Trains are reported by number and final_stop, the way platform boards list them,
rather than by line. The line name a train has at your boarding station can belong to the
other half of its run, e.g. a Suburban → Claymont train shows up as "Chestnut Hill East".
Live boards drop late trains. SEPTA's Arrivals and NextToArrive often stop listing a
train once its scheduled time passes, even when it's running late and hasn't reached the
station yet, so the board makes it look like the train already left. next_trains and
station_departures check the timetable for trains due from 30 minutes ago to 20 minutes
from now that the board leaves out. They look up each one's live position (TrainView) and
stop-by-stop times (RRSchedules), and put back any train that is running and has no actual
departure time at the station yet, marked on_live_board: false with its expected time.
Station names differ between feeds. The real-time API predates the current GTFS and
uses older names for about 30 stations ("Temple U", "30th Street Station", "Ft Washington",
"Elwyn Station"), and its endpoints don't even agree with each other (NextToArrive wants
"Wayne Station", Arrivals wants "Wayne"). realtime.py translates automatically; the map is
checked against the live API by tests/test_live.py.
Metro stations are many stops. The Metro's GTFS (part of SEPTA's bus feed) has a stop
per platform or direction, and separate ones per line at transfer stations: 15th St/City
Hall is eight stops across the L1, B1, B2 and the T trolleys. metro.py merges them into
one stop per station, by parent station and by name, so transfers between lines work, and
links stations within a short walk of each other (Girard Av & Front St on the G1 and
Front-Girard on the L1) so itineraries can include the walk.
The bus feed is big. It has ~2.8 million stop times, about 15 times the Metro's;
parsed into Python objects like the rail and Metro feeds it would need several hundred MB.
bus.py converts it once per download into a compact form (stop times as flat int
arrays), which loads in about a second and takes ~70 MB.
Bus stops are corners. The feed names each stop after the street the bus is on first, so the same intersection is "Broad St & Walnut St" for one route and "Walnut St & Broad St" for another, with a stop on each side. Stops are grouped by their set of streets, so "Broad and Walnut" in either order, or the number on the stop's sign, finds the corner, and results say which stop there (side of the street, and its number) each bus leaves from.
Places come from OpenStreetMap. Landmarks, addresses, neighborhoods and towns are
looked up with a Nominatim geocoder, limited to SEPTA's region
(Trenton to Wilmington, out to Chester and Bucks counties). The public server allows one
request a second from an app that identifies itself, which this keeps to and caches for a
day; set SEPTA_GEOCODER_URL to use another Nominatim-compatible server. Stops near
coordinates, along a street, or by name work without it. Results credit OpenStreetMap, as
its license requires.
No live subway data. SEPTA's real-time JSON API reports trolley and M1 positions and
delays (TransitView), but nothing for the L1 or the Broad Street Line; its GTFS-realtime
feed has no real subway data either. And riders can't count on a subway train's minute,
only on how often trains come. So subway results always include at least three times per
line and direction (even past the requested window), each with minutes_after_previous,
plus subway_frequency ("L1 toward Frankford Transit Center runs about every 6 min around
8:02 AM"), and Claude is told to present them as approximate. At a busy stop that would
swamp the departure board (15th St/City Hall has six subway directions), so past 12 entries
the board cuts subway directions to two times each, then one; each keeps its gap to the
train before, and the result says to ask for one line to see more.
Two alert feeds. SEPTA's JSON alerts API (/Alerts/get_alert_data.php) leaves out
many alerts that its GTFS-realtime alert feeds have: station closures (11th St on the L1),
M1 and D shuttle buses, platform changes and elevator outages. service_alerts reads both
and drops GTFS-realtime alerts whose text the JSON API already has. GTFS-realtime alerts
that name only single trips (cancelled bus trips) are left out. They're read with a small
built-in protobuf decoder (septa_mcp/gtfsrt.py), so there's no protobuf dependency.
Run it
Requires Python 3.11+.
pip install .
septa-mcp --transport http --port 8000 # Streamable HTTP at http://localhost:8000/mcp
septa-mcp # stdio, for local MCP clientsOn first start it downloads SEPTA's schedules (≈27 MB) from SEPTA, falling back to SEPTA's
GitHub releases. It keeps the rail feed and the Metro and bus routes from the bus feed,
about 12 MB together; processing a download takes 10–15 seconds. It re-checks weekly, since
SEPTA announces timetable changes well ahead, and sooner if the cached timetable runs out.
septa-mcp --refresh-feeds downloads the newest feeds into the cache and exits, for running
on a schedule instead. SEPTA publishes a new feed every few weeks,
a few days before it takes effect, and its download link switches to it right away. For a date
before the newest feed starts (today, until the switch, or a past date), the server uses the
cached feed that was in effect, or downloads the GitHub release in effect that date and keeps
the last few it used (about 12 MB each). On a feed's first day it also adds the previous
feed's trips from the night before that are still running after midnight, which only that
feed has. Results include valid_from/valid_until, and a
warning if you ask about a date no feed covers.
A feed's version is named for its start date, and SEPTA folds later schedule changes into the
same feed, so results also list timetable_changes_included to show the data is current.
Use it in Claude Code (development version)
The repo includes a .mcp.json, so starting claude in a checkout loads the server from
your working copy. It needs uv, which builds a .venv from
uv.lock on first start and installs the package in editable mode:
git clone https://github.com/ellbur/septa-claude-connector && cd septa-claude-connector
claude # approve the "septa" server when prompted, then check with /mcpCode edits take effect the next time the server starts: restart Claude Code, or reconnect
from /mcp.
Add it to Claude as a custom connector
Claude's custom connectors need a public HTTPS URL, so deploy the server somewhere first.
deploy.sh puts it on Google Cloud Run, which scales to zero when nobody's using it (low
traffic fits in Cloud Run's free tier):
gcloud auth login
PROJECT=my-gcp-project ./deploy.sh # optional: REGION, SERVICE, BUCKET, JOB, REFRESH_SCHEDULEBesides the service, it creates a Cloud Run job, septa-mcp-refresh, that runs
septa-mcp --refresh-feeds against the same bucket, and a Cloud Scheduler job that starts
it every Saturday at 11:17 PM Philadelphia time. The service picks up the new timetables on
its next request, so downloading and processing never hold up a rider's question. The
service only downloads them itself if the job hasn't succeeded in 8 days, or the cached
timetable has run out. To refresh now: gcloud run jobs execute septa-mcp-refresh --region us-east4.
To redeploy from GitHub on every push to the deploy branch, connect the repository to
Cloud Build; it's all sign-ins and taps, so it works from a phone. After running
deploy.sh once, in the Cloud console go to Cloud Build > Triggers > Create trigger:
Event: push to a branch. Source: connect a new repository, pick GitHub, sign in, and install the Cloud Build app on this repository. Branch:
^deploy$.Configuration: Cloud Build configuration file,
/cloudbuild.yaml. It assumes Cloud Run regionus-east4; ifdeploy.shused another, add a substitution variable_REGION.Service account: the Compute Engine default service account.
cloudbuild.yaml builds the image and points both the service and the refresh job at it.
Then deploy with git push origin HEAD:deploy.
The service gets 1 GiB of memory: with every feed loaded, and the next schedule too in the days before it starts, the server uses up to ~350 MB, and processing a download briefly adds ~170 MB more.
It mounts a Cloud Storage bucket at /data so the schedule survives cold starts, and caps the service at one instance so the real-time API
cache and rate limit (10 s, 60 requests/minute by default) apply to the whole service.
When the limit is hit, tools fall back to the published schedule or report the error.
The included Dockerfile also works on any other container host (Fly.io, Render, Railway, a
VPS behind Caddy, etc.); run a single instance with a persistent /data:
docker build -t septa-mcp .
docker run -p 8000:8000 -v septa-data:/data septa-mcpThen add a custom connector in Claude's settings with the URL
https://<your-host>/mcp. No authentication is required: everything it serves is SEPTA's
public data and every tool is read-only.
For local use only, point a desktop MCP client at the stdio command instead:
{
"mcpServers": {
"septa": { "command": "septa-mcp" }
}
}Configuration
Env var | Default | Purpose |
|
| Where the downloaded schedule is cached |
| — | Use a local |
| SEPTA, then GitHub | Comma-separated download URLs |
|
| Re-download the timetables when the cache is older than this |
|
| Real-time API base (point at the mock for offline dev) |
|
| GTFS-realtime base, for the alerts the JSON API leaves out |
|
| Nominatim-compatible place search |
| — | Contact address sent to the geocoder, which Nominatim asks heavier users for |
|
| Seconds per real-time request |
|
| How long identical real-time requests share a response |
|
| How long alerts are reused (the Metro tools fetch them with every answer) |
|
| Most real-time requests sent to SEPTA in any 60 seconds (per process) |
|
| Server transport settings |
Tests
uv run --extra dev pytest -q # or: pip install -e ".[dev]" && pytest -q
SEPTA_LIVE=1 pytest -q tests/test_live.py # against SEPTA's real servers and NominatimSchedule tests run against the real GTFS feed (downloaded, or SEPTA_GTFS_ZIP).
Real-time tests use tests/mock_septa_api.py, which serves responses in SEPTA's format;
you can also run it standalone for offline development.
Limits
NJ Transit, PATCO and Amtrak aren't covered.
No trip planning across modes, and bus trips only on routes that go directly: a bus trip that needs a change is planned leg by leg.
Transfer itineraries are limited to one transfer; that covers any pair of Regional Rail stations and most Metro trips.
SEPTA's real-time API is unofficially documented and its response formats occasionally shift;
realtime.pyparses defensively but is the first place to look if a live tool breaks.
Available Tools
6 toolsfind_stationARead-onlyIdempotent
Look up SEPTA Regional Rail stations by (partial) name.
Returns matching stations with the lines that serve them. Use this when a station name is ambiguous or you're unsure what the user means.
Inputs: query (required).
next_trainsBRead-onlyIdempotent
Next trains from one station to another right now, with live delay status.
Uses SEPTA's real-time NextToArrive API (includes connecting trips when there is no direct train). Falls back to the published schedule if the live API is down.
Inputs: to_station (required), from_station (required), count.
rail_alertsARead-onlyIdempotent
Current SEPTA Regional Rail service alerts and advisories. Optionally filter by line name, e.g. "Media/Wawa" or "Paoli".
Inputs: line.
station_departuresBRead-onlyIdempotent
Live departure board for a Regional Rail station: upcoming trains, destinations, delays and track numbers.
direction: "N" or "S" as SEPTA labels trains at that station (roughly: N = headed toward the northern end of the system, S = toward the south/west); omit for both.
Inputs: station (required), count, direction.
train_scheduleARead-onlyIdempotent
Scheduled Regional Rail trains from one station to another on a given day.
Args: from_station: Boarding station, e.g. "Jefferson", "30th Street", "Swarthmore". to_station: Destination station. date: "today", "tomorrow", a weekday name ("saturday" = the next Saturday, or today if it is Saturday), or YYYY-MM-DD. depart_after: Earliest departure, e.g. "05:00" or "7:30 AM". Defaults to now when the date is today, otherwise start of day. For "morning" use 05:00–12:00, "afternoon" 12:00–17:00, "evening" 17:00–23:59. depart_before: Latest departure. Defaults to end of service day. max_results: Cap on trains returned.
Returns direct trains (train number, departure, arrival, line). If there are no direct trains, returns one-transfer itineraries instead. Times are scheduled, not live; for trips in the next hour or so, also call next_trains for delays.
Inputs: to_station (required), from_station (required), date, max_results, +2 more.
train_statusARead-onlyIdempotent
Where a specific train is right now and how late it's running, plus its stop-by-stop estimated times. train_number is SEPTA's train number, e.g. "5305".
Inputs: train_number (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool has a distinct role: station lookup, station departure board, origin-destination live trains, origin-destination schedules, train status, and alerts. The main possible overlap is between station_departures and next_trains, but the descriptions make the distinction reasonably clear: one station's board versus a specific origin-destination trip.
All tool names use consistent snake_case and are readable. Most are noun-phrase style (station_departures, train_status) while find_station uses verb_noun, which is a minor deviation but not confusing.
Six tools is well-scoped for a Regional Rail information server. Each tool covers a distinct transit query type without unnecessary bloat.
The surface covers the core Regional Rail workflows: station discovery, scheduled trips, live next trains, station departure boards, train status, and service alerts. No obvious operational gap remains for typical rider questions.
Maintenance
Related MCP Connectors
SEPTA MCP — Philadelphia SEPTA real-time transit (www3.septa.org/api, keyless)
NJ Transit rail, bus and PATH schedules via MCP (scheduled, plus live PATH)
Belgian trains for AI agents: live journeys, departures, delays, disruptions. Free, no key.
Cached UK train departures and qualified platforms, powered by National Rail Enquiries.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables users to interact with Washington DC Metro (WMATA) transit system through natural language queries. Provides real-time train arrivals, service alerts, station information, trip planning, and accessibility updates.2,969 npmMIT
- AlicenseNot gradedqualityBmaintenanceProvides real-time San Francisco Bay Area Rapid Transit data, enabling queries about BART schedules, routes, and station information through natural language.129 npmMIT
- AlicenseNot gradedqualityCmaintenanceProvides real-time SEPTA transit data via the keyless SEPTA API, enabling users to query schedules, routes, and arrivals for Philadelphia's public transit system.309 npmMIT
- AlicenseAqualityBmaintenanceLets Claude answer questions about GB National Rail trains, including live departures and arrivals, platforms, timetables, service details, and journey planning with live delay overlay.9MIT