Project · Lakshay Mehandru · Aug 2026

Ask My Car

Your car has a diagnostic port and a standard way to read it. I put an AI on the other end, read-only. Here's what's possible, how to start, and what I'd tell you before you do.

TypeScriptExpoMCPHardwareSecurity

Bus census

0 of 14 modules answered

Waiting for the first module to answer.

A mock-up. The labels are the kinds of module the census found on my car. The readings are not captured data.

Your car already has an API

What would my car say if something actually listened?

Turns out, a lot. I can ask Claude “is the check engine light on, and why?” or “has someone cleared the codes recently?” and it answers by reading the car itself. Every car sold in the US since 1996 has a diagnostic port under the dashboard, and it speaks a standard called OBD-II. A cheap adapter turns that port into something your laptop, and your AI, can talk to.

What's possible

Once the car can answer questions, a lot opens up. A few I like:

  • Ask why the check engine light is on before you call a mechanic, and walk in knowing the code.
  • Check a used car before you buy it. No codes plus unfinished self-tests usually means someone cleared them recently.
  • Watch live data on a drive: engine speed, coolant temperature, load.
  • Have an agent check the car every morning and only speak up if something changed.
  • Keep a history of codes over time, so a problem that comes and goes has a record.

I've built the first three. The last two are where it gets fun.

How to get started

  1. Buy a Wi-Fi or Bluetooth ELM327 adapter and plug it into the port near your knees.
  2. Connect with any free OBD app first, just to see live data and codes come through.
  3. Write a small script that sends the standard requests and decodes the replies. Mine is a Node script with no dependencies that serves a JSON API.
  4. Put a dashboard on that API if you want one.
  5. Wrap the API in an MCP server so Claude can use it. Mine gets seven tools.
car_status
Summary: connection, adapter, warning light, key gauges.
car_pids
Every decoded reading, filterable by name.
car_codes
Stored, pending and permanent codes, with descriptions.
car_modules
The modules found on the bus.
car_module
One module's identification and data.
car_log
The last lines of the raw conversation with the adapter.
car_send
One raw request, only if it is a read.

Lessons, so you can start further along

Put the allowlist in before the AI. The adapter will carry anything you send it, including requests that clear codes or change settings. So every request goes through a list of nine read services, checked in the MCP server and again in the API, and everything else is refused before it leaves the laptop.

const READS = [
  "01", "02", "03", "07", "09",
  "0A", "21", "22", "1A",
]

function allowed(cmd) {
  return READS.includes(cmd.slice(0, 2))
}
// anything else: refused, never sent

Give the AI narrow tools, not a raw pipe. “Read the codes” is a better tool than “send anything”. The one raw tool I kept only takes reads.

One question at a time. The adapter handles one command at a time, and a slow answer can land after your timeout and get read as the answer to the next question. Drop anything that shows up late.

A failed read is not a clean car. If a query didn't come back, say so. Never report no codes when you just didn't get an answer.

Record once, build anywhere. I saved a sanitized capture and wrote a fake adapter that replays it, so most of this got built at a desk, not in the driveway.

Keep a local code table. P0301 means nothing on its own. A local table of about 9,000 standard code descriptions turns it into “misfire in cylinder 1” without a network call.

Leave locked things locked. A few modules in my car wouldn't say what they were. I leave them alone. What the port offers is plenty.

Why read-only first

Most companies have a version of this car: older systems full of useful data that nobody wants an AI changing. Read-only is how you start, and write access can come later, once reading has earned it.

If you've hooked an AI up to your car, or to anything old and stubborn, I'd love to hear what you'd do differently.

How it's built

Hardware and protocol

A Wi-Fi ELM327 adapter sits in the diagnostic port. On the car side it speaks CAN (ISO 15765-4, 11-bit identifiers); on the laptop side it is a TCP socket. A small state machine handles one command in flight, frames each reply and catches errors like NO DATA. It takes a transport instead of opening a socket itself, so a Bluetooth transport can drive the same decoding later. The decoding code has no Node built-ins, so the server and the Expo app run the same copy.

Discovery

A one-off script scans every address from 700 to 7FF with a short ping, asks each module that answers who it is using the standard identification reads, then sweeps its data IDs. Fourteen answered on my car. Names are graded confirmed, likely, guess or locked, because a module that answers is not a module that tells you what it is. A sanitized copy of the result lets a fake adapter replay the whole thing without a car.

The read-only boundary

A request goes to the car only if its service is on an allowlist of nine read services, plus a few adapter queries like voltage and version. The check runs in the MCP server and again in the API, and a refused request gets a 403 and never leaves the laptop. Clearing codes, sessions, security access, resets and anything that writes are all refused. There is no clear-codes endpoint.

The dashboard

An Expo app, web first, on Bun. Status leads with a one-word verdict, then Live, Modules and Console tabs. The server pushes state over a stream throttled to ten updates a second. Code descriptions come from a local table of 9,075 generic codes, so no code lookup touches the network. If a query failed, the app says it did not see it, and it never calls the car clean on codes it did not read.