Skip to main content
Glama
README.md
# Bicycle routes generator MCP

**English** Β· [EspaΓ±ol](README.es.md)

**[⬇️ Download the Claude Desktop extension (bicycle-routes-generator-mcp.mcpb)](https://github.com/Marc-Armillas/bicycle-routes-generator-mcp/releases/latest/download/bicycle-routes-generator-mcp.mcpb)**

Generates **mountain bike** and **road cycling** routes from a location and saves them as
**GPX with elevation**. The track is computed by a routing engine (BRouter or
OpenRouteService) on **real OpenStreetMap trails and roads**: neither the AI nor the
application invents coordinates.

You can use it:

- **With Claude** (Claude Desktop or Claude Code) through a local MCP server: "make me a
  ~25 km MTB loop from El Papiol, little climbing, on dirt tracks".
- **From the terminal**, without AI: `bicycle-routes generate "El Papiol" --km 25 --type tracks`.

Every route comes with a summary (distance, climbing, maximum grade, % of pavement, dirt
track and singletrack) and **warnings**: sections with motor traffic, grades over 12 %,
protected areas, ways where bikes are not allowed and technical sections.

> πŸ’Ύ **GPX files go to `/tmp/bicycle-routes` by default.** macOS empties `/tmp` on restart and cleans
> up old files, so copy the routes you want to keep (or set `BIKE_GPX_DIR` to another folder).

> ⚠️ **Always check the route on a map before riding.** OpenStreetMap data may be incomplete
> or out of date, and does not reflect temporary closures or every local regulation. Climbing
> is computed from a digital elevation model (~90 m) and **may differ from what a barometric
> watch or bike computer records** (often by 10-20 %); short ramps may be smoothed out.

## Easy install in Claude Desktop (`.mcpb` extension)

1. [Download `bicycle-routes-generator-mcp.mcpb`](https://github.com/Marc-Armillas/bicycle-routes-generator-mcp/releases/latest/download/bicycle-routes-generator-mcp.mcpb) (always the latest release), or build it with
   `npx @anthropic-ai/mcpb pack . dist/bicycle-routes-generator-mcp.mcpb`.
2. Double-click the file (or drag it into Claude Desktop β†’ Settings β†’ Extensions) and click
   **Install**.
3. Fill in the configuration: the GPX folder (`/tmp/bicycle-routes` by default) and a contact email
   (required by the OpenStreetMap/Nominatim usage policy). The other fields are optional.
4. Ask Claude: "make me a 25 km MTB loop from El Papiol on dirt tracks".

No need to install Python: this is a `uv` extension, so Claude Desktop downloads Python and the
dependencies automatically the first time (it can take a minute). `.mcpb` is the successor of
the `.dxt` format; older Claude Desktop versions that only support `.dxt` cannot run `uv`
extensions.

## Manual install (CLI and Claude Code)

Requirements: macOS, Windows or Linux with [uv](https://docs.astral.sh/uv/) (it installs Python for you).

```bash
git clone <repository-url> bicycle-routes-generator-mcp
cd bicycle-routes-generator-mcp
uv sync
cp .env.example .env   # and edit it
```

### Configuration (environment variables)

There are no keys or personal data in the code: everything is configured through environment
variables (or a `.env` file in the folder you run the CLI from).

| Variable | Default | Purpose |
|---|---|---|
| `BIKE_GPX_DIR` | `/tmp/bicycle-routes` | Folder where GPX files are saved. The app only writes here (and to its `.bicycle-routes/` subfolder with cache and index). On Windows the default is the system temp folder. |
| `BIKE_CONTACT` | β€” | Email or URL sent in the User-Agent. **Recommended**: required by Nominatim's policy. |
| `BIKE_ENGINE` | `brouter` | `brouter` or `ors`. |
| `BIKE_BROUTER_URL` | `https://brouter.de` | BRouter server (public or local, see below). |
| `ORS_API_KEY` | β€” | Free [OpenRouteService](https://openrouteservice.org) key if `BIKE_ENGINE=ors`. |
| `BIKE_PROTECTED_AREAS` | `1` | Check protected areas with the Overpass API. |
| `BIKE_GARMIN_MCP_COMMAND` | β€” | Only for `--garmin` in the CLI (see "Garmin"). |
| `BIKE_DATA_DIR` | `<GPX folder>/.bicycle-routes` | Where to keep the cache and index. |

## Command-line usage

```bash
uv run bicycle-routes generate "El Papiol" --km 25 --type tracks --max-climb 500
uv run bicycle-routes generate "41.4386,2.0108" --km 15            # coordinates work too
uv run bicycle-routes generate "El Papiol" --km 20 --end "Sant Cugat del Vallès"   # point-to-point
uv run bicycle-routes generate "El Papiol" --km 30 --via "Castellbisbal" --via "Molins de Rei"  # loop through via points
uv run bicycle-routes generate "El Papiol" --km 40 --seed 7        # another reproducible alternative
uv run bicycle-routes generate "El Papiol" --km 25 --json          # JSON output
uv run bicycle-routes list
```

Trail types (`--type`):

- `tracks`: favours wide dirt tracks, little singletrack and no extreme ramps.
- `mixed` (default): general MTB.
- `singletrack`: more singletrack, less pavement.
- `road`: road bike. Paved roads with little traffic; warns about unpaved sections and only
  about busy roads (main roads or heavy estimated traffic), not ordinary secondary roads.

Real example (abridged):

```text
MTB El Papiol 26 km loop
  Distance: 25.9 km Β· Climbing: +442 m / -443 m Β· Elevation 20–266 m
  Max grade: 17.0 % uphill, 18.0 % downhill
  Surface: paved 18.7 % Β· track 67.3 % Β· singletrack 14.1 %
  GPX: /tmp/bicycle-routes/mtb-el-papiol-26-km-loop-20261017-121732.gpx
  Warnings:
   - [traffic] 1.2 km on roads with motor traffic in 6 section(s); ride carefully.
   - [grade] 8 sections (about 2.2 km) are steeper than 12 %; listing the 6 steepest.
   - [protected_area] The route crosses the protected area Β«Parc Agrari del Baix Llobregat»…
```

More examples with real output in [docs/pruebas-reales.md](docs/pruebas-reales.md) (in Spanish).

## Using it with Claude (MCP server)

The server exposes two tools:

- `generate_route(start, distance_km, max_climb_m?, trail_type?, loop=true, end?, name?, seed?, via?)`
  β†’ GPX path, summary and warnings. `via` is a list of places the route must pass through, in
  order (e.g. "a loop from El Papiol passing through Castellbisbal and Molins de Rei").
- `list_routes()` β†’ saved routes.

### Claude Code

```bash
claude mcp add bicycle-routes-generator-mcp \
  -e BIKE_GPX_DIR=/tmp/bicycle-routes -e BIKE_CONTACT=you@example.com \
  -- uv run --directory /path/to/bicycle-routes-generator-mcp bicycle-routes-mcp
```

### Claude Desktop (without the extension)

The `.mcpb` extension is the easiest option (see above). To configure it by hand instead:
Settings β†’ Developer β†’ Edit config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "bicycle-routes-generator-mcp": {
      "command": "/absolute/path/to/uv",
      "args": ["run", "--directory", "/path/to/bicycle-routes-generator-mcp", "bicycle-routes-mcp"],
      "env": {
        "BIKE_GPX_DIR": "/tmp/bicycle-routes",
        "BIKE_CONTACT": "you@example.com"
      }
    }
  }
}
```

Use the absolute path to `uv` (`which uv` on macOS/Linux, `where uv` on Windows), because
Claude Desktop does not always inherit the terminal's `PATH`. Restart Claude Desktop.

### Building the extension

`manifest.json` describes the extension (`uv` type, tools and configuration fields) and
`.mcpbignore` keeps the tests, docs and your `.env` out of the package.

```bash
npx @anthropic-ai/mcpb validate manifest.json
npx @anthropic-ai/mcpb pack . dist/bicycle-routes-generator-mcp.mcpb
python scripts/make_icon.py    # only if you want to regenerate icon.png
```

### Publishing a release

1. Bump `version` in `manifest.json`, `pyproject.toml` and `src/bicycle_routes/__init__.py` together.
2. Create and push a matching tag:

   ```bash
   git tag v0.1.0
   git push origin v0.1.0
   ```

3. The `Release` GitHub Actions workflow runs the tests, builds `bicycle-routes-generator-mcp.mcpb` and attaches
   it to a new GitHub Release. The download link at the top of this README always points to
   the latest one.

## Garmin, Wahoo and other platforms

The application is platform-independent: the output is a standard GPX file. Sending to
platforms is done by optional **exporters** sharing a common interface
(`export(route, config) -> ExportResult`, in `src/bicycle_routes/exporters/`).

- **disk** (default): saves the GPX in `BIKE_GPX_DIR`.
- **garmin**: delegates to an already installed Garmin MCP server (`upload_course` tool).
  - *With Claude (recommended)*: nothing to configure. Ask "upload it to Garmin" and Claude
    will call the Garmin MCP server's `upload_course` with the generated GPX and
    `activity_type="mountain_biking"` (`"road_biking"` for road routes).
  - *From the CLI*: set `BIKE_GARMIN_MCP_COMMAND` to the command that starts that server and use
    `--garmin`. Garmin credentials are handled by that server, never by this app.
  - Community Garmin MCP servers use Garmin Connect's unofficial web API and may stop working.
    The official API (Courses API) is only available to approved businesses.
- **wahoo**: not implemented. The Cloud API has `POST /v1/routes`, but you must request an app
  and get it approved (notes in `exporters/wahoo.py`).
- **Komoot** (API only for partners under contract) and **Strava** (its API cannot create
  routes): import the GPX manually on their websites.

## Routing engines

- **BRouter** (default, MIT, no key). Uses the `mtb` profile with `StrictNOBicycleaccess=1`
  (avoids ways tagged `bicycle=no`) and `avoid_unsafe=1`; for `road`, the `fastbike` profile
  with `consider_traffic=1` (the same routes as `fastbike-lowtraffic`). The public `brouter.de` server makes
  no availability guarantees: the app sends at most 1 request per second and caches responses.
  For heavy use, run a local one (not yet tested in this project):

  ```bash
  mkdir -p segments4
  curl -o segments4/E0_N40.rd5 https://brouter.de/brouter/segments4/E0_N40.rd5   # Catalonia
  docker run -d -p 17777:17777 -v "$PWD/segments4:/segments4" ghcr.io/abrensch/brouter
  # and in .env: BIKE_BROUTER_URL=http://localhost:17777
  ```

- **OpenRouteService** (optional): `BIKE_ENGINE=ors` and `ORS_API_KEY` (free plan: 2,000
  routes/day, 40/min). Profile `cycling-mountain`, or `cycling-road` for `road`.

### How the loop is generated

1. The place is geocoded with Nominatim (or the coordinates you give are used).
2. 3-4 waypoints are placed on a circle passing through the start, with a random orientation
   (reproducible with `--seed`).
3. The engine routes start β†’ waypoints β†’ start on real trails.
4. The radius is adjusted until the requested distance is approached (about 2-4 requests).
5. Six candidates are generated and the best one is picked by distance, traffic, repeated
   trails, surface, forbidden access, climbing and grades.

With via points (`--via` / `via`), your points replace the random ones: the engine routes
start β†’ via 1 β†’ … β†’ via N β†’ start (or β†’ end, for point-to-point), in the given order. The
distance comes from those points, not from `--km`; a warning is added if it differs by more
than 15 %.

## External services and their policies

- **Nominatim** (place lookup): [usage policy](https://operations.osmfoundation.org/policies/nominatim/).
  At most 1 request/second, an identifying User-Agent, cached results, no autocomplete or bulk
  queries. For heavy use, run your own Nominatim.
- **Overpass API** (protected areas): about 10,000 requests/day per IP as a guideline; one
  cached query per route.
- **brouter.de**: public server without guarantees; use a local one for heavy use.

## Development

```bash
uv run pytest              # tests with simulated engine, geocoder and areas (no network)
uv run pytest -m network   # optional test against the real services
```

Layout:

```text
src/bicycle_routes/
β”œβ”€β”€ core/            # models, loop generator, analysis and GPX (no network, no platforms)
β”œβ”€β”€ engines/         # BRouter and OpenRouteService (RoutingEngine interface)
β”œβ”€β”€ geo/             # Nominatim and protected areas (Overpass)
β”œβ”€β”€ exporters/       # disk, garmin (via MCP), wahoo (pending)
β”œβ”€β”€ net.py           # HTTP client: User-Agent, rate limiting, retries and cache
β”œβ”€β”€ service.py       # reusable orchestration (CLI, MCP and a future remote server)
β”œβ”€β”€ mcp_server.py    # MCP server (stdio)
└── cli.py
```

The core does not depend on the CLI or the MCP server, so it could later be offered as a
remote server (HTTP transport) by changing only the exporter and the transport.

## License and attribution

Code under the [MIT](LICENSE) license. See [NOTICE](NOTICE).

**Trail and place data Β© [OpenStreetMap](https://www.openstreetmap.org/copyright)
contributors, under the ODbL license.** Generated GPX files include this attribution in their
metadata; keep it if you share them. BRouter elevation: SRTM (CGIAR-CSI).

TDQS

A3.9/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one creates a new route, the other lists existing saved routes. There is no overlap that could cause misselection.

Naming Consistency5/5

Both names follow a clean verb_noun snake_case pattern (generate_route, list_routes). The singular/plural noun difference is a natural consequence of the operation, not an inconsistency.

Tool Count3/5

Two tools is thin for a route-management server; only creation and listing are exposed. It is defensible for a narrow generation utility but sits at the borderline of the 1-2 tool range.

Completeness3/5

Create and list are covered, but there is no get (retrieve a route's details/GPX), delete, or export tool, leaving notable gaps for managing saved routes. An agent can work around this only via the filesystem, which is fragile.

Maintenance

ActivityMaintained
ResponsivenessNo issues