Skip to main content
Glama

Bicycle routes generator MCP

English · Español

Bicycle routes generator MCP server

⬇️ Download the Claude Desktop extension (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 Casa de Campo in Madrid, little climbing, on dirt tracks" (see what happens when you ask Claude).

  • From the terminal, without AI: bicycle-routes generate "Casa de Campo, Madrid" --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 (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 Casa de Campo in Madrid 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.

Related MCP server: RandoForge

Manual install (CLI and Claude Code)

Requirements: macOS, Windows or Linux with uv (it installs Python for you).

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 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

uv run bicycle-routes generate "Casa de Campo, Madrid" --km 25 --type tracks --max-climb 500
uv run bicycle-routes generate "40.4242,-3.7559" --km 15            # coordinates work too
uv run bicycle-routes generate "Casa de Campo, Madrid" --km 20 --end "El Pardo, Madrid"   # point-to-point
uv run bicycle-routes generate "Casa de Campo, Madrid" --km 30 --via "El Pardo, Madrid" --via "Aravaca, Madrid"  # loop through via points
uv run bicycle-routes generate "Madrid" --km 60 --type road         # road bike
uv run bicycle-routes generate "Casa de Campo, Madrid" --km 40 --seed 7   # another reproducible alternative
uv run bicycle-routes generate "Casa de Campo, Madrid" --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):

MTB Casa de Campo 38 km loop
  Start: Casa de Campo, Madrid, Comunidad de Madrid, España
  Distance: 37.8 km · Climbing: +336 m / -338 m · Elevation 615–725 m
  Max grade: 10.3 % uphill, 13.3 % downhill
  Surface: paved 28.8 % · track 40.6 % · singletrack 30.6 %
  Engine: brouter (mtb) · candidates evaluated: 6
  GPX: /tmp/bicycle-routes/mtb-casa-de-campo-38-km-loop-20261017-163919.gpx
  Warnings:
   - [traffic] 0.3 km on roads with motor traffic in 1 section(s); ride carefully.
   - [traffic] Road with traffic (km 27.3–27.6).
   - [grade] Steep descent over 12 % (max ~13 %, km 9.2–9.3).
   - [general] Climbing and grades come from a digital elevation model (~90 m resolution)…
   - [general] Route generated automatically from OpenStreetMap: check it on a map before riding…

More examples with real output in 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 Casa de Campo passing through El Pardo and Aravaca").

  • list_routes() → saved routes.

Example: what happens when you ask Claude

You: "Make me a ~40 km MTB loop from Casa de Campo in Madrid."

1. Claude calls the tool, choosing the arguments from your request:

generate_route({ "start": "Casa de Campo, Madrid", "distance_km": 40, "trail_type": "mixed" })

2. The MCP server computes the route on your computer (10-60 seconds):

  1. Looks up "Casa de Campo, Madrid" in Nominatim (OpenStreetMap) → 40.424, -3.756.

  2. Generates 6 candidate loops with BRouter on real trails, adjusting each one to about 40 km.

  3. Scores them (distance, traffic, repeated trails, surface, forbidden access, climbing) and keeps the best one.

  4. Analyses it: climbing, grades, surface, sections with traffic, technical sections and protected areas (Overpass API).

  5. Saves the GPX with elevation in your GPX folder.

3. It returns this to Claude (real output, abridged; obtained with "seed": 2026 so it can be reproduced: without a seed every request gives a different loop):

{
  "name": "MTB Casa de Campo 38 km loop",
  "gpx_file": "/tmp/bicycle-routes/mtb-casa-de-campo-38-km-loop-20261017-163919.gpx",
  "start": { "name": "Casa de Campo, Madrid, Comunidad de Madrid, España", "lat": 40.4241907, "lon": -3.755858 },
  "summary": {
    "distance_km": 37.8, "ascent_m": 336, "descent_m": 338,
    "min_elevation_m": 615, "max_elevation_m": 725,
    "max_uphill_grade_pct": 10.3, "max_downhill_grade_pct": 13.3,
    "surface_pct": { "paved": 28.8, "track": 40.6, "path": 30.6, "unknown": 0.0 },
    "warnings": [
      { "kind": "traffic", "message": "0.3 km on roads with motor traffic in 1 section(s); ride carefully." },
      { "kind": "traffic", "message": "Road with traffic (km 27.3–27.6).", "start_km": 27.32, "end_km": 27.61 },
      { "kind": "grade", "message": "Steep descent over 12 % (max ~13 %, km 9.2–9.3).", "start_km": 9.22, "end_km": 9.34 },
      { "kind": "general", "message": "Climbing and grades come from a digital elevation model (~90 m resolution)…" },
      { "kind": "general", "message": "Route generated automatically from OpenStreetMap: check it on a map before riding…" }
    ]
  },
  "engine": "brouter (mtb)",
  "activity_type": "mountain_biking",
  "candidates_evaluated": 6,
  "attribution": "Trail data © OpenStreetMap contributors (ODbL) — https://www.openstreetmap.org/copyright"
}

4. Claude answers you in your language, with the summary and every warning. For example (illustrative, Claude's wording varies):

I've created a 37.8 km loop from Casa de Campo with +336 m of climbing: 41 % dirt track, 31 % singletrack and 29 % paved. Watch out for 0.3 km on a road with traffic (km 27.3–27.6) and a steep descent of ~13 % at km 9.2. Climbing comes from an elevation model and may differ from your watch, so check the route on a map before riding. GPX: /tmp/bicycle-routes/mtb-casa-de-campo-38-km-loop-20261017-163919.gpx

From there you can keep going in the same conversation:

You say

What Claude does

"Give me another option"

Calls generate_route again with a different seed

"Less climbing, at most 200 m"

Adds max_climb_m: 200

"Make it pass through El Pardo"

Adds via: ["El Pardo, Madrid"]

"Now a 60 km road ride from Madrid"

Uses trail_type: "road" (paved, little traffic)

"Upload it to Garmin"

Calls the Garmin MCP server's upload_course with the GPX, if you have it installed

"What routes have I saved?"

Calls list_routes

Claude Code

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):

{
  "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.

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:

    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.

  4. A second job publishes that version to the official MCP Registry as io.github.Marc-Armillas/bicycle-routes-generator-mcp: it fills in server.json with the version, the release URL and the SHA-256 of the .mcpb, and logs in with GitHub OIDC (no secret needed). If the registry rejects it, the GitHub Release is already published.

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):

    mkdir -p segments4
    curl -o segments4/W5_N40.rd5 https://brouter.de/brouter/segments4/W5_N40.rd5   # Madrid and central Spain
    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. 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

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

Layout:

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. See NOTICE.

Trail and place data © OpenStreetMap 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).

