Bike Router
Plan bike routes with a self-hosted OSM service: facts only, with weather and sights.
Installation
Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).
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"
}
}
}
}
Variables d'environnement
BIKE_ROUTER_API_URLobligatoire Address of the running pyBikeRouter API this server asks (http or https).
Documentation
Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.
Sommaire
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.
Questions fréquentes
Qu'est-ce que Bike Router ?
Plan bike routes with a self-hosted OSM service: facts only, with weather and sights.
Comment installer Bike Router ?
Il s'exécute sur votre machine. Dans 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. Pour les autres clients, collez la configuration JSON de cette page dans leurs réglages MCP.
Faut-il une clé API pour Bike Router ?
Oui. Il attend BIKE_ROUTER_API_URL, à renseigner dans sa configuration.
Bike Router est-il open source ?
Oui. Son code est sur GitHub sous licence MIT.
Qui maintient Bike Router ?
Il est publié par JonasHeinickeBio. AI.tn le référence sans le maintenir ni le recommander.
Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.