nova-control-mcp-server 0.0.4 → 0.0.5
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/README.md +14 -2
- package/dist/nova-control-mcp-server.js +125 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nova-control-mcp-server
|
|
2
2
|
|
|
3
|
-
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server for controlling the [NOVA DIY Artificial Intelligence Robot](https://www.creoqode.com/nova) by Creoqode. It exposes the same servo commands as [nova-control-command](../nova-control-command) as MCP tools, allowing any MCP-capable AI assistant (Claude Desktop, Cursor, …) to control the robot arm directly.
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server for controlling the [NOVA DIY Artificial Intelligence Robot](https://www.creoqode.com/nova) by Creoqode. It exposes the same servo commands as [nova-control-command](../nova-control-command/README.md) as MCP tools, allowing any MCP-capable AI assistant (Claude Desktop, Cursor, …) to control the robot arm directly.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -119,6 +119,7 @@ The server exposes the following tools. The serial connection is opened lazily o
|
|
|
119
119
|
|---|---|---|
|
|
120
120
|
| `home` | — | send all servos to their home positions |
|
|
121
121
|
| `move` | `shift_to?`, `roll_to?`, `pitch_to?`, `rotate_to?`, `lift_to?` (all `number`, at least one required) | set one or more servo positions atomically |
|
|
122
|
+
| `move_to` | `within_ms: number` (required), `s1?`, `s2?`, `s3?`, `s4?`, `s5?` (all optional `number`) | move one or more servos to target positions in exactly `within_ms` milliseconds, using a trapezoidal ramp-up/ramp-down profile |
|
|
122
123
|
| `shift_to` | `deg: number` | shift head forward (>90°) or back (<90°) — s1 |
|
|
123
124
|
| `roll_to` | `deg: number` | roll head clockwise (>90°) or counter-clockwise (<90°) — s2 |
|
|
124
125
|
| `pitch_to` | `deg: number` | pitch head up (>110°) or down (<110°) — s3 |
|
|
@@ -126,6 +127,7 @@ The server exposes the following tools. The serial connection is opened lazily o
|
|
|
126
127
|
| `lift_to` | `deg: number` | lift head on secondary axis, range 20°–150° — s5 |
|
|
127
128
|
| `wait` | `ms: number` | pause for `ms` milliseconds before the next action |
|
|
128
129
|
| `get_state` | — | return current servo positions as a JSON object with keys `s1`–`s5` |
|
|
130
|
+
| `run_script` | `script: string` | execute a multi-line movement script (one command per line; blank lines and `#`-comments ignored; commands: `home`, `shift-to`, `roll-to`, `pitch-to`, `rotate-to`, `lift-to`, `move`, `wait`) |
|
|
129
131
|
|
|
130
132
|
### Servo mapping
|
|
131
133
|
|
|
@@ -142,14 +144,24 @@ The server exposes the following tools. The serial connection is opened lazily o
|
|
|
142
144
|
Once the server is running inside Claude Desktop you can give natural-language instructions like:
|
|
143
145
|
|
|
144
146
|
- *"Move NOVA's head to look straight up."* → `pitch_to(deg: 130)`
|
|
145
|
-
- *"Rotate the body 45° to the
|
|
147
|
+
- *"Rotate the body 45° to the right."* → `rotate_to(deg: 45)`
|
|
148
|
+
- *"Move the head to 120° over 800 ms."* → `move_to(within_ms: 800, s1: 120)`
|
|
146
149
|
- *"Shift to 100°, wait half a second, then return to home."* → `shift_to(100)` + `wait(500)` + `home()`
|
|
147
150
|
- *"What is the current servo state?"* → `get_state()`
|
|
151
|
+
- *"Run the greeting sequence from this script."* → `run_script(script: "home\nwait 500\nshift-to 110\nwait 400\nhome")`
|
|
148
152
|
|
|
149
153
|
## Exit behaviour
|
|
150
154
|
|
|
151
155
|
The server exits cleanly on `SIGINT` (Ctrl+C) or `SIGTERM`, closing the serial connection before quitting.
|
|
152
156
|
|
|
157
|
+
## Related packages
|
|
158
|
+
|
|
159
|
+
| package | description |
|
|
160
|
+
|---|---|
|
|
161
|
+
| [`nova-control-browser`](../nova-control-browser/README.md) | browser ESM module — Web Serial API (Chrome / Edge 89+) |
|
|
162
|
+
| [`nova-control-node`](../nova-control-node/README.md) | Node.js ESM module — `serialport` package (used internally by this server) |
|
|
163
|
+
| [`nova-control-command`](../nova-control-command/README.md) | CLI — one-shot commands, interactive REPL, and script files |
|
|
164
|
+
|
|
153
165
|
## License
|
|
154
166
|
|
|
155
167
|
MIT
|
|
@@ -7,9 +7,9 @@ import { Server as i } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport as a } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { StreamableHTTPServerTransport as o } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
9
|
import { CallToolRequestSchema as s, ListToolsRequestSchema as c } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import { openNova as l } from "nova-control-node";
|
|
10
|
+
import { openNova as l, runScript as u } from "nova-control-node";
|
|
11
11
|
//#region src/nova-control-mcp-server.ts
|
|
12
|
-
function
|
|
12
|
+
function d() {
|
|
13
13
|
try {
|
|
14
14
|
let { values: e } = r({
|
|
15
15
|
args: process.argv.slice(2),
|
|
@@ -46,20 +46,20 @@ function u() {
|
|
|
46
46
|
process.stderr.write(`nova-control-mcp: ${e.message ?? e}\n`), process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
var
|
|
50
|
-
async function
|
|
51
|
-
return
|
|
49
|
+
var f = "", p = 9600, m;
|
|
50
|
+
async function h() {
|
|
51
|
+
return m ??= await l(f, p), m;
|
|
52
52
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
53
|
+
function g() {
|
|
54
|
+
m != null && (m.destroy(), m = void 0);
|
|
55
55
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
56
|
+
function _(e, t = 9600) {
|
|
57
|
+
f = e, p = t;
|
|
58
58
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
59
|
+
function v() {
|
|
60
|
+
g(), f = "", p = 9600;
|
|
61
61
|
}
|
|
62
|
-
var
|
|
62
|
+
var y = [
|
|
63
63
|
{
|
|
64
64
|
name: "home",
|
|
65
65
|
description: "send all servos to their home positions",
|
|
@@ -102,11 +102,11 @@ var v = [
|
|
|
102
102
|
description: "shift head forward (>90°) or back (<90°) — s1",
|
|
103
103
|
inputSchema: {
|
|
104
104
|
type: "object",
|
|
105
|
-
properties: {
|
|
105
|
+
properties: { angle: {
|
|
106
106
|
type: "number",
|
|
107
107
|
description: "target angle in degrees"
|
|
108
108
|
} },
|
|
109
|
-
required: ["
|
|
109
|
+
required: ["angle"]
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
{
|
|
@@ -114,11 +114,11 @@ var v = [
|
|
|
114
114
|
description: "roll head clockwise (>90°) or counter-clockwise (<90°) — s2",
|
|
115
115
|
inputSchema: {
|
|
116
116
|
type: "object",
|
|
117
|
-
properties: {
|
|
117
|
+
properties: { angle: {
|
|
118
118
|
type: "number",
|
|
119
119
|
description: "target angle in degrees"
|
|
120
120
|
} },
|
|
121
|
-
required: ["
|
|
121
|
+
required: ["angle"]
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
{
|
|
@@ -126,11 +126,11 @@ var v = [
|
|
|
126
126
|
description: "pitch head up (>110°) or down (<110°) — s3",
|
|
127
127
|
inputSchema: {
|
|
128
128
|
type: "object",
|
|
129
|
-
properties: {
|
|
129
|
+
properties: { angle: {
|
|
130
130
|
type: "number",
|
|
131
131
|
description: "target angle in degrees"
|
|
132
132
|
} },
|
|
133
|
-
required: ["
|
|
133
|
+
required: ["angle"]
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
136
|
{
|
|
@@ -138,11 +138,11 @@ var v = [
|
|
|
138
138
|
description: "rotate body around Z-axis — s4",
|
|
139
139
|
inputSchema: {
|
|
140
140
|
type: "object",
|
|
141
|
-
properties: {
|
|
141
|
+
properties: { angle: {
|
|
142
142
|
type: "number",
|
|
143
143
|
description: "target angle in degrees"
|
|
144
144
|
} },
|
|
145
|
-
required: ["
|
|
145
|
+
required: ["angle"]
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
148
|
{
|
|
@@ -150,11 +150,45 @@ var v = [
|
|
|
150
150
|
description: "lift head on secondary axis, range 20°–150° — s5",
|
|
151
151
|
inputSchema: {
|
|
152
152
|
type: "object",
|
|
153
|
-
properties: {
|
|
153
|
+
properties: { angle: {
|
|
154
154
|
type: "number",
|
|
155
155
|
description: "target angle in degrees"
|
|
156
156
|
} },
|
|
157
|
-
required: ["
|
|
157
|
+
required: ["angle"]
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "move_to",
|
|
162
|
+
description: "move one or more servos smoothly to their target positions, completing the movement in the specified number of milliseconds using a trapezoidal ramp-up/ramp-down profile",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
within_ms: {
|
|
167
|
+
type: "number",
|
|
168
|
+
description: "total movement duration in milliseconds (must be > 0)"
|
|
169
|
+
},
|
|
170
|
+
s1: {
|
|
171
|
+
type: "number",
|
|
172
|
+
description: "head shift target angle (optional)"
|
|
173
|
+
},
|
|
174
|
+
s2: {
|
|
175
|
+
type: "number",
|
|
176
|
+
description: "head roll target angle (optional)"
|
|
177
|
+
},
|
|
178
|
+
s3: {
|
|
179
|
+
type: "number",
|
|
180
|
+
description: "head pitch target angle (optional)"
|
|
181
|
+
},
|
|
182
|
+
s4: {
|
|
183
|
+
type: "number",
|
|
184
|
+
description: "body rotate target angle (optional)"
|
|
185
|
+
},
|
|
186
|
+
s5: {
|
|
187
|
+
type: "number",
|
|
188
|
+
description: "head lift target angle (optional)"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
required: ["within_ms"]
|
|
158
192
|
}
|
|
159
193
|
},
|
|
160
194
|
{
|
|
@@ -176,82 +210,111 @@ var v = [
|
|
|
176
210
|
type: "object",
|
|
177
211
|
properties: {}
|
|
178
212
|
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "run_script",
|
|
216
|
+
description: "execute a multi-line movement script — one command per line; blank lines and lines starting with # are ignored; commands: home | shift-to <deg> | roll-to <deg> | pitch-to <deg> | rotate-to <deg> | lift-to <deg> | move [shift-to <deg>] [roll-to <deg>] [pitch-to <deg>] [rotate-to <deg>] [lift-to <deg>] | wait <ms>",
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: { script: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "multi-line movement script"
|
|
222
|
+
} },
|
|
223
|
+
required: ["script"]
|
|
224
|
+
}
|
|
179
225
|
}
|
|
180
226
|
];
|
|
181
|
-
async function
|
|
182
|
-
return await (await
|
|
227
|
+
async function b() {
|
|
228
|
+
return await (await h()).home(), "all servos moved to home positions";
|
|
183
229
|
}
|
|
184
|
-
async function
|
|
230
|
+
async function x(e) {
|
|
185
231
|
let t = {};
|
|
186
232
|
if (e.shift_to != null && (t.s1 = Number(e.shift_to)), e.roll_to != null && (t.s2 = Number(e.roll_to)), e.pitch_to != null && (t.s3 = Number(e.pitch_to)), e.rotate_to != null && (t.s4 = Number(e.rotate_to)), e.lift_to != null && (t.s5 = Number(e.lift_to)), Object.keys(t).length === 0) throw Error("move: at least one of shift_to, roll_to, pitch_to, rotate_to, lift_to is required");
|
|
187
|
-
let n = await
|
|
233
|
+
let n = await h();
|
|
188
234
|
return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
|
|
189
235
|
}
|
|
190
|
-
async function
|
|
191
|
-
let t = Number(e.
|
|
236
|
+
async function S(e) {
|
|
237
|
+
let t = Number(e.angle), n = await h();
|
|
192
238
|
return n.State = { s1: t }, await n.sendServoState(), `s1 (shift) → ${t}°`;
|
|
193
239
|
}
|
|
194
|
-
async function
|
|
195
|
-
let t = Number(e.
|
|
240
|
+
async function C(e) {
|
|
241
|
+
let t = Number(e.angle), n = await h();
|
|
196
242
|
return n.State = { s2: t }, await n.sendServoState(), `s2 (roll) → ${t}°`;
|
|
197
243
|
}
|
|
198
|
-
async function
|
|
199
|
-
let t = Number(e.
|
|
244
|
+
async function w(e) {
|
|
245
|
+
let t = Number(e.angle), n = await h();
|
|
200
246
|
return n.State = { s3: t }, await n.sendServoState(), `s3 (pitch) → ${t}°`;
|
|
201
247
|
}
|
|
202
|
-
async function
|
|
203
|
-
let t = Number(e.
|
|
248
|
+
async function T(e) {
|
|
249
|
+
let t = Number(e.angle), n = await h();
|
|
204
250
|
return n.State = { s4: t }, await n.sendServoState(), `s4 (rotate) → ${t}°`;
|
|
205
251
|
}
|
|
206
|
-
async function
|
|
207
|
-
let t = Number(e.
|
|
252
|
+
async function E(e) {
|
|
253
|
+
let t = Number(e.angle), n = await h();
|
|
208
254
|
return n.State = { s5: t }, await n.sendServoState(), `s5 (lift) → ${t}°`;
|
|
209
255
|
}
|
|
210
|
-
async function
|
|
256
|
+
async function D(e) {
|
|
257
|
+
let t = Number(e.within_ms);
|
|
258
|
+
if (isNaN(t) || t <= 0) throw Error("move_to: within_ms must be a positive number");
|
|
259
|
+
let n = {};
|
|
260
|
+
if (e.s1 != null && (n.s1 = Number(e.s1)), e.s2 != null && (n.s2 = Number(e.s2)), e.s3 != null && (n.s3 = Number(e.s3)), e.s4 != null && (n.s4 = Number(e.s4)), e.s5 != null && (n.s5 = Number(e.s5)), Object.keys(n).length === 0) throw Error("move_to: at least one servo target (s1–s5) must be specified");
|
|
261
|
+
return await (await h()).moveTo(n, t), "move completed";
|
|
262
|
+
}
|
|
263
|
+
async function O(e) {
|
|
211
264
|
let t = Number(e.ms);
|
|
212
265
|
if (isNaN(t) || t < 0) throw Error(`wait: invalid duration '${e.ms}' — expected a non-negative number`);
|
|
213
266
|
return await new Promise((e) => setTimeout(e, t)), `waited ${t} ms`;
|
|
214
267
|
}
|
|
215
|
-
async function
|
|
216
|
-
let e = await
|
|
268
|
+
async function k() {
|
|
269
|
+
let e = await h();
|
|
217
270
|
return JSON.stringify(e.State);
|
|
218
271
|
}
|
|
219
|
-
function
|
|
272
|
+
async function A(e) {
|
|
273
|
+
let t = String(e.script ?? "");
|
|
274
|
+
return await u(await h(), t), "script executed successfully";
|
|
275
|
+
}
|
|
276
|
+
function j() {
|
|
220
277
|
let e = new i({
|
|
221
278
|
name: "nova-control-mcp-server",
|
|
222
|
-
version: "0.0.
|
|
279
|
+
version: "0.0.5"
|
|
223
280
|
}, { capabilities: { tools: {} } });
|
|
224
|
-
return e.setRequestHandler(c, async () => ({ tools:
|
|
281
|
+
return e.setRequestHandler(c, async () => ({ tools: y })), e.setRequestHandler(s, async (e) => {
|
|
225
282
|
let t = e.params.name, n = e.params.arguments ?? {};
|
|
226
283
|
try {
|
|
227
284
|
let e;
|
|
228
285
|
switch (t) {
|
|
229
286
|
case "home":
|
|
230
|
-
e = await
|
|
287
|
+
e = await b();
|
|
231
288
|
break;
|
|
232
289
|
case "move":
|
|
233
|
-
e = await
|
|
290
|
+
e = await x(n);
|
|
234
291
|
break;
|
|
235
292
|
case "shift_to":
|
|
236
|
-
e = await
|
|
293
|
+
e = await S(n);
|
|
237
294
|
break;
|
|
238
295
|
case "roll_to":
|
|
239
|
-
e = await
|
|
296
|
+
e = await C(n);
|
|
240
297
|
break;
|
|
241
298
|
case "pitch_to":
|
|
242
|
-
e = await
|
|
299
|
+
e = await w(n);
|
|
243
300
|
break;
|
|
244
301
|
case "rotate_to":
|
|
245
|
-
e = await
|
|
302
|
+
e = await T(n);
|
|
246
303
|
break;
|
|
247
304
|
case "lift_to":
|
|
248
|
-
e = await
|
|
305
|
+
e = await E(n);
|
|
306
|
+
break;
|
|
307
|
+
case "move_to":
|
|
308
|
+
e = await D(n);
|
|
249
309
|
break;
|
|
250
310
|
case "wait":
|
|
251
|
-
e = await
|
|
311
|
+
e = await O(n);
|
|
252
312
|
break;
|
|
253
313
|
case "get_state":
|
|
254
|
-
e = await
|
|
314
|
+
e = await k();
|
|
315
|
+
break;
|
|
316
|
+
case "run_script":
|
|
317
|
+
e = await A(n);
|
|
255
318
|
break;
|
|
256
319
|
default: return {
|
|
257
320
|
content: [{
|
|
@@ -276,14 +339,14 @@ function O() {
|
|
|
276
339
|
}
|
|
277
340
|
}), e;
|
|
278
341
|
}
|
|
279
|
-
async function
|
|
342
|
+
async function M(e) {
|
|
280
343
|
let t = new a();
|
|
281
344
|
await e.connect(t);
|
|
282
345
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, () => {
|
|
283
|
-
|
|
346
|
+
g(), process.exit(0);
|
|
284
347
|
});
|
|
285
348
|
}
|
|
286
|
-
async function
|
|
349
|
+
async function N(e, t) {
|
|
287
350
|
let r = new o({ sessionIdGenerator: void 0 });
|
|
288
351
|
await e.connect(r);
|
|
289
352
|
let i = n(async (e, t) => {
|
|
@@ -295,17 +358,17 @@ async function A(e, t) {
|
|
|
295
358
|
}), i.once("error", n);
|
|
296
359
|
});
|
|
297
360
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, async () => {
|
|
298
|
-
await r.close(), i.close(),
|
|
361
|
+
await r.close(), i.close(), g(), process.exit(0);
|
|
299
362
|
});
|
|
300
363
|
}
|
|
301
|
-
async function
|
|
302
|
-
let { Port: e, BaudRate: t, Transport: n, ListenPort: r } =
|
|
303
|
-
|
|
304
|
-
let i =
|
|
305
|
-
n === "http" ? await
|
|
364
|
+
async function P() {
|
|
365
|
+
let { Port: e, BaudRate: t, Transport: n, ListenPort: r } = d();
|
|
366
|
+
f = e, p = t;
|
|
367
|
+
let i = j();
|
|
368
|
+
n === "http" ? await N(i, r) : await M(i);
|
|
306
369
|
}
|
|
307
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
370
|
+
t(process.argv[1]) === e(import.meta.url) && P().catch((e) => {
|
|
308
371
|
process.stderr.write(`nova-control-mcp: fatal: ${e.message ?? e}\n`), process.exit(1);
|
|
309
372
|
});
|
|
310
373
|
//#endregion
|
|
311
|
-
export {
|
|
374
|
+
export { v as _destroyForTests, _ as _setupForTests, j as createServer };
|