Overview
krl-mcp is a Model Context Protocol server that lets any MCP host (Claude Code, Claude Desktop, Cursor) answer questions about Jakarta's KRL Commuter trains: departure boards, line catalogs, nearest-station lookups, and a direct-trip planner. It is deliberately zero-infrastructure — no database, no cache, no network, no Docker. The open-krl schedule snapshot (~5 MB of artifacts) is pinned in the repo and embedded into the executable at build time, so the entire deployment story is go install + claude mcp add, running from any working directory.
It is the MCP face of krl-schedule-service — my production system with a REST API, Postgres, Redis, and a trip planner. krl-mcp ports the same query logic over the same data contract into plain in-memory scans, proving the domain logic stands on its own.
Technical Architecture & Flow
boards/*.json ──────┐
stations.json ──────┤ parse once ┌──────────────────────────────┐
GTFS stop_times ────┼─────────────▶│ in-memory store (~5 MB RAM) │
GTFS trips ─────────┘ └──────────────┬───────────────┘
│ stdio (JSON-RPC 2.0)
▼
MCP host (Claude Code, ...)
The embed. asset.go go:embeds the pinned snapshot; the store parses it once at boot into plain slices and maps. One static ~15 MB binary, no working-directory assumptions — an MCP host can spawn it from anywhere.
SQL becomes memory structures. What the production service runs as SQL becomes prebuilt indexes here: the planner's trip self-join is a stop → (trip, seq) index; line colors are reduced in-memory; the JSON output shapes are byte-compatible with the prod API, and every golden assertion in the test suite was verified against the artifacts before being written.
Station resolution, 4 tiers. "Tanah Tinggi", "tanah tinggi", "TANAHTINGGI", and "THI" all resolve: exact code → exact name → normalized → substring, implemented as three maps plus a fallback scan.
The service-day clock. 24:04 means 00:04 after midnight of the same service day — the GTFS convention that keeps late-night trains sortable. The planner accepts hours up to 47.
The honesty contract. When no direct trip exists, plan_trip returns meta.transfer_candidates — the stops where a one-transfer route is possible — so an LLM caller can reason about transfers from data instead of inventing routes. Empty means none exists. Likewise get_trip_stops exists because a headsign is not a route: train 5133D says "ANGKE" but runs the southern ring via Manggarai.
Six Tools
| Tool | What it answers ||---|---|| get_stations | The 111-station catalog (Jabodetabek + Yogyakarta–Solo) with coordinates || get_departures | One station's departure board in a time window || get_lines | The commercial lines with canonical colors || plan_trip | Direct-trip planner: depart_at or arrive_by, with intermediate stops || find_nearest_station | GPS coordinate → closest stations with distances || get_trip_stops | One train's full ordered stop sequence — the auditable route narrative |
Engineering Notes
- Dogfooding as QA: the LLM itself surfaced three real bugs — a relative data path that silently served empty boards, a name-resolution gap between description and code, and a route hallucination caused by missing intermediate-stop data (fixed by exposing
via+get_trip_stops, the origin of the honesty contract). - CI from push one: the release pipeline builds 5-platform binaries automatically; CI (vet + test) once caught an unanchored
.gitignorepattern that had silently excludedcmd/from the public repo — the local build worked, strangers' clones didn't. - Licensing split: code is MIT; the bundled open-krl data is CC-BY 4.0 (OSM coordinates ODbL) — attribution lives in
NOTICE, kept explicit and separate.
Tech stack
Architecture
krl-mcp/
├── asset.go # go:embed — pinned boards/stations/GTFS snapshot in-binary
├── cmd/krl-mcp/main.go # stdio entry point (JSON-RPC), cwd-independent
├── internal/server/ # MCP layer: 6 tools over go-sdk + e2e test (builds & spawns own binary)
├── internal/store/ # in-memory store: 4-tier station resolution, planner,
│ # stop→(trip,seq) index, service-day clock (h ≤ 47)
├── data/ # pinned open-krl artifacts + manifest (CC-BY 4.0 / ODbL)
├── Makefile · NOTICE · LICENSE (MIT)
└── .github/workflows/ # CI (vet+test) + Release (5-platform binaries)
…