Skip to content

bostonmove

A command-line tool and MCP server for getting around Boston: live MBTA departures and alerts, Bluebikes bike and dock counts, and door-to-door options with arrival times. Every number says where it came from and whether it's live, scheduled or an estimate.

Case study5 min read

Northeastern to Harvard on a Friday afternoon. No subway line goes the whole way, so the two fastest trains change lines, and each says where, when the second train leaves and when you get in.
Role
Solo project: design, code, tests and release.
When
October 2026
Stack
Python, MCP (Model Context Protocol), httpx, MBTA V3 API, GBFS, OpenStreetMap, pytest, GitHub Actions, PyPI

The problem

Ask a coding agent when the next Red Line train is and it guesses. The real answer is public, but it's split across the MBTA's API, the Bluebikes feed and a geocoder, each with its own IDs, limits and licence terms.

What I built

One read-only engine over all three, with two ways in: a `bostonmove` command for people, and an MCP server with five tools, so Claude Code, Claude Desktop or Cursor can answer with real numbers.

What happened

On PyPI and the official MCP registry. A version tag publishes to both, with no stored token or login. 36 offline tests replay 57 recorded real responses with the clock frozen.

  • 5

    MCP tools, each also a CLI command

  • 3

    public data sources joined

  • 4–16

    MBTA requests per trip plan

  • 57

    recorded real responses the tests replay

01

A question agents guess at

“When's the next train at Park Street?” and “Is there a free dock near Kendall?” have exact answers, published live and for free. A language model doesn't have them, so without a tool it makes something up that sounds right.

bostonmove gives it the tool. It answers five questions, each one as an MCP tool and as a command: the next departures from a stop, the service alerts in effect, the nearest Bluebikes stations with bikes, e-bikes and docks, what's near a place, and how to get from A to B. A place can be a station name, a stop ID, a Bluebikes station, an address or landmark, or a latitude and longitude.

Live departures at Park Street, cancellations included, and Bluebikes near Kendall. The Kendall T station has 22 bikes and no free docks, which is the kind of thing you want to know before you ride there.

02

Saying where every number came from

A tool an agent quotes has to be clear about what it knows. So every result carries its sources and an `as_of` time, and every number is one of three kinds, labelled as such.

  • Live: departures are the MBTA's own predictions, and ride times come from that trip's predicted times at both stops. Bike counts are as of the feed time, about a minute old.
  • Scheduled: when a stop has no predictions, late at night or on a route that doesn't publish them, the timetable is used and each time is marked as scheduled.
  • Estimated: walking and cycling are straight-line distance times 1.3 for streets, at 5 km/h and 14 km/h, and say so.
  • Alerts on a trip are only the ones that matter for it: severe ones on its routes, or any at a stop where it boards, changes or gets off.

03

One change, inside a rate limit

The first version planned only trips on one route, and plenty of trips need a change: from Northeastern to Harvard, no line goes the whole way. So bostonmove looks for a subway or light-rail line near the start and another near the destination that share a station, ranks the pairs by walking plus stops travelled, and prices the best two with live departures. The second train has to leave at least three minutes after the first gets in.

The first version of that hit the MBTA's limit inside a single plan. Without a key, the MBTA allows 20 requests a minute, and pricing two transfers stop by stop used them up. Each leg now costs one request: departures at both of its stops come back together and are paired by trip. A plan costs 4 to 16 requests. When the limit is hit anyway, bostonmove waits for the reset if it's under 20 seconds, and past that it leaves the train options out, says why, and still returns walking and Bluebikes.

Testing it on real trips turned up three smaller problems. When the limit hit during a transfer search, the alerts call failed too, and the direct options were lost with it, so alerts are now best effort. Green Line branches showed up as “E” and “B”, the MBTA's short names, so rail now uses the full name. And a trip that needs two changes, Harvard to the airport, was told no stop was close enough, which wasn't the reason. It now says it plans at most one change and points to the MBTA's own planner.

04

Testing against live data

Live data makes for flaky tests: the next train is different every minute. So a recorder script runs real scenarios against the real APIs once and saves every response, and the offline tests replay them with the clock frozen at the moment they were recorded. So the offline tests run on real MBTA and Bluebikes responses and give the same answer every time.

The data comes with terms. The Bluebikes licence doesn't allow redistributing its feed as a stand-alone dataset, so the recorder keeps only the stations in central Boston that the tests need. OpenStreetMap's geocoder asks for at most one lookup a second and an identifying User-Agent, and bostonmove caches what it looks up for 30 days. A separate live test, run before each release, checks the real APIs still answer in the shape the code expects.

05

Shipping it

CI runs on Python 3.10 to 3.14 on Linux, and on macOS and Windows. Pushing a version tag builds the package and uploads it to PyPI through trusted publishing. A second workflow then lists the release in the official MCP registry, signing in as the repository through GitHub Actions instead of with a login code. Neither step stores a password or token anywhere.

Making the screenshots for this page turned up two bugs. Departure rows didn't line up when one route's name was shorter than another's, and the Bluebikes feed time was printed in UTC while everything else was in Boston time. Version 0.1.1 fixes both, with a test for each that fails on 0.1.0.

Use itCommand
Run once, no installuvx bostonmove plan "South Station" "Kendall"
Next trainsbostonmove next "Park Street"
Add to Claude Codeclaude mcp add --scope user bostonmove -- uvx bostonmove mcp

What I'd carry forward

  1. 1Label every number live, scheduled or estimated. An agent repeats whatever you give it with the same confidence.
  2. 2Design for the rate limit from the first call: count requests per answer, not per feature.
  3. 3When one source fails, return what the others still know, and say what's missing.
  4. 4Record real responses and freeze the clock, and live data becomes a test you can repeat.
  5. 5Look at your own output closely. Making screenshots found two bugs the tests didn't.

Get in touch

Questions about my work, a role, or a project idea. Send a message here or email me directly.

Srikarmks@gmail.com