Skip to main content
Glama
bosch-ebike-connect-classic
by Edocsyl

bosch-ebike-connect-classic

Remote MCP server for the Bosch eBike Connect Classic portal (ebike-connect.com). Connect your account to Claude, ChatGPT, Cursor or any MCP client and ask about your rides, statistics and bikes. Multi-user (everyone signs in with their own account), OAuth 2.1, one Docker container.

Unofficial. Not affiliated with Bosch. It uses the portal's own JSON endpoints, which can change without notice, and Bosch's terms may restrict automated access. Use at your own risk. Classic system only (ebike-connect.com), not the Smart System / eBike Flow app.

Developers and contributors: everything technical (architecture, tests, security rules, releases, portal API notes) is in AGENTS.md. This README is for people who run and use the server.

Quick start

Run the published image. You need Docker with Compose and one file in an empty folder.

1. docker-compose.yml

services:
  bosch-ebike-connect-classic:
    image: ghcr.io/edocsyl/bosch-ebike-connect-classic:1.0.0
    user: "1000:1000"               # owner of ./config (run `id -u` / `id -g`)
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      PUBLIC_URL: http://localhost:8080     # address your clients use (https://... when public)
      # TRUST_PROXY: "xff"                  # behind a reverse proxy ("cloudflare" for a Cloudflare tunnel)
      # LOG_LEVEL: info                     # debug | info | warning | error
      # GATEWAY_SECRET: ...                 # optional: else generated into ./config/gateway.secret
    volumes:
      - ./config:/data              # everything the server keeps: database + secret
    restart: unless-stopped
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    mem_limit: 512m
    pids_limit: 200

2. Start

mkdir -p config                         # create it yourself, so it belongs to you and not to root
docker compose up -d
curl http://localhost:8080/healthz      # -> ok

3. Connect a client, for example Claude Code:

claude mcp add --transport http bosch-ebike-connect-classic http://localhost:8080/mcp
# then in Claude Code: /mcp -> bosch-ebike-connect-classic -> Authenticate

Sign in with your eBike Connect e-mail and password in the browser, then ask: "Show my e-bikes" or "When was my last ride?"

The image is private until you make it public (GitHub → Packages → Package settings → Visibility), or log in first: echo "$TOKEN" | docker login ghcr.io -u <github-user> --password-stdin (token with read:packages). To build it yourself instead: clone this repository and run docker compose up -d --build.

Related MCP server: mcp-garmin

Connect clients

The server URL is <PUBLIC_URL>/mcp; the landing page at <PUBLIC_URL>/ shows it.

Client

How

Claude (web, desktop)

Settings → Connectors → Add custom connector → https://<host>/mcp (needs public https)

Claude Code

claude mcp add --transport http bosch-ebike-connect-classic https://<host>/mcp, then /mcp → Authenticate

ChatGPT, Cursor, VS Code, MCP Inspector, …

add a remote MCP server of type Streamable HTTP with the URL

Standard MCP Streamable HTTP and OAuth 2.1 (dynamic client registration, PKCE S256, refresh-token rotation, RFC 9728 / 8414 / 8707 / 9207), so no client-specific setup. The legacy SSE transport is not supported.

Configuration and data

Everything is set in the environment: block of the compose file:

Variable

Default

Meaning

PUBLIC_URL

http://localhost:8080

Address your clients use, no trailing slash. Use your https address when public.

GATEWAY_SECRET

generated

≥ 32 characters. If unset, one is generated on first start and kept in ./config/gateway.secret.

TRUST_PROXY

off

xff (or 1): behind a reverse proxy, the client IP is taken from X-Forwarded-For. cloudflare: from CF-Connecting-IP. Only enable it when the server is reachable only through that proxy, otherwise clients can forge their IP and skip the rate limits.

PROXY_HOPS

1

xff mode: number of trusted proxies in front of the server.

LOG_LEVEL

info

debug, info, warning or error; see "Logs" below.

ALLOWED_ORIGINS

empty

Extra browser origins allowed to call /mcp (browser-based clients only).

ALLOWED_REDIRECT_HOSTS

empty

Comma list. If set, https redirect URIs of clients must use one of these hosts (e.g. claude.ai,chatgpt.com). Strongest protection against phishing via self-registered clients on a public server.

ALLOWED_REDIRECT_SCHEMES

empty

Extra custom URL schemes clients may use (built in: cursor, vscode, claude, windsurf, zed, … and any reverse-DNS scheme like com.example.app).

ALLOW_INSECURE_HTTP

off

1 permits a plain http:// PUBLIC_URL on a non-local address. Passwords would travel unencrypted: do not use on the internet.

Limits (all optional, whole numbers):

Variable

Default

Limits

RATE_LOGIN_IP

10

sign-in attempts per IP per 10 min

RATE_LOGIN_ACCOUNT

5

failed sign-ins per e-mail per 10 min (successful ones do not count)

RATE_REGISTER_IP

20

client registrations per IP per hour

RATE_TOKEN_IP

60

token requests per IP per minute

RATE_MCP_USER

120

MCP calls per user per minute

MAX_BODY_BYTES

16384

