Skip to main content
Glama

transit-mcp-server

MCP server for the 511.org SF Bay Open Data transit API. Gives an LLM live Bay Area transit data — agencies, routes, stops, real-time departures, vehicle positions and service alerts — across BART, Muni, AC Transit, Caltrain, VTA and every other 511-reporting operator.

6 tools, all read-only.

Requirements

Related MCP server: Bay Area Transit MCP

Install

npm install
npm run build

Configure

{
  "mcpServers": {
    "transit": {
      "command": "node",
      "args": ["/absolute/path/to/transit-mcp-server/dist/index.js"],
      "env": { "TRANSIT_511_API_KEY": "your-token-here" }
    }
  }
}

Variable

Required

Default

Purpose

TRANSIT_511_API_KEY

yes

—

Token from https://511.org/open-data/token

TRANSIT_511_BASE_URL

no

https://api.511.org

Override the API host

TRANSIT_511_REQUEST_TIMEOUT_MS

no

30000

Per-request timeout

TRANSPORT

no

stdio

stdio or http

PORT / HOST

no

3000 / 127.0.0.1

HTTP transport bind address

MCP_PATH_SECRET

when hosted

—

Serves the endpoint at /mcp/<secret>. Required when HOST is not loopback

ALLOWED_ORIGINS

no

localhost + claude.ai

Comma-separated origin allowlist

The quota is the main constraint

511 allows 60 requests per hour per key, shared across every endpoint. That is low enough to shape how these tools should be used:

  • Resolve operator codes and stop codes once, then reuse them. They do not change.

  • Prefer transit_list_service_alerts with no operator_id — one call covers every agency.

  • Never poll transit_next_departures in a loop. Ten checks over a commute is a sixth of the hourly budget.

transit_list_operators reports how much budget is left, read from the RateLimit-Remaining header 511 returns on every response. Exceeding the quota returns 429; request an increase from transitdata@511.org.

Deploying (for Claude mobile / claude.ai connectors)

Same shape as any hosted MCP server: generate a path secret with openssl rand -hex 32, set TRANSIT_511_API_KEY and MCP_PATH_SECRET in the platform dashboard, and the included Dockerfile and railway.json work as-is on Railway, Render or Fly. The server refuses to start on a public interface without a secret. /healthz is an unauthenticated liveness probe.

Then on claude.ai in a browser: Customize → Connectors → Add custom connector, URL https://your-app.up.railway.app/mcp/<secret>.

Tools

Network — transit_list_operators, transit_list_lines, transit_find_stops

Real-time — transit_next_departures, transit_list_vehicles

Alerts — transit_list_service_alerts

Every tool takes response_format: "markdown" | "json". Markdown is the default and is optimized for an LLM reading it; JSON is the full structured payload. structuredContent is always populated regardless of format.

Examples

"When's the next N Judah?" → transit_find_stops with operator_id="SF", query="judah" to get the stop code, then transit_next_departures with that code and line="N".

"Is BART running normally?" → transit_list_service_alerts with operator_id="BA".

"Anything wrong on my commute?" → transit_list_service_alerts with no operator — one call sweeps every Bay Area agency.

"Where are the trains right now?" → transit_list_vehicles with operator_id="BA".

Design notes

Read-only by construction. 511 publishes no write endpoints, and every tool carries readOnlyHint: true. A test asserts it.

One operator_id, mapped per endpoint. 511 calls this parameter operator_id on its static endpoints and agency on its real-time ones, for the same value. Every tool here takes operator_id and the client maps it. That split is 511's problem, not the caller's.

The two real-time endpoints have genuinely different envelopes. StopMonitoring has no Siri root wrapper; VehicleMonitoring does. The published spec shows one for both — the spec is wrong, and parsing the documented shape would return nothing at all for departures. Both are parsed as the live API actually emits them, with a test pinning each.

Arrivals carry the countdown, not departures. ExpectedDepartureTime is null in essentially every real row, so keying a countdown off it would show a stop with no service. ExpectedArrivalTime is the reliable field.

A UTF-8 BOM is stripped before parsing. 511 prefixes JSON bodies with U+FEFF, which makes a naive JSON.parse throw on a perfectly valid payload. Auth failures are plain text with no BOM, so the strip happens after the status check.

