feat: add bus sign MCP #7

Merged
Jc-965 merged 4 commits from feat/bus-sign into main 2026-09-20 01:01:02 +00:00
Member

Description

Adds a bus service to the composed MCP server using live predictions from ScottyLabs Bus Sign.

The service supports current arrival lookups for the two configured Forbes Avenue and Morewood Avenue stops, including route and place filtering, human-readable stop information, and CMU PRT rider guidance. Closes #8

Changes

  • Add five bus_ MCP tools:
    • bus_get_bus_predictions
    • bus_get_stop_predictions
    • bus_get_next_bus
    • bus_search_bus_predictions
    • bus_get_cmu_prt_rider_guide
  • Mount the bus service on the composed MCP server.
  • Fetch predictions from https://bus-sign.scottylabs.org by default, with support for overriding the base URL through BUS_SIGN_API_URL.
  • Validate backend responses with Pydantic and translate backend failures into a consistent BusBackendError.
  • Preserve exact arrival seconds and add ceiling-rounded minutes for human-readable responses.
  • Include UTC and America/New_York retrieval timestamps without treating the MCP retrieval time as the upstream PRT generation time.
  • Include stop IDs alongside human-readable stop names and directions:
    • 4407: inbound near Tepper Quad toward Downtown/Oakland
    • 7117: outbound near the Cohon University Center
  • Support one case-insensitive place filter that resolves configured neighborhoods first and otherwise searches live route destinations.
  • Normalize route identifiers such as 61D, 61d, 61 d, and 61-D.
  • Return one result per direction when a route-only request does not specify a direction.
  • Resolve Squirrel Hill from outbound stop 7117 into separate destination areas: Forbes & Murray via 61A/61B/61C/61D, and Wilkins & Murray via 67/69.
  • Include route-specific destination-area context and alternatives, such as explaining that route 67 serves Wilkins & Murray rather than Forbes & Murray.
  • Describe coverage by the two configured stops; route availability is taken dynamically from each backend response.
  • Add CMU Ready2Ride guidance with the official CMU Transportation source.
  • Declare httpx and Pydantic as direct dependencies.
  • Add focused unit tests using controlled HTTP responses and prediction fixtures.

Example questions supported

  • When is the next bus?
  • When is the next 61D bus?
  • When is the next bus to Downtown?
  • When is the next bus to the Waterfront?
  • When is the next bus to Squirrel Hill?
  • When is the next inbound bus near Tepper?
  • How do CMU students ride PRT buses?

Running locally

From the mcp-server repository root, install the locked dependencies and start the composed MCP server:

uv sync --dev
uv run mcp-server

The server runs at http://127.0.0.1:5050, with the MCP endpoint at http://127.0.0.1:5050/mcp. By default, the bus service fetches predictions from https://bus-sign.scottylabs.org.

In a second terminal, call the next-bus tool through MCP Inspector:

npx --yes @modelcontextprotocol/inspector@latest --cli \
  --server-url http://127.0.0.1:5050/mcp \
  --transport http \
  --method tools/call \
  --tool-name bus_get_next_bus \
  --tool-args-json '{"place":"waterfront"}'

To use a locally running bus-sign backend instead, set BUS_SIGN_API_URL when starting the MCP server:

BUS_SIGN_API_URL=http://127.0.0.1:8080 uv run mcp-server

Testing

uv run pytest

Result: 12 passed.

The tests cover:

  • Backend response validation and derived arrival minutes
  • Route normalization and per-direction selection
  • Downtown and Waterfront live-destination resolution through place
  • Squirrel Hill destination-area grouping and route-specific context
  • Invalid-input and no-match response envelopes
  • HTTP, malformed JSON, invalid schema, and network failures
  • MCP tool registration and the CMU rider-guide source URL

Scope and limitations

  • Predictions are limited to the stops and routes returned by the Bus Sign backend.
  • Arrival countdowns describe when a bus reaches the listed boarding stop, not travel time to its destination.
  • Squirrel Hill is currently the only configured neighborhood. Other place names are matched only when they appear as live route destinations.
  • This PR changes only mcp-server. Agent-side tool selection and routing will be implemented in a follow-up PR after the MCP changes are merged.
