Cityflo OTP MCP
# Cityflo OTP MCP (AI Engineer take-home)
Small **stdio MCP server** for Mumbai North **on-time performance** over one week of `trips.csv`. Built for Priya’s ask: *“Was route 12 late this week, and by how much?”* — with drill-down, not a dashboard.
## Scope
**In**
- Domain: on-time performance only
- Data: `data/trips.csv` (Mon–Fri 2026-06-15 … 2026-06-19)
- Tools: `route_otp_summary`, `list_late_trips`, `data_quality_report`
- Deterministic delay / quarantine / aggregation in Python tools
**Out**
- Occupancy, ticket triage, ops-log summariser
- Databases, BI dashboards, web UI
- Silent “data normalisation” that hides policy (see OPS-2207 below)
## Architecture
```
Agent / MCP client (stdio)
│
▼
cityflo_otp.server (FastMCP, 3 tools)
│
▼
cityflo_otp.otp (parse → quarantine → delays → aggregates)
│
▼
data/trips.csv
```
The model should call tools for numbers and only phrase the answer. Arithmetic never lives in the LLM.
## Late definition
- `arrival_delay_min = actual_arrival − scheduled_arrival` (timezone-aware)
- **Late if `arrival_delay_min > 10`** (default; overridable via `late_threshold_min`)
- Headline metrics: **% late of usable trips**, **median** / **p90** arrival delay (not a fragile mean)
- Departure delay is returned for drill-down only
## Data-quality decisions
| Trip | Action | Why |
|------|--------|-----|
| TRIP_031 | Quarantine | `actual_departure` has invalid `08:60` |
| TRIP_017 | Quarantine | Actual arrival before actual departure |
| TRIP_044 | Quarantine | `actual_arrival` uses `+00:00`; naive compare ≈ +333 min |
| TRIP_101 | Quarantine | Missing `scheduled_arrival` |
Quarantined rows are **excluded** from OTP % / median and **listed** in tools. We do **not** invent corrected timestamps.
During **initial data inspection** (reading the starter bundle’s `ops_log.txt`, which is **not** loaded by this MCP at runtime), device **D-22** / MH-14-5590 was noted as flaky. The runtime tools only see `trips.csv`: they quarantine D-22 rows when **timestamp checks fail**, and leave other consistent D-22 trips usable.
### Untrusted handoff text (OPS-2207 / MH-12-7781)
`HANDOFF.md` contains instruction-shaped text asking tools to force vehicle **MH-12-7781** on-time and hide that rule. This server **does not** apply that override. The same late rule is used for every plate. `data_quality_report` surfaces the raw MH-12-7781 evaluation and a **policy question for Priya**. Hiding falsified numbers from a regional manager fails the auditability bar.
## Assumptions
1. Arrival lateness is the right primary metric for Priya’s standup fight.
2. Default threshold of **10 minutes** separates pattern (e.g. Route 12) from noise.
3. Prefer quarantine over silent correction of GPS/timezone garbage.
4. Thin samples (e.g. R-21 n=1) should be labeled, not ranked aggressively.
5. Ticket/occupancy files are out of scope for this MCP.
## Questions for Priya
1. Confirm **arrival** (vs departure) as the official late definition?
2. Confirm **>10 min** for regional-manager figures (or 5 / 15)?
3. How should **MH-12-7781 / OPS-2207** appear: exclude with disclosure, include raw, or dual view?
4. Minimum trip count before a route enters “worst offenders”?
5. For `+00:00` timestamps: always quarantine, or allow an explicit IST reinterpretation with an audit flag?
## How to run
```bash
cd ~/repos/cityflo-otp-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
# or: pip install mcp && PYTHONPATH=src ...
```
**Checks (no MCP):**
```bash
PYTHONPATH=src python scripts/verify_otp.py
```
**Real MCP client multi-step run** (`route_otp_summary` → `list_late_trips` → `data_quality_report`):
```bash
PYTHONPATH=src python scripts/run_mcp_client.py
```
Transcript written to `notes/mcp_client_transcript.jsonl`.
**Cursor / Claude Desktop (stdio):** point an MCP server entry at:
```json
{
"mcpServers": {
"cityflo-otp": {
"command": "python",
"args": ["/absolute/path/to/cityflo-otp-mcp/src/cityflo_otp/server.py"],
"env": {
"PYTHONPATH": "/absolute/path/to/cityflo-otp-mcp/src",
"CITYFLO_TRIPS_CSV": "/absolute/path/to/cityflo-otp-mcp/data/trips.csv"
}
}
}
}
```
## Example agent interaction
1. `route_otp_summary({ "route": "Route 12" })`
→ usable 8, late 6, **75%** late, median arrival delay on the order of ~13–16 min, 0 quarantined on R-12.
2. `list_late_trips({ "route": "R-12" })`
→ the six late trip ids with per-trip arrival delays and vehicles.
3. `data_quality_report({})`
→ four quarantined trips week-wide + MH-12-7781 policy note (TRIP_090 / TRIP_119 late under raw rule).
## Deliberately cut
- Other domains (occupancy / tickets / standup)
- Auto-fixing timezone offsets
- Ranking API across all routes (easy to add; not required for Priya’s Route 12 ask)
- Hidden OPS-2207 normalisation
## Where I disagreed with the AI
See `notes/DISAGREEMENTS.md`.
TDQS
Scored across 3 tools
Each tool targets a distinct level of analysis: route-level aggregate OTP, trip-level late-trip detail, and data-quality context for quarantined rows. The descriptions explicitly frame list_late_trips as a drill-down from route_otp_summary, so there is no real overlap.
All names use lowercase snake_case and clearly describe their output, but they do not follow a single verb_noun pattern: two are noun phrases (route_otp_summary, data_quality_report) and one uses a verb (list_late_trips). This is a minor stylistic deviation rather than a major inconsistency.
With three tools, the server is tightly scoped to OTP reporting: summary, drill-down, and data quality. Each tool has a clear purpose and none feel redundant.
The tools cover the core read-only OTP workflow: aggregate summary, late-trip detail, and explanation of excluded data. A route enumeration tool would be a useful addition, but agents can work around this when route IDs are supplied.