03Lab / Projects
Live
Tesla Fleet MCP
Tesla Fleet MCP connects Jay’s 2019 Tesla Model 3 Long Range to Grok Bot through a custom Model Context Protocol server on a Raspberry Pi 5. Chat can send a navigation destination, read charge and cabin status from cache, start or stop climate, and ask for a door lock that Jay still has to confirm himself.
What it does
The Pi is the only machine that holds the vehicle-command private key and the Tesla OAuth tokens. Grok Bot never talks to Tesla on its own. From chat, Jay can:
- send a navigation destination
- check battery, charge, cabin, and lock status
- start or stop climate preconditioning
- request a door lock or unlock
Status reads default to live: false. They hit Tesla’s Fleet API cache and leave a parked car asleep, so a status check does not drain the battery. Navigation and climate can wake the car when they need a live command. Lock and unlock never fire from the tool call. They create a short-lived pending request that Jay confirms on his own Tailscale network.
How a request moves
Cyan is public MCP and Fleet traffic. Amber is the boundary that stays on Jay’s machines: the vehicle key, the encrypted tokens, and the tap that actually moves a door.
Process flow
MCP and Fleet
Key trust and confirm
One-time OAuth and virtual key
Tesla will not pair a third-party app without a public HTTPS host for an ECDSA P-256 public key, and it will not finish OAuth without a public HTTPS callback. The public site does both of those jobs and nothing more sensitive. After Tesla returns an authorization code, the site forwards that one-time code to the Pi with an HMAC signature. The client secret, the token exchange, and the encrypted tokens never live in the cloud. The matching private key never leaves the Pi.
Daily MCP requests
Grok Bot’s cloud MCP client needs a public HTTPS endpoint and a bearer token. Tailscale Funnel publishes /mcp and forwards it to the process on the Pi. Cached reads go to Tesla’s Fleet API with live: false and do not wake the car. Navigation and climate may wake it. Those commands are ECDH-signed by Tesla’s tesla-http-proxy on the Pi, which is the process that holds the vehicle private key.
Door lock and unlock
A language model is not a locksmith. Lock and unlock do not actuate doors from chat. The MCP server stores a short-lived pending request and returns a private Tailscale link. That link only works on Jay’s tailnet. Confirm is a POST. GET does nothing, so a preview bot that fetches the URL cannot open the car.
Kept private and safe by design
The vehicle private key and the OAuth tokens sit on the Pi, encrypted at rest with AES-256-GCM. The public site hosts only the matching ECDSA P-256 public key, which Tesla needs for virtual-key pairing. VIN and GPS coordinates are stripped from MCP responses, so a chat log never holds a location. Door confirmations reject GET, which stops a link prefetcher from treating a preview as a command. The Pi also enforces hourly caps on wakes and commands, and a kill switch can halt the daemon without a redeploy. A gitleaks hook runs before every commit and is never skipped.
Why it got so complicated
Most of the moving parts are Tesla’s and Grok Bot’s rules, not taste. A smaller pile of them was us being careful in the wrong places.
What the platforms actually required
Tesla needs a public HTTPS domain for the ECDSA public key. Signed vehicle commands require a persistent local tesla-http-proxy Go daemon that performs ECDH with the car. A serverless Vercel Hobby deployment cannot host that daemon, which is why the Pi exists. Tesla OAuth needs a public HTTPS callback, while the client secret belongs on the Pi. Grok Bot’s cloud MCP client needs a public HTTPS endpoint with bearer auth. Physical door locks should not trust an LLM tool call alone.
Where v1 overcomplicated it
The first version grew in five cautious milestones next to an existing Pi project. That produced two Node servers, oauth-ingest and fleet-mcp, sharing encrypted tokens through a lock file, plus three local listeners. Funnel path stripping and DNS-rebinding Host checks showed up late and had to be patched in. In v2 the Pi side collapses into one Node daemon beside tesla-http-proxy. The moving parts drop by nearly half. The safety boundaries stay.
Jay wanted the car in chat. He did not want the chat to hold the keys.