## Description Adds a bus service to the composed MCP server using live predictions from [ScottyLabs Bus Sign](https://bus-sign.scottylabs.org). The service supports current arrival lookups for the two configured Forbes Avenue and Morewood Avenue stops, including route and place filtering, human-readable stop information, and CMU PRT rider guidance. Closes #8 ## Changes - Add five `bus_` MCP tools: - `bus_get_bus_predictions` - `bus_get_stop_predictions` - `bus_get_next_bus` - `bus_search_bus_predictions` - `bus_get_cmu_prt_rider_guide` - Mount the bus service on the composed MCP server. - Fetch predictions from `https://bus-sign.scottylabs.org` by default, with support for overriding the base URL through `BUS_SIGN_API_URL`. - Validate backend responses with Pydantic and translate backend failures into a consistent `BusBackendError`. - Preserve exact arrival `seconds` and add ceiling-rounded `minutes` for human-readable responses. - Include UTC and `America/New_York` retrieval timestamps without treating the MCP retrieval time as the upstream PRT generation time. - Include stop IDs alongside human-readable stop names and directions: - `4407`: inbound near Tepper Quad toward Downtown/Oakland - `7117`: outbound near the Cohon University Center - Support one case-insensitive `place` filter that resolves configured neighborhoods first and otherwise searches live route destinations. - Normalize route identifiers such as `61D`, `61d`, `61 d`, and `61-D`. - Return one result per direction when a route-only request does not specify a direction. - Resolve `Squirrel Hill` from outbound stop `7117` into separate destination areas: Forbes & Murray via `61A/61B/61C/61D`, and Wilkins & Murray via `67/69`. - Include route-specific destination-area context and alternatives, such as explaining that route `67` serves Wilkins & Murray rather than Forbes & Murray. - Describe coverage by the two configured stops; route availability is taken dynamically from each backend response. - Add CMU Ready2Ride guidance with the official [CMU Transportation source](https://www.cmu.edu/transportation/transport/prt.html). - Declare `httpx` and Pydantic as direct dependencies. - Add focused unit tests using controlled HTTP responses and prediction fixtures. ## Example questions supported - When is the next bus? - When is the next 61D bus? - When is the next bus to Downtown? - When is the next bus to the Waterfront? - When is the next bus to Squirrel Hill? - When is the next inbound bus near Tepper? - How do CMU students ride PRT buses? ## Running locally From the `mcp-server` repository root, install the locked dependencies and start the composed MCP server: ```bash uv sync --dev uv run mcp-server ``` The server runs at `http://127.0.0.1:5050`, with the MCP endpoint at `http://127.0.0.1:5050/mcp`. By default, the bus service fetches predictions from `https://bus-sign.scottylabs.org`. In a second terminal, call the next-bus tool through MCP Inspector: ```bash npx --yes @modelcontextprotocol/inspector@latest --cli \ --server-url http://127.0.0.1:5050/mcp \ --transport http \ --method tools/call \ --tool-name bus_get_next_bus \ --tool-args-json '{"place":"waterfront"}' ``` To use a locally running bus-sign backend instead, set `BUS_SIGN_API_URL` when starting the MCP server: ```bash BUS_SIGN_API_URL=http://127.0.0.1:8080 uv run mcp-server ``` ## Testing ```bash uv run pytest ``` Result: `12 passed`. The tests cover: - Backend response validation and derived arrival minutes - Route normalization and per-direction selection - Downtown and Waterfront live-destination resolution through `place` - Squirrel Hill destination-area grouping and route-specific context - Invalid-input and no-match response envelopes - HTTP, malformed JSON, invalid schema, and network failures - MCP tool registration and the CMU rider-guide source URL ## Scope and limitations - Predictions are limited to the stops and routes returned by the Bus Sign backend. - Arrival countdowns describe when a bus reaches the listed boarding stop, not travel time to its destination. - Squirrel Hill is currently the only configured neighborhood. Other place names are matched only when they appear as live route destinations. - This PR changes only `mcp-server`. Agent-side tool selection and routing will be implemented in a follow-up PR after the MCP changes are merged.
test(bus): add bus MCP unit tests
All checks were successful
kennel/build build succeeded
kennel/deploy deployment healthy
725deb2f27

Kennel Deployments

Torn down at 2026-09-20 01:01:03 UTC.

<!-- kennel-deployment --> ### Kennel Deployments Torn down at 2026-09-20 01:01:03 UTC.
Jc-965 requested changes 2026-09-14 23:56:21 +00:00
Dismissed
Jc-965 left a comment

Looks good overall, just a few small nitpicks/suggestions

Looks good overall, just a few small nitpicks/suggestions
@ -0,0 +186,4 @@
)
fetched = await fetch_predictions()
if normalized_neighborhood and neighborhood_rule is None:
Member

There's a small chance that the model puts "waterfront" in neighborhood instead of destination. In this case, the tool says "not in the curated bus waypoint directory" and stops, even though a 61D to WATERFRONT is on the board and listed as available in the same response. It might be better if an unknown neighborhood falls through and is tried as a destination before giving up.

