perron
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@perronwhen is the next train from Amsterdam Centraal to Utrecht?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
perron
An MCP server for Dutch Railways (NS) travel information. It gives Claude, or any other MCP client, the journey planner, live departure and arrival boards, disruptions and engineering works, fares, train composition and crowding, station facilities, and the last mile: shared scooters nearby, where you may park them, and GVB metro and ferry departures.
Not an official NS project. perron uses the public NS API portal with your own API key. NS is not involved and does not support it.
Tools
Tool | What it answers |
| Find a station by name, synonym, NS code or UIC code, typo-tolerant |
| Journey options with live delays, track changes, transfers, crowding and the fare |
| Refresh one |
| Domestic fare: single or return, 1st or 2nd class, adults and children, joint-journey discount |
| Live departure board: delays, destinations, tracks and track changes, cancellations, notices |
| Live arrival board |
| All stops of one train with times, tracks, delays and crowding |
| Disruptions, engineering works and calamities, nationwide or per station, with details by id |
| Rolling stock, units, coaches, length, facilities, first-class seats, shortened or not, crowding |
| OV-fiets availability, lift status, and facilities and shops with today's opening hours |
| Available shared scooters (default: Check) near an address, station or place, nearest first |
| Whether a scooter may be parked there, or how far to the nearest parking zone and on which street |
| GVB metro 52 and IJ ferry (F2, F3, F4) departures, with live delays for the metro |
Every tool accepts station names ("Utrecht", "Den Bosch"), NS codes (UT)
and UIC codes (8400621). When a name fits several stations, the tool returns
an error listing them so the assistant can pick one.
Responses are compact JSON plus a one-line-per-item text summary. Times are in
Europe/Amsterdam, HH:mm, with the date only when it is not today. Delays are
whole minutes, prices are euros, and track changes, cancellations and
disruptions are separate fields rather than buried in text.
Last mile
scooters_nearby and nearest_zone take a locatie (address, station or
place) or lat/lon, and every answer echoes the coordinates, address and
source it used, so a wrong geocode is visible. A name with "station" in it, or
an exact station name, uses the NS station's own coordinates. Anything else
goes to the PDOK Locatieserver;
when PDOK's hit does not contain the words asked for, perron tries
Nominatim (OpenStreetMap, at most one request a
second). Geocodes are kept for good in SQLite under DATA_DIR.
Vehicles come from the open Available Vehicles API of Dashboard Deelmobiliteit (CROW): every unrented shared vehicle in the country, several megabytes, fetched only when a tool needs it and at most once per
VEHICLES_CACHE_SECONDS, kept in memory as compact typed columns. Reserved and disabled vehicles are left out.vehicle_idchanges after every ride, so it is not returned.Parking zones are the operators' service areas from the MDS service_area API, per municipality (taken from PDOK) and operator, cached 24 hours. Outside a zone, perron returns the distance to the nearest zone edge as the crow flies and a point a few metres inside it, with the street from PDOK.
Metro and ferry times come from the OVapi GTFS feed: once a day perron downloads the national zip (~230 MB), keeps only the
OV_AGENCYmetro and ferry lines in SQLite underDATA_DIR, and deletes the zip. Until the first import is done (a minute or two after start),ov_departuresasks to try again later. Delays and cancellations come from GTFS-RealtimetripUpdates.pb, cached 30 seconds; the ferries have none there, so their departures sayrealtime: false.
Related MCP server: ns-bridge
No authentication: put it behind something
perron has no auth of its own. Anyone who can reach /mcp can spend your NS
API key's rate limit. Run it on localhost, or behind a reverse proxy or gateway
that authenticates (an OAuth gateway, Cloudflare Access, Tailscale, a VPN, …).
Do not expose it to the internet as is.
Getting an NS API key
Create an account at apiportal.ns.nl.
Under Products, subscribe to Ns-App. That one product covers every API perron uses.
Your profile then shows a primary and a secondary key. Use either one as
NS_API_KEY; keep the other for rotating without downtime.
The key allows 300 requests per 5 minutes. perron stays under that with a
token bucket, respects Retry-After on a 429, and caches responses (see below).
Configuration
Variable | Default | Purpose |
| — | Required. Subscription key for the Ns-App product. |
|
| HTTP port. |
|
| Interface to listen on. Use |
| (empty) | Prefix when a proxy forwards a sub-path unchanged, e.g. |
| (empty) | Redis for the cache, e.g. |
| (empty) | Icon shown by clients, as an https URL or |
|
| How long one nationwide shared-vehicle download is reused (15–600). |
|
| GVB timetable database and geocode cache. |
|
| GTFS agency for |
| (empty) | Comma-separated lines to keep, e.g. |
| OVapi | Static GTFS zip, downloaded once a day. |
| OVapi | GTFS-Realtime trip updates. |
|
|
|
Endpoints: POST /mcp (Streamable HTTP, stateless) and GET /healthz. With a
BASE_PATH both are also served under that prefix.
Running
Docker
docker run --rm -p 127.0.0.1:3000:3000 -e NS_API_KEY=your-key \
-v perron-data:/app/data ghcr.io/ultimatelemon/perron:latestThe image runs as a non-root user and has a health check on /healthz. The
volume keeps the timetable and geocodes across restarts; without it the
timetable is downloaded again on every start. The daily import needs about
300 MB of free disk while it runs.
npm run check:last-mile -- <url> checks the last-mile tools against a running
server with live data, using places measured by hand.
From source
Needs Node 24.
git clone https://github.com/ultimatelemon/perron.git
cd perron
npm ci
cp .env.example .env # and fill in NS_API_KEY
npm run dev # http://localhost:3000/mcpnpm run build && npm start runs the compiled version.
Connecting a client
Claude Code
claude mcp add --transport http perron http://localhost:3000/mcpClaude Desktop and claude.ai
Add a custom connector with the public URL of your deployment. These clients reach the server from the internet, so this only works behind an authenticating gateway that speaks MCP's OAuth flow.
MCP Inspector
npx @modelcontextprotocol/inspectorChoose transport "Streamable HTTP" and URL http://localhost:3000/mcp.
Caching
Data | Kept for |
Station list | 24 hours |
Station facilities, fares | 1 hour |
OV-fiets availability, lift status | 2 minutes |
Disruptions | 1 minute |
Trips, boards, journeys, composition | 30 seconds |
Development
npm test # vitest, against recorded responses in test/fixtures
npm run lint
npm run typecheck
npm run format:check
npm run probe # calls every NS endpoint once and saves raw responses to fixtures/
node scripts/smoke.ts http://localhost:3000/mcp # calls every tool on a running serverThe NS OpenAPI specs are not in this repository. Download them from the API
portal into specs/, which is ignored by git. A few things the specs do not tell you, found by calling the
real API:
/reisinformatie-api/api/v3/pricereturns a 500 for every request on the Ns-App product; perron uses/api/v2/price, which returns the same fares.The Virtual Train API spec lists both
/v1/...and/api/v1/...; only/v1/...answers.The Places API rejects
Accept-Language: *, which Node'sfetchsends by default, so every request sets a language explicitly.Departures and arrivals use
/api/v2/departuresand/api/v2/arrivals. Their successor, the timetable API, is not part of the Ns-App product. Both calls live in one function insrc/ns/reisinformatie.ts.
The parts that know nothing about NS live in their own packages:
@ultimatelemon-eu/fetch-client
(rate limiting, Retry-After, retries) and
@ultimatelemon-eu/mcp-http
(the Streamable HTTP server). src/lib/ keeps the cache, with Redis or
in-memory storage.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Transport for London (TfL) Unified API MCP — keyless.
Swiss Transport MCP — wraps Transport Open Data API (free, no auth)
Official MCP server for MAQAMI, a hotel and flight booking platform with 3M+ hotels. Search live hotel rates and flights, look up places, airports and hotel details, then prebook and book. Remote Streamable HTTP endpoint, no API key required.
iRail MCP — Belgian rail (SNCB/NMBS) real-time via the community iRail API
Related MCP Servers
- AlicenseAqualityDmaintenanceAn MCP server that provides access to NS (Dutch Railways) travel information.837 npm62MIT
- AlicenseAqualityCmaintenanceAn MCP server that enables AI assistants to interact with the Netherlands Railways (NS) API for route planning, pricing, and real-time departure information. It provides tools for searching stations, planning trips with connections, and viewing real-time departure boards.31MIT
- FlicenseNot gradedqualityDmaintenanceProvides real-time Dutch Railways (NS) data for journey planning, live departures, disruptions, and station search.3-
- FlicenseNot gradedqualityDmaintenanceMCP server for querying French railway services (SNCF) via the Navitia API, enabling station search, journey planning, and departure board retrieval.-