Skip to content
Harvis Lab

Back to Lab / Projects

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

Tesla Fleet MCP process flowThree stacked flows: one-time OAuth and virtual-key trust, daily MCP requests for cached status and signed commands, and a door lock loop that requires a human POST confirmation on a private tailnet.01OAuth and virtual keyPublic siteHTTPS domainHosts the P-256 public keyPairingVirtual keyPublic key only; private key on PiTeslaOAuth callbackCode lands on the public siteHandoffHMAC to the PiAuth code, never the secretRaspberry PiEncrypted tokensSecret and tokens stay here02Daily MCP requestsChatGrok BotMCP tool callEdgeFunnel to the PiHTTPS /mcp, bearer authStatusCached Fleet readlive: false, car stays asleepCommandstesla-http-proxyECDH signer; private key stays03Door lock and unlockChatLock or unlockTool call never actuatesPi MCPPending requestShort-lived, local onlyJayConfirm POSTPrivate tailnet linkCarDoors moveGET is ignored

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.