Python 3.10+ Tests Code style: black License
A developer-focused server that exposes vehicle data via a Model Context Protocol (MCP) interface. Originally built for Volkswagen vehicles — but since moving to the Tibber Data API backend, it isn't limited to VW: Tibber's vehicle integration is built on Enode, which covers 30+ EV brands (VW Group included), so any vehicle paired to your Tibber account works identically, regardless of make. This project is designed for integration, automation, and experimentation with connected car data.
Claude showing vehicle status GitHub Copilot preparing for trip
Access your vehicle's status through AI assistants like Claude Desktop and GitHub Copilot
Note
Why Tibber, not VW directly? In May 2026, VW shut down third-party access to its WeConnect
API (new device-attestation requirements open-source projects can't obtain).
This project's original direct integration (the carconnectivity library) stopped working
because of that, so the whole server was redesigned around the read-only
Tibber Data API instead. The old VW-direct code still
exists, unmaintained, on the permanent
carconnectivity branch.
That redesign is a trade-off: it loses most of what the old integration could do (see below), but in exchange it's no longer VW-specific — Tibber's vehicle integration covers 30+ EV brands, so this server now works with any vehicle paired to your Tibber account, not just VW.
The Tibber Data API is read-only and covers only what Tibber's 5 confirmed vehicle capabilities expose: identity (VIN, brand, model, name, online state) plus charging/range (state of charge, target SoC, remaining range, plug status, charging state) for electric vehicles. There is no door/window/tyre/light/climate/GPS/maintenance data, and no remote commands (lock, climate, charging control, lights) at all — Tibber's API has no write endpoints whatsoever.
vehicle_id resolution (VIN/name/license-plate lookup) and the response shape are the same
regardless of make — the brand field just reflects whatever your paired vehicle actually is
(e.g. "Volkswagen" for the vehicle this project was built and verified against).
See the full 51-point comparison against the old VW-direct data, the OAuth2/API research behind
this backend, and the current architecture in ARCHITECTURE.md.
- No license plate data (Tibber API limitation): The Tibber Data API does not provide license plate information, so there's no
license_platefield in any tool response and no way to identify a vehicle by license plate either. This is a limitation of Tibber's API, not this server. - No door/window/tyre/light/climate/GPS/maintenance data: Tibber's confirmed capabilities cover only identity and charging/range — see above.
- Read-only: No remote commands (lock, climate, charging control, lights) are possible — Tibber's API has no write endpoints at all.
- Refresh token rotation: Tibber rotates the refresh token on every use; the token file must be on writable, persisted storage or re-authentication will eventually be required.
- Vehicle pairing is manual, outside this server: a vehicle only shows up in
get_vehicles()after the user has paired it to their Tibber account in the Tibber app. This server has no tool to perform or check that pairing — if a vehicle is missing, that's the fix, not a bug here.
- MCP Server: Provides a standard MCP interface for accessing vehicle data
- Tibber Data API backend — read-only, via Tibber (an official VW integration partner); works despite VW's third-party API block (see What This Server Can Do)
- AI Assistant Ready: Works with Claude Desktop, VS Code Copilot, ChatGPT, Claude.ai and other MCP-compatible tools
- Cloud Deployable: Ships with
Dockerfile,docker-compose.ymland Railway config for one-command cloud deployment - API-Key Authentication: Bearer token auth for secure public HTTP endpoints
- Flexible CLI: Multiple transport modes (stdio for local, HTTP for cloud)
- Configurable: Credentials via config file or environment variables (for Docker / Railway)
Get up and running in 3 steps:
-
Install
git clone https://github.com/Smengerl/weconnect_mvp.git cd weconnect_mvp ./scripts/setup.sh -
Configure — register an OAuth2 client and log in once:
cp src/tibber_config.example.json src/tibber_config.json # edit src/tibber_config.json with your client_id/client_secret python -m weconnect_mcp.cli.tibber_login_cli src/tibber_config.json # one-time interactive login
See Setting Up Tibber Credentials for where to get the client id/secret and other options.
-
Connect an AI assistant
./scripts/create_mcp_config.sh claude # Claude Desktop -- copy output to Claude's configRestart Claude Desktop and ask: "What vehicles are available?"
See Connecting AI Assistants below for GitHub Copilot, Microsoft Copilot Desktop, Cline, or a cloud deployment (ChatGPT, Claude.ai, ...).
- Python 3.8+
- A Tibber account with a vehicle paired to it (any brand Tibber/Enode supports — not just VW, see What This Server Can Do), and an OAuth2 client registered at data-api.tibber.com (see Setting Up Tibber Credentials)
- (Recommended) Virtual environment
Quick Start (Recommended):
Simply run the setup script which handles everything automatically:
git clone https://github.com/Smengerl/weconnect_mvp.git
cd weconnect_mvp
./scripts/setup.shThe script will:
- ✅ Detect your Python installation
- ✅ Create a virtual environment at
.venv/ - ✅ Install the project in editable mode (
pip install -e .) - ✅ Create configuration template
Manual Installation (Alternative):
git clone https://github.com/Smengerl/weconnect_mvp.git cd weconnect_mvp python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -e .
For running tests locally, install test extras:
pip install -e ".[test]"The setup script automatically detects and avoids Microsoft Store Python (which doesn't work). If you see errors about Python not found:
-
Install Python from python.org (not Microsoft Store)
- Download from python.org
- ✅ Check "Add Python to PATH" during installation
-
Disable Microsoft Store Python alias (if you have it):
- Settings → Apps → Advanced app settings → App execution aliases
- Turn OFF:
python.exe,python3.exe,python3.x.exe
-
Verify your Python installation:
# Should return a path like: C:\Program Files\PythonXXX\python.exe where python
-
Register an OAuth2 client at https://data-api.tibber.com/clients/manage/ — see
ARCHITECTURE.mdfor the exact scopes to select and redirect URI to use. -
Provide credentials — two options, and you can mix them (environment variables override the file when both are present):
Option A — file (recommended for Claude Desktop / VS Code Copilot: those launch the server with their own environment, not your shell's, so
exported variables never reach it):cp src/tibber_config.example.json src/tibber_config.json # edit src/tibber_config.json with your client_id/client_secretsrc/tibber_config.jsonis gitignored.Option B — environment variables (recommended for Docker/Railway):
export TIBBER_CLIENT_ID="your-client-id" export TIBBER_CLIENT_SECRET="your-client-secret" export TIBBER_REDIRECT_URI="http://localhost:8515/callback" # optional, this is the default # export TIBBER_TOKEN_PATH="/custom/path/tibber_tokens.json" # optional -- default is an # OS-standard per-user data directory (e.g. ~/Library/Application Support/weconnect-mcp on # macOS), NOT the current directory, so every local MCP client shares one token file by # default. Only set this to deliberately opt out (e.g. isolated test accounts).
-
Run the one-time interactive login (opens a browser; only needs to be done once — the server itself never opens a browser, it only refreshes the resulting token non-interactively).
tibber_login_clitakes the same optional credentials-file argument as the server, with identical file/env precedence — pass it if you used Option A above:python -m weconnect_mcp.cli.tibber_login_cli src/tibber_config.json # Option A (file) python -m weconnect_mcp.cli.tibber_login_cli # Option B (env vars)
On success this writes the token to
token_path(from the file,TIBBER_TOKEN_PATH, or its OS-standard per-user default, see above) and lists the vehicle(s) found in your Tibber account. You won't be asked to log in again — every later run just refreshes this token. -
Start the server — the config file is optional (pass it if you used Option A above):
python -m weconnect_mcp.cli.mcp_server_cli [src/tibber_config.json]
./scripts/create_mcp_config.sh {claude,copilot-desktop,vscode} (see
Connecting AI Assistants)
already generates configs pointing at src/tibber_config.json with a correct "cwd" — no manual
editing of the generated MCP client config needed. If you hand-edit an MCP client config instead,
still give it a "cwd" pointing at this repo, so a relative config.json argument resolves
correctly — but note that token_path's own default no longer depends on cwd at all: it's a
fixed per-user directory (see step 2 above), specifically so that multiple local MCP clients
(Claude Desktop, VS Code Copilot, Claude Code, ...) launching this server with different working
directories still converge on the same cached token instead of each silently getting its own
(which used to make Tibber's rotating refresh_token strand whichever client refreshed second — see
ARCHITECTURE.md for troubleshooting specific error messages:
missing credentials, no cached token, invalid_grant).
The server supports two transport modes depending on the AI agent you want to use:
- stdio: When running MCP server locally on the same machine as your AI agent (Claude Desktop, VS Code Copilot)
- http: For cloud deployment or when the local AI agent requires this mode (e.g. ChatGPT)
You can start the MCP server using the provided CLI scripts or directly via Python:
1. Starting the server in foreground (with logs to console)
./scripts/start_server_fg.sh
2. Starting the server in background (with logs to file)
./scripts/start_server_bg.sh
If started in the background, stop the server using the script:
./scripts/stop_server_bg.sh
Alternatively, kill the process via PID.
3. Starting the server directly via Python
# No config file needed if TIBBER_CLIENT_ID/TIBBER_CLIENT_SECRET are set as env vars: python -m weconnect_mcp.cli.mcp_server_cli --port 8089 # With a credentials file: python -m weconnect_mcp.cli.mcp_server_cli src/tibber_config.json --port 8089
./scripts/start_server_fg.shand./scripts/start_server_bg.shboth forward extra arguments, so e.g../scripts/start_server_fg.sh src/tibber_config.json --port 8765works too.
The MCP server can be started with several command-line parameters to control its behavior:
| Parameter | Default | Description |
|---|---|---|
config |
(none) | Path to a Tibber credentials JSON file; optional — env vars alone are sufficient |
--log-level |
INFO |
Set logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
--log-file |
(stderr only) | Path to log file (if not set, logs to stderr only) |
--transport |
stdio |
Transport mode: stdio (for AI) or http (for API) |
--port |
8089 |
Port for HTTP mode (only relevant with --transport http) |
Example:
python -m weconnect_mcp.cli.mcp_server_cli --log-level DEBUG --log-file server.log --transport http --port 8089
Generate your configuration for Claude Desktop with the following script and follow the instructions to add it to your Claude Desktop configuration:
cd /path/to/weconnect_mvp
./scripts/create_mcp_config.sh claudeReload Claude Desktop and ask questions like:
- "What vehicles are available?"
- "Show me my car's battery status"
The screenshots and video below were captured against the old VW-direct (
carconnectivity) backend, before VW blocked third-party access and this project moved to Tibber — kept here for illustration. See MCP Tool & Prompt Reference below for what actually works today: battery status and charging status still work exactly like this; vehicle position and starting/stopping a charging session do not (Tibber has no position data at all, and no write endpoints).
Check battery status and state of charge (still works today):
Get complete vehicle status (today: identity + battery/charging only, no doors/climate/position):
Interactive demo video (recorded against the old carconnectivity backend):
Generate your configuration for GitHub Copilot with the following script and follow the instructions to add it to your VS Code settings:
cd /path/to/weconnect_mvp
./scripts/create_mcp_config.sh vscodeRestart VS Code and verify installation by typing /list in Copilot Chat. Look for tools starting with mcp_weconnect_
Captured against the old
carconnectivitybackend, same caveat as the Claude Desktop screenshots above — the doors/location parts of this workflow don't work with Tibber, only battery/charging status does.
Prepare for a trip - check battery, charging status, doors, and location:
GitHub Copilot preparing for trip
Interactive demo video (recorded against the old carconnectivity backend):
GitHub Copilot interaction demo
Generate your configuration for Microsoft Copilot Desktop with the following script:
cd /path/to/weconnect_mvp
./scripts/create_mcp_config.sh copilot-desktopCopy the configuration file to Microsoft Copilot Desktop's config directory:
mkdir -p ~/Library/Application\ Support/Microsoft/Copilot cp tmp/copilot_desktop_mcp.json ~/Library/Application\ Support/Microsoft/Copilot/mcp.json
Restart Microsoft Copilot Desktop completely and test
The server uses the standard MCP protocol and works with all MCP-compatible tools.
Cline (VS Code Extension) - Configuration in .vscode/cline_mcp_settings.json:
{
"mcpServers": {
"weconnect": {
"command": "python",
"args": [
"-m",
"weconnect_mcp.cli.mcp_server_cli",
"/path/to/your/config.json"
]
}
}
}You can also start the server in HTTP mode locally, for programmatic access or to test the cloud setup before deploying.
Port strategy for HTTP mode
- Railway / cloud: Railway injects
$PORTautomatically (default in image:8080). No manual configuration needed.- Local Docker: Container runs internally on
8080;docker-compose.ymlmaps host port8089→ container port8080. Access viahttp://localhost:8089.- Local CLI (no Docker):
start_server_http.shdefaults to port8089. Use a different port only when that port is already in use.Using a non-standard port (
8089) for local Docker/CLI avoids conflicts when multiple MCP servers are running side by side.
Via script (recommended):
# Reads credentials from .env automatically ./scripts/start_server_http.sh # starts on http://localhost:8089 (default) ./scripts/start_server_http.sh 8090 # override port if needed
Inline (manual override):
MCP_API_KEY=your-secret-key \ TIBBER_CLIENT_ID=your-client-id \ TIBBER_CLIENT_SECRET=your-client-secret \ ./scripts/start_server_http.sh 8089
The server will then be available at http://localhost:8089.
- MCP endpoint:
http://localhost:8089/mcp - Health check:
http://localhost:8089/health
Connecting AI clients (VS Code Copilot, Claude Code) to a local HTTP server:
// VS Code: %APPDATA%\Code\User\mcp.json { "servers": { "weconnect": { "type": "http", "url": "http://localhost:8089/mcp", "headers": { "Authorization": "Bearer <YOUR_MCP_API_KEY>" } } } }
// Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json { "mcpServers": { "weconnect": { "url": "http://localhost:8089/mcp", "headers": { "Authorization": "Bearer <YOUR_MCP_API_KEY>" } } } }
This MCP server provides 3 tools and 11 prompts that AI assistants can use. There is no
separate MCP Resources layer: it would have been a 1:1 duplicate of the tools with no added
capability for the clients this project targets (Claude Desktop, VS Code Copilot, Claude Code) —
see src/weconnect_mcp/server/mixins/read_tools.py for the reasoning. All 3 tools are fully
functional — everything the Tibber Data API doesn't provide (doors, windows, tyres, lights,
climate, GPS position, maintenance, and any remote command) simply has no tool at all, rather than
a tool that always returns an error. get_charging_status can still return {"error": "..."} for
an individual vehicle that resolves but doesn't support charging, separately from the usual
"vehicle not found" case.
Source of truth: The canonical, up-to-date reference — including the exact wording each tool/prompt reports — lives in
src/weconnect_mcp/server/AI_INSTRUCTIONS.mdand insrc/weconnect_mcp/server/mixins/{read_tools,prompts}.py.
| Tool | Description |
|---|---|
get_vehicles |
List all vehicles: VIN, name, model (no license_plate field — Tibber doesn't provide one) |
get_vehicle_info |
Manufacturer, model, name, online state, last-seen timestamp, plus a quick energy snapshot (electric range, charging flag, plug-connected flag) |
get_charging_status |
Resolved vehicle VIN/name (confirms which vehicle matched, since vehicle_id accepts partial names), charging/plug state, target/current SOC, electric range, last-seen timestamp |
✅ List vehicles and identify them by name or VIN ✅ Read battery level, range, and charging/plug status ✅ Answer "How much charge does my car have?" / "Is it plugged in?" ❌ Cannot read doors, windows, climate, position, tyres, lights, or maintenance data — not available via Tibber ❌ Cannot execute any remote command (lock, climate, charging control, lights) — the Tibber Data API is read-only, full stop
The server ships with a Dockerfile and supports full cloud deployment, enabling connections from web-based AI services such as ChatGPT, Claude.ai, or any other MCP-compatible client.
The server connects to Tibber (a non-interactive token refresh, then an initial vehicle-list fetch)
synchronously, once, before it starts serving any request — the same order stdio mode has always
used. There is no separate "still starting" state or error_type for it: by the time /health or
any tool call is reachable at all, that connection attempt has already resolved one way or the
other. (Docker/docker-compose's HEALTHCHECK gives the container a 60s start-period before the
first check even counts, which comfortably covers this.)
If the connection attempt fails — not configured, invalid credentials, the login was never done,
or a network problem — the server still starts, with every tool call (and /health) reporting the
real cause instead of crashing or silently returning an empty result. It also keeps retrying:
whenever a tool call or a /health probe hits the failure, the server attempts to reconnect
(subject to a cooldown that backs off the longer it stays broken, capped at 5 minutes) — so fixing
the underlying problem (finishing the login, correcting TIBBER_CLIENT_ID/SECRET) heals the
deployment on its own, without a restart, the next time either a tool is called or /health is
probed. See "Error Handling" in AI_INSTRUCTIONS.md
for the full list of error_type codes both tool calls and /health report:
{"status": "unavailable", "ready": false, "error_type": "not_configured",
"message": "TIBBER_CLIENT_ID, TIBBER_CLIENT_SECRET not set. ..."}
⚠️ Cloud deployment — token bootstrap. The Tibber OAuth login is a one-time interactive step (browser + human click) that cannot run inside a headless container, and Tibber has noclient_credentialsgrant (confirmed live,ARCHITECTURE.md) —client_id/client_secretalone can never mint a fresh access token, so arefresh_tokenmust persist across restarts one way or another. The bridge: runpython -m weconnect_mcp.cli.tibber_login_clilocally first, then paste that run's token file contents into theTIBBER_TOKEN_JSONenvironment variable. On first boot only, the server writes that into the file atTIBBER_TOKEN_PATH(Dockerfile default:/tmp/tibber-tokens/tibber_tokens.json, on thetibber-tokensvolume indocker-compose.yml). Every token refresh after that rewrites the file directly (including Tibber's rotatingrefresh_token) — as long asTIBBER_TOKEN_PATHis on a persisted volume, it survives future restarts andTIBBER_TOKEN_JSONis never read again. Without a volume, each restart re-seeds from the same (increasingly stale) env var, which works until that seed'srefresh_tokenis rotated away — set up a volume for anything beyond quick local testing.
Railway is a platform-as-a-service that builds and runs your Docker container automatically. It detects the Dockerfile and railway.toml in this repo with zero configuration.
Step 1 – Install Railway CLI and log in
brew install railway # macOS; see https://docs.railway.com/guides/cli for other OSes
railway loginStep 2 – Create project and deploy
cd /path/to/weconnect_mvp railway init # creates a new Railway project linked to this directory railway up --detach # builds the Docker image and deploys it
Step 3 – Set secret environment variables
Never put credentials in the repository. Set them in the Railway dashboard instead (see the token
bootstrap caveat above before deploying):
railway variables set TIBBER_CLIENT_ID="your-client-id" railway variables set TIBBER_CLIENT_SECRET="your-client-secret" railway variables set MCP_API_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" # First deploy only -- paste in the contents of the token file produced by # running `python -m weconnect_mcp.cli.tibber_login_cli` locally: railway variables set TIBBER_TOKEN_JSON="$(cat tibber_tokens.json)"
Then, in the Railway dashboard, add a Volume to the service mounted at
/tmp/tibber-tokens (Service → Settings → Volumes) so the token the server writes there survives
redeploys — without it, every redeploy re-seeds from the (increasingly stale) TIBBER_TOKEN_JSON
above, which stops working once Tibber rotates that seed's refresh_token away.
Or go to: railway.com → your project → service → Variables
Step 4 – Get the public URL
railway domain # e.g. https://weconnectmcp-production.up.railway.appStep 5 – Verify
curl https://<your-subdomain>.up.railway.app/health # → {"status": "ok", "ready": true, "service": "weconnect-mcp"}
Every git push followed by railway up redeploys the service.
Local test with Docker Compose:
cp .env.example .env # fill in your real credentials # First run only: seed the token (see the caveat above). # tibber_login_cli doesn't load .env itself, so export it into the shell first: set -a && source .env && set +a python -m weconnect_mcp.cli.tibber_login_cli echo "TIBBER_TOKEN_JSON=$(cat tibber_tokens.json)" >> .env docker compose up --build
The server is then available at http://localhost:8089. The tibber-tokens volume in
docker-compose.yml persists the refreshed token across docker compose restart/rebuilds, so the
tibber_login_cli step above is only needed once, the very first time.
Credentials and the API key are passed via environment variables — never put them in the repository:
| Variable | Required | Description |
|---|---|---|
TIBBER_CLIENT_ID |
Yes (or via file) | OAuth2 client id from data-api.tibber.com |
TIBBER_CLIENT_SECRET |
Yes (or via file) | OAuth2 client secret |
TIBBER_REDIRECT_URI |
Optional | Default: http://localhost:8515/callback |
TIBBER_TOKEN_PATH |
Optional | Image default: /tmp/tibber-tokens/tibber_tokens.json (mount a volume here — see the caveat above) |
TIBBER_TOKEN_JSON |
First boot only | Contents of a token file produced locally by tibber_login_cli — bootstraps TIBBER_TOKEN_PATH once, see the caveat above |
MCP_API_KEY |
Yes | Bearer token clients must send for authentication |
PORT |
Auto | HTTP port (Railway injects this automatically; default: 8080) |
CORS_ORIGINS |
Optional | Comma-separated allowed origins (default: *) |
Generate a strong API key:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"Once deployed, point any MCP-compatible client at your public URL:
- MCP endpoint:
https://<your-host>/mcp - Authentication: HTTP header
Authorization: Bearer <MCP_API_KEY>
Claude.ai:
Settings → Integrations → Add MCP Server → enter URL and header
ChatGPT Custom GPT:
Configure → Actions → select MCP → enter URL and Authorization: Bearer <key>
GitHub Copilot (VS Code) via remote server:
Add to .vscode/mcp.json:
{
"servers": {
"weconnect-cloud": {
"type": "http",
"url": "https://<your-host>/mcp",
"headers": {
"Authorization": "Bearer <MCP_API_KEY>"
}
}
}
}MCP_API_KEY – without it the server runs unauthenticated (locally or in the cloud)
.env or src/tibber_config.json – both are gitignored
tibber_tokens.json or wherever TIBBER_TOKEN_PATH points) contains session tokens – keep it secure
MCP_API_KEY immediately if it was ever accidentally exposed (e.g. pasted into a chat)
/health endpoint is intentionally unauthenticated (required for Railway / Docker health checks)
Run the test suite with:
./scripts/test.sh # Run with verbose output ./scripts/test.sh -v # Show help ./scripts/test.sh --help
Test Structure:
- 47 tests - Run in ~0.1 seconds, no Tibber account needed (mock adapter + real fixture data)
- No slow/real-API tests exist today — the Tibber Data API is read-only, so there's nothing beyond what the mock adapter and the extraction-logic fixtures already cover
For detailed test documentation, see tests/README.md
Contributions are welcome! Please see CONTRIBUTING.md and follow the code of conduct.
- ARCHITECTURE.md - Full Tibber Data API research, the 51-point data comparison against the old VW-direct (
carconnectivity) backend, current adapter architecture, and project history - scripts/README.md - All available scripts and how to use them
- scripts/lib/README.md - Python detection library documentation
- tests/README.md - Test suite overview
- CONTRIBUTING.md - Contribution guidelines
This project is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0) — see LICENSE.txt for details or visit http://creativecommons.org/licenses/by-sa/4.0/
This project was originally built on top of the excellent CarConnectivity library by Till Steinbach, which provided direct VW WeConnect API access before VW blocked third-party clients. That integration lives on, unmaintained, on the permanent carconnectivity branch.