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 |
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。