Using AI to write, compile, and flash CAN bus firmware

How to Connect Claude Desktop to the CANFDuino via MCP Servers

Here is how you can describe a CAN bus task in plain English and have AI write the firmware, compile it, and flash it to your board automatically? We did this with Claude Desktop and a custom MCP (Model Context Protocol) server.

In this post we walk through how we connected Claude Desktop to the CANFDuino using a small Python MCP server. By the end, Claude can write Arduino sketches, compile them with arduino-cli, and upload them directly to the board, all from a chat prompt.

What You’ll Need

  • Claude Desktop (the desktop app from Anthropic)
  • Arduino Command Line Interface (CLI)
  • Python and MCP servers

Step 1: Implement Getting Started With CANFDuino

Before Going any further, you will need to follow these directions to make sure the full hardware/software package for the CANFDuino is functional. This must be in place first for anything to work.

Step 2: Install Claude Desktop

Claude Desktop is Anthropic’s desktop application for Windows and Mac. It’s what hosts the MCP servers and gives Claude the ability to use local tools.

  1. Go to claude.ai/download and download the installer for your platform.
  2. Run the installer and sign in with your Anthropic account (or create one if you don’t have one yet).
  3. Once installed, open Claude Desktop and confirm it launches correctly.


Step 3: Find Your arduino-cli Path

Claude runs in a restricted environment and needs the full path to arduino-cli. Find it by running:

where arduino-cli

On a typical Windows installation with Arduino IDE 2.x it will be something like:

C:\Users\YourName\AppData\Local\Programs\Arduino IDE\arduino-cli.exe

Note this path — you’ll use it in the Python MCP server.

If you do not have it, install this from the arduino website https://github.com/arduino/arduino-cli


Step 4: Create the Arduino MCP Server

Create a file called arduino_mcp.py. This is the bridge between Claude and your board. Here’s the full working code:

python

import subprocess
import os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Arduino_Controller")

BASE = r"C:\Users\YourName\AppData\Local\Programs\Arduino IDE"
SKETCH_PATH = os.path.join(BASE, "mcp_sketch", "mcp_sketch.ino")
CLI = os.path.join(BASE, "arduino-cli.exe")

@mcp.tool()
def write_sketch(code: str) -> str:
    """Writes C++ code to the local Arduino sketch file."""
    os.makedirs(os.path.dirname(SKETCH_PATH), exist_ok=True)
    with open(SKETCH_PATH, "w") as f:
        f.write(code)
    return f"Sketch written to {SKETCH_PATH}"

@mcp.tool()
def upload_to_arduino(fqbn: str, port: str) -> str:
    """Compiles and uploads the current sketch.
    Example FQBN: 'CANFDuino:samd:CANFDuino', Port: 'COM7'
    """
    SKETCH_DIR = os.path.dirname(SKETCH_PATH)
    compile_cmd = f'"{CLI}" compile --fqbn {fqbn} --output-dir "{SKETCH_DIR}" "{SKETCH_DIR}"'
    subprocess.run(compile_cmd, shell=True, capture_output=True)

    upload_cmd = f'"{CLI}" upload -p {port} --fqbn {fqbn} --input-dir "{SKETCH_DIR}"'
    subprocess.run(upload_cmd, shell=True, capture_output=True)
    return "Upload successful!"

@mcp.tool()
def list_ports() -> str:
    """Lists all available serial ports and connected Arduino boards."""
    cmd = f'"{CLI}" board list'
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout if result.stdout else "No ports found"

if __name__ == "__main__":
    mcp.run()

A few things worth noting about this code:

  • --output-dir on the compile command and --input-dir on the upload command are critical. Without them, arduino-cli writes compiled output to a system cache and the upload picks up a stale cached version of your sketch instead of the freshly compiled one. This was a subtle but important bug to track down.
  • capture_output=True on the compile step suppresses the “Sketch uses X bytes…” output lines that would otherwise confuse the MCP JSON protocol.
  • The docstring on upload_to_arduino includes example values — Claude reads these to know what format to use for the FQBN and port.

Step 5: Install the MCP Python Package


