⚠️ Alpha, v0.1.0. Early code, shared now so we can find out what breaks. Tool names and response shapes may change without notice. Tell us what's wrong
MCP, set in white on a blue circle
BetaNYC · civic tech, open source

mta-mcp

An MCP server that answers one question well: is the subway line we recommend disrupted on that date, at that station?

Every demo below is badged. Verified 2026-09-21 means that exact call was run and that is what came back. Illustrative means it shows the shape of an answer, not live values. Saved feed means we ran the tool on our own machine against copies of MTA's feeds saved in the repo, with no live request. The answer is real for those copies, and each panel says when they were saved.
MTA's feed changes, so a verified or saved-feed panel shows what came back on that date.

When to use something else

If your question is "is the L running right now," use mta.info or any transit app. They're free, official, and faster than this.

This server is for a narrower question: a future date, a specific station, and a yes or no you'd put in an email to everyone coming. That's tedious to check by hand and easy to get wrong.

Simple first: which station did you mean?

"Which station is 125 St?" Verified 2026-09-21
By hand
  1. Search "125 St subway station."
  2. Get four different stations back, on four unrelated lines.
  3. Work out which one your event is near.
  4. Find its GTFS id if you need to check anything programmatically.
Answerable, but you have to already know the system.
mta-mcp
match_count
4
unambiguous
false
stop_idroutes
1161
2252, 3
6214, 5, 6, 6X
A15A, B, C, D
It doesn't guess. 193 of 496 stations share a name with another. Pass route_id to narrow it, or pick a stop_id.
This panel won't go out of date. resolve_station reads a station list generated from MTA's static GTFS and committed to the repo. It makes no network call, so this answer holds until MTA renames a station.

Harder: "affected" can mean better

This is the case that trips up a simple implementation.

"Is the 4 train disrupted at 68 St–Hunter College on Sat Sep 26?" Verified 2026-09-21
By hand
  1. Open mta.info/alerts. Filter to the 4, set the date to 09/26.
  2. Read the results. Route names render as images, so a copy-paste or a screen reader loses which train each alert is about.
  3. Find "4 runs local between 125 St and Grand Central." Your station is on that stretch, so it is mentioned.
  4. Decide what "mentioned" means. Is local service better or worse for you?
  5. Notice a second alert with no station list at all, and decide whether it matters.
Two judgment calls, and step 4 is where people get it wrong.
mta-mcp
disrupted
true
alert_count
2
station_level_detail
false
alerteffectcounts as disruption
Planned - Express to Local
tags your station
added_at_local_stopsno
Planned - Stops Skipped
names no stations
reducedyes
One answer covers both alerts. The express-to-local alert tags 68 St and doesn't count as a disruption, because running local means more trains stop there. The second alert names no stations, so the tool won't say your station is clear. It errs on the side of caution and says which alert caused it.
Why this matters. A check that asks "is my station mentioned in any alert?" reports a disruption here that doesn't exist, because the alert mentioning 68 St is added service. None of the four NYC subway MCPs listed below says in its README that it tells added service apart from reduced service. This server classifies on MTA's alert_type instead.

How to use it

Once it's installed (see Running it), you don't call the tools yourself. You ask Claude a question in plain English, and Claude picks a tool and fills in the arguments. It works best when the question names a line, a station, and a date.

Try asking

Illustrative Claude chooses the tool, so the call it makes may differ a little from the one shown. The tool names and parameters are the current ones.

You askThe tool Claude would likely call
"Is the 6 train running normally to 68 St–Hunter College on Saturday, October 3? It's for our event." check_route_on_date
{"route_id": "6", "date": "2026-10-03", "station": "68 St-Hunter College"}
"What's happening on the 6 train this weekend?" get_service_alerts, once per day
{"route_id": "6", "date": "2026-09-26"}
"Our flyer says 125 St. Which station is that?" resolve_station
{"query": "125 St"}
Four stations match, so it asks you which one.
"Are any elevators out at 161 St–Yankee Stadium on the 4 train on September 26? If so, what's the way around?" get_accessibility_outages
{"stop_id": "414", "route_id": "4", "date": "2026-09-26"}
The way around is MTA's alternative_route text.
"Is 14 St–Union Sq accessible on the 6 train?" resolve_station
{"query": "14 St-Union Sq", "route_id": "6"}
ADA status is per station. The 4, 5, and 6 station there is listed as not accessible, and the L and N, Q, R, W stations as fully accessible.
"Check the 6 train and the elevators at 68 St–Hunter College for October 3." check_route_on_date
{"route_id": "6", "date": "2026-10-03", "station": "68 St-Hunter College", "include_accessibility": true}
"What elevator outages are scheduled on the 7 train?" get_accessibility_outages
{"route_id": "7", "upcoming": true}
"Our flyer says 'take the 6 train to 68 St.' Should we add a travel warning for October 3?" check_route_on_date with include_accessibility: true, as above.
Claude reads disrupted, station_level_detail, and MTA's own wording for when a change applies, and should tell you to confirm at mta.info closer to the date.

