Skip to main content
Glama
Adelaide Metro MCP server

Adelaide Metro MCP server

by Niranjen99

Adelaide Metro MCP server

CI

An MCP server that lets Claude (or any MCP client) answer Adelaide Metro questions, such as "when's the next tram from City South?", using the official GTFS timetable.

Tech stack: Python 3.10+, the official MCP Python SDK (v2, stdio transport), SQLite, pytest, GitHub Actions.

What it can do

Tool

Example question

What it returns

find_stops

"Find stops on King William St"

Matching stops with the stop number from the sign and the address

routes_at_stop

"Which routes stop at City South?"

Every bus, train and tram route that calls there

next_departures

"When's the next bus from stop D2?" / "Which buses go from Sefton Park to King William St?"

Scheduled departures in the next 2 hours (adjustable), optionally for one route or only trips that go directly to a destination stop, with arrival times

trip_stops

"Where does this bus go? Where do I get off for Hindley St?"

Every stop one trip calls at after your stop, in order, with times, marking the last stop and stops where you can't get off

timetable_info

"How current is your timetable?"

Timetable version, dates it's valid for, size

All tools are read-only and validate their inputs. Departures come from the published timetable only. Live delays and service disruptions are planned (see Roadmap).

Example next_departures output (real data, timetable version 1714):

Scheduled departures from Stop D2 King William St - East side (stop 16286, id 6455) - 96 King William St, Adelaide
As of Wed 04 Nov 15:41 (timetable only, no live delays):
- 15:44  Bus T721 to Noarlunga (in 3 min), trip 1174247
- 15:54  Bus 721X to Noarlunga (in 13 min), trip 1173546
- 15:54  Bus T722 to Noarlunga (in 13 min), trip 1174304
- 15:55  Bus 719 to Flinders University (in 14 min), trip 1173415

With a destination (to_stop_id), from a real test asking Claude how to get from Sefton Park to Hindley St:

Scheduled direct trips from Stop 17 Main North Rd - East side (stop 12297, id 2393) - 255 Main North Rd, Sefton Park to Stop B1 King William St - East side (stop 13301, id 3414) - 12 King William St, Adelaide
As of Wed 04 Nov 16:38 (timetable only, no live delays):
- 16:40  Bus 222 to City (in 2 min), arrives 17:00 (20 min ride), trip 1178543
- 16:41  Bus 228F to City (in 3 min), arrives 16:58 (17 min ride), trip 1178742
- 16:54  Bus 222 to City (in 16 min), arrives 17:14 (20 min ride), trip 1178540

The 228F leaves a minute later but arrives two minutes earlier. Without the destination, Claude could list departures but couldn't tell which buses reach the city stop or when.

When the user only names a street ("Hindley St"), Claude may not know which stop the bus uses. trip_stops shows where a trip goes, so Claude can pick the stop nearest the destination (shortened, real data):

Bus 228F to City (trip 1178729)
Stops after Stop 17 Main North Rd - East side (stop 12297, id 2393) - 255 Main North Rd, Sefton Park, timetable times:
- 17:12  Stop 16 Main North Rd - East side (stop 12349, id 2445) - Sefton Plaza Shopping Centre, Prospect (set down only)
...
- 17:29  Stop B1 King William St - East side (stop 13301, id 3414) - 12 King William St, Adelaide
- 17:31  Stop D2 King William St - East side (stop 16286, id 6455) - 96 King William St, Adelaide
- 17:34  Stop VS3 Victoria Sq - East side (stop 13474, id 3589) - 242 Victoria Square, Adelaide (last stop)

Related MCP server: gtfs-pro-mcp

Quick start (Windows, Command Prompt)

git clone https://github.com/Niranjen99/adelaide-metro-mcp.git
cd adelaide-metro-mcp
py -3.12 -m venv .venv
.venv\Scripts\python.exe -m pip install -e ".[dev]"

:: Download the timetable (15 MB) and build data\gtfs.sqlite. Re-run any time: it skips the
:: download if the published version hasn't changed. Add --force to always rebuild.
.venv\Scripts\python.exe -m adelaide_metro_mcp.gtfs_loader

:: Run the tests (no network or downloaded data needed)
.venv\Scripts\python.exe -m pytest

On Linux/macOS, use python3 -m venv .venv and .venv/bin/python instead.

Connect it to Claude

Claude starts the server itself. It doesn't start it in the project folder, so you have to tell the server where the data is with ADELAIDE_METRO_DATA_DIR. Replace C:\path\to\adelaide-metro-mcp with your clone's full path.

Claude Code:

claude mcp add adelaide-metro -e ADELAIDE_METRO_DATA_DIR="C:\path\to\adelaide-metro-mcp\data" -- "C:\path\to\adelaide-metro-mcp\.venv\Scripts\python.exe" -m adelaide_metro_mcp

Claude Desktop: add this to %APPDATA%\Claude\claude_desktop_config.json, then restart Claude Desktop:

{
  "mcpServers": {
    "adelaide-metro": {
      "command": "C:\\path\\to\\adelaide-metro-mcp\\.venv\\Scripts\\python.exe",
      "args": ["-m", "adelaide_metro_mcp"],
      "env": { "ADELAIDE_METRO_DATA_DIR": "C:\\path\\to\\adelaide-metro-mcp\\data" }
    }
  }
}

