Skip to main content
Glama

QueensCoach ♔♘

CI

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:

https://queenscoach.adamwanninger.com/mcp

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.

  1. Open Settings → Connectors. On the web that's claude.ai/settings/connectors; in the desktop app, Settings then Connectors.

  2. Click Add custom connector at the bottom of the list.

  3. 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.

  4. Click Add, then Connect on the connector that appears. A browser window opens for the Google sign-in; approve it and it closes itself.

  5. 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

claude mcp add --transport http queenscoach https://queenscoach.adamwanninger.com/mcp

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.

Related MCP server: OC Transpo MCP Server

Tools

Tool

Purpose

plan_trip

Trips between two places by bus and train, with walking and transfers, adjusted by live delays.

get_arrivals

Live predicted arrivals at a named stop, or at the station nearest a location.

get_schedule

The published timetable at a stop for any date: departures, first and last trips.

get_route

One route on a date: destinations, stops in order, hours, and how often it runs.

get_service_alerts

Current and upcoming detours and disruptions, system-wide or for a route, stop, or place.

list_stops

Stops and stations with the routes serving each, nearest first from a location.

list_vehicles

GPS positions of buses and trains in service: one by number, a route's, or all of them.

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

Argument

Type

Notes

origin_stop

string

Where to start: a stop id, code, or name.

origin_latitude, origin_longitude

number

Or start from a location, walking to nearby stops.

destination_stop

string

Where to go: a stop id, code, or name.

destination_latitude, destination_longitude

number

Or finish at a location.

date

YYYY-MM-DD

Service date in Charlotte (default today).

depart_at

HH:MM

Leave no earlier than this. Default: now.

arrive_by

HH:MM

Or arrive no later than this.

max_transfers

integer

0 to 3 (default 2).

max_walk_meters

integer

Longest walk to the first stop or from the last, 100 to 2000 (default 800).

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

Argument

Type

Notes

stop

string

Stop id (02400), stop code, or part of a stop name (CTC Station).

latitude, longitude

number

Instead of stop: use the nearest station to this location.

route

string

Optional route filter.

mode

bus | train

Optional filter.

limit

integer

Max arrivals (default 10, cap 50).

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

Argument

Type

Notes

stop

string

Stop id, code, or name.

latitude, longitude

number

Or the nearest stop serving the route or mode.

route

string

Optional route filter.

mode

bus | train

Optional filter.

date

YYYY-MM-DD

Service date (default today).

after, before

HH:MM

Time window. after defaults to now today, or the start of service.

limit

integer

Max departures (default 20, cap 100).

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

Argument

Type

Notes

route

string

Required. 9, 501, Blue Line, Airport.

date

YYYY-MM-DD

Service date (default today).

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

Argument

Type

Notes

route

string

Alerts naming the route or any stop it serves.

stop

string

Alerts naming the station or any route serving it.

latitude, longitude

number

Alerts affecting stops within 800 m.

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

Argument

Type

Notes

latitude, longitude

number

A location; results are then nearest first, with metersAway.

query

string

Stop id, stop code, or part of a stop name.

route

string

Only stops this route serves.

mode

bus | train

Only stops with that service, e.g. train for the nearest rail station.

limit

integer

Max stations (default 10 with a location, 250 without; cap 250).

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

Argument

Type

Notes

vehicle

string

Vehicle number as shown on the bus/train, e.g. 2301, LRV307.

route

string

Route: 9, 501, Blue Line, Mt. Holly Road.

mode

bus | train

Optional filter.

limit

integer

Max vehicles to return (default and cap: 250).

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.

URI

Type

Contents

gtfs://static

JSON

The files in the static GTFS archive, their sizes and URIs, and when it was fetched.

gtfs://static/{file}

CSV

Any table in the archive as CATS publishes it. routes.txt, stops.txt, trips.txt, and stop_times.txt are also listed individually.

gtfs://realtime/vehicle-positions

JSON

The decoded VehiclePositions feed.

gtfs://realtime/trip-updates

JSON

The decoded TripUpdates feed.

gtfs://realtime/alerts

JSON

The decoded Alerts feed.

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+.

pip install queenscoach

Or run it without installing anything, which is how most MCP clients launch it:

uvx queenscoach

From a clone instead, to hack on it or to run the HTTP transport from source:

python3 -m venv .venv
.venv/bin/pip install .          # '.[gcp]' adds the Firestore token store

Transports

Pick one with --transport or QUEENSCOACH_TRANSPORT; the default is stdio.

queenscoach                                  # stdio (default)
queenscoach --transport http --port 8000     # streamable HTTP + Google OAuth

