Cityflo on-time performance MCP

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.

Category
Visit Server

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

  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

Recommended Servers

playwright-mcp

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured