Skip to main content
Glama
devbathani

Cityflo on-time performance MCP

by devbathani

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


Related MCP server: Cityflo OTP MCP

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

  1. Open the site → sign in (cityflo.mcp@gmail.com / tester) → Copy Cursor MCP JSON (JWT embedded).

  2. 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)

  1. Late = arrival lateness actual_arrival − scheduled_arrival strictly greater than the threshold (default 10 minutes). Exactly 10 minutes counts as on-time.

  2. Early arrivals (negative lateness) are on-time; median and max include all valid trips.

  3. When start_date / end_date are omitted, summary and evidence both use min..max service_date in the loaded export (“this week” ≈ the export window).

  4. Times are interpreted in IST (+05:30). Mixed-offset rows coerce non-IST wall clocks to IST and set timezoneNormalised (see TRIP_044).

  5. Unparseable times, arrival-before-departure, missing scheduled_arrival, and calendar mismatches are excluded from metrics and surfaced in the quality report.

  6. Exact operational duplicates count once (lowest trip_id kept); extras are duplicate_of.

  7. Headlines use late rate + median + max, not mean alone — one bad GPS row must not swing standup.

  8. 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

  1. Is 10 minutes the regional review threshold, or do you want 5 / 15 for standup?

  2. Should we judge arrival, departure, or both?

  3. When GPS rows look impossible, do you prefer exclude-from-metrics (current) or keep-with-flag in the headline?

  4. 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

  1. Timezone on TRIP_044. A naïve “parse ISO as instants” path made the mixed +00:00 arrival 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:30 and flagged timezoneNormalised (~3 minutes late).

  2. 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.

  3. 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.

  4. Scope. Building all four domains was tempting. Priya’s question is Route 12 lateness with evidence — the other three domains stayed out.

  5. Hardcoded calendar window. An early draft baked 2026-06-15..19 into 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

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    C
    maintenance
    Provides on-time performance analysis for Mumbai North bus routes via tools for route summaries, late trip drill-down, and data quality reports. It quarantines invalid data and surfaces policy questions instead of hidden normalizations.
    3
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A dependency-free MCP server for Mumbai North operations that computes route lateness metrics, exposes underlying late trips, and reports data quality issues to answer whether a route was late and by how much.
    -
  • F
    license
    B
    quality
    C
    maintenance
    Provides auditable Mumbai route lateness metrics, including route ranking, performance summaries, and trip-level evidence, derived from a reloadable CSV dataset.
    3
    -