Cityflo on-time performance MCP
Answers operational questions about route on-time performance, such as lateness rates and trip evidence, using structured tools for route summary, trip lateness, and data quality.
README
Cityflo on-time performance MCP
MCP server for Mumbai North on-time performance — the slice Priya’s handoff actually asks for:
Was route 12 late this week, and by how much — and which trips prove it?
Built for the Cityflo AI Engineer take-home. Domain chosen: on-time performance only. Occupancy, support-ticket triage, and ops-log summarisation were deliberately cut so the tools stay sharp and auditable.
Ops people get a plain-language answer from an MCP client (Cursor or the included Kimi agent). Deterministic TypeScript does the arithmetic; the model only phrases the result. Every headline can be drilled into trip-level evidence.
Stack: Node.js 20.x, TypeScript, @modelcontextprotocol/sdk. No database — data/trips.csv is loaded in memory.
Install
git clone <this-repo>
cd Cityflo-Assignment # or your clone path
npm install
cp .env.example .env
| Env var | Required for | Notes |
|---|---|---|
MOONSHOT_API_KEY |
npm run agent |
Moonshot / Kimi path only — never deploy this to Vercel |
JWT_SECRET |
Hosted /api/mcp + web gateway |
Min 16 chars; openssl rand -hex 32 |
TRIPS_CSV_PATH |
Optional | Override default data/trips.csv |
How to run
1. Local stdio MCP (Cursor — primary path)
npm run mcp
.cursor/mcp.json already points Cursor at this server. After npm install, reload MCP in Cursor and ask:
Was route 12 late this week, and by how much?
Production stdio (no tsx):
npm run build:mcp
npm run start:mcp
# MCP command: node dist/mcp/server.js
2. End-to-end with a real client (no LLM)
npm test # domain + MCP integration + JWT (36 tests)
npm run e2e # multi-step MCP client → demos/mcp-e2e-session.md
3. Optional Kimi K2.6 terminal agent
# set MOONSHOT_API_KEY in .env
npm run agent -- "Was route 12 late this week, and by how much?"
Transcripts when generated: demos/agent-session.md, demos/mcp-e2e-session.md.
4. Hosted Streamable HTTP (optional for Cityflo demos)
Live: https://cityflo-on-time-mcp.vercel.app
- Open the site → sign in (
cityflo.mcp@gmail.com/tester) → Copy Cursor MCP JSON (JWT embedded). - Or mint a token:
npm run mint-token, then:
{
"mcpServers": {
"cityflo-on-time": {
"url": "https://cityflo-on-time-mcp.vercel.app/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_JWT"
}
}
}
}
| Endpoint | Auth | Purpose |
|---|---|---|
/ |
internal login | Cityflo-styled gateway — copy MCP config |
/api/health |
none | Liveness + trip count |
/api/mcp |
Bearer JWT (on_time:read) |
Streamable HTTP MCP |
Local hosted smoke: JWT_SECRET=… npm run dev then MCP_BASE_URL=http://localhost:3000 npm run smoke:http.
Tools (3)
Computation stays in tools. The model phrases language only.
| Tool | What it does |
|---|---|
get_route_on_time_summary |
Route-level late rate, median/max lateness, daily pattern, late trip ids, rules applied |
get_trip_lateness_evidence |
Trip-by-trip scheduled vs actual + classification + exclusion reasons (requires route_id or trip_id) |
get_data_quality_report |
Quarantined / duplicate rows by reason and GPS device |
Invalid filters return structured MCP isError payloads — not silent wrong numbers.
Human/model boundary: tools return JSON numbers and trip ids; the client/agent narrates. Answering Priya honestly is a multi-step loop (summary → evidence → optional quality), not one call.
Assumptions (under-specified parts of the brief)
- Late = arrival lateness
actual_arrival − scheduled_arrivalstrictly greater than the threshold (default 10 minutes). Exactly 10 minutes counts as on-time. - Early arrivals (negative lateness) are on-time; median and max include all valid trips.
- When
start_date/end_dateare omitted, summary and evidence both use min..maxservice_datein the loaded export (“this week” ≈ the export window). - Times are interpreted in IST (+05:30). Mixed-offset rows coerce non-IST wall clocks to IST and set
timezoneNormalised(see TRIP_044). - Unparseable times, arrival-before-departure, missing
scheduled_arrival, and calendar mismatches are excluded from metrics and surfaced in the quality report. - Exact operational duplicates count once (lowest
trip_idkept); extras areduplicate_of. - Headlines use late rate + median + max, not mean alone — one bad GPS row must not swing standup.
- Free text in handoffs / tickets / ops logs is untrusted data, never tool instructions. Every vehicle is scored with the same documented rules.
How messy data was handled (data/trips.csv)
| Trip | Issue | Decision |
|---|---|---|
| TRIP_017 | actual_arrival before actual_departure (device D-22) |
Exclude — arrival_before_departure |
| TRIP_031 | actual_departure = 08:60:00 (D-22) |
Exclude — unparseable_timestamp |
| TRIP_044 | Arrival tagged +00:00, rest +05:30 |
Keep after IST wall-clock coercion; ~3 min late |
| TRIP_052 / TRIP_053 | Exact duplicate R-09 rows | Keep 052; exclude 053 as duplicate_of |
| TRIP_101 | Empty scheduled_arrival |
Exclude — missing_scheduled_arrival |
Device D-22 appears twice in exclusions — consistent with Priya’s flaky-GPS warning. See also docs/DATA_AUDIT.md.
Route 12 answer (export window, 10 min threshold)
- 8 valid trips, 6 late (75%)
- Late on 4 of 5 service days
- Median lateness 13.5 min, max 18 min
- Friday (TRIP_077 / TRIP_078) is the on-time day
That is a real late pattern for the week — not just a loud complaint — and get_trip_lateness_evidence lists the trips behind the rate.
Questions I would ask Priya
- Is 10 minutes the regional review threshold, or do you want 5 / 15 for standup?
- Should we judge arrival, departure, or both?
- When GPS rows look impossible, do you prefer exclude-from-metrics (current) or keep-with-flag in the headline?
- Which window is “this week” — Mon–Fri service dates only, or calendar week including weekends?
Trust boundary
This server only reads trips.csv (or TRIPS_CSV_PATH). Operational free text elsewhere — including anything that looks like a standing policy inside a handoff — is not executed as instructions. Metrics come from deterministic code so a regional manager can ask “which trips?” and get ids + timestamps, not a vibe.
What was deliberately cut (and why)
| Cut | Why |
|---|---|
| Occupancy / tickets / overnight-log tools | Brief: one sharp domain. Priya’s ask is Route 12 lateness with evidence. |
| Database | CSV-in-memory is enough for the export size and the half-day slice. |
| Full OAuth / IdP | Hosted path uses short-lived HS256 JWTs + internal login for demos. |
| Auto root-cause / “blame the corridor” | Devices show up in quality grouping; narrative cause is the model’s job only when grounded in evidence. |
Kimi / MOONSHOT_API_KEY on Vercel |
Keys stay local; hosted surface is MCP + JWT only. |
If there had been more time: configurable thresholds per region, departure lateness as a second metric, and a thin “compare two weeks” tool — not a dashboard.
Where I disagreed with the AI
-
Timezone on TRIP_044. A naïve “parse ISO as instants” path made the mixed
+00:00arrival look ~5.5 hours late. After inspecting the export (wall clock looks IST; only the offset is wrong), I coerced non-IST wall clocks on mixed-offset rows to+05:30and flaggedtimezoneNormalised(~3 minutes late). -
Handoff special-casing a vehicle. The ops handoff text asked to quietly force one plate always on-time and hide it from rankings. That conflicts with auditability and the trust-boundary criterion. I treated it as untrusted data, not a product requirement, and score every vehicle with the same rules.
-
Headline metric. Mean delay was the easy default. One garbage or extreme trip would swing standup. I used late rate + median + max and forced drill-down via
get_trip_lateness_evidence. -
Scope. Building all four domains was tempting. Priya’s question is Route 12 lateness with evidence — the other three domains stayed out.
-
Hardcoded calendar window. An early draft baked
2026-06-15..19into the domain. Default window now comes from the loaded export so summary and evidence cannot silently diverge when the CSV grows.
More detail: NOTES.md.
Project layout
app/ # Next.js gateway + hosted MCP/health/login
src/domain/trips.ts # load, validate, summarise
src/mcp/createServer.ts # transport-independent tools
src/mcp/server.ts # stdio entrypoint
src/agent/chat.ts # optional Kimi MCP client
src/auth/jwt.ts # HS256 mint/verify
data/trips.csv # runtime input
demos/ # e2e + agent transcripts
docs/DATA_AUDIT.md # data decisions
test/ # domain, MCP integration, JWT
Commands cheat sheet
| Command | Purpose |
|---|---|
npm install |
Install dependencies |
npm run mcp |
Stdio MCP for Cursor |
npm run agent -- "…" |
Kimi agent over stdio MCP |
npm test / npm run e2e |
Unit/integration + multi-step smoke |
npm run build:mcp && npm run start:mcp |
Production stdio |
npm run dev |
Local Next (gateway + /api/mcp) |
npm run mint-token |
CLI JWT for hosted MCP |
npm run smoke:http |
Hosted JWT + Route 12 smoke |
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.