request body size for /oauth/*

MAX_MCP_BODY_BYTES

1048576

request body size for /mcp

MAX_CLIENTS

10000

registered OAuth clients (unused ones are purged after 7 days)

MAX_PORTAL_RESPONSE_BYTES

10485760

largest accepted reply from the Bosch portal

Rate limits live in memory of the one container; they reset on restart.

Logs: docker compose logs -f. LOG_LEVEL decides how much is written:

Level

Logs

error

only failures that need attention (unexpected errors)

warning

plus rejected sign-ins, rate limits, foreign origins, expired Bosch sessions, invalid tokens

info (default)

plus start-up, sign-ins, registered clients, disconnects, portal errors

debug

plus every tool call, every request to the portal (path, status, duration), token requests, HTTP access log

Logs never contain passwords, tokens, cookies or the full e-mail address (shown as a***@e***.com).

Data: the ./config folder holds everything the server keeps: gateway.sqlite3 (encrypted Bosch sessions, OAuth clients, tokens) and gateway.secret. Back it up as a whole. Create it yourself before the first start (otherwise Docker creates it as root and the container cannot write); user: in the compose file must be its owner. To put the data elsewhere, change the left side of the volume, e.g. /srv/ebike:/data. Deleting the folder (container stopped) removes all users' sessions. For a stricter setup set GATEWAY_SECRET yourself and keep it out of ./config, so a copy of the folder alone is useless.

Updates: pin an exact version in image: (e.g. :1.0.0), change it to update, then docker compose pull && docker compose up -d. The data in ./config is kept; go back by setting the old version. What changed in each version: Releases.

Going public: put TLS in front (for example cloudflared tunnel --url http://localhost:8080), set PUBLIC_URL to the https address and TRUST_PROXY (xff or cloudflare); for a public server also set ALLOWED_REDIRECT_HOSTS. You then hold other people's Bosch sessions and ride data: provide a privacy notice (GDPR / Swiss DSG) and start with a trusted circle.

Tools

Read-only, except disconnect_account. Errors come back as Error: … with isError: true.

Tool

Returns

get_user_profile

name, units, e-mail

get_my_ebikes

drive unit, display, batteries, firmware

get_statistics

totals per year and month, plus all-time

get_activities

trips with their rides; offset (epoch ms) pages backwards

get_activity_details

ride or trip aggregates; include_series=true adds speed, cadence, heart rate, GPS, power (large)

export_activity

GPX, TCX or CSV; timezone (IANA, default Europe/Zurich). Prefer GPX/CSV, TCX is very large

search_rides

filter by title, *_distance_km, *_duration_min

get_routes, get_route_details, export_route_gpx

saved routes (segmented=true for route-builder routes)

get_saved_places

favourites

get_weather_forecast

daily or hourly forecast for a latitude / longitude

get_dashboard_feed

news and latest activities incl. GPS tracks; keep count small

get_integrations

Strava / Komoot connection state (no tokens)

get_user_settings, get_licenses

account settings; purchased licences

disconnect_account

signs out and deletes the user's data on this server; needs confirm=true

Disconnect / remove an account

Ask the assistant to run disconnect_account with confirm=true. It sends the portal's logout request, then deletes that user's stored session, tokens and codes. Afterwards the connector must be authorised again.

The portal does not revoke the old session on logout (verified: the cookie still worked afterwards). To really end the session at Bosch, change your password on ebike-connect.com.

As operator you can also delete one account (replace the e-mail, lower case) or everything:

docker compose exec bosch-ebike-connect-classic python -c "
import sqlite3; db = sqlite3.connect('/data/gateway.sqlite3'); k = 'you@example.com'
[db.execute(f'delete from {t} where account_key=?', (k,)) for t in ('accounts', 'tokens', 'codes')]
db.commit()"
# everything: docker compose down && rm -rf config

How it works and security

Client ──OAuth 2.1──▶ /oauth/*  ── sign-in page ──▶ ebike-connect.com
Client ──POST /mcp + Bearer──▶ guard ─▶ MCP tools ─▶ ebike-connect.com
  • The password goes to Bosch once and is never stored. Only the session cookie is kept, encrypted with AES-256-GCM (key derived from GATEWAY_SECRET).

  • Tokens and authorization codes are stored only as SHA-256 hashes; refresh tokens rotate; codes are single-use; redirect URIs must match exactly.

  • Tools take no account parameter: the user comes from the validated token, so users cannot reach each other.

  • An expired Bosch session drops the tokens; the next call gets 401 and the client signs in again.

  • /mcp rejects foreign browser origins; sign-in, registration, token and MCP calls are rate limited.

  • Sign-in page shows where your browser is sent afterwards and that the app name is unverified; custom URL schemes are restricted, request bodies are size-capped, rate limits are bounded and configurable.

  • A replayed refresh token revokes all tokens of that client and account.

  • The container runs as a non-root user with a read-only filesystem, no capabilities, and memory / process limits.

Known limitations

  • Unofficial API: a portal change can break tools. Tested live with one real Classic account, with a scripted OAuth client and with Claude Code; not with Claude Desktop, Claude web or other vendors' clients.

  • Not verified with real data: get_route_details, export_route_gpx, non-empty get_saved_places and get_licenses (the test account has none). The expired-session path is tested with a fake Bosch only.

  • SQLite and in-memory rate limits in one container: fine for a small group, not horizontally scalable.

  • Reviewed by an automated security review, not by an independent audit or penetration test. If you run it publicly, you carry the responsibility for the sessions you store.

Contributing, credits and license

Contributions are welcome: start with AGENTS.md. Endpoints found in the portal's web bundle and in ebike-connect-js. Gateway architecture inspired by missingmcp and garmin_mcp (no code shared). Licensed under the MIT License; "Bosch" and "eBike Connect" are trademarks of their owners.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Connects MCP clients to Garmin Connect data, enabling queries about activities, sleep, heart rate, body battery, and training status.
    16
    2
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Exposes Garmin Connect data through MCP, enabling retrieval of activities, health metrics, training status, and workouts, with OAuth authentication and confirmed write operations.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables OAuth-authenticated access to Garmin account data through MCP, with a read-only web dashboard for monitoring account connections and activity.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables MCP clients to read and update Garmin Connect data, including activities, health metrics, training status, and more, with per-account multi-tenant authentication.
    3
    MIT