stl-transit
Click on "Install 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., "@stl-transitrun the assertion suite and check for feed drift"
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.
stl-transit
Developer tooling for the Light Phone 3 St. Louis transit tool: GTFS and GTFS-Realtime inspection, golden-fixture generation for the Kotlin test gate, and feed surveillance.
Never ships in the APK. This is build-time tooling and Light never vets it. The shipped tool is a separate Kotlin repo, and unlike this one it cannot use Metro's trademarks.
Ships as one thing usable two ways:
CLI —
stl ..., 64 commands, for interactive work and cron/CI.MCP server —
stl-mcpon stdio, 45 tools, for Claude Cowork / Desktop / Code.
Both are thin shells over stl_transit.core.service. See SPEC.md for the full
design and the CLI-to-MCP contract; tests/test_wiring.py enforces it.
Install
git clone https://github.com/tyleryancey/stl-transit
cd stl-transit
python3 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/stl doctor # sanity checkRequires Python 3.12+. No API keys — every source is public and unauthenticated, which is what keeps MCP configuration trivial.
Related MCP server: db-mcp
First run
.venv/bin/stl snapshot fetch metro_gtfs # ~3.5 MB zip, expands to ~29 MB
.venv/bin/stl gtfs import # build the SQLite index
.venv/bin/stl report brief # is anything wrong right now?
.venv/bin/stl gtfs stop-resolve # answers the stop_code vs stop_id question
.venv/bin/stl rt fetch --entity trip_updates
.venv/bin/stl assert run # the assumptions the app depends onSnapshots land in ~/.local/share/stl-transit (override with $STL_HOME).
Attaching to Claude Cowork / Desktop
Add to your MCP config (Cowork: Settings → Connectors → Add local server;
Desktop: claude_desktop_config.json). Use absolute paths:
{
"mcpServers": {
"stl-transit": {
"command": "/absolute/path/to/stl-transit/.venv/bin/stl-mcp",
"env": {
"STL_HOME": "/absolute/path/to/stl-transit"
}
}
}
}Verify before wiring it up:
.venv/bin/stl-mcp # should sit silently waiting on stdio; Ctrl-C to exitPointing STL_HOME at a directory inside the repo (gitignored) keeps every
snapshot the agent touches next to the code, which makes a Cowork session
reproducible after the fact.
What the 45 tools cover
Group | Tools |
Orientation |
|
Feed shape |
|
Entities |
|
Schedule |
|
Realtime |
|
Assumptions |
|
Drift |
|
Web sources |
|
Ship artifacts |
|
Digests |
|
Oracle |
|
Support |
|
Escape hatch |
|
stl_gtfs_query is why the surface stays at 45 rather than 64: anything the
named tools do not cover is expressible as read-only SQL. It is also the only
tool with real blast radius, so it is constrained at the SQLite driver —
mode=ro, an authorizer denying ATTACH/PRAGMA/every write, a wall-clock
timeout, and row/byte caps. DROP TABLE routes comes back as a structured
UNSAFE_QUERY error with a remedy, not a stack trace.
A note on surface size.
SPEC.md§10 targets 20–25 MCP tools, arguing from context-window economics. This server exposes 45, which is a deliberate departure: every group added since answers a question no other tool can, and the ones that would genuinely flood a context (bundle stops-indexat 646 KB,bundle compact,support bundle) are CLI-only for exactly that reason. The mitigation for a larger surface is the question-to-tool index at the top of the server instructions, so a cold-start model does not have to read 45 descriptions to find its entry point.
The parts that carry the most weight
core/gtfs/calendar.py + departures.py — the reference implementation the
Kotlin engine is graded against. GTFS measures times from noon-minus-twelve-hours,
not local midnight; a 00:12 departure is usually encoded 24:12:00 on the
previous service date; pickup_type=1 means a rider cannot board. Every
result carries service_date and gtfs_time beside the resolved local time, so
an implementation that gets the instant right by luck and the service date wrong
fails visibly instead of quietly.
The time arithmetic is done in UTC throughout, and that is not stylistic.
Subtracting or adding a timedelta on a zone-aware datetime is wall-clock
arithmetic in Python — and in java.time.LocalDateTime — so noon - 12h
collapses back to local midnight and every departure on the two DST transition
days each year shifts by an hour. Port the UTC conversion, not just the formula.
core/rt/wire.py + schema.py — a hand-rolled protobuf reader, deliberately
not gtfs-realtime-bindings. The LP3 has no protobuf runtime on the Light SDK
dependency allow-list, so the on-device decoder is either
kotlinx-serialization-protobuf or hand-written. schema.py is the porting
table and it marks signedness explicitly: delay is int32, and a decoder that
reads it unsigned turns "three minutes early" into 18446744073709551436. About
31% of the delay values in Metro's feed are negative, so this is the common case,
not the edge case.
core/assertions/ — 16 things the app depends on that Metro never promised:
that stop_code is populated and unique, that no trip runs past 28:00, that the
agency timezone does not move, that ≥95% of realtime trip ids resolve into the
static feed. Every result reports the observed value beside the threshold,
because "coverage 0.982, threshold 0.99" is actionable and "FAIL" is not. Three
outcomes, not two: skip means the measurement could not be taken, and a
stability check with no baseline has not been performed.
core/oracle.py — 19 golden-fixture cases, each present because it can break
independently: DST spring-forward and fall-back, 24:xx rollover queried from both
sides of midnight, Labor Day (MetroBus → Sunday, MetroLink → Weekend — different
concepts), an ordinary Monday that is not a holiday, expired feed as a distinct
state, realtime absent. A case that legitimately raises is a first-class
expectation compared on error type, so it does not read as permanent drift.
Coverage
metro_gtfs covers MetroBus in both Missouri and Illinois — St. Clair County
service is operated by Metro under contract and lives in this feed — plus
MetroLink. Two Illinois-side sources are configured but blocked on unresolved
URLs; stl snapshot sources reports them with discovery notes:
mct_gtfs— Madison County Transit, own buses, not in Metro's feed. Resolve via Transitland (f-madison~county~transit~il~us) or Mobility Database.loop_trolley— seasonal streetcar. Check whether its trips are already insidemetro_gtfsbefore adding it separately.
Resolving either is a one-line edit to src/stl_transit/data/sources.toml.
Exit codes
0 ok · 1 error · 2 usage · 3 assertion violated · 4 drift detected ·
5 network unavailable · 6 feed expired. Codes 3, 4 and 6 exist so
stl assert run, stl web check, stl oracle verify and stl report brief
drop into cron or a GitHub Action without anyone parsing output.
Tests
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest -q # 431 tests, no networkEverything runs against miniature synthetic feeds in tests/fixtures.py — three
stops, two routes, a calendar exception, a 24:12 rollover trip — small enough to
reason about completely, which is the only way to be sure the calendar math is
right rather than merely self-consistent. Protobuf tests run against
hand-encoded bytes with known field numbers.
Two suites go further than unit coverage:
tests/test_wiring.pyenforces the CLI-to-MCP contract itself: tool naming, the four annotations on every tool, thatcore/never importscli/ormcp/and never prints, and that no MCP tool exists without a CLI equivalent to debug it from.scripts/verify_fixes.pyre-runs the 2026-08-03 audit's failures against a real snapshot. The synthetic feeds have no negative RT delays and no 489k-row table to time out on, so these checks cannot be unit tests.scripts/smoke_mcp.pyspeaks real JSON-RPC tostl-mcpover stdio. Importing the tool functions proves the wiring but not that the server starts, negotiates a protocol version, or serializes its schemas — and those failures all happen before any of this code runs.
tests/test_review_fixes.py is worth reading on its own: it pins a class of bug
that appeared six separate times in this codebase. Adding a timedelta to a
zone-aware datetime, or subtracting or comparing two datetimes that share a
tzinfo object, is wall-clock arithmetic in Python. It is correct on 363 days
a year. On the fall-back night it made a bus 45 minutes away report as
−15 minutes and disappear from the board entirely; a fixed 1440-minute probe
missed the 25th hour and told a developer a stop had no service when it did.
If you touch time arithmetic here, do it in UTC, and add a test dated in March
or November — the shipped feed only covers about a month, so no real snapshot
can exercise a transition.
The second recurring theme is reporting success on a measurement never taken:
a survival rate of null beside meets_assumption: true, a drift check that
returned ok having verified zero fixtures, an assumption that failed every
morning because it measured how long since you last fetched rather than what it
claimed to. That failure mode does not announce itself — it looks exactly like
good news. skip is a first-class outcome here for that reason.
Evaluations
evaluations/ holds 10 question/answer pairs pinned to a specific snapshot,
each requiring several tool calls to answer. evaluations/verify_answers.py
recomputes all ten by two independent routes and tells you which have moved when
the pinned snapshot is eventually replaced.
Not yet built
Per SPEC.md: job handles for long operations (snapshot fetch, rt poll,
history pull, gtfs validate still block rather than returning a job id), the
history group over Mobility Database archives, and rt record / rt replay.
rt replay is a file emitter when built; the local HTTP server that mimics
Metro's endpoints for on-device testing is backlogged.
Terms
Metro's data is licensed non-exclusively, limited, and revocably, with no
trademark use permitted. Full terms:
https://www.metrostlouis.org/developer-resources/. Be a good guest — the HTTP
layer sends an identifying User-Agent, uses conditional requests, rate-limits
per host, and caps web-page fetches at one per day. stl assert run watches the
terms page for changes, because the redistribution right this project rests on
is one Metro can withdraw.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityBmaintenanceMCP server that provides tools for querying live transit data (stops, departures, routes, vehicles, alerts) from any WP GTFS Pro site, enabling AI assistants to answer rider questions.Last updated28GPL 2.0
- AlicenseAqualityBmaintenanceAn MCP server that exposes the Deutsche Bahn public transport API to any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue, etc.). Five tools cover station search, departures, journey planning, trip details, and nearby stations.Last updated6MIT
- AlicenseCqualityBmaintenanceMCP server for the UK Bus Open Data Service, enabling timetable queries, stop search, route discovery, journey planning, and real-time bus tracking.Last updated16MIT
- Flicense-qualityDmaintenanceLocal MCP server that exposes fixed tools for GPT, Claude, and Gemini while routing to any OpenAI-compatible chat completions backend with independent configuration per target.Last updated1
Related MCP Connectors
Transitland MCP — global GTFS aggregator
MBTA MCP — Boston real-time transit via the MBTA v3 API (api-v3.mbta.com)
SEPTA MCP — Philadelphia SEPTA real-time transit (www3.septa.org/api, keyless)
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/tyleryancey/stl-transit'
If you have feedback or need assistance with the MCP directory API, please join our Discord server