Values that look like numbers and booleans often are not. Coordinates and bearings arrive as JSON strings, VehicleAtStop is the string "false", and "" is used throughout where null is meant. Coercing blindly would turn a missing position into a valid-looking 0,0 off the coast of Africa, so empty strings are treated as absent rather than zero.

The epoch-zero sentinel is not a timestamp. A trip that is scheduled but has no vehicle assigned reports RecordedAtTime of 1970-01-01T00:00:00Z. It renders as "no vehicle assigned yet" rather than "recorded 56 years ago".

GTFS-Realtime enums are decoded. 511's JSON alert rendering emits "effect": 3 where the XML rendering says SignificantDelays. Both cause and effect are mapped back to words.

511-internal pseudo-agencies are filtered out. 5E, 5F, 5O and 5S are 511 Emergency, Flap Sign, Operations and Staff — they appear in the operator list carrying no service data.

Everything is Pacific. Timestamps arrive as UTC and are rendered in America/Los_Angeles, so daylight saving is handled once here rather than by the model twice a year. Note that 511's own TimeZone field reports America/Vancouver for every Bay Area agency — a known upstream data bug, ignored deliberately.

Truncation is always stated. 511 does not paginate; it returns whole collections, and a large agency has thousands of stops. Tools take a client-side limit and every trimmed result says how much was withheld, because a silently shortened list reads as "that is everything".

Caveats

  • The hourly quota is 60 requests across all endpoints. This is the binding constraint on any workflow.

  • Operator codes are easy to guess wrong: VTA is SC (not VT), Capitol Corridor is AM (not CC), Tri Delta is 3D. transit_list_operators prints these traps in its output.

  • Stop codes belong to one operator and are not interchangeable between agencies.

  • transit_find_stops filters on this server, so a narrow query does not save quota — the full stop list is fetched either way.

  • Real-time predictions extend roughly 90 minutes ahead, and 511 omits a route's final arrival-only stop from the departures feed.

  • tripupdates and vehiclepositions are protobuf-only with no JSON option, so they are deliberately not exposed — supporting them would mean taking on a protobuf dependency for data the SIRI endpoints already cover.

Project layout

src/
├── index.ts               # entry point, transport selection
├── constants.ts           # enums, limits, operator-code traps
├── types.ts               # interfaces for every 511 entity
├── services/
│   └── transit-client.ts  # fetch wrapper, auth, BOM stripping, quota tracking, errors
├── schemas/
│   ├── inputs.ts          # Zod input schemas
│   └── outputs.ts         # structuredContent schemas
├── formatters/
│   ├── response.ts        # limiting, truncation, Pacific-time rendering
│   └── entities.ts        # per-entity markdown rendering
└── tools/
    ├── network.ts         # operators, lines, stops
    ├── departures.ts      # real-time arrivals and vehicles
    └── alerts.ts          # service alerts

Tests

npm run build
npm test            # 42 checks: handshake, BOM, envelopes, quirks, errors (mocked API)
npm run test:http   # 17 checks: config validation, path-secret gating, method handling, origins

Both suites run against a local mock that deliberately reproduces 511's real quirks — the BOM, the missing Siri wrapper, stringified booleans and coordinates, the epoch sentinel, and plain-text error bodies — because those are exactly what a naive client gets wrong.

Available Tools

6 tools
transit_find_stopsFind Transit StopsA
Read-onlyIdempotent

Find an agency's stops by name, and get the stop codes the real-time tools need.

This is the bridge between "Downtown Berkeley" and the code transit_next_departures wants. Pass a query: a large agency has thousands of stops, and 511 returns all of them in one unpaginated response.

Args:

  • operator_id (string): agency code from transit_list_operators

  • query (string): substring match on the stop name — strongly recommended

  • limit (number): maximum stops to return (default: 25)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "stops": [ { "id": string, "Name": string, "Location": { "Latitude": string, "Longitude": string } } ] }

Examples:

  • "When's the next train from Downtown Berkeley?" -> operator_id='BA', query='downtown berkeley', then pass the id to transit_next_departures

  • "Find Muni stops on Judah" -> operator_id='SF', query='judah'

  • Don't use when: you already have the stop code

