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.
- Go to claude.ai/download and download the installer for your platform.
- Run the installer and sign in with your Anthropic account (or create one if you don’t have one yet).
- 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-diron the compile command and--input-diron the upload command are critical. Without them,arduino-cliwrites 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=Trueon the compile step suppresses the “Sketch uses X bytes…” output lines that would otherwise confuse the MCP JSON protocol.- The docstring on
upload_to_arduinoincludes 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.




