hk-bus-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
hk-bus-mcp
A Hong Kong bus assistant exposed as a local MCP server. The same code runs in Xiaozhi (小智) voice hardware and in Codex / any MCP client.
Ask it "我想去淘大花園" and it answers with walk-plus-one-bus options it can actually verify, spoken in Cantonese:
一,六一九號,三分鐘後到牛頭角道,坐約八分鐘,落車行五分鐘。
All data comes from public, keyless Hong Kong government and operator APIs. There is no account, no API key, and no bundled map service.
Contents
Related MCP server: Hong Kong Health Data MCP Server
Why one bus and no transfers
Neither the KMB nor the Citybus API does journey planning. Reliable transfer routing would need a validated stop-interchange graph that no operator publishes, and a confidently wrong two-bus instruction is the worst possible answer for the people this is built for. So v1 answers only:
which stops are near a place, and how far the walk is;
when the next bus arrives (real-time ETA, per route);
which single route connects an origin to a destination, ranked by walk + wait + estimated ride + walk;
and when no direct bus exists, it says so instead of improvising a transfer.
Quick start
Requires Python 3.10+ and uv.
git clone https://github.com/wedsamuel1230/hk-bus-mcp.git
cd hk-bus-mcp
uv sync --extra dev
uv run hk-bus-cli find-stops 太古城中心
uv run hk-bus-cli next-bus 720 --place 太古城中心
uv run hk-bus-cli plan-trip 淘大花園 --origin 太古城中心
uv run hk-bus-cli route-info 1A --include-stopsA prebuilt src/hk_bus_mcp/data/snapshot.json ships with the repository, so the first
command works offline for stop lookup and needs no build step. Rebuild it (a few
minutes on the first run) when you want current routes and stops:
uv run hk-bus-refreshUse it from Codex
codex mcp add hk-bus -- uv run --directory "$(pwd)" hk-bus-mcp
codex mcp listAny MCP client that supports stdio servers can use the same command:
uv run hk-bus-mcp (or .venv/bin/hk-bus-mcp).
Use it from Xiaozhi
Xiaozhi does not connect to an HTTP MCP URL. xiaozhi-esp32-server publishes an
"MCP 接入点" WebSocket address, and your tool runs as a local process that dials out to
it. tools/mcp_pipe.py is that bridge: it starts this stdio server
and forwards frames in both directions, reconnecting with exponential backoff.
cp .env.example .env # put your ws://... MCP_ENDPOINT in it
uv sync --extra xiaozhi
uv run python tools/mcp_pipe.pyThen refresh the MCP tool list in the Xiaozhi console. Tools appear as plan_trip,
next_bus, find_stops, route_info.
To keep the bridge running after you close the terminal, load it as a macOS agent. The
endpoint token stays in the gitignored .env, not in the plist.
mkdir -p logs
sed -e "s|__PROJECT_DIR__|$(pwd)|g" -e "s|__UV_BIN__|$(command -v uv)|g" \
deploy/com.xiaozhi.hk-bus-mcp.plist > ~/Library/LaunchAgents/com.xiaozhi.hk-bus-mcp.plist
launchctl load ~/Library/LaunchAgents/com.xiaozhi.hk-bus-mcp.plist
launchctl list | grep hk-bus-mcp # status
tail -f logs/mcp_pipe.log # what the server is asking forSet MCP_PIPE_DEBUG=1 in .env to log every WebSocket frame, which is how you see an
incoming tools/call and its answer. Tokens in log lines are redacted.
Verify the bridge without a real endpoint first:
uv run python tools/mock_mcp_endpoint.py --serve # terminal 1
MCP_ENDPOINT=ws://127.0.0.1:8765/mcp_endpoint/mcp/?token=test uv run python tools/mcp_pipe.py # terminal 2Tools
Four narrow tools rather than one broad one: the caller is usually a language model, and a tool with one obvious meaning per parameter is much harder to call wrongly.
Tool | Question it answers | Arguments |
| 我想去淘大花園,點去? |
|
| 720 幾時到太古城? |
|
| 太古城中心附近有咩站? |
|
| 1A 由邊度去邊度? |
|
Every tool returns a single JSON object containing speech_zh_hk, one short Cantonese
sentence the assistant can read verbatim, plus structured fields for programmatic use.
Status values
status is part of the product, not error handling. A host should speak
speech_zh_hk and, when the status says the request was underspecified, ask a
follow-up question.
| Meaning |
| A verified answer follows. |
| The trip needs a starting point and no default is configured. |
|
|
| The place name matches several distinct locations; |
| No such place, stop, or route in the data. |
| The place resolved, but the route serves no stop near it. |
| The stop exists, but this route does not call there. |
| A direct bus exists, but there is no live ETA right now. |
| No single route connects the two ends; the tool refuses to invent a transfer. |
no_direct_route and no_arrivals are deliberately different: conflating them tells
someone "there is no bus" when a bus is simply unmeasured.
Response contract
plan_trip (abridged):
{
"status": "ok",
"speech_zh_hk": "一,六一九號,三分鐘後到牛頭角道,坐約八分鐘,落車行五分鐘。",
"origin": {"name": "太古城中心", "lat": 22.28676, "lon": 114.21734},
"destination": {"name": "淘大花園", "lat": 22.32409, "lon": 114.21643},
"options": [
{
"route": "619",
"operator": "KMB",
"board_stop_tc": "太古城中心",
"alight_stop_tc": "牛頭角道",
"eta_minutes": 3,
"estimated_ride_minutes": 8,
"walk_to_minutes": 2,
"walk_from_minutes": 5,
"total_minutes": 18,
"stops_on_board": 6,
"destination_tc": "順利"
}
],
"note": "eta_minutes 係實時數據;estimated_ride_minutes、walk_* 同 total_minutes 都係估算。只支援行路加一程巴士,唔支援轉車。"
}Field semantics are a truthfulness rule:
eta_minutesis live operator data — a fact, refreshed at call time.estimated_ride_minutes,walk_*andtotal_minutesare derived estimates and are always labelled as such. The official APIs publish no journey times.Missing data stays missing. The tool never fills a gap with a guess.
next_bus searches sibling stops by base name within 250 m, groups the answer by
direction, and uses towards to pick the one you actually want. This matters because a
route's two directions normally stop at different stop ids on opposite sides of the
road — 九龍灣站 (KT673) towards 九龍城碼頭 and 九龍灣站(東九文化中心) (KT319) towards
觀塘 are 81 m apart.
Data sources
Source | Endpoint | Used for |
KMB / LWB |
| routes, stops, route-stop, real-time ETA |
Citybus (CTB) |
| routes, stops, route-stop, real-time ETA |
Address Lookup Service |
| turning place names into coordinates |
Notes that cost real debugging time and are encoded in the code:
directionisoutbound/inboundin the URL path, butO/Iinside the records — in both APIs.Only
CTBis accepted as a Citybus company code.NWFBanswers with an empty list;BTH,LWB,KMB,GMBare rejected with HTTP 422.There is no green-minibus endpoint under
rt.data.gov.hk/v2/transport/gmb/(404), so minibuses are out of scope.ALS is an address service. Ask it for 旺角 and the top hit is a refuse collection point; ask it for 迪士尼樂園 and you get 康樂園 in Tai Po. It also never fails cleanly — a nonsense query still returns addresses. The code therefore combines a curated alias table, a score threshold, a distance cluster check, and a fallback that matches official bus-stop names.
Refreshing the snapshot
uv run hk-bus-refresh rebuilds src/hk_bus_mcp/data/snapshot.json from the live APIs.
The snapshot holds stops, routes, route-stop lists, and a simplified→traditional
character table learned from the operators' own name_sc/name_tc pairs.
KMB publishes whole-table endpoints, so three calls cover it. Citybus only publishes per-route and per-stop endpoints, so the refresh walks every route in both directions and then fetches each unique stop it does not already have. Re-runs reuse the previous snapshot's stops and are fast. Expect a few minutes on the first run.
The committed snapshot is a convenience, not a contract. Re-run the refresh for current routes; the ETA calls are always live and never served from the snapshot.
Customising places
places.yaml has three sections:
pinned— names you use, with coordinates you trust (fastest, no network);aliases— spoken name → text sent to ALS;my_places— your own names, e.g. 屋企.
Set default_origin: 屋企 so plan_trip can answer without asking where you are.
Every coordinate currently in pinned came from a live ALS response and is listed in
docs/VERIFICATION.md.
Environment variable | Purpose |
| Path to an alternative |
| Path to an alternative |
| Minimum ALS match score (default 70). |
| Xiaozhi WebSocket endpoint, used only by |
| Set to |
Architecture
flowchart LR
subgraph hosts["MCP hosts"]
X["Xiaozhi / ESP32<br/>WebSocket"]
C["Codex / MCP client<br/>stdio"]
L["hk-bus-cli"]
end
P["tools/mcp_pipe.py<br/>stdio ↔ WebSocket bridge"]
S["hk-bus-mcp<br/>stdio MCP server"]
V["BusService<br/>resolve · nearest · rank"]
D[("snapshot.json<br/>stops · routes · sc→tc")]
K[("KMB / LWB API")]
T[("Citybus API")]
A[("Address Lookup Service")]
X --> P --> S
C --> S
L --> V
S --> V
V --> D
V --> K
V --> T
V --> AOne business-logic layer (service.py) backs the MCP tools, the CLI, and the tests, so
what the tests prove is what the device runs.
Development
uv sync --extra dev --extra xiaozhi
uv run pytest -q # 49 tests, offline, < 1 sThe suite is offline by design: tests/fixtures/als/*.xml are recorded Address Lookup
Service responses, and the snapshot fixture is hand-built. No test may depend on live
traffic, because a test that needs the internet cannot gate a release.
Live checks against the real APIs, run the way a host would:
uv run python tools/mcp_battery.py # 11 scenarios, PASS/FAIL per scenario
uv run python tools/mcp_battery.py --json # machine readableThe battery exits non-zero if any scenario fails, so it doubles as a deployment smoke
test. tools/mcp_smoke.py is the minimal single-request version.
Path | Contents |
| MCP tool definitions and stdio transport |
| Business logic: resolution, nearest stops, ranking, speech |
| Single-ride candidate search and ETA collection |
| ALS client, alias/pinned resolution, disambiguation |
| HTTP clients for KMB, Citybus, ALS |
| Snapshot loading, indexes, simplified→traditional map |
| Snapshot rebuild script |
| Cantonese sentence generation |
| Developer CLI over the same service |
| Xiaozhi bridge, mock endpoint, live smoke tests |
| Verification log and lessons learned |
| macOS LaunchAgent for the bridge |
Limitations
No transfers, no MTR, no green minibus.
Ride time is estimated from stop counts (2 minutes per stop), not measured.
Speech recognition often returns simplified Chinese while operator data is traditional. The snapshot carries a simplified→traditional table learned from the operators' own
name_sc/name_tcpairs (359 characters), so 九龙湾 and 九龍灣 both work.Live diversions are not covered. The Transport Department publishes
https://resource.data.one.gov.hk/td/tc/specialtrafficnews.xml, but it is road-level traffic news, not per-route bus changes. What the operator APIs do expose — destination,service_type(特別班次) andrmk_tcon each arrival — is surfaced instead.Device-side behaviour (wake word, ASR accuracy, the Xiaozhi LLM's intent detection) is not verified here; this repository guarantees the MCP tool layer and its data.
Evidence and lessons
docs/VERIFICATION.md records the live API probes, the ALS
observations behind places.yaml, the snapshot build output, the test run, real-ETA
tool runs, the stdio and Xiaozhi-bridge handshakes, and an explicit list of what was
not verified. Six bugs found during verification — including two that only appear on
real data — are listed there with their regression tests.
docs/LESSONS.md collects the durable lessons: why the tool's real
client is a language model, why simplified-Chinese input broke every lookup, why "no
data" must not become "no bus", and the firmware bring-up traps.
License
MIT. Bus and stop data remains subject to the terms of the respective operators and DATA.GOV.HK.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Hong Kong public transport trips (MTR, bus, minibus, ferry), live times, fares and border queues.
Provide real-time transportation data including bus arrivals, train service alerts, carpark availa…
Get real-time bus arrival times for any Singapore bus stop by code, with optional service filterin…
Real-time transit stops, routes, arrivals, vehicle positions, and schedules via OneBusAway APIs.
Related MCP Servers
- AlicenseAqualityDmaintenanceA Model Context Protocol server that provides real-time access to Hong Kong's KMB and Long Win Bus route information and arrival times, enabling Language Models to answer user questions about bus routes, stops, and ETAs.54Apache 2.0
- AlicenseAqualityCmaintenanceEnables querying of current A\&E waiting times, specialist outpatient waiting times, and general outpatient clinic quotas in Hong Kong using natural language through MCP.398 PyPI4MIT
- FlicenseNot gradedqualityDmaintenanceProvides real-time Singapore bus information, including arrival times, routes, and stop details via the LTA DataMall API.1-
- AlicenseNot gradedqualityBmaintenanceEnables realtime bus arrival queries, route and stop searches, nearby station lookups, and transit planning for Chelaile-supported cities through MCP tools.11MIT