Available Tools

2 tools
generate_routeA

Generate an MTB or road cycling route on real trails/roads, save it as GPX and return the summary and warnings.

Takes 10 to 60 seconds (it evaluates several candidates and picks the best one).

NameRequiredDescriptionDefault
endNoEnd point if the route is not a loop
viaNoPlaces the route must pass through, in order (names or 'lat,lon')
loopNoReturn to the start point
nameNoRoute name (optional)
seedNoSeed to repeat the same route; change it for an alternative
startYesStart location: place name ('Casa de Campo, Madrid') or 'lat,lon'
trail_typeNotracks = wide, non-technical dirt tracks; mixed = general MTB; singletrack = more singletrack; road = road bike, paved roads with little trafficmixed
distance_kmYesApproximate distance in km
max_climb_mNoMaximum desired climbing in metres

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses a 10-60 second runtime, explains the internal behavior ('evaluates several candidates and picks the best one'), and states the side effect of writing a GPX file. It stops short of covering permission/auth requirements, whether an existing GPX is overwritten, or what happens on failure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences with zero filler. The core capability is front-loaded and the runtime caveat follows immediately, which is exactly the ordering an agent needs before committing to a call that blocks for up to a minute.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so the description needn't enumerate return fields, and the rich input schema covers the nine parameters. For a complex, slow, side-effecting generation tool the description covers purpose, latency, and output side effects; the remaining gap is failure behavior and overwrite semantics.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all nine parameters, including the trail_type enum semantics and the seed/loop behavior. The description adds no parameter-level detail beyond what the schema provides, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Generate an MTB or road cycling route'), plus the routing substrate (real trails/roads) and two side effects (saves GPX, returns summary and warnings). It never names the sibling list_routes, but 'generate' vs 'list' is self-evidently distinct, so the only missing element is explicit sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied by the phrase 'MTB or road cycling route on real trails/roads', which tells the agent this is for route creation rather than retrieval. However there is no explicit when-to-use guidance, no statement of when NOT to use it, and no pointer to list_routes as the alternative for inspecting existing routes.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_routesA

