
Charlotte Area Transit (CATS)
io.github.ajwann由 GitHub 帳號 ajwann 發布v2.1.1更新於 Nov 2, 2026
CATS bus and light rail trip planning, live arrivals, timetables, routes, and service alerts
概覽
針對夏洛特地區大眾運輸(CATS)公車與輕軌的 MCP 伺服器,提供行程規劃、即時到站、時刻表、路線、站點、車輛與服務公告。
- 可以做什麼
- 在站點或座標之間規劃公車與輕軌行程,包含步行、轉乘與即時延誤
- 查詢某站或距離某位置最近車站的即時預估到站,以及任一日的公布時刻表
- 查看某條路線的站點順序、終點、首末班時間與班距
- 列出站點及行經路線,並查看營運中公車與列車的 GPS 位置
- 讀取全系統或依路線、站點、地點篩選的目前與即將生效服務公告
- 以 MCP 資源形式讀取底層 GTFS 時刻表表格與解碼後的即時資料來源
- 適用情境
- 適合讓助理回答夏洛特公共運輸問題:某站下一班車何時到、離我最近的車站在哪、如何搭公車與輕軌前往某地,或某條路線是否受改道影響。
- 執行需求
- 可使用託管 HTTP 端點(需以允許名單內的 Google 帳號登入),或在本機以 Python 3.11+ 執行(pip install queenscoach,或以 uvx queenscoach 走 stdio,無需驗證)。需要能連線 CATS 的 GTFS 與 GTFS-Realtime 資料來源。自行部署 HTTP 傳輸需 Google OAuth 用戶端憑證與允許名單設定。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Charlotte Area Transit (CATS),將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。
交給代理程式安裝
把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。
閱讀 https://sourceweft.com/mcp/io.github.ajwann%2Fqueenscoach/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。其他 MCP 客戶端
把它新增到你客戶端的 mcpServers 設定中。
{
"mcpServers": {
"charlotte-area-transit-cats": {
"type": "http",
"url": "https://queenscoach.adamwanninger.com/mcp"
}
}
}README
QueensCoach ♔♘
QueensCoach is an MCP server for Charlotte Area Transit System (CATS) buses and light rail, built on the agency's public GTFS schedule and GTFS-Realtime feeds. It plans trips, predicts arrivals, reads the timetable, describes routes, and reports service alerts. It runs over stdio, launched by the MCP client that uses it, or over HTTP with Google OAuth in front of it, for a hosted server. Both transports serve the same tools and resources.
Hosted server
A public instance runs on Google Cloud Run. Sign in with any Google account:
There is zero guarantee of uptime. The hosted server is provided as-is. It may be
slow, down, switched off by its spending cap, or retired without notice. For anything
you rely on, run your own: over stdio, or on your own Google Cloud project
with deploy/GCP.md.
Signing in tells the server your Google account's email address, which is used only to decide whether to admit you, and is never stored. The tokens it issues record your account's opaque Google ID and nothing else about you. A location you share with a tool arrives as an argument to that call, is used to answer it, and is not stored. The privacy policy and terms of service cover the hosted server.
Adding it to Claude
Claude calls a remote MCP server a connector. Custom connectors are available on Claude's paid plans.
- Open Settings → Connectors. On the web that's claude.ai/settings/connectors; in the desktop app, Settings then Connectors.
- Click Add custom connector at the bottom of the list.
- Give it a name,
QueensCoach, and paste the URL above as the remote MCP server URL. Leave the advanced OAuth fields empty: this server registers your client automatically. - Click Add, then Connect on the connector that appears. A browser window opens for the Google sign-in; approve it and it closes itself.
- In a chat, open the tools menu and check that QueensCoach is enabled. Its seven tools then appear.
The connector belongs to your Claude account, so it follows you across web, desktop, and mobile. To disconnect, remove it from that same Connectors page; that revokes the tokens this server issued.
Adding it to Claude Code
Then run /mcp, pick queenscoach, and choose Authenticate, which opens the same
Google sign-in. /mcp shows the connection's state afterwards. A server added this way
loads when Claude Code next starts.
Any other client
Any MCP client that supports remote servers over streamable HTTP with OAuth works: give it the same URL and it discovers the rest.
Tools
Ask Claude "what's the closest train station to me?", "when's the next train at my stop?", or "how do I get to the airport from here?" and it passes your device's location to the tools. The server never sees a location it isn't handed, so this needs a client that shares the device's location with the model.
plan_trip
Returns up to three itineraries, each with walking and riding legs: the route, the
vehicle's real destination, where to board and get off, times, the wait at each
transfer, and stops travelled. Journeys with more transfers are offered only when they
arrive sooner (or, for arrive_by, leave later). For trips starting within three hours
of now, rides carry live delays and are marked live. walkingIsAnOption appears when
the two places are within the walking limit of each other.
Walking is an estimate: the straight-line distance stretched by 30%, at 4.5 km/h. The feed has no street map, so real walks can be longer. Transfers allow up to 400 m on foot and two minutes of slack.
get_arrivals
Give either stop or a location. With a location, the filters choose the station too:
mode: train finds the nearest stop a train calls at, not a closer bus stop, and the
response carries its metersAway.
Returns minutes away, predicted and scheduled times, schedule deviation, the vehicle
number, the platform (stopId), and that vehicle's live position. When a name query
is ambiguous, the best match is used and the runners-up are listed under
otherStopsMatchingQuery. Current service alerts affecting the stop or its routes are
attached as serviceAlerts, in the same shape get_service_alerts returns. For times
beyond the next hour or so, use get_schedule.
get_schedule
Scheduled departures with each trip's route and destination, plus firstDeparture
and lastDeparture for the day. A service day runs past midnight, as GTFS does: the
last trains of Tuesday leave at 1:31 am on Wednesday, and 25:30 in after or before
means 1:30 am that night. Use get_arrivals for live predictions.
get_route
For each direction: the destination, the stops in order (following the pattern most trips use, with any others summarized), first and last departure, and the typical minutes between trips in the early morning, morning rush, midday, afternoon rush, evening, and late night. Also trips on each of the next seven days and the number of active alerts. A query matching several routes lists them instead.
get_service_alerts
Give at most one; with none, every alert. Ended alerts are left out. Each alert has its
headline, description, effect and cause with CATS's detail text, whether it is
active or upcoming, its periods (untilFurtherNotice for open-ended ones), and the
routes and stops it names.
list_stops
Each station lists its stopIds, coordinates, modes, and routes (id, number, long
name, mode): for example 501 · Light Rail - Lynx Blue Line, 510 · CityLYNX Gold Line, or bus routes like 29. Stops no scheduled trip calls at are left out, and
nothing farther than 50 km from the location is returned.
list_vehicles
All arguments are optional; with none, every vehicle in service is returned. Each
vehicle has position, heading, speed, occupancy (CATS currently reports none),
headsign, and the next scheduled stop. matches and countsByMode report the total
even when the list is truncated.
Resources
The feeds behind the tools, for clients and models that want the data itself.
Static tables come from the same cached download the tools use. Realtime resources
are the decoded feed entities with their GTFS-Realtime field names, raw ids, and Unix
timestamps, without the schedule joins the tools add. Each resource's fetchedAt is
Eastern time, like the tools. stop_times.txt is large
(about 12 MB in the live feed).
Install
Requires Python 3.11+.
Or run it without installing anything, which is how most MCP clients launch it:
From a clone instead, to hack on it or to run the HTTP transport from source:
Transports
Pick one with --transport or QUEENSCOACH_TRANSPORT; the default is stdio.
stdio
For a server the client launches itself. No authentication: the client already owns the process.
Register it with Claude Code:
Or in an MCP client config file:
An installed copy works just as well, given an absolute path
(/absolute/path/to/.venv/bin/queenscoach): MCP clients rarely share your shell's
PATH. python -m queenscoach runs the same server, so any interpreter with the
package installed works as the command.
stdout carries MCP protocol traffic only; all diagnostics go to stderr.
HTTP with Google OAuth
For a hosted server anyone with the URL can reach. Every request to /mcp needs a
bearer token, and the only way to get one is to sign in with a Google account that is
on the allow list.
How the sign-in works. MCP clients register themselves dynamically and expect an authorization server at the MCP server's own origin. Google offers neither dynamic registration nor tokens audience-restricted to a third-party resource, so this server is its own OAuth 2.1 authorization server and delegates only the login to Google:
Google's answer is used exactly once, to learn which account signed in. That email is checked against the allow list, and only then does this server mint its own tokens. Google's tokens are never handed to the client.
One-time setup in Google Cloud. At console.cloud.google.com/auth/clients, create an OAuth client of type Web application and add one authorized redirect URI:
It must match QUEENSCOACH_PUBLIC_URL exactly. The server logs the URI it expects at startup.
Copy the client ID and secret into the environment below.
Run it. .env.example lists every setting; the shell form is:
Then point a client at https://queenscoach.example.com/mcp; it discovers the rest and opens
a browser for the Google sign-in. In Claude Code:
Access is denied by default. Startup fails unless QUEENSCOACH_ALLOWED_EMAILS,
QUEENSCOACH_ALLOWED_DOMAINS, or an explicit QUEENSCOACH_ALLOW_ANY_GOOGLE_ACCOUNT=true says who
may get in, so a misconfigured deployment is unreachable rather than open to every
Google account on the internet. Unverified Google addresses are always refused.
Endpoints.
scripts/install.sh does a whole deployment: a system
user under /opt, a Cloudflare tunnel and its DNS record created over the API,
both systemd units, and a verification pass. No port forwarding, so it works
behind CGNAT or a locked router. See deploy/.
scripts/deploy-gcp.sh does the same on Google Cloud
Run, in your own GCP project: the project itself, Firestore for sign-ins, the
client secret in Secret Manager, a container built by Cloud Build, a monthly
budget with an optional hard spend cap, and the same verification pass. It scales
to zero, so a personal server costs next to nothing. See
deploy/GCP.md.
Deployment notes.
- By default the server speaks plain HTTP and expects a tunnel or proxy to
terminate TLS, which is what the install script sets up. Setting
QUEENSCOACH_TLS_CERTandQUEENSCOACH_TLS_KEYinstead makes it serve HTTPS itself, for a deployment with nothing in front of it. QUEENSCOACH_PUBLIC_URLis what clients dial and is this server's OAuth issuer identifier, so it must be the external URL, not the bind address.- Token state is in memory by default and therefore per-process: restarting
invalidates outstanding tokens.
QUEENSCOACH_TOKEN_STORE=firestorekeeps it in Firestore instead (install thegcpextra:pip install 'queenscoach[gcp]'), so sign-ins survive restarts and every instance shares them. Pair it withQUEENSCOACH_STATELESS_HTTP=trueso that any instance can answer any request. - Access tokens last an hour and refresh tokens 30 days, both rotated on refresh.
Data sources
Realtime (GTFS-Realtime protobuf, refreshed every 20s):
https://gtfsrealtime.ridetransit.org/GTFSRealTime/Vehicle/VehiclePositions.pbhttps://gtfsrealtime.ridetransit.org/GTFSRealTime/TripUpdate/TripUpdates.pbhttps://gtfsrealtime.ridetransit.org/GTFSRealTime/Alert/Alerts.pb
Static schedule (cached 6h), which turns feed identifiers into route names, stop names, and coordinates, and is the timetable the schedule and planning tools read:
https://gtfsrealtime.ridetransit.org/GTFSStatic/api/GTFSDownload/GTFS.zip
The server reads agency.txt (for the time zone), routes.txt, stops.txt,
trips.txt, stop_times.txt, and calendar.txt and calendar_dates.txt where
present. stop_times.txt is streamed and packed into arrays per trip, about 15 MB in
memory for the full CATS feed. shapes.txt is not read. Every file in the archive is
still available as a resource.
Feed quirks this server works around
Verified against live feed captures:
VehiclePosition.stop_idandcurrent_stop_sequenceare unusable. None of the 158 vehicle stop ids in a sample capture matched any stop in the published schedule, and reported sequence numbers exceeded the trip's own stop count (e.g. sequence 192 on a 52-stop trip). This server never surfaces them; next-stop data comes from the TripUpdates feed instead, whose stop ids resolve 100%.StopTimeEvent.delayis never populated. Schedule deviation is computed fromtimeminusscheduled_time, which are both present.- TripUpdates cover ~83% of active vehicles, so
nextStopis omitted rather than guessed for the remainder. - Route matching is exact-first, so a query of
5returns route 5, not 501 or 510. - Most headsigns name only a direction. 61 of 64 routes label trips just
"Inbound" or "Outbound", so the schedule and planning tools report each trip's last
stop as its
destination. - Occupancy is never reported. Every vehicle's
occupancy_statusisNO_DATA_AVAILABLE. - The schedule looks only a few weeks ahead. CATS publishes
calendar_dates.txtalone, covering about four weeks (8 September to 4 October 2026 in the feed checked). Timetable answers report the range asscheduleCoversand refuse dates outside it. - Open-ended alerts end in the year 3000. An alert ending more than five years out
is reported as
untilFurtherNotice. parent_stationis too loose to group platforms. CATS uses it for trip-planner places spanning up to 900 m of unrelated stops, so stations are grouped by name and distance instead.
Behavior notes
- Arrival predictions already in the past are filtered out; no negative ETAs.
- Same-named stops within 200 m are one station: the two platforms of a Gold Line
stop, or bus stops facing each other across a street.
list_stops,get_arrivals,get_schedule, andplan_tripall treat them as one place. - Feed responses are capped in size and time-bounded; one slow feed cannot hang a call.
- Concurrent calls share a single in-flight fetch per feed, and one call giving up does not abort a fetch the others are awaiting.
- If a refresh fails but cached data exists, the last good data is served rather than
an error.
feedAgeSecondson every response shows how stale it is. - The alerts feed is supplementary for
get_arrivalsandget_route: if it fails, they answer without alerts.plan_triplikewise falls back to the timetable, and says so, when live predictions are unavailable. - Dates and clock times in arguments are Charlotte local time. A service day runs past midnight, as GTFS does, and its clock times count from noon minus 12 hours on the service date, so they stay right across daylight-saving changes.
- Parsing the schedule, trip planning, and timetable scans run in a worker thread, so a slow one does not stall other requests. The first call after the schedule is downloaded waits a second or so for it to parse; a trip search then takes well under a second.
- Every time in a tool response is Eastern time (
America/New_York), ISO 8601 with the offset in effect on that date:2026-09-08T17:59:48-04:00during daylight saving time,2026-12-21T19:00:00-05:00otherwise. The two 1:30 am's on the night clocks fall back are told apart by their offsets. Coordinates are WGS84 decimal degrees.
Configuration
Feeds (both transports)
All optional; defaults target the CATS feeds above. Durations are in milliseconds.
Feed URLs must be http or https; anything else is rejected at startup.
Transport
HTTP transport
Read only when --transport http is selected.
One of the three allow-list settings is required; see above.
Layout
Plus scripts/install.sh, which deploys the HTTP
transport onto a Debian host, and scripts/deploy-gcp.sh
with the Dockerfile, which deploy it to Google Cloud Run.
Development
Tests run against protobuf and GTFS fixtures captured from the live feeds, so they are
deterministic and make no network calls. The GTFS fixture is a trimmed extract of the
real archive: trips on route 29, the Blue Line, and the Gold Line, the stops they call
at, and the full service calendar. The realtime capture is from a weekday evening, so
a Blue-to-Gold transfer can be planned against it. tests/test_feed_http.py is the exception: it
serves canned responses from a loopback socket so the byte cap and timeout are exercised
for real.
tests/test_http.py drives the whole OAuth handshake against the real ASGI app -
registration, /authorize, the Google callback, /token, then an authenticated
tools/list - with Google's token endpoint replaced by a stub, so no account or network
is needed.
tests/test_token_store.py runs every token-store test against both stores. The
Firestore half needs the emulator (gcloud emulators firestore start, then set
FIRESTORE_EMULATOR_HOST) and is skipped without it. tests/test_stdio.py starts the
real stdio server in a child process; CI also runs it against a plain pip install .,
to prove stdio needs none of the optional extras.
License
MIT - see LICENSE.
來源:README.md,提交 af0e8c5
工具
0版本歷史
2- v2.1.1最新Sep 20, 2026
- v2.1.0Sep 16, 2026
評分
已滿足適用檢查項的 85%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通必需25 分
- 已宣告認證方式必需10 分
- 已列出工具必需15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分