If a station name matches more than one station, the tool returns the candidates instead of guessing, and Claude should ask you which one you meant.

Worked example: a travel note for an event

"We're at 68 St–Hunter College on Saturday, September 19. Is the 6 train OK, and are any elevators out?" Saved feed
The call
check_route_on_date({
  "route_id": "6",
  "date": "2026-09-19",
  "stop_id": "628",
  "include_accessibility": true
})
What came back, trimmed

Service alerts feed timestamped 2026-09-16, elevator feed saved 2026-09-17, station ADA snapshot pulled 2026-09-22. Provenance fields and the alert's full text are cut here.

{
  "route_id": "6",
  "date": "2026-09-19",
  "disrupted": false,
  "station": {
    "stop_id": "628",
    "stop_name": "68 St-Hunter College",
    "routes": ["4", "6", "6X"],
    "accessibility": { "status": "fully_accessible", "mta_notes": null }
  },
  "station_level_detail": true,
  "alert_count": 1,
  "alerts": [{
    "alert_type": "Planned - Part Suspended",
    "effect": "reduced",
    "header_text": "No [6] between Hunts Point Av and 125 St",
    "human_readable_active_period": "Sep 18 - Oct 19, Fri 9:30 PM to Mon 5:00 AM",
    "affected_stops": [
      { "stop_id": "614", "stop_name": "Longwood Av" },
      …five more Bronx stations…
    ],
    "affects_this_station": false,
    "relevant_to_station": false
  }],
  "accessibility_outages": {
    "outage_count": 0,
    "outages": [],
    "note": "… No outage is listed here. A missing outage is not proof
      the station is usable: the feed can lag, and an outage MTA hasn't
      entered won't show. Confirm at
      https://www.mta.info/elevator-escalator-status before travel."
  },
  "disclaimer": "Unofficial. BetaNYC is not the MTA …"
}
What Claude can tell you from it
The one alert on the 6 that weekend is in the Bronx, between Hunts Point Av and 125 St, and MTA didn't tag 68 St, so disrupted is false for this station. Guests coming from the Bronx on the 6 would still hit the gap, and MTA's alert text says free shuttle buses cover it. MTA lists the station as fully accessible, and no elevator outage was listed that day. That isn't proof the elevators work, so a flyer should still say to check mta.info before traveling.

Worked example: elevators and the way around

"Are any elevators out at 161 St–Yankee Stadium on the 4 train on Saturday, September 26?" Saved feed
The call
get_accessibility_outages({
  "stop_id": "414",
  "route_id": "4",
  "date": "2026-09-26"
})
What came back, trimmed

Elevator feed saved 2026-09-17. Station ADA and equipment inventory snapshots pulled 2026-09-22. The last of five outages shown, and MTA's alternate route text is cut.