stdio

For a server the client launches itself. No authentication: the client already owns the process.

Register it with Claude Code:

claude mcp add queenscoach -- uvx queenscoach

Or in an MCP client config file:

{
  "mcpServers": {
    "queenscoach": {
      "command": "uvx",
      "args": ["queenscoach"]
    }
  }
}

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:

MCP client  <--OAuth-->  queenscoach  <--OAuth-->  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:

https://your-public-url/auth/google/callback

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:

export QUEENSCOACH_GOOGLE_CLIENT_ID=...apps.googleusercontent.com
export QUEENSCOACH_GOOGLE_CLIENT_SECRET=...
export QUEENSCOACH_ALLOWED_EMAILS=you@example.com
export QUEENSCOACH_PUBLIC_URL=https://queenscoach.example.com

queenscoach --transport http --port 8000

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:

claude mcp add --transport http queenscoach https://queenscoach.example.com/mcp

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.

Path

Purpose

/mcp

The MCP endpoint. Requires Authorization: Bearer <token>.

/.well-known/oauth-protected-resource/mcp

Points clients at the authorization server.

/.well-known/oauth-authorization-server

This server's OAuth metadata.

/register

Dynamic client registration (RFC 7591).

/authorize, /token, /revoke

The OAuth endpoints.

/auth/google/callback

Where Google returns the user.

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_CERT and QUEENSCOACH_TLS_KEY instead makes it serve HTTPS itself, for a deployment with nothing in front of it.

  • QUEENSCOACH_PUBLIC_URL is 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=firestore keeps it in Firestore instead (install the gcp extra: pip install 'queenscoach[gcp]'), so sign-ins survive restarts and every instance shares them. Pair it with QUEENSCOACH_STATELESS_HTTP=true so 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.pb

  • https://gtfsrealtime.ridetransit.org/GTFSRealTime/TripUpdate/TripUpdates.pb

  • https://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_id and current_stop_sequence are 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.delay is never populated. Schedule deviation is computed from time minus scheduled_time, which are both present.

  • TripUpdates cover ~83% of active vehicles, so nextStop is omitted rather than guessed for the remainder.

  • Route matching is exact-first, so a query of 5 returns 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_status is NO_DATA_AVAILABLE.

  • The schedule looks only a few weeks ahead. CATS publishes calendar_dates.txt alone, covering about four weeks (8 September to 4 October 2026 in the feed checked). Timetable answers report the range as scheduleCovers and 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_station is 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, and plan_trip all 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. feedAgeSeconds on every response shows how stale it is.

  • The alerts feed is supplementary for get_arrivals and get_route: if it fails, they answer without alerts. plan_trip likewise 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:00 during daylight saving time, 2026-12-21T19:00:00-05:00 otherwise. 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.

Variable

Default

QUEENSCOACH_VEHICLE_POSITIONS_URL

CATS vehicle positions feed

QUEENSCOACH_TRIP_UPDATES_URL

CATS trip updates feed

QUEENSCOACH_ALERTS_URL

CATS alerts feed

QUEENSCOACH_STATIC_GTFS_URL

CATS static GTFS zip

QUEENSCOACH_REALTIME_TTL_MS

20000

QUEENSCOACH_STATIC_TTL_MS

21600000

QUEENSCOACH_REQUEST_TIMEOUT_MS

30000

QUEENSCOACH_MAX_FEED_BYTES

33554432

QUEENSCOACH_MAX_STATIC_BYTES

268435456

Feed URLs must be http or https; anything else is rejected at startup.

Transport

Variable

CLI

Default

QUEENSCOACH_TRANSPORT

--transport

stdio

HTTP transport

Read only when --transport http is selected.

Variable

CLI

Default

Notes

QUEENSCOACH_HTTP_HOST

--host

127.0.0.1

Bind address.

QUEENSCOACH_HTTP_PORT

--port

8000

Bind port.

QUEENSCOACH_PUBLIC_URL

--public-url

http://localhost:<port>

External origin; the OAuth issuer.

QUEENSCOACH_GOOGLE_CLIENT_ID

required

From Google Cloud credentials.

QUEENSCOACH_GOOGLE_CLIENT_SECRET

required

From Google Cloud credentials.

QUEENSCOACH_ALLOWED_EMAILS

—

Allowed addresses, comma- or space-separated.

QUEENSCOACH_ALLOWED_DOMAINS

—

Allowed bare domains, e.g. example.com.

QUEENSCOACH_ALLOW_ANY_GOOGLE_ACCOUNT

false

Opt in to admitting every Google account.

