norway-location-transport-mcp
# norway-location-transport-mcp
An MCP server that gives AI agents structured access to Norwegian **public transport** and **geospatial** data, through the free [Entur](https://developer.entur.org/) and [Kartverket / Geonorge](https://www.kartverket.no/api-og-data) APIs.
The two sources compose: trip planning needs geocoding, elevation needs address resolution, and admin lookups tie coordinates to places. One server answers practical local questions — "how do I get from here to Bergen?", "what is the elevation along this hike?", "which kommune is this address in?", "are there e-scooters nearby?".
Both data sources are free with no paid tiers and no API keys. Entur requires only a self-identifying `ET-Client-Name` header (see below); Kartverket requires nothing.
## Tools
### Location & geodata (Kartverket)
- `search_address(query?, street?, number?, postal_code?, municipality_number?, max_results)` — search official addresses by free text or fields; supports the `*` wildcard. Returns coordinates and cadastral numbers.
- `find_addresses_near(latitude, longitude, radius_m, max_results)` — all addresses within a radius of a point, with distances.
- `validate_address(address)` — validate and normalize a free-text address (fuzzy). Returns `valid`, the normalized match, and alternatives.
- `search_place_names(query, max_results)` — search official place names; returns feature type (Fjell, Innsjø, …), municipality, county, and coordinates. Supports `*`.
- `find_place_names_near(latitude, longitude, radius_m, max_results)` — nearest named features to a point, with distances.
- `get_elevation(latitude, longitude)` — terrain elevation from the national elevation model (DTM).
- `get_elevation_profile(path, samples)` — elevation sampled along a path; returns the profile plus total distance, ascent, and descent.
- `transform_coordinates(x, y, from_epsg, to_epsg)` — convert between reference systems (e.g. WGS84 4326 → UTM33 25833).
- `lookup_admin_unit(latitude?, longitude?, address?)` — which kommune and fylke a coordinate or address falls in, with official codes.
- `lookup_property(address?, latitude?, longitude?, max_results)` — cadastral (matrikkel) identifiers (kommune / gnr / bnr / fnr) via the open address register.
### Public transport (Entur)
- `resolve_stop_place(query, max_results)` — resolve a place or address to Entur Stop Place IDs.
- `find_stops_near(latitude, longitude, radius_m, max_results)` — reverse-geocode a coordinate to the nearest stop places.
- `plan_trip(from_place?/from_latitude/from_longitude, to_place?/to_latitude/to_longitude, date_time?, arrive_by?, transport_modes?, walk_speed?, num_trip_patterns)` — plan a point-to-point journey; filter by mode and walk speed. Returns trip patterns with legs, modes, durations, and realtime times.
- `get_departures(stop_place_id, count)` — next departures from a stop, with realtime delays, cancellations, and platform.
- `get_realtime_status(stop_place_id? | line_id?)` — realtime health: for a stop, on-time/delayed/cancelled counts; for a line, active vehicles and delays.
- `get_stop_accessibility(stop_place_id)` — per-quay wheelchair access and a summary. (See the accessibility note below.)
- `list_quays(stop_place_id)` — all quays (platforms) at a stop, with codes and coordinates.
- `get_fare_zones(stop_place_id)` — the fare (tariff) zone(s) a stop belongs to.
- `find_shared_vehicles(latitude, longitude, range_m, form_factors?, count)` — free-floating shared vehicles (bikes, e-scooters, cars, mopeds) across all operators.
- `find_docking_stations(latitude, longitude, range_m, count)` — docking stations with available vehicle and free-dock counts.
- `get_vehicle_positions(codespace_id?, line_id?, max_results)` — live vehicle positions with bearing, speed, and delay.
Tool definitions live in `src/kartverket.ts` and `src/entur.ts`, combined in `src/tools.ts` and shared by both the stdio and HTTP entry points.
## Setup
```bash
npm install
npm run build
npm test
```
`npm test` spawns the server over stdio and calls every tool against the live APIs.
### Run it
Over **stdio** (for most MCP clients):
```bash
node dist/index.js
```
Over **Streamable HTTP** (for HTTP-based clients):
```bash
node dist/http.js
# listens on http://localhost:3000/mcp (override with PORT)
```
## The Entur client-name requirement
Entur's terms require every request to carry an identifying `ET-Client-Name` header, formatted like `company-app`. This server sends one on every Entur request. Set your own value with the `ET_CLIENT_NAME` environment variable:
```bash
ET_CLIENT_NAME="acme-travelbot" node dist/index.js
```
If unset, a package default is used. Kartverket needs no header or key.
## Use with Claude Code
```bash
claude mcp add norway -- node C:\Users\trond\norway-location-transport-mcp\dist\index.js
```
Then ask, for example: "Plan a trip from Oslo S to Bergen and tell me the elevation of Galdhøpiggen."
## Remote version (Cloudflare Workers)
`worker/` holds a remote deployment: the same 21 tools served over MCP Streamable HTTP, so the server can be added as a claude.ai connector. It shares the tool definitions with the local server (`src/tools.ts` takes `z` as a parameter, so it works with both the local MCP SDK v1 / zod 3 and the worker's SDK v2 / zod 4).
```bash
cd worker
npm install
npm run dev # local test at http://127.0.0.1:8787/mcp
node test-http.mjs
```
Deploy it (needs a Cloudflare account and `wrangler login`):
```bash
cd worker
npm run deploy
```
Set your Entur client name as a worker variable:
```bash
cd worker
npx wrangler secret put ET_CLIENT_NAME
```
After deploy, add the `remotes` entry with your `workers.dev` URL to `server.json`.
## Example calls
Get the next departures from Oslo S:
```json
{ "name": "get_departures", "arguments": { "stop_place_id": "NSR:StopPlace:59872", "count": 5 } }
```
Plan a bus/tram-only trip with a set walk speed:
```json
{ "name": "plan_trip", "arguments": {
"from_place": "NSR:StopPlace:59872",
"to_latitude": 59.95, "to_longitude": 10.78,
"transport_modes": ["bus", "tram"], "walk_speed": 1.4
} }
```
Elevation profile along a short path:
```json
{ "name": "get_elevation_profile", "arguments": {
"path": [{ "latitude": 61.636, "longitude": 8.312 }, { "latitude": 61.64, "longitude": 8.32 }],
"samples": 20
} }
```
Convert a WGS84 coordinate to UTM33:
```json
{ "name": "transform_coordinates", "arguments": { "x": 10.7461, "y": 59.9127, "from_epsg": 4326, "to_epsg": 25833 } }
```
## Notes and limits
- **Coordinates** are WGS84 latitude/longitude everywhere. The Kartverket APIs are queried with EPSG 4326 so the numbers match; EUREF89 (4258) differs by well under a metre in Norway.
- **Accessibility**: Entur's free JourneyPlanner exposes wheelchair access per quay. Deeper equipment detail (elevators, tactile guidance) lives only in Entur's authenticated Stop Register API and is not available here; the tool says so.
- **Rate limiting** from either source is surfaced as a clear error asking the agent to retry.
- **Attribution**: data from Kartverket / Geonorge and Entur is under NLOD. Credit the sources when you show the data.
## How it works
- `src/index.ts` speaks MCP over stdio (JSON-RPC on stdin/stdout). `src/http.ts` speaks MCP over Streamable HTTP with session management. Both build the same server via `src/server.ts`.
- Each tool declares a Zod input schema with descriptions the LLM reads directly — no external docs needed to call a tool.
- Results are JSON text trimmed to what an agent needs, not the raw upstream payloads.
- Never write to stdout with `console.log`; it corrupts the stdio protocol. Log to stderr.
## License
MIT © Trond Bjørøy. Data © Kartverket and Entur, under NLOD.
TDQS
Scored across 21 tools
Each tool targets a distinct resource and action: address search vs. place name search vs. near-location queries, separate tools for elevation, coordinate transformation, admin lookup, property lookup, and transport-specific tools (stops, departures, trip planning, realtime status, accessibility, quays, fare zones, shared mobility, vehicle positions). No two tools have overlapping purposes that would cause misselection.
The naming follows a consistent verb_noun pattern throughout (e.g., search_address, find_place_names_near, get_elevation, lookup_admin_unit, plan_trip, get_departures). The 'find_*_near' variant is uniformly applied to both addresses and place names, and all tools use snake_case with descriptive verbs. Minor variation like 'list_quays' is still within a clear convention.
21 tools is on the higher end of the typical range, but the server covers a broad domain combining location data (addresses, place names, elevation, coordinates) and transport (stops, journeys, realtime, mobility). Each tool serves a distinct function, so the count feels justified for the scope, though it is slightly heavier than the ideal 3-15 tool range.
The tool surface is remarkably complete for Norwegian location and transport services. It covers address and place name search (with near-location variants), validation, elevation (point and profile), coordinate transformation, administrative unit lookup, property lookup, stop place resolution and reverse geocoding, journey planning, departures, realtime status, accessibility, quay listing, fare zones, and both free-floating and docking shared mobility. No obvious gaps in core workflows were identified.