{
  "date": "2026-09-26",
  "query": "161 St-Yankee Stadium",
  "stop_id": "414",
  "route_id": "4",
  "station_accessibility": { "status": "fully_accessible", "mta_notes": null },
  "outage_count": 5,
  "date_caveats": [],
  "outages": [
    …four more elevators…
    {
      "station": "161 St-Yankee Stadium",
      "trainno": "B/D/4",
      "equipment": "EL133",
      "equipmenttype": "EL",
      "serving": "mezzanine to Manhattan-bound 4 platform",
      "ADA": "Y",
      "outagedate": "11/06/2024 09:00:00 AM",
      "estimatedreturntoservice": "09/30/2026 11:59:00 PM",
      "reason": "Capital Replacement",
      "matched_by": "name",
      "match_note": "MTA's equipment inventory places EL133 at 161 St-Yankee Stadium (D11), another station in this complex. The outage feed's station name fits this station too, so it is listed here.",
      "inventory": {
        "stop_ids": ["D11"],
        "ada_compliant": "YES",
        "redundant_elevator": "-",
        "alternative_route": "To access street level: Take a Manhattan-bound
          4 to 125 St. Use elevators to transfer to a Bronx-bound 6. Exit at
          Hunts Point Av and transfer to a Washington Heights-bound Bx6-SBS
          bus to E 161 St and River Av. …"
      }
    }
  ]
}
What Claude can tell you from it
MTA lists this station as fully accessible, and on this saved feed five of its elevators are out, three of them with an estimated return of January 31, 2027. This is why the ADA status alone can't tell you a station is usable. Each outage carries MTA's own alternate route. Read those carefully: the one above sends riders to the 6 train between 125 St and Hunts Point Av, and the saved alerts feed shows no 6 train on that stretch that weekend. The return dates are MTA's estimates. Confirm at MTA's elevator and escalator status page before you send anyone.

Every parameter and every field in every answer is in the tool reference. The elevator, escalator, and ADA data, and where it falls short, are in docs/accessibility.md.

What it cannot do

Questions this server can't answer:

Not thisWhy
"When is the next 6 train?" No arrivals or trip updates. Those are protobuf-only feeds and would pull in three custom .proto files for a feature this tool does not need.
"Is the M15 bus delayed?" Bus Time needs an API key and is out of scope. Bus alerts are in the same keyless family and could be added.
"Are Times Sq and Port Authority connected?" Stations linked only by transfer, under different names, are not modeled. 60 such pairs exist. Asking about one name will not surface alerts filed against the other.
"Can I get step-free to the platform today?" Only in part. Each station carries MTA's ADA status, and elevator outages come with MTA's alternate route, but the status is a dated snapshot and a missing outage is not proof the station is usable. Confirm on MTA's elevator and escalator status page.
"Is my station definitely fine?" MTA tags stations only when it considers the change significant. Every response carries station_level_detail so a quiet answer is never mistaken for a guarantee.

Why this is not on npm

BetaNYC publishes seven MCP servers to npm. This one is intentionally not one of them, because of MTA's data terms.

MTA's data feed terms, term 1:

"In developing your app, you will provide that the MTA data feed is available to others only from a non-MTA server. Accordingly, you will download and store the MTA data feed on a non-MTA server which users of your app will access in order to obtain data. MTA prohibits the development of an app that would make the data available to others directly from MTA's server(s)." (Emphasis ours.)

A published package would have every user's machine fetch straight from api-endpoint.mta.info, with no server of ours in between. You could argue that each user is just fetching for themselves. We don't think that holds up, because the clause defines the non-MTA server as the one "which users of your app will access in order to obtain data." That covers those same users.

So package.json sets "private": true, and an accidental publish fails.

The same terms apply to every answer. We make no claim that the data is accurate, complete, or timely, and anything cached says when it was fetched. MTA route bullets are licensed separately from the free data, so this page and every response write "the 6 train" instead. The blue circle at the top of this page is the MCP logo, not an MTA mark.

Running it

⚠️ Read this before you install it

This is v0.1.0 alpha. Nobody outside BetaNYC has used it yet, and we're sharing it now to find out what breaks. Tool names, parameters, and response shapes may change without notice, so please don't build anything important on it yet.

Found something wrong? Open an issue.

1. Clone and build

You need Node 18 or newer, and no API key.

git clone https://github.com/BetaNYC/mta-mcp.git
cd mta-mcp
npm install    # also builds

2. Point your AI app at the build

For Claude Code, in a project's .mcp.json:

{
  "mcpServers": {
    "mta-mcp": {
      "command": "node",
      "args": ["${HOME}/Code/mta-mcp/dist/index.js"]
    }
  }
}

For Claude Desktop, in claude_desktop_config.json. Claude Desktop doesn't expand ${HOME}, so use the full path to your copy:

{
  "mcpServers": {
    "mta-mcp": {
      "command": "node",
      "args": ["/absolute/path/to/mta-mcp/dist/index.js"]
    }
  }
}

