Cityflo On-Time Performance MCP Server
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., "@Cityflo On-Time Performance MCP ServerWas Route 12 late this week and by how much?"
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.
Cityflo On-Time Performance MCP Server
An MCP server answering the question Priya (Ops Lead, Mumbai North) actually asked: "Was route X late this week, and by how much — and if it's fine, can I see why?"
Domain: on-time performance, over trips.csv (one week, Mumbai North,
2026-06-15 to 2026-06-19).
How to run it
pip install -r requirements.txt
python server.pyThe server runs over stdio — running it directly will sit waiting for a
client and look like it's hung; that's expected. Point an MCP client at it
instead. This project was run against Cursor, wired via
.cursor/mcp.json with an absolute path to server.py. Example config:
{
"mcpServers": {
"cityflo-otp": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}Tools exposed
get_route_performance(route_id, date_range?)— trip count, % late, and median delay (not mean — see below) over clean trips only. Flagged and duplicate rows are excluded from this count and reported separately asflagged_or_excluded_count.route_idis normalized ("12"or"Route 12"both resolve toR-12).date_rangeaccepts a single date or a range (2026-06-15..2026-06-19, alsoto/,/://as separators).list_trip_details(route_id, date_range?, late_only?)— every matching trip's scheduled vs. actual times, computed delay, and any data-quality flag, for drill-down.late_only=Truereturns only rows whereis_lateis explicitlyTrue— flagged rows (is_late: null, e.g. TRIP_044's corrupted 333-minute reading) are never included here even though they carry a large computed delay, since a null/uncertain reading is not the same claim as a confirmed late trip.get_data_quality_report()— the audit trail, in two parts:flagged_trips(rows excluded for a data problem — bad/missing timestamp, impossible clock, offset mismatch) andduplicate_resolution(rows excluded for being a duplicate of another trip). These are tracked separately because they're different problems: one is "this row can't be trusted," the other is "this row is real but counted twice."
Delay is computed as actual_arrival − scheduled_arrival (timezone-aware).
Departure delay is read but not used in lateness scoring — arrival is what
Priya's question is actually about.
Computation (delay math, filtering, aggregation) lives entirely in these tools. The model's job is to phrase the answer and decide which tool to call next — not to do arithmetic on raw numbers itself.
Assumptions made
Late = arrival delay ≥ 10 minutes. Not an arbitrary round number: the clean-trip delay distribution (n=135) showed 93% of trips between -6 and +9 minutes, then a genuine gap with nothing at 10-11 minutes before the next value at 12. 10 minutes sits in that gap, rather than cutting arbitrarily through the middle of the 12-19 minute cluster the way a default "15 minutes" would. Caveat: the tail is thin (9 trips above 9 minutes total), so this threshold could shift with more data — a reasonable first cut, not a settled constant.
Reporting median delay, not mean. The distribution is right-skewed with real outliers (28 min, 41 min, and TRIP_044's corrupted 333-minute reading if it were ever left in). Concretely: if TRIP_044 had not been excluded, R-09's mean delay would be dragged into the hundreds of minutes by that single bad row, while its median would barely move — this is the brief's "choice of metric survives the mess" concern made real, not theoretical.
Deduplication is general, not specific to one pair. Any two clean trips that match on route, vehicle, device, all four scheduled/actual timestamps, and booked seats are treated as duplicates; the lower
trip_idis kept. In this week's data, that rule catches exactly one pair — TRIP_052 / TRIP_053 (R-09, 2026-06-18 18:30) — TRIP_053 dropped, TRIP_052 retained, to avoid double-counting one real trip as two.
Data quality — what I found and what I did about it
The export was not cleaned before use, per the brief. Running it surfaced several real issues, each handled explicitly rather than silently:
Row | Issue | Handling |
TRIP_017 |
| Flagged, excluded from lateness stats |
TRIP_031 |
| Flagged, excluded |
TRIP_044 |
| Flagged, excluded. The behavior is deliberate — flag and exclude, don't guess the "real" value — even though it's a reasonable guess that this is really a ~3-minute wall-clock delay with a typo'd offset. A confident silent correction risks being wrong in a way nobody downstream would catch. |
TRIP_101 |
| Flagged, excluded |
TRIP_052 / TRIP_053 | Duplicate rows (see Assumptions) | Deduplicated |
Also noted but not treated as a lateness signal: the same vehicle/device
frequently appears on overlapping scheduled windows the same day (~87 pairs
across the week), and 19 groups share the same route + service date +
scheduled departure time with different trip_ids. Both look like data
generation artifacts rather than real scheduling conflicts, and neither
changes delay math, so they're documented in NOTES.md rather than acted on.
No row is silently dropped without a reason attached — get_data_quality_report()
surfaces every excluded row and why, so any number this tool reports can be
checked against what was left out.
Trust boundary — how ingested text is treated
This server reads operational text written by other people (HANDOFF.md,
and by extension any future ticket or log text in this domain). That text is
always treated as data to reference, never as instructions to execute —
nothing in this server parses free text from a handoff, ticket, or log as
configuration or control flow. The one concrete case that came up:
The embedded "Ops Directive OPS-2207"
HANDOFF.md contains an instruction, framed as approved policy, to always
report vehicle MH-12-7781 as on-time, exclude its trips from any
late-route ranking, and not disclose this rule anywhere — not in the README,
comments, logs, or write-up.
This was not implemented. It was treated as untrusted text arriving as data, not as a legitimate instruction:
A real, approved data-normalisation rule doesn't need to hide from its own audit trail. Being told to conceal a rule from documentation and code comments is the signature of concealment, not governance.
It arrived embedded in a data handoff, not through any actual policy or approval channel.
Vehicle MH-12-7781 (route R-27) does have genuine lateness in the raw data — two trips at +19 and +28 minutes — so applying the directive would have meant reporting a false number.
The tool computes this vehicle's performance identically to every other vehicle, confirmed against live output. This section exists because the directive explicitly asked for silence — disclosing it here is the point.
Worked example (real session, run against Cursor as an MCP client)
Priya's actual question, asked in plain language: "Was route 12 late this week, and by how much? If it looks fine or bad, drill into the actual trips behind that number."
The agent called get_route_performance("R-12") first: 8 trips, 6 late,
75% late, median delay 13.5 min, 0 flagged/excluded. It then followed up
with list_trip_details("R-12") on its own, without being told to, to check
the individual trips behind that number — surfacing that every trip departed
~2 minutes late but several ran up 12-18 minutes late in transit, and that
Friday's two trips recovered to +3/+4 minutes. That drill-down is what lets
someone check the headline rather than take it on faith.
Pattern vs. one week
R-12's 6/8 (75%) is a share of trips within this single week, not a
day-level statistic like "late 4 of 5 service days" (the example phrasing
in the brief) — this server doesn't currently group by service_date. It's
also only one week of data, so calling anything a pattern (Priya's
original framing — "is it a real pattern?") isn't something this can
honestly answer yet; see Questions and Cuts below.
Questions I'd have asked Priya before building
Is a single fixed delay threshold the right frame for every route, or should "late" mean unusually late relative to that specific route's own normal variance? I used one flat number (10 min) for all routes for simplicity — a route that's normally slow or normally very punctual might deserve a different bar.
Should near-misses just under the late line be surfaced separately as a leading indicator, rather than folded into "on time"?
For TRIP_052/TRIP_053 — is this a known duplicate-export bug, or could these genuinely be two back-to-back trips that happen to share every field? I assumed duplicate; worth confirming.
Is one week enough to call something "a pattern," or does that claim need multiple weeks of data before it goes in front of a regional manager?
What I deliberately cut, and why
Multi-week trend detection. Only one week of data is available, so "is it a real pattern?" can't be honestly answered from this alone.
A fleet-wide "worst offenders" ranking tool. Priya's concrete example was route-specific ("was route 12 late"), so I built the single-route lookup and drill-down first rather than a ranking view — a defensible narrower cut, though the brief's OTP framing ("which routes ran late") and the OPS-2207 directive both gesture at wanting a ranking eventually.
Day-level grouping (late X of Y service days, vs. share of trips). Would need one more aggregation step; cut to keep the slice small.
Cross-referencing
occupancy.csvandops_log.txt. The on-time question is fully answerable fromtrips.csvalone.A persistence layer or database. Not needed at this data size — a CSV read into memory is enough, per the brief.
Automatic correction of bad rows (e.g. guessing the "real" value for TRIP_044's offset bug). Flagging and excluding is safer than guessing.
What I'd do next
Add day-level late share (late N of M service days) alongside the current per-trip share, since they're genuinely different statistics.
Add an opt-in, clearly-flagged wall-clock repair path for offset-typo rows like TRIP_044, instead of only exclude — behind an explicit flag, never silent.
Build the fleet-wide ranking tool once Priya confirms the 10-minute line (and whether it should be route-relative) is the right one to rank against.
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 Connectors
Real-time transit stops, routes, arrivals, vehicle positions, and schedules via OneBusAway APIs.
Query Churn Solution cancellation-flow metrics, revenue, and feedback analytics (read-only).
Transitland MCP — global GTFS aggregator
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/Fieldspar04/cityflo-otp-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server