ns-bridge
Click on "Install 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., "@ns-bridgePlan a trip from Amsterdam Centraal to Utrecht Centraal for this afternoon"
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.
MCP Server for Netherlands NS Trains
mcp-name: ns-bridge
A Model Context Protocol (MCP) server that enables AI assistants to interact with the Netherlands Railways (NS) API for route planning, pricing, and real-time departure information.
Compatible with any MCP client, including Claude Desktop, custom implementations, and AI agent frameworks.
Features
MCP Tools
search_stations - Find train stations by name or country
Search by station name
Filter by country code
Returns station codes needed for trip planning
search_trips - Plan routes between stations
Get multiple trip options with connections
View detailed leg-by-leg journey information
See pricing with discount options
Choose travel class (1st or 2nd)
Search by departure or arrival time
get_departures - View real-time departure boards
See upcoming departures from any station
Track delays and cancellations
Monitor platform changes
MCP Resources
station://{code}- Get detailed information about a specific station
Related MCP server: MCP Trenitalia
Installation
Prerequisites
NS API key from NS API Portal
Choose one installation method:
Docker (easiest - no Python installation needed)
uv (recommended for development)
pip (traditional Python workflow)
Option 1: Docker (Easiest)
Prerequisites: Docker Desktop or Docker Engine
# Pull the pre-built image from Docker Hub
docker pull ezegodoy26/mcp-server-ns-bridge:latestThen configure your MCP client (see Usage section below for configuration examples).
For detailed Docker usage, troubleshooting, and advanced options, see DOCKER.md.
Option 2: Setup with uv (Recommended for Development)
# Clone the repository
cd mcp-server-ns-bridge
# Install dependencies
uv sync --all-extras
# Set up pre-commit hooks (recommended for development)
uv run pre-commit install
# Copy environment template
cp .env.example .env
# Edit .env and add your NS API key
# NS_API_KEY=your_actual_api_key_hereOption 3: Setup with pip
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
pip install -e ".[dev]"
# Set up environment
cp .env.example .env
# Edit .env and add your NS API keyGetting an NS API Key
Go to NS API Portal
Create an account
Subscribe to the following APIs:
Reisinformatie API (Travel Information)
NS-APP Stations API
Copy your subscription key
Add it to your
.envfile asNS_API_KEY
Usage
Running the MCP Server
Development Mode (with Inspector)
export PATH="$HOME/.local/bin:$PATH"
uv run mcp dev src/ns_bridge/server.pyThis opens the MCP Inspector for interactive testing.
Configuring MCP Clients
This server works with any MCP-compatible client. Below are configuration examples for popular clients.
Claude Desktop
Automatic Installation:
uv run mcp install src/ns_bridge/server.pyThis will automatically configure Claude Desktop to use your MCP server.
Manual Configuration:
Add to your Claude Desktop config file (location varies by OS):
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json
For Docker installation:
{
"mcpServers": {
"ns-bridge": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-e",
"NS_API_KEY",
"ezegodoy26/mcp-server-ns-bridge:latest"
],
"env": {
"NS_API_KEY": "your_api_key_here"
}
}
}
}Note: The
-e NS_API_KEYin theargsarray tells Docker to pass the environment variable from the host (set by theenvsection) into the container. Without this, the API key won't be available inside Docker.
For uv installation:
{
"mcpServers": {
"ns-bridge": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/mcp-server-ns-bridge",
"run",
"src/ns_bridge/server.py"
],
"env": {
"NS_API_KEY": "your_api_key_here"
}
}
}
}After updating the configuration, restart your MCP client to load the server.
Other MCP Clients
For other MCP-compatible clients (custom implementations, AI agent frameworks, etc.), refer to your client's documentation for stdio server configuration. The server accepts standard MCP protocol messages via stdin/stdout.
Example Queries
Once configured, you can ask your AI assistant:
"What trains are departing from Utrecht Centraal in the next hour?"
"Find me a train from Amsterdam to Rotterdam tomorrow at 9 AM"
"What's the fastest route from Den Haag to Groningen?"
"How much does a first-class ticket from Eindhoven to Maastricht cost?"
"Show me stations near the German border"
Development
Project Structure
mcp-server-ns-bridge/
├── src/
│ └── ns_bridge/
│ ├── __init__.py # Package initialization
│ ├── server.py # MCP server implementation
│ ├── ns_api_client.py # NS API wrapper
│ ├── models.py # Data models (Pydantic)
│ └── config.py # Configuration management
├── tests/ # Test suite
├── pyproject.toml # Project configuration & dependencies
├── .env.example # Environment template
└── README.md # This fileDeveloper Tools
This project uses modern Python development tools for code quality:
Pre-commit Hooks
Pre-commit hooks automatically check your code before each commit:
General checks: Trailing whitespace, EOF fixes, YAML/TOML validation
Ruff: Fast linting and code formatting
MyPy: Static type checking
Bandit: Security vulnerability scanning
# Install pre-commit hooks (one-time setup)
uv run pre-commit install
# Run manually on all files
uv run pre-commit run --all-files
# Hooks will automatically run on git commitCode Formatting & Linting
You can also run tools manually:
# Format code with Ruff
uv run ruff format src/ tests/
# Lint and auto-fix
uv run ruff check --fix src/ tests/
# Type checking
uv run mypy src/
# Security scanning
uv run bandit -r src/See DEVELOPER_TOOLS.md for detailed documentation on each tool and why we use them.
Testing
pytest: Test framework
pytest-asyncio: Async test support
pytest-cov: Code coverage
pytest-httpx: HTTP mocking for API tests
Run tests:
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov
# Run specific test file
uv run pytest tests/test_models.py
# Run with verbose output
uv run pytest -vVirtual Environment Note
Yes, uv is compatible with virtual environments! uv automatically creates and manages a .venv directory in your project. You can activate it manually if needed:
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # WindowsHowever, uv run automatically uses the virtual environment, so activation is optional for most tasks.
API Documentation
NS API Endpoints Used
Stations API (
/nsapp-stations/v2)Search and list train stations
Filter by country
Trips API (
/reisinformatie-api/api/v3/trips)Route planning with connections
Pricing information
Support for via stations
Departures API (
/reisinformatie-api/api/v2/departures)Real-time departure information
Delay and cancellation tracking
Station Codes
Common station codes:
ut- Utrecht Centraalasd- Amsterdam Centraalrtd- Rotterdam Centraalgvc- Den Haag Centraalehv- Eindhoven Centraalnm- Nijmegengn- Groningen
Use the search_stations tool to find more station codes.
Contributing
Suggestions and improvements are welcome!
Development Workflow
Create a feature branch
Make your changes
Run the test suite:
uv run pytestPre-commit hooks will automatically run on commit (or run manually:
uv run pre-commit run)Submit a pull request
Note: Pre-commit hooks automatically handle formatting, linting, type checking, and security scanning.
License
MIT License - see LICENSE file for details
Acknowledgments
Model Context Protocol - Anthropic's MCP specification
NS API - Netherlands Railways API
FastMCP - Python MCP SDK
Roadmap
Future enhancements planned:
Add support for disruptions API
Include station facilities information
Add journey details (crowdedness predictions)
Support for international routes
Caching for frequently accessed data
Rate limiting to respect API quotas
Available Tools
3 toolsget_departuresA
Get upcoming train departures for a specific station.
Use this to check the departure board at a station, including real-time updates about delays, cancellations, and platform changes.
Args: station: Station code (e.g., "ut" for Utrecht Centraal). Use search_stations to find codes. max_journeys: Maximum number of departures to return (default: 10, max: 40) date_time: Date and time to show departures from in ISO format. Defaults to current time.
Returns: A dictionary containing: - station: Station code - departures: List of departures with: - direction: Destination of the train - name: Train identification (e.g., "Intercity 2800") - planned_time: Scheduled departure time - actual_time: Actual departure time (if different) - planned_track: Scheduled platform - actual_track: Actual platform (if changed) - cancelled: Whether the departure is cancelled - delay_minutes: Delay in minutes (if applicable) - count: Number of departures returned
Example: get_departures(station="ut", max_journeys=5) get_departures(station="asd", date_time="2025-11-20T08:00:00")
| Name | Required | Description | Default |
|---|---|---|---|
| station | Yes | ||
| max_journeys | No | ||
| date_time | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes what the tool returns (real-time updates about delays, cancellations, and platform changes) and provides detailed return structure. However, it doesn't mention potential limitations like rate limits, authentication requirements, or error conditions that might be important for an agent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear sections (purpose, usage, args, returns, examples) and every sentence adds value. It's appropriately sized for a tool with 3 parameters and detailed return structure, with no redundant or unnecessary information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (real-time data with multiple parameters) and the presence of an output schema, the description provides complete context. It covers purpose, usage guidelines, parameter semantics, and return structure. The output schema handles the detailed return format, so the description doesn't need to duplicate that information.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0% schema description coverage, the description fully compensates by providing detailed parameter documentation. It explains what each parameter means, provides examples (e.g., 'ut' for Utrecht Centraal), specifies defaults (10 for max_journeys, current time for date_time), and gives constraints (max: 40). This adds significant value beyond the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose with specific verb ('Get') and resource ('upcoming train departures for a specific station'). It distinguishes from sibling tools by focusing on departure information rather than station search or trip planning, making it easy for an agent to understand when this tool is appropriate.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states 'Use this to check the departure board at a station' and provides a clear alternative ('Use search_stations to find codes') for one of the parameters. It distinguishes from sibling tools by focusing on real-time departure information rather than station search or trip planning, giving the agent clear guidance on when to use this specific tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_stationsA
Search for train stations by name or filter by country.
Use this tool to find station codes needed for trip planning.
Args: query: Search query for station name (minimum 2 characters). Leave empty to list all stations. country_codes: Comma-separated country codes to filter (e.g., "nl,de,be"). Common codes: nl (Netherlands), de (Germany), be (Belgium) limit: Maximum number of results to return (default: 10, max: 100)
Returns: A dictionary containing: - stations: List of matching stations with their codes and locations - count: Number of stations returned
Example: search_stations(query="Amsterdam", limit=5) search_stations(country_codes="nl", limit=20)
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | ||
| country_codes | No | ||
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and does well by disclosing key behaviors: it describes the return format (dictionary with stations list and count), default and max values for limit, and search constraints (minimum 2 characters for query). It doesn't cover rate limits or authentication needs, but provides substantial operational context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded with the core purpose, followed by detailed parameter explanations and examples. Every sentence adds value—no redundancy or fluff—making it efficient and easy to parse for an AI agent.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's moderate complexity (3 parameters, no annotations), the description is complete: it explains purpose, usage, parameters, return values, and provides examples. With an output schema present, it doesn't need to detail return structure further, and it adequately covers all necessary context for effective tool invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate fully. It adds significant meaning beyond the schema: explains that 'query' searches station names with a 2-character minimum and can be empty to list all, clarifies 'country_codes' format and provides examples, and specifies 'limit' default and max values. This covers all parameters thoroughly.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the specific verb ('search') and resource ('train stations') with filtering capabilities ('by name or filter by country'). It distinguishes from siblings like 'get_departures' (focused on departure times) and 'search_trips' (focused on trip planning) by emphasizing station code retrieval for trip planning.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states 'Use this tool to find station codes needed for trip planning,' providing clear context for when to use it. However, it doesn't mention when NOT to use it or explicitly compare to sibling tools like 'search_trips,' which might be a better alternative for certain trip-related queries.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_tripsA
Search for train trips between two stations with pricing information.
This is the main tool for route planning. It returns trip options with detailed information about connections, travel times, and prices.
Args: origin: Origin station code (e.g., "ut" for Utrecht, "asd" for Amsterdam). Use search_stations to find codes. destination: Destination station code (e.g., "rtd" for Rotterdam). Use search_stations to find codes. date_time: Departure/arrival date and time in ISO format (e.g., "2025-11-18T14:30:00"). Defaults to current time. search_for_arrival: If true, date_time is treated as arrival time. If false (default), it's departure time. via_station: Optional intermediate station code to route through travel_class: Travel class - either "first" or "second" (default: "second") discount: Discount type - "none" (default), "20_percent", or "40_percent" num_trips: Number of trip options to return (default: 5)
Returns: A dictionary containing: - trips: List of trip options, each with: - duration_minutes: Total travel time - transfers: Number of transfers required - departure_time: Planned departure time - arrival_time: Planned arrival time - status: Trip status (e.g., "NORMAL", "CANCELLED") - legs: List of individual journey segments - price: Fare information in cents and formatted - origin: Origin station name - destination: Destination station name
Example: search_trips(origin="ut", destination="asd", num_trips=3) search_trips(origin="rtd", destination="ams", date_time="2025-11-20T09:00:00", travel_class="first")
| Name | Required | Description | Default |
|---|---|---|---|
| origin | Yes | ||
| destination | Yes | ||
| date_time | No | ||
| search_for_arrival | No | ||
| via_station | No | ||
| travel_class | No | second | |
| discount | No | none | |
| num_trips | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries full burden and does well by detailing what the tool returns (trip options with specific fields like duration, transfers, status, price), default behaviors (e.g., date_time defaults to current time), and practical usage notes (e.g., using search_stations for codes). It doesn't mention rate limits or authentication needs, but covers core behavior adequately.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear sections (purpose, args, returns, example) and front-loaded key information. It's appropriately sized but could be slightly more concise by integrating the example more tightly or trimming some redundancy in parameter explanations.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the complexity (8 parameters, no annotations, but has output schema), the description is highly complete. It covers purpose, usage, all parameters with semantics, return structure, and examples. The output schema exists, so the description appropriately focuses on explaining the return values' meaning rather than just structure.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate fully. It provides detailed semantics for all 8 parameters: examples (e.g., 'ut' for Utrecht), explanations (e.g., search_for_arrival controls date_time interpretation), default values, and valid options (e.g., travel_class values). This adds significant value beyond the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose as 'Search for train trips between two stations with pricing information' and identifies it as 'the main tool for route planning.' It distinguishes from sibling tools by specifying it returns trip options with connections, travel times, and prices, unlike get_departures (likely real-time departures) and search_stations (station lookup).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides clear context by stating this is 'the main tool for route planning' and includes guidance on using search_stations to find station codes for parameters. However, it doesn't explicitly state when to use this vs. get_departures (e.g., for planning vs. real-time info) or any exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
TDQS
Each tool has a clearly distinct purpose with no overlap: get_departures retrieves departure board information for a single station, search_stations finds station codes, and search_trips plans routes between stations. The descriptions clearly differentiate their functions, making misselection unlikely.
All tools follow a consistent verb_noun pattern with snake_case naming: get_departures, search_stations, and search_trips. The verbs 'get' and 'search' are appropriately chosen for their respective operations, creating a predictable and readable naming convention throughout.
With only 3 tools, the server feels somewhat thin for a train travel domain. While the tools cover core functionalities (departure info, station search, trip planning), additional operations like booking tickets, checking disruptions, or managing favorites would enhance coverage. The count is borderline minimal but functional.
The tool surface covers essential train travel workflows: finding stations, checking departures, and planning trips with pricing. Minor gaps exist, such as no ticket booking, real-time disruption alerts, or saved trip management, but agents can work effectively with the provided tools for basic planning and information retrieval.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
The Ferryhopper MCP server is a connector for LLMs and AI Agents in maritime travel that exposes ferry routes, schedules, and booking options. It enables AI assistants to search ports and connections across 33 countries and 190+ ferry operators, provide real-time ferry itineraries with indicative prices, and assist users with planning island-hopping or multi-leg journeys by processing natural language queries about ferry times, passenger counts, and travel durations.
Hosted MCP server for live public-data APIs and Skills for AI agents.
MCP server for AI dialogue using various LLM models via AceDataCloud
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
Related MCP Servers
- AlicenseAqualityAmaintenanceAn MCP server for Swiss Federal Railways (SBB) open data, enabling AI models to query real-time disruptions, passenger frequencies, station details, and more without an API key.102MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server for real-time Italian railway data, enabling natural language queries about train schedules, delays, departures, arrivals, and live tracking via the Viaggiatreno API.6MIT
- AlicenseAqualityDmaintenanceAn MCP server that exposes the Deutsche Bahn public transport API to any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue, etc.). Five tools cover station search, departures, journey planning, trip details, and nearby stations.6MIT
- FlicenseNot gradedqualityDmaintenanceMCP server for querying Dutch railway data via the NS API, with planned tools for station search, departures, trip planning, and disruptions.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/eze-godoy/mcp-server-ns-bridge'
If you have feedback or need assistance with the MCP directory API, please join our Discord server