3. Ask a question

Restart the app and ask something from Try asking, like "Is the 6 train running normally to 68 St–Hunter College on Saturday?"

Using Claude Code and want to keep conversations lighter? The same tools also come as a skill, with no MCP server to run. See skills/README.md.

Being gentle with a public feed

MTA publishes no rate limit, no refresh cadence, and no cache headers, so the server sets its own limits. Six are on by default: a 60-second response cache, single-flight de-duplication, a process-wide one-second floor between requests, bounded retry that honors Retry-After, a 15-second timeout, and a User-Agent that identifies the project.

There's no background polling, prefetch, or warm-up fetch. It fetches only when a tool is called. The test suite makes no network requests, so continuous integration never touches MTA.

What we could not verify

Other NYC subway MCP servers

For most subway questions, one of these is a better fit. Several are further along than ours, and all of them do live arrivals, which this server doesn't. If your question is "when is the next train," start with one of these.

ProjectLanguageWhat it does that this one does not
where_is_my_train_mcp
sasabasara · also on Smithery
TypeScript The broadest of these. Live arrivals with crowding where available, fuzzy station search, elevator status, transfers, and nearby-station lookup by latitude and longitude.
nyc-subway-mcp-server
hardparking
TypeScript Real-time arrivals, live vehicle positions, and a line-status overview. Runs over HTTP as well as stdio.
metro-mcp
Aarekaz
TypeScript Covers DC Metro as well as NYC. Anonymous, read-only, deployed on Cloudflare Workers, so there is nothing to install.
mta-mcp
nkasmanoff · same name as ours, no relation
Python Next-train arrivals by station and direction. Small and readable, which makes it a good first thing to look at if you are learning how these fit together.

What this server adds: it takes a date. The servers above answer about right now. Of the four, none documents a date parameter in its README, and none says it separates service that got worse from service that got better at a station. That's the gap this server fills.

Community

Where to take your question:

Your questionWhere it goes
A tool returns the wrong shape, a schema is wrong, this server crashes Our issue tracker
Especially while this is alpha. For an alert-matching question, include the entity_id from the response so we can find the alert in the raw feed.
A feed field is undocumented, an alert looks wrong, MTA's API behaves oddly MTA Developer Google Group
MTA points developers to this public Google Group. You'll get a better answer there than in our tracker, and it helps everyone else building on the same feeds. It's also where we'd take the open questions in "What we could not verify" above.
You want to build civic tech in New York with other people BetaBuilders
BetaNYC's community, below.

BetaBuilders

In BetaNYC's own words, "a BetaBuilder is a New Yorker who sustains the city's civic infrastructure." The BetaNYC Discord is where we plan classes, fellowships, and open data work, post projects and job openings, and talk through tools like this one while they're still rough.

There are several ways to join:

Most of what we do stays free. The weekly newsletter, public events, classes, open data resources, and research don't require a membership. Neither does this server.

Learn more about BetaBuilders →

Credits

Data from the MTA service alerts feed, free to use under MTA's terms. Station names from MTA's published static GTFS. MTA's own implementation notes live at nymta/gtfs-documentation, alongside MTA's developer pages.

About BetaNYC

BetaNYC builds and maintains this project. We're New York's civic technology and open data community, and we work to improve lives in New York through civic design, technology, and data.

Where to lookWhat is there
About BetaNYC Who we are, the team, and how the organization is structured.
Our work Programs, research, and the open-data work this tool exists to support.
Events calendar What is coming up. Attending a class is also one of the doors into BetaBuilders.
NYC School of Data Our annual open-data conference, free and open to the public.
CityCamp NYC The unconference where New Yorkers and city government work on civic problems together. Getting people there is the reason this server was built.
github.com/BetaNYC Everything else we publish, including seven MCP servers for NYC civic data that are on npm.

These tools are free and open source. If you want to help keep the work going, donating is the most direct way.

Link note. Every outbound link on this page was HTTP-checked on 2026-09-21. The mta.info and claude.ai links return 403 to a command-line request because of bot protection, not because they are dead; both were confirmed in a real browser. MTA redirects www.mta.info to mta.info, so the bare form is used here.

AI disclosure. This server and this page were largely written with Claude. Every response shape shown here comes from a real call, which is what the badges mark.