Error Handling:

  • Stop codes belong to ONE operator and are not interchangeable between agencies

  • Filtering happens on this server, so a narrow query does not save quota — the full list is fetched either way

  • 511 allows 60 requests per hour across ALL endpoints

Inputs: operator_id (required), limit, query, response_format. Returns: count, stops, total, truncated

transit_list_linesList Transit LinesA
Read-onlyIdempotent

List an agency's routes.

Use this to resolve a line name a user mentions into the id the real-time tools filter on, or to answer "what routes does this agency run".

Args:

  • operator_id (string): agency code from transit_list_operators

  • query (string): substring match on the line name or public code

  • limit (number): maximum lines to return (default: 25)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "lines": [ { "Id": string, "Name": string, "PublicCode": string, "TransportMode": string, "Monitored": boolean } ] }

Examples:

  • "What BART lines are there?" -> operator_id='BA'

  • "Is there an N line on Muni?" -> operator_id='SF', query='N'

  • Don't use when: you want stops on a line (use transit_find_stops)

Error Handling:

  • A large agency returns many lines; use query to narrow rather than raising limit

  • 511 allows 60 requests per hour across ALL endpoints

Inputs: operator_id (required), limit, query, response_format. Returns: count, lines, total, truncated

transit_list_operatorsList Transit OperatorsA
Read-onlyIdempotent

List Bay Area transit agencies and their operator codes.

Start here. Every other tool needs an operator code, and this is what produces them — BART is 'BA', Muni is 'SF', AC Transit is 'AC', Caltrain is 'CT'.

The Monitored flag matters: agencies reporting real-time data support live departures and vehicle positions, while schedule-only agencies do not.

Args:

  • monitored_only (boolean): only agencies publishing real-time data (default: false)

  • limit (number): maximum operators to return (default: 50)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "operators": [ { "Id": string, "Name": string, "Monitored": boolean, "PrimaryMode": string, "TimeZone": string } ] }

Examples:

  • "What transit agencies are there?" -> call with no arguments

  • "Which ones have live tracking?" -> monitored_only=true

  • Call this first whenever the user names an agency, to resolve its code

Error Handling:

  • Ignore the TimeZone field: 511 reports "America/Vancouver" for every Bay Area agency, which is a known upstream data bug. Everything here is Pacific time

  • 511-internal pseudo-agencies (5E, 5F, 5O, 5S) are filtered out — they carry no service data

  • 511 allows 60 requests per hour across ALL endpoints, so cache this rather than re-fetching

Inputs: limit, monitored_only, response_format. Returns: count, total, operators, truncated

transit_list_service_alertsList Transit Service AlertsA
Read-onlyIdempotent

Service alerts — delays, outages, detours and planned disruptions.

Answers "is BART running normally", "why is my line delayed" and "anything I should know before I leave". Omit operator_id to sweep every Bay Area agency in a single request, which is also the kinder option against the hourly quota.

Args:

  • operator_id (string): limit to one agency; omit for all operators

  • query (string): substring match on the headline or description

  • active_only (boolean): only alerts whose active period covers now (default: true)

  • limit (number): maximum alerts to return (default: 25)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "alerts": [ { "id": string, "header": string, "description": string, "effect": string, "cause": string, "routes": [string], "start": number, "end": number } ] // epoch SECONDS }

Examples:

  • "Any BART delays?" -> operator_id='BA'

  • "Anything wrong on my commute?" -> omit operator_id, read across agencies

  • "Weekend track work?" -> active_only=false, query='weekend'

  • Don't use when: you want a specific stop's arrivals (use transit_next_departures)

Error Handling:

  • No alerts is genuinely good news, not an error — it means normal service

  • Timestamps here are epoch seconds, unlike the ISO strings elsewhere in this API

  • Setting active_only=false surfaces future planned work as well as current problems

  • 511 allows 60 requests per hour across ALL endpoints

Inputs: limit, query, active_only, operator_id, +1 more. Returns: count, total, alerts, truncated

transit_list_vehiclesList Transit Vehicle PositionsA
Read-onlyIdempotent

Live positions of an agency's vehicles currently in service.

Answers "where are the trains right now" and "how many buses are running on this line". For "when does one get to me", use transit_next_departures instead.