Install python https://www.python.org/, once you have have python installed, open a terminal and run pip install mcp

Test that your server starts correctly by running it directly:

python arduino_mcp.py

It should hang without errors — that means it’s running and waiting for connections.


Step 6: Register the MCP Server with Claude Desktop

Open your Claude Desktop config file. On Windows it’s at:

%APPDATA%\Claude\claude_desktop_config.json

Add your MCP server entry:

json

{
  "mcpServers": {
    "arduino-manager": {
      "command": "python",
      "args": [
        "C:\\Users\\YourName\\AppData\\Local\\Programs\\Arduino IDE\\arduino_mcp.py"
      ]
    }
  }
}

Restart Claude Desktop. You should see a hammer icon (🔨) near the message input — click it to confirm your tools (write_sketch, upload_to_arduino, list_ports) are listed.


Step 7: Prompting Claude for CAN Bus Firmware

With the MCP server running, Claude can now write and flash firmware. Here are some prompting strategies that work well.

Basic LED / Sanity Check

Always start with a simple blink to confirm the upload pipeline is working:

“Write a sketch that blinks the GP LED on the CANFDuino (pin 28) at 500ms and upload it to COM7.”

Note: the CANFDuino’s GP LED is on digital pin 28, not LED_BUILTIN.

Specifying the Board

Always include the FQBN and port in your prompt so Claude doesn’t have to guess:

“Upload to CANFDuino:samd:CANFDuino on COM7”

Or let Claude figure it out first:

“List the available ports, then upload to the CANFDuino.”

CAN Bus Gateway Example

For more complex CAN tasks, be specific about IDs, channels, and baud rates:

“Write a sketch for the CANFDuino that listens on CAN0 for messages with ID 0x120 at 500kbaud, and retransmits them on CAN1 with ID 0x210. Upload it to COM7.”

Claude will use the CANFDuino library (CANFDuino.h) and generate something like:

cpp

#include <CANFDuino.h>

cAcquireCAN CanPort0(0, CAN500K);
cAcquireCAN CanPort1(1, CAN500K);
TX_QUEUE_FRAME tx;

void setup() {
  Serial.begin(115200);
  CanPort0.Init();
  CanPort1.Init();
}

void loop() {
  CanPort0.RxMsgs();
  for (int i = 0; i < CanPort0.numRxMsgs; i++) {
    if (CanPort0.rxMsgs[i].rxMsgInfo.id == 0x120) {
      tx.id  = 0x210;
      tx.len = CanPort0.rxMsgs[i].rxMsgInfo.data_len;
      for (int j = 0; j < tx.len; j++) {
        tx.data[j] = CanPort0.rxMsgs[i].rxMsgInfo.data[j];
      }
      CanPort1.TxMsg(&tx);
    }
  }
}

Tips for Better Prompts

  • Mention the library by name if you know it: “use the CANFDuino.h library”
  • Specify baud rates explicitly: “500kbaud on both channels”
  • Give pin numbers directly when you know them: “GP LED is on pin 28”
  • Ask Claude to show the code before uploading if you want to review it first: “show me the sketch, then upload it”
  • Iterate naturally: “that worked — now change it to also print the CAN ID to serial whenever a message arrives”

Troubleshooting

Upload succeeds but the board doesn’t change behaviour This is almost always a stale cache issue. Make sure your compile command uses --output-dir pointing to a dedicated folder, and your upload uses --input-dir pointing to the same folder. Without these flags, arduino-cli may compile fresh but avrdude uploads the old cached hex.

“accepts at most 1 arg(s), received 2” error The compile command has a malformed argument structure. The sketch directory should appear only once and as a positional argument at the end, not duplicated.

MCP JSON parse errors (“Unexpected token ‘S'”) These happen when arduino-cli‘s stdout (e.g. “Sketch uses 922 bytes…”) leaks into the MCP JSON stream. Add capture_output=True to your subprocess.run() compile call to suppress it.


Example MCP & JSON

Can be found on Github repository under AI coding agents.


Built and tested with Claude Sonnet, CANFDuino hardware from Togglebit, and arduino-cli on Windows 11.