Unofficial · MIT licensed · TypeScript
Stations, schedules, fares, Komuter timetables, and live vehicle positions for the entire KTMB network — as a typed library, REST API, and MCP server. Powered by data.gov.my GTFS feeds.
Station search
156 stations across ETS, Intercity, Komuter, and Shuttle Tebrau — searchable by code, English name, or Bahasa Melayu name with Fuse.js scoring.
Schedules & fares
All ETS, Intercity, and Shuttle Tebrau departures within the GTFS calendar window — joined with fare data from KTMB's booking site.
Komuter timetables
Pick a line, pick a station — get the next 12 departures rolling forward from now. Calendar-aware; weekday and weekend services swap automatically.
Realtime · GTFS-RT
Live vehicle positions polled from data.gov.my's GTFS-Realtime feed — typed, validated, and re-emitted with consistent ISO-8601 +08:00 timestamps.
Library
A single read-only core powers the typed library, a Hono-based REST server, and a stdio MCP server for Claude Desktop and Claude Code.
import { GtfsLoader, createKtmb, ktmbGetAvailability, fetchVehiclePositions } from "ktmb"; // Load the GTFS Static feed once at boot. const loader = new GtfsLoader( "https://api.data.gov.my/gtfs-static/ktmb" ); const r = await loader.load(); if (!r.ok) throw new Error(r.error.message); const ktmb = createKtmb({ store: r.data, fareGetter: ktmbGetAvailability, realtimeFetcher: () => fetchVehiclePositions( "https://api.data.gov.my/gtfs-realtime/vehicle-position/ktmb" ), }); const stations = ktmb.stations.search("KL"); const trains = ktmb.schedules.listSchedules({ from: "KUL", to: "BTW", date: "2026-05-01", });
# Start the server $ npx ktmb-api # PORT defaults to 8787 # Search stations (fuzzy, returns best matches) $ curl "localhost:8787/v1/stations?q=Subang" # List ETS schedules $ curl "localhost:8787/v1/schedules?from=KUL&to=BTW&date=2026-05-01" # Komuter timetable for a line + station $ curl "localhost:8787/v1/komuter/lines/POR/timetable?station=KAJ&date=2026-05-01" # Live vehicle positions $ curl "localhost:8787/v1/realtime/vehicles" # All responses use the discriminated envelope: # { "ok": true, "data": ... } | { "ok": false, "error": { "code", "message" } }
// claude_desktop_config.json { "mcpServers": { "ktmb": { "command": "npx", "args": ["ktmb-mcp"] } } } // Six tools exposed to the model: // search_stations - fuzzy station lookup // list_schedules - ETS / Intercity / Shuttle Tebrau // get_fare_availability - per-class fare + seats left // list_komuter_lines - all Komuter line ids // get_komuter_timetable - departures by line + station // get_vehicle_positions - live GTFS-RT vehicle feed
REST surface
Every response is either { ok: true, data } or { ok: false, error: { code, message } }. Typed errors map cleanly to HTTP statuses; outside_calendar_window is 422.