nova-control-mcp-server 0.0.5 → 0.0.6
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 +17 -13
- package/dist/nova-control-mcp-server.js +113 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,21 +113,24 @@ Point your MCP client at `http://<host>:3000/mcp`. For Claude Desktop with a rem
|
|
|
113
113
|
|
|
114
114
|
## Tools
|
|
115
115
|
|
|
116
|
-
The server exposes the following tools. The serial connection is opened lazily on the first tool call
|
|
116
|
+
The server exposes the following tools. The serial connection is opened lazily on the first tool call; call `disconnect` to free the port, or let the server exit cleanly.
|
|
117
|
+
|
|
118
|
+
All movement tools accept an optional `within_ms` parameter. When provided, the robot moves smoothly to the target using a trapezoidal velocity profile (ramp-up → constant speed → ramp-down) over the given number of milliseconds. Without `within_ms`, the servos move at constant maximum speed.
|
|
117
119
|
|
|
118
120
|
| tool | parameters | description |
|
|
119
121
|
|---|---|---|
|
|
120
|
-
| `home` |
|
|
121
|
-
| `move` | `shift_to?`, `roll_to?`, `pitch_to?`, `rotate_to?`, `lift_to?` (
|
|
122
|
-
| `move_to` | `within_ms: number` (required
|
|
123
|
-
| `shift_to` | `
|
|
124
|
-
| `roll_to` | `
|
|
125
|
-
| `pitch_to` | `
|
|
126
|
-
| `rotate_to` | `
|
|
127
|
-
| `lift_to` | `
|
|
122
|
+
| `home` | `within_ms?` | send all servos to their home positions |
|
|
123
|
+
| `move` | `shift_to?`, `roll_to?`, `pitch_to?`, `rotate_to?`, `lift_to?` (at least one required); `within_ms?` | set one or more servo positions atomically |
|
|
124
|
+
| `move_to` | `within_ms: number` (required, must be > 0); `s1?`, `s2?`, `s3?`, `s4?`, `s5?` | move one or more servos smoothly to target positions using a trapezoidal ramp-up/ramp-down profile |
|
|
125
|
+
| `shift_to` | `angle: number`; `within_ms?` | shift head forward (>90°) or back (<90°) — s1 |
|
|
126
|
+
| `roll_to` | `angle: number`; `within_ms?` | roll head clockwise (>90°) or counter-clockwise (<90°) — s2 |
|
|
127
|
+
| `pitch_to` | `angle: number`; `within_ms?` | pitch head up (>110°) or down (<110°) — s3 |
|
|
128
|
+
| `rotate_to` | `angle: number`; `within_ms?` | rotate body around Z-axis — s4 |
|
|
129
|
+
| `lift_to` | `angle: number`; `within_ms?` | lift head on secondary axis, range 20°–150° — s5 |
|
|
128
130
|
| `wait` | `ms: number` | pause for `ms` milliseconds before the next action |
|
|
129
131
|
| `get_state` | — | return current servo positions as a JSON object with keys `s1`–`s5` |
|
|
130
132
|
| `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`) |
|
|
133
|
+
| `disconnect` | — | close the serial connection to free the port; the connection reopens automatically on the next movement command |
|
|
131
134
|
|
|
132
135
|
### Servo mapping
|
|
133
136
|
|
|
@@ -143,12 +146,13 @@ The server exposes the following tools. The serial connection is opened lazily o
|
|
|
143
146
|
|
|
144
147
|
Once the server is running inside Claude Desktop you can give natural-language instructions like:
|
|
145
148
|
|
|
146
|
-
- *"Move NOVA's head to look straight up."* → `pitch_to(
|
|
147
|
-
- *"Rotate the body 45° to the right."* → `rotate_to(
|
|
148
|
-
- *"Move the head to 120° over 800 ms."* → `move_to(within_ms: 800, s1: 120)`
|
|
149
|
-
- *"Shift to 100
|
|
149
|
+
- *"Move NOVA's head to look straight up."* → `pitch_to(angle: 130)`
|
|
150
|
+
- *"Rotate the body 45° to the right."* → `rotate_to(angle: 45)`
|
|
151
|
+
- *"Move the head to 120° smoothly over 800 ms."* → `move_to(within_ms: 800, s1: 120)`
|
|
152
|
+
- *"Shift to 100° with a fluid ramp, wait half a second, then return to home."* → `shift_to(angle: 100, within_ms: 600)` + `wait(500)` + `home()`
|
|
150
153
|
- *"What is the current servo state?"* → `get_state()`
|
|
151
154
|
- *"Run the greeting sequence from this script."* → `run_script(script: "home\nwait 500\nshift-to 110\nwait 400\nhome")`
|
|
155
|
+
- *"Free the serial port."* → `disconnect()`
|
|
152
156
|
|
|
153
157
|
## Exit behaviour
|
|
154
158
|
|
|
@@ -62,18 +62,25 @@ function v() {
|
|
|
62
62
|
var y = [
|
|
63
63
|
{
|
|
64
64
|
name: "home",
|
|
65
|
-
description: "send all servos to their home positions",
|
|
65
|
+
description: "send all servos to their home positions — pass within_ms for smooth, fluid motion with automatic velocity ramp-up and ramp-down; without within_ms the robot moves at constant maximum speed",
|
|
66
66
|
inputSchema: {
|
|
67
67
|
type: "object",
|
|
68
|
-
properties: {
|
|
68
|
+
properties: { within_ms: {
|
|
69
|
+
type: "number",
|
|
70
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down — servos glide gradually to the target instead of jumping; omit for constant-speed movement"
|
|
71
|
+
} }
|
|
69
72
|
}
|
|
70
73
|
},
|
|
71
74
|
{
|
|
72
75
|
name: "move",
|
|
73
|
-
description: "set one or more servo positions atomically — at least one of shift_to, roll_to, pitch_to, rotate_to, lift_to is required",
|
|
76
|
+
description: "set one or more servo positions atomically — at least one of shift_to, roll_to, pitch_to, rotate_to, lift_to is required; pass within_ms for smooth, fluid motion with automatic velocity ramp-up and ramp-down",
|
|
74
77
|
inputSchema: {
|
|
75
78
|
type: "object",
|
|
76
79
|
properties: {
|
|
80
|
+
within_ms: {
|
|
81
|
+
type: "number",
|
|
82
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down — servos glide gradually to their targets instead of jumping; omit for constant-speed movement"
|
|
83
|
+
},
|
|
77
84
|
shift_to: {
|
|
78
85
|
type: "number",
|
|
79
86
|
description: "shift head forward (>90°) or back (<90°) — s1"
|
|
@@ -99,61 +106,91 @@ var y = [
|
|
|
99
106
|
},
|
|
100
107
|
{
|
|
101
108
|
name: "shift_to",
|
|
102
|
-
description: "shift head forward (>90°) or back (<90°) — s1",
|
|
109
|
+
description: "shift head forward (>90°) or back (<90°) — s1; pass within_ms for smooth motion with trapezoidal velocity profile (ramp-up → constant speed → ramp-down)",
|
|
103
110
|
inputSchema: {
|
|
104
111
|
type: "object",
|
|
105
|
-
properties: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
properties: {
|
|
113
|
+
angle: {
|
|
114
|
+
type: "number",
|
|
115
|
+
description: "target angle in degrees"
|
|
116
|
+
},
|
|
117
|
+
within_ms: {
|
|
118
|
+
type: "number",
|
|
119
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
109
122
|
required: ["angle"]
|
|
110
123
|
}
|
|
111
124
|
},
|
|
112
125
|
{
|
|
113
126
|
name: "roll_to",
|
|
114
|
-
description: "roll head clockwise (>90°) or counter-clockwise (<90°) — s2",
|
|
127
|
+
description: "roll head clockwise (>90°) or counter-clockwise (<90°) — s2; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
115
128
|
inputSchema: {
|
|
116
129
|
type: "object",
|
|
117
|
-
properties: {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
properties: {
|
|
131
|
+
angle: {
|
|
132
|
+
type: "number",
|
|
133
|
+
description: "target angle in degrees"
|
|
134
|
+
},
|
|
135
|
+
within_ms: {
|
|
136
|
+
type: "number",
|
|
137
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
121
140
|
required: ["angle"]
|
|
122
141
|
}
|
|
123
142
|
},
|
|
124
143
|
{
|
|
125
144
|
name: "pitch_to",
|
|
126
|
-
description: "pitch head up (>110°) or down (<110°) — s3",
|
|
145
|
+
description: "pitch head up (>110°) or down (<110°) — s3; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
127
146
|
inputSchema: {
|
|
128
147
|
type: "object",
|
|
129
|
-
properties: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
properties: {
|
|
149
|
+
angle: {
|
|
150
|
+
type: "number",
|
|
151
|
+
description: "target angle in degrees"
|
|
152
|
+
},
|
|
153
|
+
within_ms: {
|
|
154
|
+
type: "number",
|
|
155
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
133
158
|
required: ["angle"]
|
|
134
159
|
}
|
|
135
160
|
},
|
|
136
161
|
{
|
|
137
162
|
name: "rotate_to",
|
|
138
|
-
description: "rotate body around Z-axis — s4",
|
|
163
|
+
description: "rotate body around Z-axis — s4; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
139
164
|
inputSchema: {
|
|
140
165
|
type: "object",
|
|
141
|
-
properties: {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
properties: {
|
|
167
|
+
angle: {
|
|
168
|
+
type: "number",
|
|
169
|
+
description: "target angle in degrees"
|
|
170
|
+
},
|
|
171
|
+
within_ms: {
|
|
172
|
+
type: "number",
|
|
173
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
145
176
|
required: ["angle"]
|
|
146
177
|
}
|
|
147
178
|
},
|
|
148
179
|
{
|
|
149
180
|
name: "lift_to",
|
|
150
|
-
description: "lift head on secondary axis, range 20°–150° — s5",
|
|
181
|
+
description: "lift head on secondary axis, range 20°–150° — s5; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
151
182
|
inputSchema: {
|
|
152
183
|
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
properties: {
|
|
185
|
+
angle: {
|
|
186
|
+
type: "number",
|
|
187
|
+
description: "target angle in degrees"
|
|
188
|
+
},
|
|
189
|
+
within_ms: {
|
|
190
|
+
type: "number",
|
|
191
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
157
194
|
required: ["angle"]
|
|
158
195
|
}
|
|
159
196
|
},
|
|
@@ -213,7 +250,7 @@ var y = [
|
|
|
213
250
|
},
|
|
214
251
|
{
|
|
215
252
|
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 <
|
|
253
|
+
description: "execute a multi-line movement script — one command per line; blank lines and lines starting with # are ignored; commands: home | shift-to <angle> | roll-to <angle> | pitch-to <angle> | rotate-to <angle> | lift-to <angle> | move [shift-to <angle>] [roll-to <angle>] [pitch-to <angle>] [rotate-to <angle>] [lift-to <angle>] | wait <ms>",
|
|
217
254
|
inputSchema: {
|
|
218
255
|
type: "object",
|
|
219
256
|
properties: { script: {
|
|
@@ -222,61 +259,73 @@ var y = [
|
|
|
222
259
|
} },
|
|
223
260
|
required: ["script"]
|
|
224
261
|
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "disconnect",
|
|
265
|
+
description: "close the serial connection to the robot — call this when you are done to free the serial port; the connection reopens automatically on the first subsequent movement command",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {}
|
|
269
|
+
}
|
|
225
270
|
}
|
|
226
271
|
];
|
|
227
|
-
async function b() {
|
|
228
|
-
|
|
272
|
+
async function b(e) {
|
|
273
|
+
let t = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
274
|
+
return await (await h()).home(t), "all servos moved to home positions";
|
|
229
275
|
}
|
|
230
276
|
async function x(e) {
|
|
231
277
|
let t = {};
|
|
232
278
|
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");
|
|
233
|
-
let n =
|
|
234
|
-
return
|
|
279
|
+
let n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
280
|
+
return await (await h()).moveTo(t, n), `servos updated: ${JSON.stringify(t)}`;
|
|
235
281
|
}
|
|
236
282
|
async function S(e) {
|
|
237
|
-
let t = Number(e.angle), n =
|
|
238
|
-
return
|
|
283
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
284
|
+
return await (await h()).shiftHeadTo(t, n), `s1 (shift) → ${t}°`;
|
|
239
285
|
}
|
|
240
286
|
async function C(e) {
|
|
241
|
-
let t = Number(e.angle), n =
|
|
242
|
-
return
|
|
287
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
288
|
+
return await (await h()).rollHeadTo(t, n), `s2 (roll) → ${t}°`;
|
|
243
289
|
}
|
|
244
290
|
async function w(e) {
|
|
245
|
-
let t = Number(e.angle), n =
|
|
246
|
-
return
|
|
291
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
292
|
+
return await (await h()).pitchHeadTo(t, n), `s3 (pitch) → ${t}°`;
|
|
247
293
|
}
|
|
248
294
|
async function T(e) {
|
|
249
|
-
let t = Number(e.angle), n =
|
|
250
|
-
return
|
|
295
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
296
|
+
return await (await h()).rotateBodyTo(t, n), `s4 (rotate) → ${t}°`;
|
|
251
297
|
}
|
|
252
298
|
async function E(e) {
|
|
253
|
-
let t = Number(e.angle), n =
|
|
254
|
-
return
|
|
299
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
300
|
+
return await (await h()).liftHeadTo(t, n), `s5 (lift) → ${t}°`;
|
|
301
|
+
}
|
|
302
|
+
async function D() {
|
|
303
|
+
return m == null ? "not connected" : (g(), "disconnected");
|
|
255
304
|
}
|
|
256
|
-
async function
|
|
305
|
+
async function O(e) {
|
|
257
306
|
let t = Number(e.within_ms);
|
|
258
307
|
if (isNaN(t) || t <= 0) throw Error("move_to: within_ms must be a positive number");
|
|
259
308
|
let n = {};
|
|
260
309
|
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
310
|
return await (await h()).moveTo(n, t), "move completed";
|
|
262
311
|
}
|
|
263
|
-
async function
|
|
312
|
+
async function k(e) {
|
|
264
313
|
let t = Number(e.ms);
|
|
265
314
|
if (isNaN(t) || t < 0) throw Error(`wait: invalid duration '${e.ms}' — expected a non-negative number`);
|
|
266
315
|
return await new Promise((e) => setTimeout(e, t)), `waited ${t} ms`;
|
|
267
316
|
}
|
|
268
|
-
async function
|
|
317
|
+
async function A() {
|
|
269
318
|
let e = await h();
|
|
270
319
|
return JSON.stringify(e.State);
|
|
271
320
|
}
|
|
272
|
-
async function
|
|
321
|
+
async function j(e) {
|
|
273
322
|
let t = String(e.script ?? "");
|
|
274
323
|
return await u(await h(), t), "script executed successfully";
|
|
275
324
|
}
|
|
276
|
-
function
|
|
325
|
+
function M() {
|
|
277
326
|
let e = new i({
|
|
278
327
|
name: "nova-control-mcp-server",
|
|
279
|
-
version: "0.0.
|
|
328
|
+
version: "0.0.6"
|
|
280
329
|
}, { capabilities: { tools: {} } });
|
|
281
330
|
return e.setRequestHandler(c, async () => ({ tools: y })), e.setRequestHandler(s, async (e) => {
|
|
282
331
|
let t = e.params.name, n = e.params.arguments ?? {};
|
|
@@ -284,7 +333,7 @@ function j() {
|
|
|
284
333
|
let e;
|
|
285
334
|
switch (t) {
|
|
286
335
|
case "home":
|
|
287
|
-
e = await b();
|
|
336
|
+
e = await b(n);
|
|
288
337
|
break;
|
|
289
338
|
case "move":
|
|
290
339
|
e = await x(n);
|
|
@@ -305,16 +354,19 @@ function j() {
|
|
|
305
354
|
e = await E(n);
|
|
306
355
|
break;
|
|
307
356
|
case "move_to":
|
|
308
|
-
e = await
|
|
357
|
+
e = await O(n);
|
|
309
358
|
break;
|
|
310
359
|
case "wait":
|
|
311
|
-
e = await
|
|
360
|
+
e = await k(n);
|
|
312
361
|
break;
|
|
313
362
|
case "get_state":
|
|
314
|
-
e = await
|
|
363
|
+
e = await A();
|
|
315
364
|
break;
|
|
316
365
|
case "run_script":
|
|
317
|
-
e = await
|
|
366
|
+
e = await j(n);
|
|
367
|
+
break;
|
|
368
|
+
case "disconnect":
|
|
369
|
+
e = await D();
|
|
318
370
|
break;
|
|
319
371
|
default: return {
|
|
320
372
|
content: [{
|
|
@@ -339,14 +391,14 @@ function j() {
|
|
|
339
391
|
}
|
|
340
392
|
}), e;
|
|
341
393
|
}
|
|
342
|
-
async function
|
|
394
|
+
async function N(e) {
|
|
343
395
|
let t = new a();
|
|
344
396
|
await e.connect(t);
|
|
345
397
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, () => {
|
|
346
398
|
g(), process.exit(0);
|
|
347
399
|
});
|
|
348
400
|
}
|
|
349
|
-
async function
|
|
401
|
+
async function P(e, t) {
|
|
350
402
|
let r = new o({ sessionIdGenerator: void 0 });
|
|
351
403
|
await e.connect(r);
|
|
352
404
|
let i = n(async (e, t) => {
|
|
@@ -361,14 +413,14 @@ async function N(e, t) {
|
|
|
361
413
|
await r.close(), i.close(), g(), process.exit(0);
|
|
362
414
|
});
|
|
363
415
|
}
|
|
364
|
-
async function
|
|
416
|
+
async function F() {
|
|
365
417
|
let { Port: e, BaudRate: t, Transport: n, ListenPort: r } = d();
|
|
366
418
|
f = e, p = t;
|
|
367
|
-
let i =
|
|
368
|
-
n === "http" ? await
|
|
419
|
+
let i = M();
|
|
420
|
+
n === "http" ? await P(i, r) : await N(i);
|
|
369
421
|
}
|
|
370
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
422
|
+
t(process.argv[1]) === e(import.meta.url) && F().catch((e) => {
|
|
371
423
|
process.stderr.write(`nova-control-mcp: fatal: ${e.message ?? e}\n`), process.exit(1);
|
|
372
424
|
});
|
|
373
425
|
//#endregion
|
|
374
|
-
export { v as _destroyForTests, _ as _setupForTests,
|
|
426
|
+
export { v as _destroyForTests, _ as _setupForTests, M as createServer };
|