Bike Router
Plan bike routes with a self-hosted OSM service: facts only, with weather and sights.
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
bike-routerclaude mcp add bike-router -e 'BIKE_ROUTER_API_URL=http://127.0.0.1:8000' -- uvx bike-router==0.3.0 mcp serve
{
"mcpServers": {
"bike-router": {
"command": "uvx",
"args": [
"bike-router==0.3.0",
"mcp",
"serve"
],
"env": {
"BIKE_ROUTER_API_URL": "http://127.0.0.1:8000"
}
}
}
}
Environment variables
BIKE_ROUTER_API_URLrequired Address of the running pyBikeRouter API this server asks (http or https).
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
bike-routing-agent
An OSM-based bike-routing service: a typed, provider-neutral domain model, a full openrouteservice API client plus routing adapter, and a LangGraph workflow that turns a validated route request into an explainable, exportable cycling route.
The LLM (where used) may parse free text, ask clarification questions, and explain a chosen route. It never invents coordinates, geometry, elevation, or route suitability -- all of that comes from OSM-derived routing engines.
Easy install (no programming needed)
Download pybikerouter-easy-<version>.zip from the
Releases page, unzip it, install
Docker Desktop, and double-click the Start
file for your computer. The first start asks one question -- where you ride -- downloads that
area's map data, and opens the planner in your browser. Step by step, in plain words:
docs/easy-install.md (and START-HERE.txt in the download).
Documentation
The full documentation suite lives in docs/:
| Document | Contents |
|---|---|
| Architecture | Graph nodes, state, branching, terminal statuses, error model |
| API | Endpoint contracts, response statuses, artifact serving |
| Configuration | Every environment variable, validation rules, deployment notes |
| Providers | Protocols, ORS client/adapter behaviour, geocoders, BRouter/Valhalla adapters, adding a backend |
| Backend comparison | Measured ORS vs BRouter behaviour per bike type, combining both engines |
| Geocoding | Nominatim vs Pelias, confidence/ambiguity semantics, tuning |
| Scoring & exports | Score math, uncertainty policy, explanation rules, GeoJSON/GPX |
| Plan from words | Describe a ride in a sentence; an optional LLM turns it into the structured request (never coordinates) |
| Weather | Forecast along the route at your departure time: temperature, rain, wind vs. riding direction (free, keyless providers) |
| Self-hosted stack | Run routing and/or geocoding locally from one OSM extract; hardware requirements and small-PC options |
| Testing | Test layout, mocking conventions, live tests, CI |
| Web planner | Putting places into the trip; click a point on the map and choose where in the route it goes |
| iPhone / Tailscale | Use the planner from an iPhone as a home-screen app over your tailnet |
| Roadmap | Planned work and explicit non-goals |
Architecture
FastAPI
|
v
LangGraph RouteAgentState
|
+--> parse_request (structured input first; LLM extraction is an injectable dependency)
+--> validate_request (Pydantic)
+--> geocode_locations (Nominatim by default, public or self-hosted; ambiguity -> clarification, never a guess)
+--> route_with_provider (openrouteservice; engine-neutral RoutingRequest in, RouteCandidate out)
+--> score_candidates (deterministic distance/elevation/warning scoring)
+--> explain_and_export (facts-only explanation + GeoJSON/GPX export)
|
v
JSON response + GeoJSON/GPX artifacts
Graph branches:
START -> parse_request
-> validate_request -> (invalid | awaiting_clarification) -> END
-> geocode_locations -> (awaiting_clarification | provider_failure) -> END
-> route_with_provider -> (no_route | provider_failure) -> END
-> score_candidates -> (no_route) -> END
-> explain_and_export -> END
Every path ends with an explicit status; the graph never calls a routing
provider unless validation and geocoding both succeeded, and it never
returns without a terminal status.
Setup
Requires Python 3.11+ and Poetry.
poetry install
cp .env.example .env
poetry install creates an in-project virtualenv (.venv/, see
poetry.toml) with both runtime and dev dependencies. Run any command
inside it with poetry run <command>, or poetry shell to activate it
directly.
Edit .env and set ORS_API_KEY to a valid
openrouteservice API key.
Environment variables
| Variable | Purpose | Default |
|---|---|---|
ORS_API_KEY | openrouteservice API key | (required for live routing) |
ORS_BASE_URL | openrouteservice base URL | https://api.openrouteservice.org |
ORS_TIMEOUT_S | ORS request timeout (seconds) | 10.0 |
ORS_MAX_RETRIES | Retries for timeouts/5xx | 2 |
GEOCODER_PROVIDER | Geocoder backend: nominatim (public or self-hosted) or pelias (needs a Pelias API behind ORS_BASE_URL, see docs/geocoding.md; refuses the public ORS URL at startup) | nominatim |
GEOCODER_BASE_URL | Nominatim base URL (ignored when GEOCODER_PROVIDER=pelias) | https://nominatim.openstreetmap.org |
GEOCODER_TIMEOUT_S | Geocoder request timeout | 5.0 |
GEOCODER_USER_AGENT | Required by Nominatim's usage policy | bike-routing-agent/0.1 |
GEOCODER_CACHE_TTL_S | In-memory geocode cache TTL | 3600 |
GEOCODER_AMBIGUITY_MARGIN | Confidence gap below which top-2 results are "ambiguous" | 0.05 |
GEOCODER_MIN_CONFIDENCE | Minimum confidence to auto-accept a top result | 0.3 |
VALHALLA_BASE_URL | Base URL of a self-hosted Valhalla (meili) server | http://localhost:8002 |
VALHALLA_TIMEOUT_S | Per-request Valhalla timeout (seconds) | 30.0 |
VALHALLA_MAX_RETRIES | Retries for Valhalla timeouts/5xx | 1 |
EXPORT_DIR | Local directory for GeoJSON/GPX artifacts | exports |
DATABASE_URL | PostgreSQL/PostGIS URL: records route history, enables /v1/history/* (docs/persistence.md) | unset |
ARTIFACT_BACKEND | local or database (artifacts stored in PostgreSQL) | local |
LOG_LEVEL | Log level | INFO |
Running the API
poetry run uvicorn bike_routing_agent.api:app --reload
The app also serves a built-in web UI (map-based route planner) at
http://localhost:8000/, with the interactive API docs at /docs.
Command-line interface
Installing the package exposes a bike-router command (also runnable as
python -m bike_routing_agent.cli) organised into subcommand groups:
bike-router route plan --origin "Berlin Hbf" --destination "Potsdam" \
--bike-type gravel --target-distance-km 30 --output plan.json
bike-router serve start --port 8000
bike-router docker up # docker compose -f docker/compose.yaml up -d --build
bike-router docker logs -f
bike-router config show # effective settings, secrets redacted
bike-router config check # validate settings against the environment
bike-router providers list # provider names and bike-type -> ORS profile map
bike-router route plan ... --format text --gpx best.gpx # alternatives, sight stops, exports
bike-router route show plan.json # a saved plan again; `route export` writes any alternative
bike-router poi along --route plan.json # sights near a route, ranked by fame
bike-router brouter needed|info|download # BRouter map tiles (asks before downloading)
bike-router history list|show|stats # past plans (needs DATABASE_URL)
bike-router status show # readiness of every component and the active features
bike-router chat start # plan routes by chatting (docs/chat.md)
Everything the API and the web form do is available from the CLI -- see docs/cli.md.
route plan --text "50 km gravel loop from Braunschweig" uses the optional LLM parser (docs/llm-parser.md).
route plan accepts lat,lon pairs or place text for --origin,
--destination and repeated --via waypoints, prints the JSON result and
exits 0 on ready, 1 on clarification/provider failure, 2 on usage
errors. With DATABASE_URL set it also records the plan in the route history
(best effort; plan_id in the output, --no-record to skip), a forecast along
the route at --departure-time (docs/weather.md), and
bike-router db migrate / db status manage the schema
(docs/persistence.md).
Running with Docker
All container assets live in docker/:
cp .env.example .env # set ORS_API_KEY
docker compose -f docker/compose.yaml up --build
Open http://localhost:8000/ for the web UI. Route artifacts are stored in
the named pybikerouter-exports volume. See
docs/self-hosted.md for the optional local routing /
geocoding stack -- including what it needs and how to run just one half on a
small machine.
Example request
curl -X POST http://localhost:8000/v1/route/plan \
-H 'Content-Type: application/json' \
-d '{
"origin": "Braunschweig Hauptbahnhof",
"destination": "Wolfenbüttel",
"constraints": {
"bike_type": "gravel",
"avoid_high_traffic_roads": true,
"max_ascent_m": 500
}
}'
Example response
{
"status": "ready",
"route": {
"provider": "ors",
"provider_profile": "cycling-regular",
"geometry_geojson": {"type": "LineString", "coordinates": []},
"metrics": {
"distance_m": 12500,
"duration_s": 2700,
"ascent_m": 85,
"descent_m": 90,
"surface_coverage": {},
"unknown_surface_fraction": null
},
"score": 0.91,
"score_breakdown": {"distance_fit": 1.0, "elevation_fit": 1.0, "warning_penalty": 0.0},
"warnings": [],
"provenance": {"provider": "ors", "profile": "cycling-regular"}
},
"candidates": [
{"provider": "ors", "provider_profile": "cycling-regular", "score": 0.91},
{"provider": "brouter", "provider_profile": "trekking", "score": 0.84}
],
"explanation": "...",
"artifacts": {
"geojson_url": "/v1/routes/<id>.geojson",
"gpx_url": "/v1/routes/<id>.gpx"
},
"clarification": [],
"errors": []
}
If origin/destination are ambiguous or unresolved, status is
awaiting_clarification and clarification lists the candidate places to
choose from -- no route is generated from a guess.
Bike types
constraints.bike_type selects the routing profile per engine and defaults to
gravel. Supported values: road, gravel, touring, mountain, city,
ebike, commuter (fast, low-traffic), recumbent. ORS ships four cycling
profiles, so e.g. ebike uses cycling-electric while commuter rides on
cycling-regular; the self-hosted BRouter provider differentiates further --
gravel, touring, mountain, city, commuter and recumbent each get
their own profile, while road and ebike share fastbike (BRouter has no
e-assist cost model) (see docs/configuration.md).
Make targets
make lists the everyday commands; they use the project's .venv (or the active environment):
make install # poetry install
make hooks # the git hooks (commit: lint + types; push: the whole suite)
make run # the API with auto-reload (HOST=127.0.0.1 PORT=8000)
make check # lint + types + tests without coverage, stops at the first failure
make ci # what CI runs: lint, types, tests with the 80 % coverage floor
make test-fast ARGS="tests/nodes -k geocode" # a part of the suite (no coverage floor)
make deploy # build and restart the local container (bike-router deploy up)
Linting and type checking
poetry run ruff check .
poetry run mypy src
Both run in CI on every push and pull request (see
.github/workflows/ci.yml).
Running tests
poetry run pytest
Coverage is collected on every run and the suite fails below 80%
(--cov-fail-under=80 in tool.pytest.ini_options.addopts; currently 97%
total, every module ≥84%).
Live tests that hit real external services are marked @pytest.mark.live
and excluded by default (see tool.pytest.ini_options.addopts in
pyproject.toml). Run them explicitly with:
poetry run pytest -m live
Pre-commit hooks
Quality gates run automatically before each commit/push via
pre-commit. Install both git hook stages once after
poetry install:
poetry run pre-commit install --hook-type pre-commit --hook-type pre-push
On commit it runs whitespace/EOF/YAML/TOML/merge-conflict/large-file checks,
ruff check --fix and mypy src; the full pytest suite runs only in the
pre-push stage on git push (mirroring CI). Run the commit stage manually
with poetry run pre-commit run --all-files and the pre-push stage with
poetry run pre-commit run --hook-stage pre-push --all-files.
Known limitations
- OSM tag completeness varies by region; surface/access metadata is frequently missing and is represented as uncertainty, not treated as favorable.
- Routing engines (ORS, Valhalla, BRouter) map bike categories to their own
profiles differently; the mapping in
config.pyis an approximation, especially for gravel riding. - Elevation data (ascent/descent) varies in accuracy between providers and is not always available.
- No route produced by this service is a safety guarantee. Explanations use hedged language ("better aligned with available map metadata") rather than claims like "safe route".
- BRouter and Valhalla are wired up end to end but only against
self-hosted deployments (
ROUTING_PROVIDER=brouter/valhalla/all, BRouter via the compose profile in docker/brouter/README.md), and openrouteservice remains the default engine. - The Pelias geocoder option needs a Pelias API served at
<ORS_BASE_URL>/pelias/v1: neither the publicapi.openrouteservice.orgnor a stock self-hosted ORS provides one, and the adapter has only been tested against mocks (see docs/geocoding.md). For a local geocoder use the self-hosted Nominatim.
Repository layout
src/bike_routing_agent/
├── api.py FastAPI app (POST /v1/route/plan)
├── config.py Settings + provider profile mapping
├── models.py Pydantic v2 domain models
├── state.py LangGraph RouteAgentState
├── graph.py LangGraph wiring
├── errors.py Structured provider/geocoding errors
├── providers/ GeocodeProvider / RoutingProvider adapters
│ (ors_client.py: full ORS client -- directions, export,
│ isochrones, matrix, snap, POIs, Vroom, elevation,
│ Pelias geocoding, health; ors.py: the RoutingProvider;
│ geocoder.py: Nominatim; pelias.py: Pelias on
│ self-hosted ORS)
├── nodes/ One module per graph node
├── scoring/ Deterministic route scoring
├── exporters/ GeoJSON / GPX export
├── frontend/ Static web UI served at /
└── cli/ bike-router command line (route/serve/docker/config/providers)
docker/ Dockerfile + compose.yaml
tests/
├── fixtures/ Sample provider responses
├── nodes/ One test module per graph node
├── providers/ Adapter unit tests (mocked HTTP)
├── graph/ Graph branch tests
├── exporters/ Export format tests
└── live/ Tests marked @pytest.mark.live
docs/ Documentation suite (see docs/README.md)
Licence
MIT, see LICENSE.
Third-party licences
BRouter (MIT) and the OpenStreetMap data (ODbL) this project builds on are credited, with the full BRouter licence text, in THIRD_PARTY_LICENSES.md.
Frequently asked questions
What is Bike Router?
Plan bike routes with a self-hosted OSM service: facts only, with weather and sights.
How do I install Bike Router?
It runs on your machine. In Claude Code: claude mcp add bike-router -e 'BIKE_ROUTER_API_URL=http://127.0.0.1:8000' -- uvx bike-router==0.3.0 mcp serve. For other clients, paste the JSON configuration from this page into their MCP settings.
Does Bike Router need an API key?
Yes. It expects BIKE_ROUTER_API_URL, which you set in its configuration.
Is Bike Router open source?
Yes. Its code is on GitHub under the MIT license.
Who maintains Bike Router?
It is published by JonasHeinickeBio. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.