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 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 and kept alive for the lifetime of the server process.
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` | | send all servos to their home positions |
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 |
123
- | `shift_to` | `deg: number` | shift head forward (>90°) or back (<90°) — s1 |
124
- | `roll_to` | `deg: number` | roll head clockwise (>90°) or counter-clockwise (<90°) — s2 |
125
- | `pitch_to` | `deg: number` | pitch head up (>110°) or down (<110°) — s3 |
126
- | `rotate_to` | `deg: number` | rotate body around Z-axis — s4 |
127
- | `lift_to` | `deg: number` | lift head on secondary axis, range 20°–150° — s5 |
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(deg: 130)`
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)`
149
- - *"Shift to 100°, wait half a second, then return to home."* → `shift_to(100)` + `wait(500)` + `home()`
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: { angle: {
106
- type: "number",
107
- description: "target angle in degrees"
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: { angle: {
118
- type: "number",
119
- description: "target angle in degrees"
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: { angle: {
130
- type: "number",
131
- description: "target angle in degrees"
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: { angle: {
142
- type: "number",
143
- description: "target angle in degrees"
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: { angle: {
154
- type: "number",
155
- description: "target angle in degrees"
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 <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>",
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
- return await (await h()).home(), "all servos moved to home positions";
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 = await h();
234
- return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
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 = await h();
238
- return n.State = { s1: t }, await n.sendServoState(), `s1 (shift) → ${t}°`;
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 = await h();
242
- return n.State = { s2: t }, await n.sendServoState(), `s2 (roll) → ${t}°`;
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 = await h();
246
- return n.State = { s3: t }, await n.sendServoState(), `s3 (pitch) → ${t}°`;
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 = await h();
250
- return n.State = { s4: t }, await n.sendServoState(), `s4 (rotate) → ${t}°`;
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 = await h();
254
- return n.State = { s5: t }, await n.sendServoState(), `s5 (lift) → ${t}°`;
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 D(e) {
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 O(e) {
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 k() {
317
+ async function A() {
269
318
  let e = await h();
270
319
  return JSON.stringify(e.State);
271
320
  }
272
- async function A(e) {
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 j() {
325
+ function M() {
277
326
  let e = new i({
278
327
  name: "nova-control-mcp-server",
279
- version: "0.0.5"
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 D(n);
357
+ e = await O(n);
309
358
  break;
310
359
  case "wait":
311
- e = await O(n);
360
+ e = await k(n);
312
361
  break;
313
362
  case "get_state":
314
- e = await k();
363
+ e = await A();
315
364
  break;
316
365
  case "run_script":
317
- e = await A(n);
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 M(e) {
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 N(e, t) {
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 P() {
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 = j();
368
- n === "http" ? await N(i, r) : await M(i);
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) && P().catch((e) => {
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, j as createServer };
426
+ export { v as _destroyForTests, _ as _setupForTests, M as createServer };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nova-control-mcp-server",
3
3
  "description": "MCP server for controlling a NOVA DIY Artificial Intelligence Robot by Creoqode",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "nova",