QUEENSCOACH_TLS_CERT

--tls-cert

—

PEM chain, to serve HTTPS directly.

QUEENSCOACH_TLS_KEY

--tls-key

—

PEM private key. Required with the above.

QUEENSCOACH_ACCESS_TOKEN_TTL_MS

3600000

Access token lifetime.

QUEENSCOACH_REFRESH_TOKEN_TTL_MS

2592000000

Refresh token lifetime.

QUEENSCOACH_TOKEN_STORE

memory

memory, or firestore (needs the gcp extra).

QUEENSCOACH_FIRESTORE_DATABASE

(default)

Firestore database for the token store.

QUEENSCOACH_STATELESS_HTTP

false

Serve without MCP sessions, for restarts and multiple instances.

One of the three allow-list settings is required; see above.

Layout

Module

Role

config.py

Environment parsing and validation

feed_http.py

Bounded, time-limited HTTP fetch

cache.py

TTL cache with single-flight refresh

gtfs_csv.py

GTFS-flavored CSV reading

static_gtfs.py

Static schedule: routes, stops, trips, their timetables, and the service calendar

realtime.py

GTFS-Realtime protobuf decoding

transit.py

Domain layer: joins realtime to schedule, resolves queries

timetable.py

Domain layer for the published timetable: service days, departures, route patterns

planner.py

Trip planning: a Connection Scan over the timetable, with live delays

tools.py

The tools' behavior and JSON payloads

resources.py

The GTFS feeds as MCP resources

server.py

MCP tool and resource registration, and schemas

oauth.py

OAuth authorization server, with Google as the login

token_store.py

Where OAuth state is kept, and the in-memory default

token_store_firestore.py

The Firestore token store (the gcp extra only)

http.py

Streamable HTTP transport and the Google callback route

main.py

CLI entry point and transport selection

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

.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest        # offline, against recorded feed fixtures
.venv/bin/mypy          # strict
.venv/bin/ruff check .
.venv/bin/ruff format .

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.

Available Tools

3 tools
find_vehicleFind a bus or trainA
Read-only
Inspect

Locate a specific CATS bus or train and return its current GPS coordinates. Give "vehicle" for a vehicle number (e.g. "2301"), or "route" to get every vehicle currently running a route (e.g. "9", "501", "Blue Line"). Includes heading, speed, occupancy, and next scheduled stop when available.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNo
routeNo
vehicleNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=true. The description adds useful behavioral context by specifying the return data (GPS coordinates, heading, speed, occupancy, next scheduled stop) and noting that the stop is included 'when available'. It does not contradict annotations, and the mention of 'when available' aligns with the open-world hint. However, it does not disclose behavior such as what happens when no match is found or whether both vehicle and route are allowed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the primary purpose and immediately followed by usage examples. Every word contributes to understanding the tool's behavior and parameters. There is no redundant or filler content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the main usage patterns (vehicle- and route-based lookup) and mentions the output fields. However, it does not specify behavior when both vehicle and route are provided, when neither is provided, or how the mode parameter applies. Given the existence of an output schema, the missing parameter-precedence details are a gap that could lead to incorrect usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already provides descriptions for all three parameters (mode, route, vehicle). The description adds extra meaning by explaining the mutually exclusive use of vehicle vs. route ('Give vehicle for a vehicle number... or route to get every vehicle'). It does not clarify the mode parameter or interaction between fields, so the added value over the schema is limited.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with a specific verb ('Locate'), names the resource ('a specific CATS bus or train'), and states the output ('current GPS coordinates'). It also distinguishes two search modes (by vehicle number or by route) and clarifies the scope ('every vehicle currently running a route'). This clearly differentiates it from siblings like list_vehicles and get_arrivals, which likely list all or handle scheduled times.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives parameter usage instructions ('Give vehicle for a vehicle number... or route to get every vehicle') but provides no guidance on when to choose this tool over list_vehicles or get_arrivals. It does not mention exclusions, alternatives, or when the tool is not appropriate, leaving the agent to infer tool selection from the name alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_arrivalsGet arrival times at a stopA
Read-only
Inspect