Args:

  • operator_id (string): agency code from transit_list_operators

  • line (string): only vehicles on this line, matched on name or id

  • limit (number): maximum vehicles to return (default: 25)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "vehicles": [ { "line": string, "vehicle": string, "destination": string, "latitude": number | null, "longitude": number | null, "bearing": number | null, "recorded_at": string } ] }

Examples:

  • "How many Muni trains are running on the N?" -> operator_id='SF', line='N'

  • "Where are the BART trains?" -> operator_id='BA'

  • Don't use when: you want arrival times at a stop (use transit_next_departures)

Error Handling:

  • Coordinates arrive as strings and may be empty; those vehicles report "position unavailable" rather than a false 0,0

  • An agency with no real-time feed returns nothing — check Monitored in transit_list_operators

  • 511 allows 60 requests per hour across ALL endpoints

Inputs: operator_id (required), line, limit, response_format. Returns: count, total, vehicles, truncated

transit_next_departuresNext Transit DeparturesA
Read-onlyIdempotent

Live arrival predictions for the next vehicles at a stop.

This is the tool for "when is my next train/bus". It returns real-time predictions, not the printed timetable, and reports minutes-from-now alongside Pacific clock time.

Args:

  • operator_id (string): agency code from transit_list_operators, e.g. 'BA', 'SF'

  • stop_code (string): stop code from transit_find_stops. Codes belong to ONE operator and are not interchangeable between agencies

  • line (string): only departures on this line, matched on name or id

  • limit (number): maximum departures to return (default: 10)

  • response_format ('markdown' | 'json'): output format (default: 'markdown')

Returns: { "count": number, "total": number, "truncated": boolean, "operator": string, "stop_code": string, "stop_name": string, "retrieved_at": string, "departures": [ { "line": string, "destination": string, "expected": string, "aimed": string, "minutes": number, "vehicle": string | null, "at_stop": boolean } ] }

Examples:

  • "When's the next N Judah?" -> operator_id='SF', stop_code from transit_find_stops, line='N'

  • "Next BART from Downtown Berkeley?" -> operator_id='BA', the stop's code

  • Don't use when: you want the scheduled timetable rather than live predictions

Error Handling:

  • An empty result usually means service has ended for the night, or the stop is a route's final stop — 511 omits arrival-only terminals from this feed

  • Predictions extend roughly 90 minutes ahead; nothing beyond that appears

  • Rows reading "scheduled only, no live prediction" have no vehicle assigned yet

  • 511 allows 60 requests per hour across ALL endpoints — never poll this in a loop

Inputs: stop_code (required), operator_id (required), line, limit, +1 more. Returns: count, total, operator, stop_code, +4 more

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A4.8/5.0

    Scored across 6 tools

    Disambiguation5/5

    Each tool targets a distinct resource: operators, lines, stops, departures, vehicles, and alerts. Descriptions explicitly cross-reference when to use which (e.g., 'Don't use when' sections), eliminating ambiguity. No two tools overlap in purpose.

    Naming Consistency5/5

    All tools share the 'transit_' prefix and follow a consistent verb-object pattern: list_operators, list_lines, find_stops, next_departures, list_vehicles, list_service_alerts. Minor variation between 'list', 'find', and 'next' but the style is uniform and predictable.

    Tool Count5/5

    Six tools are well-scoped for a transit information server, covering discovery (operators, lines, stops), real-time queries (departures, vehicles), and alerts. No tool is redundant, and the count feels neither sparse nor bloated.

    Completeness4/5

    The tool surface covers the full workflow of querying transit info: discover agencies, lines, stops, then get live departures, vehicle positions, and alerts. A minor gap is the lack of a dedicated scheduled timetable tool, though the departures endpoint includes aimed times. Overall, the domain is well covered.

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      Provides real-time San Francisco Bay Area Rapid Transit data, enabling queries about BART schedules, routes, and station information through natural language.
      95 npm
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables querying real-time BART and SF Muni transit data, including departures, trip planning, fares, advisories, routes, alerts, vehicle positions, and schedules, from any MCP-compatible client.
      1
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables AI assistants to access real-time and scheduled public transit data, including routes, stops, departures, and service alerts, across multiple US metropolitan areas.
      5
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to retrieve live transit departures, vehicle positions, and service alerts for San Francisco Bay Area transit operators via the regional 511.org open-data API.
      86 npm
      MIT