Performance

Measured with benchmarks\bench_queries.py on the full Adelaide Metro timetable (9,038 stops, 717 routes, 22,208 trips, 812,881 stop times):

.venv\Scripts\python.exe benchmarks\bench_queries.py --rebuild

Results on a Windows 11 desktop (AMD64, Python 3.12.10), timetable version 1714, 1,000 runs per query:

Query

Median (ms)

p95 (ms)

find_stops("king william")

2.66

2.83

routes_at_stop (busiest stop)

1.93

2.05

next_departures (busiest stop, weekday 08:00)

0.69

0.72

next_departures (random stop and time)

0.66

0.91

next_departures with to_stop_id (busiest stop, weekday 08:00)

1.34

1.42

trip_stops (longest trip, 177 stops)

0.68

0.74

next_departures through the MCP client, end to end

1.93

2.35

Full database build from the 15 MB zip: 7.3 s.

Engineering decisions

  • Trips that run past midnight. GTFS writes a trip that leaves at 00:50 on Monday's timetable as 24:50:00. Times are stored as seconds since the start of the service day, and next_departures checks yesterday's, today's and tomorrow's services. Without that, late-night trips disappear after midnight. The real feed has departures as late as 29:40:00.

  • Daylight saving. All times use Australia/Adelaide (ACST +9:30 / ACDT +10:30). GTFS counts times from "noon minus 12 hours", so on the day the clocks change, a trip at 08:00 still means 08:00 on the clock. The tzdata package is a dependency because Windows has no timezone database for Python's zoneinfo. CI runs on Windows to catch this.

  • Which trips run on a date. calendar.txt gives weekdays and a date range, and calendar_dates.txt adds or removes services on specific dates (e.g. public holidays). Both are applied.

  • Only real departures. Stops where you can't board (pickup_type = 1, 4,378 in the feed) and the last stop of each trip are excluded. Parent stations, which group platforms, are rejected with a message pointing to a platform.

  • A to B as an option, not a new tool. next_departures takes an optional to_stop_id instead of adding a new tool: fewer tools with clear jobs make it easier for the model to pick the right one. It keeps trips that reach the destination later and let you off there (drop_off_type != 1, 3,607 pickup-only stops in the feed). For loop routes that visit a stop twice (552 trips), it uses the first arrival after you board. If nothing ever goes directly, the tool says so (wrong side of the road, or a transfer is needed), so Claude doesn't tell the user to wait. Direct trips only: this is not a journey planner.

  • A separate tool for "where does this bus go?". to_stop_id only checks a destination Claude already knows. In a real test ("Sefton Park to Hindley St"), Claude guessed six city stops one by one and none were on the route: the buses run down King William St to Victoria Sq. trip_stops lists a trip's stops in stop_sequence order, so one call shows the stop to get off at. It works per trip, not per route, because one route has several stopping patterns (express variants, short runs), and the trip being caught gives the exact stops and times. next_departures prints each trip id so Claude can pass it on. It uses the existing stop_times(trip_id) index, so no schema change was needed.

  • Schema versioning. The database stores a schema version. If a newer release changes the tables, the loader rebuilds even when the timetable hasn't changed, and the server asks for a rebuild instead of failing on a missing column.

  • Fast reads. The 15 MB zip becomes a 55 MB SQLite database with an index on (stop_id, departure_secs), so a departures lookup is a short index range scan.

  • Safe updates. The loader skips the download if version.txt is unchanged. It builds the database in a temporary file and swaps it in atomically, so a failed download never leaves a half-built database.

  • Read-only and validated. The server opens SQLite in read-only mode, uses parameterised SQL only, and validates every input. Expected errors (unknown stop, bad input, missing data) become clear messages for Claude; unexpected errors are not exposed.

  • Thin protocol layer. server.py only formats text; queries live in db.py and are tested with plain pytest. The current time is passed in as a parameter, so time-dependent tests are deterministic.

  • stdio transport. stdout carries the MCP protocol, so the server never prints. Logs go to stderr.

Project layout

adelaide_metro_mcp/
  gtfs_loader.py   download the GTFS zip and build the SQLite database
  db.py            read-only queries and input validation
  server.py        MCP tool definitions (thin formatting layer)
benchmarks/
  bench_queries.py query timings on the real database
tests/
  fixtures/mini_gtfs/  small made-up GTFS feed with the real column layout

Roadmap

  • Journeys with a transfer, and "stops near here" using the stop coordinates (so Claude can pick the closest stop to a street).

  • Live delays in next_departures from the GTFS-Realtime trip updates feed (cached for its 60 s refresh interval).

  • service_disruptions tool from the GTFS-Realtime service alerts feed.

Data and attribution

Timetable data: Adelaide Metro – Department for Infrastructure and Transport, South Australia, from the Adelaide Metro GTFS feeds. The data isn't included in this repository; the loader downloads it.

This is an independent project and is not affiliated with Adelaide Metro.

License

MIT, see LICENSE.

Related MCP Connectors

Related MCP Servers