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: fleetsync-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

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

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/devbathani/Cityflo-Internal-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server