Estimated arrival times of buses or trains at a specific stop or station. Accepts a stop id, stop code, or part of a stop name. Reports minutes away, schedule deviation, the vehicle number, and any service alerts for that stop.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNoOnly show bus or train arrivals.
stopYesStop id, stop code, or part of a stop name, e.g. "02400" or "CTC Station".
limitNoMaximum arrivals to return.
routeNoOnly show arrivals for this route.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare readOnlyHint and openWorldHint; the description helps confirm these by saying 'Estimated'. It adds no operational limitations beyond what the schema provides, and it doesn't mention any rate limits, pagination, or fallback/ambiguous name handling. It does add 'service alerts' as a kind of output, which is lightly useful, so the description is adequate but not deep.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences capture the main purpose, input flexibility, and key fields. The most important information is placed first. No filler is present.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only tool with four parameters fully documented in the schema and an output schema present, the description provides enough context for an agent to know what the tool is for, what it accepts, and what it returns. It does not need to restate schemas because those are adjacent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% coverage of four parameters, each with its own description. The text like 'stop id, stop code, or part of a stop name' mainly restates the schema's stop description. It adds no extra meaning for limit, mode, or route, so meaning comes primarily from the schema itself.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names the resource ('estimated arrival times of buses or trains') and the specific action ('at a specific stop or station'), making the tool's function explicit. It also lists the input formats and useful output fields, which differentiates it from sibling tools find_vehicle and list_vehicles.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It implies when to use the tool (when you need arrival times at a stop), but it never states an explicit 'when-not-to-use' or contrasts with siblings. No exclusions are given, so an agent can infer intent, but someone selecting among the three siblings must rely on whatever context the user provides without extra guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_vehiclesList all vehicle positionsA
Read-only
Inspect

Return the current GPS coordinates of every CATS bus and train in service. Optionally filter to buses or trains, or to a single route.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNo
limitNoMaximum vehicles to return.
routeNoRestrict results to one route.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is established. The description adds the behavioral fact that it returns 'current' coordinates and only vehicles 'in service,' which clarifies the data scope. It does not go beyond that (e.g., mention of latency, rate limits, or pagination), so it makes a modest addition beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences with no waste. The core action is front-loaded ('Return the current GPS coordinates...'), and the optional filters are mentioned compactly. Everything written earns its place, making it highly efficient for an agent to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema is present, so return format details are not needed in the description. The tool is a simple listing operation with optional filters; the description covers the essence, and the schema covers parameters. The only minor gap is the lack of explicit mention of potential limits (like pagination), but the limit parameter and output schema mitigate this. Overall, it is sufficiently complete for an agent to invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 67%, meaning most parameters already have meaning in the schema (mode, limit, and route each have descriptions). The description repeats these filters ('buses or trains', 'single route') without adding extra semantics like value formats or edge cases. Since the schema already covers the parameters, the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('Return'), a clear resource ('GPS coordinates of every CATS bus and train in service'), and explicitly distinguishes this from a single-vehicle lookup ('every'). It also mentions optional filters, which makes the tool's scope unmistakable and separates it from the sibling find_vehicle.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description says 'Optionally filter to buses or trains, or to a single route,' which gives clear context on how to narrow results. However, it does not explicitly state when to prefer this tool over siblings like find_vehicle (e.g., 'for all vehicles, use this; for a specific one, use find_vehicle'). The 'every' phrasing implies bulk listing but does not name alternatives or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv1.0.0
    • First observedfind_vehicle
    • First observedget_arrivals
    • First observedlist_vehicles

TDQS

A3.7/5.0

Scored across 3 tools

Disambiguation2/5

find_vehicle and list_vehicles overlap significantly: both can return every vehicle on a route, making the boundary between them unclear. get_arrivals is distinct, but the route-listing capability in find_vehicle duplicates list_vehicles with a route filter.

Naming Consistency5/5

All tools use a consistent verb_noun snake_case pattern: find_vehicle, list_vehicles, get_arrivals. The verbs are distinct and clearly indicate the action, with no mixed conventions.

Tool Count5/5

Three tools is a reasonable, focused set for a real-time transit information server. Each tool covers a distinct core need—vehicle location, fleet listing, and arrivals—without unnecessary bloat.

Completeness4/5

The tools cover the essential real-time transit operations: locating vehicles, listing vehicles, and fetching arrivals. Minor gaps exist, such as no dedicated route or stop directory tool, but the provided inputs (route numbers, stop ids/names) make the surface practical for common queries.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Provides real-time and scheduled bus data for the Centre Area Transportation Authority (CATA) in State College, PA. Enables users to track live bus positions, get arrival predictions, search stops, view routes, and receive service alerts through natural language queries.
    7
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides real-time transit data for OC Transpo in Ottawa, including live vehicle positions and trip updates via GTFS-RT feeds. It enables AI agents to monitor arrival delays, schedule changes, and transit telemetry through the Model Context Protocol.
    2
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables real-time Chicago Transit Authority train and bus tracking, including arrivals, positions, and predictions for CTA 'L' trains and buses.
    233 npm
    MIT