How MCP Lets Claude Plan Your Train Route — krl-mcp in Practice
How MCP Lets Claude Plan Your Train Route — krl-mcp in Practice November 1, 2026 · 4 min read
AIWorkflow

The Question That Used to Be a Dead End

"Aku di Tanah Abang, harus di Kebayoran sebelum jam 6 — kereta apa?"

That's a question every Jakarta commuter asks. Chat apps can't answer it. A bare chat model sort of can — by guessing, which for train schedules is worse than useless. What the model actually needs is the timetable: real departure boards, real stop sequences, real times it can compute with instead of recite from memory.

That gap is exactly what the Model Context Protocol closes. I built krl-mcp to watch it happen on data I know well — a self-contained MCP server for Jakarta's KRL Commuter schedules, written in Go, with the whole open-krl snapshot embedded in the binary. Register it once, and Claude Code becomes a commuter trip planner that cites its numbers.

What MCP Actually Does Here

MCP is a standard wiring between an LLM host and tools. Three pieces matter:

  • The host — Claude Code, Claude Desktop, Cursor. The part that reasons and talks to you.
  • The server — a small program (here: one ~15 MB Go binary) that exposes tools over stdio, speaking JSON-RPC.
  • The tools — functions with names, descriptions, and typed inputs. The host's model decides which to call, with what arguments, based on nothing but those descriptions.

No plugin store, no API keys, no infrastructure. go install the binary, claude mcp add krl -- <path>, and the model's toolbox gains six new instruments:

| 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 || plan_trip | Direct-trip planning: depart_at (earliest first) or arrive_by (latest that still makes it) || find_nearest_station | GPS coordinates → closest stations, with distances || get_lines | The commercial lines and their canonical colors || get_trip_stops | One train's full ordered stop sequence |

One Query, Traced End to End

"Dari Juanda ke Bogor, sore ini sekitar jam 5." The model doesn't ask me anything — it calls:

plan_trip(from="Juanda", to="Bogor", depart_at="17:00")

The server scans its in-memory index and returns the direct trains, earliest first. The answer that comes back:

Train 1362 — departs Juanda 17:04, arrives Bogor 18:23 (79 minutes), calling at intermediate stops along the way.

And because the tool returns the via stops, the model can tell you whether it passes through your actual destination-adjacent station — not just "a train exists." Flip the question to "must arrive by 18:00" and the same tool, in arrive_by mode, returns the latest train that still makes it. That's a real planning primitive, not a search box.

The pattern composes. "I'm at Monas, which station do I walk to?" is find_nearest_station with coordinates. "Kereta 5133D lewat mana sih?" is get_trip_stops — and that one matters more than it looks, next section.

Why the Answers Stay Honest

Three design decisions keep the model from confidently misleading you:

Empty answers carry pointers. When no direct trip exists, plan_trip doesn't just return nothing — it returns transfer_candidates: the stops where a one-transfer route is possible. The model gets material to reason with, instead of a gap to fill with invention.

Headsigns are not routes. Train 5133D says "ANGKE" on the front, but it runs the southern ring via Manggarai. Ask the headsign and you'll be told the wrong direction — which is exactly what happened the first time I tested this with no stop data exposed: the model narrated a plausible, wrong route. get_trip_stops exists so "which way does this train actually go" is answered from data.

Times are service-day, not wall-clock. 24:04 means 00:04 after midnight of the same service day — the GTFS convention that keeps the last trains sortable. The planner accepts hours up to 47, so "lewat tengah malam" queries work.

(That hallucinated-route moment taught me the general lesson: when a model misuses your tool, the fix is usually exposing the data it needed, not tightening the prompt. The bug list the model found while dogfooding is in the repo README — tool descriptions are contracts, and LLMs test every clause.)

Try It in 60 Seconds

go install github.com/athallarizky/krl-mcp/cmd/krl-mcp@latest
claude mcp add krl --scope user -- "$(go env GOPATH)/bin/krl-mcp"

(No Go? Grab a prebuilt binary for your platform from the releases. Any MCP host works — Claude Desktop and Cursor take the same server via JSON config.)

Then just ask, in any language:

"Aku di Pasar Senen, mau ke Depok sebelum jam 8 pagi. Kereta mana?"

The Pattern Generalizes

krl-mcp is about trains, but the shape is universal: a pinned dataset + domain logic + six honest tools = a subject-matter expert any LLM can hire. Schedules, catalogs, internal APIs, lab instruments — if you can express the questions as tool calls with honest outputs, the conversational layer comes free. The model was never the bottleneck. The missing piece was giving it a way to check.

AT
Athalla Rizky
Full-Stack Engineer · Backend · AI tooling
I write about backend systems, developer experience, and running software projects with AI agents. Follow along — new posts every other week.