Skip to main content

cats-mcp

CI

An MCP server for live Charlotte Area Transit System (CATS) bus and light rail data, built on the agency's public GTFS-Realtime feeds. 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 three tools.

Tools

Tool Purpose
find_vehicle Locate one bus/train by vehicle number, or every vehicle on a route, and return GPS coordinates.
list_vehicles Current GPS coordinates of every bus and train in service.
get_arrivals Estimated arrival times at a specific stop or station.

find_vehicle

Argument Type Notes
vehicle string Vehicle number as shown on the bus/train, e.g. 2301, LRV307.
route string Route to locate: 9, 501, Blue Line, Mt. Holly Road.
mode bus | train Optional filter.

At least one of vehicle or route is required. Returns position, heading, speed, occupancy, headsign, and the next scheduled stop.

list_vehicles

Argument Type Notes
mode bus | train Optional filter.
route string Optional single-route filter.
limit integer Max vehicles to return (default and cap: 250).

Includes countsByMode and totalInService so the total is visible even when the list is truncated.

get_arrivals

Argument Type Notes
stop string Required. Stop id (02400), stop code, or part of a stop name (CTC Station).
route string Optional route filter.
mode bus | train Optional filter.
limit integer Max arrivals (default 10, cap 50).

Returns minutes away, predicted and scheduled times, schedule deviation, the vehicle number, 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. Service alerts affecting the stop or its routes are attached when present.

Install

Requires Python 3.11+.

python3 -m venv .venv
.venv/bin/pip install .

Transports

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

cats-mcp                                  # stdio (default)
cats-mcp --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 cats -- /absolute/path/to/cats-mcp/.venv/bin/cats-mcp

Or in an MCP client config file:

{
  "mcpServers": {
    "cats": {
      "command": "/absolute/path/to/cats-mcp/.venv/bin/cats-mcp"
    }
  }
}

python -m cats_mcp 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-->  cats-mcp  <--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/apis/credentials, create an OAuth client ID of type Web application and add one authorized redirect URI:

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

It must match CATS_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 CATS_GOOGLE_CLIENT_ID=...apps.googleusercontent.com
export CATS_GOOGLE_CLIENT_SECRET=...
export CATS_ALLOWED_EMAILS=you@example.com
export CATS_PUBLIC_URL=https://cats.example.com

cats-mcp --transport http --port 8000

Then point a client at https://cats.example.com/mcp; it discovers the rest and opens a browser for the Google sign-in. In Claude Code:

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

Access is denied by default. Startup fails unless CATS_ALLOWED_EMAILS, CATS_ALLOWED_DOMAINS, or an explicit CATS_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/.

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 CATS_TLS_CERT and CATS_TLS_KEY instead makes it serve HTTPS itself, for a deployment with nothing in front of it.
  • CATS_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 and therefore per-process: restarting invalidates outstanding tokens, and running several replicas behind one hostname would need a shared store instead.
  • 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), used to turn feed identifiers into route names, stop names, and coordinates:

  • https://gtfsrealtime.ridetransit.org/GTFSStatic/api/GTFSDownload/GTFS.zip

Only routes.txt, stops.txt, and trips.txt are read; stop_times.txt and shapes.txt are the bulk of the archive and are not needed.

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.

Behavior notes

  • Arrival predictions already in the past are filtered out; no negative ETAs.
  • 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: if it fails, get_arrivals still returns arrivals.
  • Times are ISO 8601 UTC; coordinates are WGS84 decimal degrees.

Configuration

Feeds (both transports)

All optional; defaults target the CATS feeds above. Durations are in milliseconds.

Variable Default
CATS_VEHICLE_POSITIONS_URL CATS vehicle positions feed
CATS_TRIP_UPDATES_URL CATS trip updates feed
CATS_ALERTS_URL CATS alerts feed
CATS_STATIC_GTFS_URL CATS static GTFS zip
CATS_REALTIME_TTL_MS 20000
CATS_STATIC_TTL_MS 21600000
CATS_REQUEST_TIMEOUT_MS 30000
CATS_MAX_FEED_BYTES 33554432
CATS_MAX_STATIC_BYTES 268435456

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

Transport

Variable CLI Default
CATS_TRANSPORT --transport stdio

HTTP transport

Read only when --transport http is selected.

Variable CLI Default Notes
CATS_HTTP_HOST --host 127.0.0.1 Bind address.
CATS_HTTP_PORT --port 8000 Bind port.
CATS_PUBLIC_URL --public-url http://localhost:<port> External origin; the OAuth issuer.
CATS_GOOGLE_CLIENT_ID required From Google Cloud credentials.
CATS_GOOGLE_CLIENT_SECRET required From Google Cloud credentials.
CATS_ALLOWED_EMAILS Allowed addresses, comma- or space-separated.
CATS_ALLOWED_DOMAINS Allowed bare domains, e.g. example.com.
CATS_ALLOW_ANY_GOOGLE_ACCOUNT false Opt in to admitting every Google account.
CATS_TLS_CERT --tls-cert PEM chain, to serve HTTPS directly.
CATS_TLS_KEY --tls-key PEM private key. Required with the above.
CATS_ACCESS_TOKEN_TTL_MS 3600000 Access token lifetime.
CATS_REFRESH_TOKEN_TTL_MS 2592000000 Refresh token lifetime.

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
realtime.py GTFS-Realtime protobuf decoding
transit.py Domain layer: joins realtime to schedule, resolves queries
tools.py The three tools' behavior and JSON payloads
server.py MCP tool registration and schemas
oauth.py OAuth authorization server, with Google as the login
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.

Development

.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest        # 130 tests, 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. 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cats_mcp-1.0.0.tar.gz (165.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cats_mcp-1.0.0-py3-none-any.whl (40.4 kB view details)

Uploaded Python 3

File details

Details for the file cats_mcp-1.0.0.tar.gz.

File metadata

  • Download URL: cats_mcp-1.0.0.tar.gz
  • Upload date:
  • Size: 165.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cats_mcp-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9abfe3d0873091e431571e58e3fac261a3e17ac8ea50c2dd273204d74c6bb7ee
MD5 ef816897eeafc9594a3a0c72043bad19
BLAKE2b-256 bb657b20df8b0cbeb9f2d02fcb662362d4fd621b5d38191b224d330a32d10424

See more details on using hashes here.

Provenance

The following attestation bundles were made for cats_mcp-1.0.0.tar.gz:

Publisher: release.yml on ajwann/cats-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cats_mcp-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: cats_mcp-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 40.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cats_mcp-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3cd18b712de9a4018fa2924b51e39547bcd42eb9439773f7cec2c3c7c7d0f29b
MD5 fe0ec4e9843798181d3de443b67ce03b
BLAKE2b-256 a64929281b2f6443365ed77d81dfec4cb22b86d87a24e500bff4c09033d9974e

See more details on using hashes here.

Provenance

The following attestation bundles were made for cats_mcp-1.0.0-py3-none-any.whl:

Publisher: release.yml on ajwann/cats-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.1

2 files

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page