List the saved GPX routes (most recent first) with their summary.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations at all, the description carries the full burden, and it does add one meaningful behavioral fact: results are ordered most recent first. However, it says nothing about pagination or result limits, and does not state that it is a read-only operation. Output schema exists, so return-value shape is covered elsewhere.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single tight sentence that front-loads the verb and resource and packs in ordering and summary detail with zero waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a parameterless list tool with an output schema defining the return shape, the description is nearly sufficient. The only meaningful omission is any hint about result volume or truncation behavior for long route lists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is no parameter semantics to document and the baseline of 4 applies. The description correctly adds no parameter information because none exists.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (List) and resource (saved GPX routes) plus a scope detail (most recent first) and return shape (with their summary). It is clear enough to distinguish from generate_route, though it does not name that sibling explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: an agent infers this is the tool for viewing existing routes as opposed to generating a new one. There is no explicit when-to-use statement, no exclusions, and no named alternative to redirect to.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.5.0
    • Changedgenerate_route1 field changed
      • changedInput schema / properties / start / description
        Previous value: -"Start location: place name ('El Papiol, Barcelona') or 'lat,lon'"New value: +"Start location: place name ('Casa de Campo, Madrid') or 'lat,lon'"
  2. 2 tool updatesv0.4.0
    • First observedgenerate_route
    • First observedlist_routes

TDQS

A3.9/5.0

Scored across 2 tools

Disambiguation5/5

generate_route and list_routes have clearly distinct purposes: one creates and saves a route, the other lists already saved routes. There is no overlap or ambiguity between them.

Naming Consistency5/5

Both tools follow a clean verb_noun snake_case pattern: generate_route and list_routes. The convention is consistent and predictable.

Tool Count3/5

Two tools is thin for a route generator that also manages saved GPX files. The surface is borderline minimal, though not an extreme mismatch for a narrow purpose.

Completeness3/5

The server covers generation and listing but lacks obvious lifecycle operations such as getting a saved route's details or deleting a saved route. These are notable gaps that could limit agent workflows.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    An AI-powered companion that provides access to the RideWithGPS API, allowing you to interact with cycling routes, trips, events, and user data through natural language.
    8
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables Claude to design reliable hiking routes on real trails, computing accurate distance and elevation gain, locating notable points, technical passages, regulated areas, bivouac and stage options, and weather, with automatic validation across roughly 35 rules. Routes can be exported as GPX (including one file per day), Obsidian notes, HTML maps, and printable roadbooks.
    23
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables users to ask ChatGPT or Claude where a runner or cyclist is in a live race, how the race is going, and what the weather along the course will be, returning live maps and 3D route flyovers inline in the chat. Also surfaces cycling tour day-by-day routes, GPX links and organiser notes, all read-only and free.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables Claude to search your Komoot tours by distance, start point, surface and scenery, and to assemble new gravel routes from the best sections of tours you already saved, exporting the result as a GPX track plus waypoints for the Komoot planner.
    9
    MIT