There's a small chance that the model puts "waterfront" in neighborhood instead of destination. In this case, the tool says "not in the curated bus waypoint directory" and stops, even though a 61D to WATERFRONT is on the board and listed as available in the same response. It might be better if an unknown neighborhood falls through and is tried as a destination before giving up.
Author
Member

Replaced separate destination and neighborhood arguments with one place argument. It checks configured neighborhoods first, then searches live destinations. For example, place="waterfront" finds the live 61D prediction.

Replaced separate destination and neighborhood arguments with one place argument. It checks configured neighborhoods first, then searches live destinations. For example, place="waterfront" finds the live 61D prediction.
Jc-965 marked this conversation as resolved
@ -0,0 +244,4 @@
if match is None:
requested = normalized_route or normalized_destination or "bus"
entry["message"] = (
f"No current {requested} prediction is available in this direction."
Member

Currently, if someone asks for "route 67 to Squirrel Hill," the result for stop 7117 (outbound) says, "No current 67 prediction is available in this direction." But Route 67 is actually predicted at 7117 outbound, and the same response lists it under available. In this case, it was filtered out because 67 isn't on the Squirrel Hill route list, which is a different reason than it not being available. So maybe the message for it can be something like "67 isn't on the Squirrel Hill route list (61A-D)," instead of implying that no 67 is coming.

Currently, if someone asks for "route 67 to Squirrel Hill," the result for stop 7117 (outbound) says, "No current 67 prediction is available in this direction." But Route 67 is actually predicted at 7117 outbound, and the same response lists it under available. In this case, it was filtered out because 67 isn't on the Squirrel Hill route list, which is a different reason than it not being available. So maybe the message for it can be something like "67 isn't on the Squirrel Hill route list (61A-D)," instead of implying that no 67 is coming.
Author
Member

Addressed by distinguishing destination areas. Routes 61A–61D serve Forbes & Murray, while 67/69 serve Wilkins & Murray. Route-specific responses include the served area and alternatives; unqualified Squirrel Hill queries return one result per area. A future feature could be to generate this index from PRT static GTFS and Pittsburgh neighborhood GIS data instead of maintaining route mappings manually.

Addressed by distinguishing destination areas. Routes 61A–61D serve Forbes & Murray, while 67/69 serve Wilkins & Murray. Route-specific responses include the served area and alternatives; unqualified Squirrel Hill queries return one result per area. A future feature could be to generate this index from PRT static GTFS and Pittsburgh neighborhood GIS data instead of maintaining route mappings manually.
Jc-965 marked this conversation as resolved
@ -0,0 +49,4 @@
COVERAGE_NOTE = (
"Coverage is currently limited to predictions observed from the bus-sign backend "
"for routes 28X, 58, 61A, 61B, 61C, 61D, and 67 across two configured "
Member

The backend doesn't filter by route and instead just returns whatever PRT predicts at stops 4407 and 7117, so this route list may not always be accurate. For example, route 69 came through today that isn't listed, and since every response carries this note, the model would say 69 isn't covered. Maybe we can just describe the two stops? Or maybe you can look into it, and there might be a better option

The backend doesn't filter by route and instead just returns whatever PRT predicts at stops 4407 and 7117, so this route list may not always be accurate. For example, route 69 came through today that isn't listed, and since every response carries this note, the model would say 69 isn't covered. Maybe we can just describe the two stops? Or maybe you can look into it, and there might be a better option
Author
Member

Updated the coverage note to describe the two configured stops instead of a fixed route list. Available routes are now derived dynamically from each backend response.

Updated the coverage note to describe the two configured stops instead of a fixed route list. Available routes are now derived dynamically from each backend response.
Jc-965 marked this conversation as resolved
feat: improved place and neighborhood resolution
All checks were successful
kennel/build build succeeded
kennel/deploy deployment healthy
e1f5b21cf8
docs: updated readme with bus tools and testing instructions
All checks were successful
kennel/build build succeeded
kennel/deploy deployment healthy
16c1815b4b
Jc-965 approved these changes 2026-09-20 00:48:58 +00:00
Jc-965 left a comment

Thanks! The changes address all three concerns, and everything looks good to me. Approving.

Thanks! The changes address all three concerns, and everything looks good to me. Approving.
Jc-965 merged commit 908d9aa6bf into main 2026-09-20 01:01:02 +00:00
Jc-965 deleted branch feat/bus-sign 2026-09-20 01:01:03 +00:00
Sign in to join this conversation.
No description provided.