arduino-mcp-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/build/arduinoCli.js +74 -0
- package/build/boardReference.js +26 -0
- package/build/index.js +1268 -0
- package/data/board-reference.json +251 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akshat Nerella
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# arduino-mcp-server
|
|
2
|
+
Arduino MCP server that wraps `arduino-cli` so AI agents can discover boards/ports, compile/upload sketches, read serial output, and query board pin references.
|
|
3
|
+
|
|
4
|
+
## Features
|
|
5
|
+
- MCP tools for:
|
|
6
|
+
- listing connected boards and serial ports
|
|
7
|
+
- detecting connected hardware with inferred FQBN and next commands
|
|
8
|
+
- checking `arduino-cli` availability with OS-specific install guidance (`arduino_cli_doctor`)
|
|
9
|
+
- auto-installing `arduino-cli` when missing (`install_arduino_cli`)
|
|
10
|
+
- ensuring required board cores are installed (`ensure_core_installed`)
|
|
11
|
+
- listing supported boards
|
|
12
|
+
- compiling sketches
|
|
13
|
+
- uploading sketches
|
|
14
|
+
- reading a serial snapshot (time-bounded monitor)
|
|
15
|
+
- fetching `arduino-cli board details`
|
|
16
|
+
- querying local board pin/reference metadata
|
|
17
|
+
- Structured JSON responses so agents can reason over output
|
|
18
|
+
- Optional sketch path sandboxing via `ARDUINO_SKETCH_ROOT`
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
- Node.js 20+
|
|
22
|
+
- `arduino-cli` installed and available on `PATH` (or set `ARDUINO_CLI_PATH`)
|
|
23
|
+
|
|
24
|
+
## Agent Workflow Contract
|
|
25
|
+
Use this workflow in AI agents:
|
|
26
|
+
1. Call `arduino_cli_doctor` first.
|
|
27
|
+
2. If `installed=false`, call `install_arduino_cli` with `{"method":"auto"}`.
|
|
28
|
+
3. If auto-install fails, use the returned OS-specific `installGuide`.
|
|
29
|
+
4. Set `ARDUINO_CLI_PATH` if the binary is not on `PATH`.
|
|
30
|
+
5. Re-run `arduino_cli_doctor` and continue only when `installed=true`.
|
|
31
|
+
6. Only then call `detect_hardware`, `compile_sketch`, `upload_sketch`, etc.
|
|
32
|
+
|
|
33
|
+
Do not attempt fallback hardware scans before `arduino-cli` is available.
|
|
34
|
+
|
|
35
|
+
When `detect_hardware` returns unresolved/non-standard board matches, the tool now includes
|
|
36
|
+
`requiresUserBoardConfirmation` and an `agentAction` question payload. Agents should ask the user
|
|
37
|
+
to confirm board model/FQBN before continuing.
|
|
38
|
+
|
|
39
|
+
`compile_sketch` and `upload_sketch` automatically ensure board core installation from FQBN by default
|
|
40
|
+
(`autoInstallCore=true`), so agents should not need manual `arduino-cli core install` in normal flows.
|
|
41
|
+
|
|
42
|
+
## Install Arduino CLI Quickly
|
|
43
|
+
Official docs: https://docs.arduino.cc/arduino-cli/installation/
|
|
44
|
+
|
|
45
|
+
- Windows (recommended): `winget install ArduinoSA.CLI`
|
|
46
|
+
- macOS: `brew install arduino-cli`
|
|
47
|
+
- Linux: `brew install arduino-cli` or official install script
|
|
48
|
+
|
|
49
|
+
If needed, set `ARDUINO_CLI_PATH`:
|
|
50
|
+
- PowerShell (current session): `$env:ARDUINO_CLI_PATH='C:\\path\\to\\arduino-cli.exe'`
|
|
51
|
+
- Bash/Zsh (current session): `export ARDUINO_CLI_PATH=/absolute/path/to/arduino-cli`
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
```bash
|
|
55
|
+
npm install
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Run
|
|
60
|
+
```bash
|
|
61
|
+
npm start
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For local development:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run dev
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment variables
|
|
71
|
+
- `ARDUINO_CLI_PATH`: path/command for Arduino CLI. Default: `arduino-cli`
|
|
72
|
+
- `ARDUINO_SKETCH_ROOT`: optional absolute path. When set, `sketchPath` inputs must resolve under this root.
|
|
73
|
+
|
|
74
|
+
## Example MCP client config (stdio)
|
|
75
|
+
Use your built `build/index.js` as the command target.
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"arduino": {
|
|
81
|
+
"command": "node",
|
|
82
|
+
"args": ["D:/Projects/arduino-mcp-server/build/index.js"],
|
|
83
|
+
"env": {
|
|
84
|
+
"ARDUINO_CLI_PATH": "arduino-cli",
|
|
85
|
+
"ARDUINO_SKETCH_ROOT": "D:/Projects/arduino-sketches"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Board Reference Data
|
|
93
|
+
The server includes a starter board reference database at `data/board-reference.json` with common pin mappings.
|
|
94
|
+
You can expand this file or replace it with data from an external source later.
|
|
95
|
+
|
|
96
|
+
## MCP Capability Coverage
|
|
97
|
+
- Tools: compile/upload/monitor/board discovery and reference lookup
|
|
98
|
+
- Resource: `arduino://boards/reference` for board metadata
|
|
99
|
+
- Prompts:
|
|
100
|
+
- `arduino-cli-bootstrap-policy` for dependency/bootstrap behavior
|
|
101
|
+
- `arduino-setup-assistant` for wiring/setup guidance
|
|
102
|
+
|
|
103
|
+
## Publish To MCP Registry
|
|
104
|
+
This repo includes a registry manifest at `server.json`.
|
|
105
|
+
|
|
106
|
+
### Prerequisites
|
|
107
|
+
1. Publish the npm package first (`identifier` and `version` in `server.json` must exist):
|
|
108
|
+
- `npm login`
|
|
109
|
+
- `npm run build`
|
|
110
|
+
- `npm publish --access public`
|
|
111
|
+
2. Get a registry auth token (Bearer token) for `registry.modelcontextprotocol.io`.
|
|
112
|
+
|
|
113
|
+
### Publish command
|
|
114
|
+
PowerShell:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
$env:MCP_REGISTRY_TOKEN="<your_registry_token>"
|
|
118
|
+
curl --request POST `
|
|
119
|
+
--url https://registry.modelcontextprotocol.io/v0.1/publish `
|
|
120
|
+
--header "Accept: application/json, application/problem+json" `
|
|
121
|
+
--header "Authorization: Bearer $env:MCP_REGISTRY_TOKEN" `
|
|
122
|
+
--header "Content-Type: application/json" `
|
|
123
|
+
--data-binary "@server.json"
|
|
124
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function resolveSketchPath(inputPath, sketchRoot) {
|
|
4
|
+
const resolved = path.resolve(inputPath);
|
|
5
|
+
if (!sketchRoot) {
|
|
6
|
+
return resolved;
|
|
7
|
+
}
|
|
8
|
+
const root = path.resolve(sketchRoot);
|
|
9
|
+
const normalizedResolved = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
10
|
+
const normalizedRoot = process.platform === "win32" ? root.toLowerCase() : root;
|
|
11
|
+
if (normalizedResolved === normalizedRoot || normalizedResolved.startsWith(`${normalizedRoot}${path.sep}`)) {
|
|
12
|
+
return resolved;
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`Path "${resolved}" is outside ARDUINO_SKETCH_ROOT "${root}".`);
|
|
15
|
+
}
|
|
16
|
+
export async function runArduinoCli(config, args, timeoutMs = 60_000) {
|
|
17
|
+
return runCommand(config.cliPath, args, timeoutMs);
|
|
18
|
+
}
|
|
19
|
+
export async function runCommand(command, args, timeoutMs = 60_000) {
|
|
20
|
+
const started = Date.now();
|
|
21
|
+
return await new Promise((resolve) => {
|
|
22
|
+
const child = spawn(command, args, {
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
24
|
+
});
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
let timedOut = false;
|
|
28
|
+
child.stdout.on("data", (chunk) => {
|
|
29
|
+
stdout += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
child.stderr.on("data", (chunk) => {
|
|
32
|
+
stderr += chunk.toString();
|
|
33
|
+
});
|
|
34
|
+
child.on("error", (err) => {
|
|
35
|
+
const durationMs = Date.now() - started;
|
|
36
|
+
resolve({
|
|
37
|
+
ok: false,
|
|
38
|
+
command,
|
|
39
|
+
args,
|
|
40
|
+
code: null,
|
|
41
|
+
stdout,
|
|
42
|
+
stderr: `${stderr}\n${err.message}`.trim(),
|
|
43
|
+
timedOut: false,
|
|
44
|
+
durationMs
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
child.kill();
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
child.on("close", (code) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
const durationMs = Date.now() - started;
|
|
54
|
+
resolve({
|
|
55
|
+
ok: code === 0 && !timedOut,
|
|
56
|
+
command,
|
|
57
|
+
args,
|
|
58
|
+
code,
|
|
59
|
+
stdout,
|
|
60
|
+
stderr,
|
|
61
|
+
timedOut,
|
|
62
|
+
durationMs
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function tryParseJson(value) {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(value);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import boardReferenceData from "../data/board-reference.json" with { type: "json" };
|
|
2
|
+
const store = boardReferenceData;
|
|
3
|
+
export function listBoardReferences() {
|
|
4
|
+
return store.boards;
|
|
5
|
+
}
|
|
6
|
+
export function findBoardReference(query) {
|
|
7
|
+
const normalized = query.trim().toLowerCase();
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return store.boards.filter((board) => {
|
|
12
|
+
if (board.id.toLowerCase().includes(normalized)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (board.displayName.toLowerCase().includes(normalized)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (board.aliases.some((alias) => alias.toLowerCase().includes(normalized))) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (board.fqbnCandidates.some((fqbn) => fqbn.toLowerCase().includes(normalized))) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
}
|