MCP Server on embedded device
In order for an AI agent to diagnose a vehicle, we needed to create a MCP server, which can send commands via the vehicle's OBD-II port. On this page, @castlebbs explains how he developed a custom firmware running a MCP server. The firmware was installed on an inexpensive OBD-II scanner (dongle).
Disclaimer: We did this just for fun, just for the technical challenge and for the learning opportunity. We don't recommend letting an AI agent control your car directly without more controls.
- Creation of a MCP Server as a custom OBD-II dongle firmware
- Creation of the Gradio Car Diagnostic AI Agent
Creation of a MCP Server as a custom OBD-II dongle firmware
I looked at the inexpensive OBD-II dongle for sale online. They generally support Bluetooth or Wifi. These are dongles, which you connect to your vehicle's OBD port. You generally use a mobile app connecting to the dongle (via Bluetooth or Wifi) to run diagnostics.
I initially thought of creating a MCP Server to act as a bridge: basically receiving MCP tool calls, and in turn calling the OBD dongles over WiFi or Bluetooth over their existing protocols.
As I was researching a bit more the hardware of these OBD dongles, I realized it would be way cooler to actually run the MCP server in the dongle's hardware themselves. That meant creating a new firmware.
The devices I was looking at have two MCUs:
- MCU to communicate with the OBD-II port of the vehicle (ELM327)
- MCU to manage Wi-Fi or Bluetooth connection with client applications.
I thought that if I could replace the firmware of this second MCU, I would be able to directly implement the MCP protocol and have the AI agent directly communicating with the dongle without any intermediary.
Disclaimer: To be clear, this is just a fun project, I don't think reprogramming your OBD-II dongle is the best approach to achieve this. I like embedded programming and I thought it was an interesting challenge. You will surely lose your warranty on your dongle, and there is a high chance you will damage and/or brick your device, or worse, your car!
I identified and ordered a "OBD II Car Diagnostic Tool Auto Scanner Code Reader WiFi" on Amazon. There were actually pictures of the electronics inside and I identified that the Wi-Fi MCU was a ESP8266. This was great as it is easy to reprogram. While I waited to receive my dongle, I started to work on a firmware for the ESP8266, we didn't have much time with the hackathon.
A few days later, my dongle was delivered. It was a big surprise because the Wi-Fi MCU was not the ESP8266 I expected. It was a different processor: WinnerMicro W600-B800. I never heard about it. I didn't even know if my project was achievable anymore, and obviously the Hackathon deadline was getting closer.
There is little information about the W600-B800, but there is a SDK, and there are some PDF documents from the manufacturer website. And kudos to WinnerMicro, their SDK just worked. The instructions to set up the toolchain worked, and while the documentation is limited, I was already familiar with FreeRTOS/lwIP which their SDK is using. The SDK includes a few code examples, which were in fact sufficient to know what I needed to do. Claude Code also helped in understanding the SDK. Coding assistants are a real game changer for embedded programming, to help understanding a new SDK. So I'd say the experience with programming the W600 was smooth sailing.
Really importantly, I found this page. The author already identified the test pads, which could be used to upload the firmware. He also successfully uploaded a different firmware. At this point I was more confident I'd be able to pull this out.
I was able to upload my firmwares by connecting to the PCB test pads:
- Programmer UART connected to W600 UART0, 115200 8N1 via needles on test pads.
- With minicom, switch to firmware upload mode during SECBOOT
- In upload mode, upload the firmware with the xmodem protocol
- WinnerMicro Instructions
1. Minimal firmware to support over-the-air (OTA) updates
Placing probes on the test pads to upload firmware or accessing logs was not ideal. My first goal was to create a minimal firmware, which would:
- Connect the device to my Wi-Fi network (STA mode + DHCP).
- Automatically check if a firmware was available at a specific url, and if available, proceed to OTA (Over The Air update)
- Send logs via UDP syslog
#1 and #2 were straightforward. There are demos doing just that, available in the SDK. I have implemented this at the beginning of App/main.c
For #3, there is no support for network logging in the SDK. Therefore I modified the existing logging library to add support for non-blocking UDP syslog. The changes were made in: Src/App/easylogger/port/elog_port.c and Include/wm_log.h At this point, I was able to very easily update the firmware on the device (OTA) and remotely monitor what the device was doing. That was great progress.
2. ELM327 Driver
The next step was to create a ELM327 driver, for the W600 to communicate with the ELM327 chip to send instructions (AT commands, OBD-II commands) and retrieve responses. I also wanted a simulation mode for when the device is powered, but not connected to a vehicle. In this case, the AT commands would be sent to the hardware, but the OBD-II commands would be answered by the simulator.
I need to say that before I erased the original firmware, I probed the serial lines between the W600 and the ELM327 and recorded the communication. This was in line with the ELM327 documentation. You donβt want to be in a situation where you donβt know how your MCUs or peripherals communicate and you donβt have the original firmware.
UART1 from W600 connected to EUSART1(First UART) on the ELM327. 38400 8N1.
ELM327 driver: Src/App/elm327/elm327.c and
ELM327 API
| Function | Purpose | Key Return Values |
|---|---|---|
elm327_init() |
Initialize UART1 @ 38400 baud | WM_SUCCESS / WM_FAILED |
elm327_send_command(cmd, resp, len, timeout) |
Send OBD-II command | >0 bytes, -1 not init, -2 invalid params, -3 UART fail, -4 timeout |
elm327_deinit() |
Cleanup resources | void |
Simulation API (optional)
Available when compiled with TLS_CONFIG_ELM327_SIMULATION.
| Function | Purpose | Returns |
|---|---|---|
elm327_set_simulation_mode(enable) |
Enable/disable simulation mode | void |
elm327_is_simulation_enabled() |
Check if simulation is active | true / false |
elm327_set_engine_state(running) |
Set simulated engine state | void |
| Simulation Notes: |
- When enabled, OBD-II commands return simulated responses
- AT commands always go to real hardware
- Engine state affects simulated RPM, temperature, and other dynamic values
Configuration
- UART: UART1, 38400 baud, 8N1, no flow control
- Max response: 512 bytes (
ELM327_MAX_RESPONSE_LEN) - Default timeout: 2000ms (
ELM327_DEFAULT_TIMEOUT)
The SDK defines two UART operation modes (interrupt and polling) in its API, but the actual implementation only supports interrupt mode. The polling mode enum exists but the corresponding code path is commented out (see Platform/Drivers/uart/wm_uart.c:376). Interrupts are always enabled regardless of the mode parameter. For the retrieval of ELM327 responses, I rely on the interrupt-driven RX callback, and the end of the response is signaled via a FreeRTOS semaphore.
3. MCP Server Architecture
The MCP server is the main part of this project. It mostly complies with the MCP HTTP Streamable transport (version 2025-06-18): https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Client β
β (AI agent, Gradio, Claude Code, etc.) β
ββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
β
β HTTP POST /mcp
β (JSON-RPC over HTTP)
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β W600 WiFi SoC β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β MCP Server Orchestrator β β
β β β β
β β β’ TCP connection management (lwIP) β β
β β β’ Request buffering & reassembly β β
β β β’ Coordinate HTTP, JSON-RPC, and method layers β β
β β β’ Response lifecycle management β β
β β β β
β β (mcp_server.c - Port 80) β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββ¬ββββββββββββββ β
β β β β β
β β β β β
β βββββββββββββΌβββββββββββ βββββββββββββββΌβββββββββββ βββββββββββΌβββββββββββββ β
β β β β β β β β
β β HTTP Protocol β β JSON-RPC Parser/ β β MCP Method Router β β
β β Handler β β Builder β β β β
β β β β β β β β
β β β’ Parse HTTP β β β’ Parse JSON-RPC β β β’ initialize β β
β β requests β β messages β β β’ tools/list β β
β β β’ Build HTTP β β β’ Build success β β β’ tools/call β β
β β responses β β responses β β β β
β β β’ Validate β β β’ Build error β β Dispatches to β β
β β headers β β responses β β tool handlers β β
β β β’ Status codes β β β’ JSON-RPC 2.0 β β β β
β β β β compliance β β (mcp_methods.c) β β
β β (mcp_http.c) β β β β β β
β β β β (mcp_jsonrpc.c) β β β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββββ βββββββββββ¬βββββββββββββ β
β β β
β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββ β
β β Tool Handlers β β
β β β β
β β βββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β β
β β β β β β β β
β β β status β β send_elm327_command β β β
β β β β β β β β
β β β β’ IP address β β β’ Send AT commands β β β
β β β β’ Network info β β β’ Send OBD-II PIDs β β β
β β β β’ System uptime β β β’ Read responses β β β
β β β β’ Memory usage β β β’ Format results β β β
β β β β β β β β
β β βββββββββββββββββββββββββββββββββ βββββββββββββββ¬βββββββββββββββββββ β β
β β β β β
β β (mcp_tools.c) β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββ β
β β β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββ β
β β ELM327 Driver β β
β β β β
β β β’ UART1 communication @ 38400 baud β β
β β β’ AT command protocol β β
β β β’ OBD-II PID queries β β
β β β’ 2-second default timeout β β
β β β’ Response parsing β β
β β β β
β β (elm327.c) β β
β βββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββ β
β β ELM327 Chip β β
β β (UART1) β β
β ββββββββββ¬ββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
β
βΌ
ββββββββββββββββββββ
β Vehicle OBD-II β
β Port β
ββββββββββββββββββββ
The firmware implements a complete MCP server stack directly on the W600 microcontroller, enabling AI agents to communicate with vehicle diagnostics hardware without any intermediary services or bridge software.
MCP Server Orchestrator
Implementation: mcp_server.c
The orchestrator is the foundation layer that manages all network operations using lwIP's callback-based TCP API. It listens on port 80, handles incoming connections, buffers and reassembles HTTP requests that may arrive in multiple TCP packets, and coordinates the request-response lifecycle across the protocol stack. The server operates through lwIP callbacks (accept, receive, error) invoked by the TCP/IP thread, allowing it to efficiently manage multiple concurrent client connections without requiring a dedicated task.
The orchestrator directly invokes three parallel layers to handle different aspects of the protocol:
- HTTP Protocol Handler (
mcp_http.c) for HTTP parsing and response building - JSON-RPC Parser/Builder (
mcp_jsonrpc.c) for JSON-RPC message handling - MCP Method Router (
mcp_methods.c) for method dispatch
This design provides clear separation of concerns while keeping the orchestration logic centralized in mcp_server.c.
HTTP Protocol Handler
Implementation: mcp_http.c
This module implements HTTP protocol parsing and response generation. It extracts HTTP methods, paths, headers, and body content from raw TCP data, validates required headers (like Content-Type and Content-Length), and constructs properly formatted HTTP responses with appropriate status codes and headers. The handler is designed to work within the memory constraints of an embedded system and is called directly by the orchestrator.
JSON-RPC Parser/Builder
Implementation: mcp_jsonrpc.c
The MCP protocol uses JSON-RPC 2.0 as its message format. This component provides parsing and building functions for JSON-RPC messages. It parses incoming JSON-RPC requests, validates the structure (checking for required fields like "jsonrpc", "method", and "id"), and packages responses back into valid JSON-RPC format with proper error handling. Note that this module only handles message formatting - the actual method routing is performed by the MCP Method Router.
MCP Method Router
Implementation: mcp_methods.c
This module implements the core MCP protocol methods defined in the specification. It handles the "initialize" handshake that establishes the MCP session, responds to "tools/list" requests with available diagnostic tools, and processes "tools/call" invocations that execute specific diagnostic operations. The router is called directly by the orchestrator and maintains protocol compliance while adapting the MCP specification to the embedded environment.
Tool Handlers
Implementation: mcp_tools.c
The actual diagnostic capabilities are implemented as MCP tools. The "status" tool reports device health information including IP address, network connectivity, uptime, and available memory. The "send_elm327_command" tool is the primary diagnostic interface, allowing AI agents to send both AT commands (for ELM327 configuration) and OBD-II PIDs (for vehicle data) and receive responses. These tools translate high-level MCP tool calls into low-level hardware operations.
ELM327 Driver Layer
Implementation: elm327.c
This driver communicates with the ELM327 chip over UART1 at 38400 baud, implementing the ELM327 command-response protocol with proper timeout handling and response parsing. It includes an optional simulation mode (enabled at compile time) that generates realistic vehicle responses when the device is powered but not connected to a vehicle, facilitating development and testing without requiring an actual car.
Hardware Interface
At the bottom of the stack, the firmware interfaces with the physical ELM327 chip, which in turn communicates with the vehicle's OBD-II port using standard automotive protocols (CAN, J1850, ISO 9141, etc.). This hardware layer translates between the UART-based ELM327 protocol and the vehicle's native diagnostic bus.