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 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 left."* → `rotate_to(deg: 45)`
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 u() {
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 d = "", f = 9600, p;
50
- async function m() {
51
- return p ??= await l(d, f), p;
49
+ var f = "", p = 9600, m;
50
+ async function h() {
51
+ return m ??= await l(f, p), m;
52
52
  }
53
- function h() {
54
- p != null && (p.destroy(), p = void 0);
53
+ function g() {
54
+ m != null && (m.destroy(), m = void 0);
55
55
  }
56
- function g(e, t = 9600) {
57
- d = e, f = t;
56
+ function _(e, t = 9600) {
57
+ f = e, p = t;
58
58
  }
59
- function _() {
60
- h(), d = "", f = 9600;
59
+ function v() {
60
+ g(), f = "", p = 9600;
61
61
  }
62
- var v = [
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: { deg: {
105
+ properties: { angle: {
106
106
  type: "number",
107
107
  description: "target angle in degrees"
108
108
  } },
109
- required: ["deg"]
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: { deg: {
117
+ properties: { angle: {
118
118
  type: "number",
119
119
  description: "target angle in degrees"
120
120
  } },
121
- required: ["deg"]
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: { deg: {
129
+ properties: { angle: {
130
130
  type: "number",
131
131
  description: "target angle in degrees"
132
132
  } },
133
- required: ["deg"]
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: { deg: {
141
+ properties: { angle: {
142
142
  type: "number",
143
143
  description: "target angle in degrees"
144
144
  } },
145
- required: ["deg"]
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: { deg: {
153
+ properties: { angle: {
154
154
  type: "number",
155
155
  description: "target angle in degrees"
156
156
  } },
157
- required: ["deg"]
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 y() {
182
- return await (await m()).home(), "all servos moved to home positions";
227
+ async function b() {
228
+ return await (await h()).home(), "all servos moved to home positions";
183
229
  }
184
- async function b(e) {
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 m();
233
+ let n = await h();
188
234
  return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
189
235
  }
190
- async function x(e) {
191
- let t = Number(e.deg), n = await m();
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 S(e) {
195
- let t = Number(e.deg), n = await m();
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 C(e) {
199
- let t = Number(e.deg), n = await m();
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 w(e) {
203
- let t = Number(e.deg), n = await m();
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 T(e) {
207
- let t = Number(e.deg), n = await m();
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 E(e) {
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 D() {
216
- let e = await m();
268
+ async function k() {
269
+ let e = await h();
217
270
  return JSON.stringify(e.State);
218
271
  }
219
- function O() {
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.4"
279
+ version: "0.0.5"
223
280
  }, { capabilities: { tools: {} } });
224
- return e.setRequestHandler(c, async () => ({ tools: v })), e.setRequestHandler(s, async (e) => {
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 y();
287
+ e = await b();
231
288
  break;
232
289
  case "move":
233
- e = await b(n);
290
+ e = await x(n);
234
291
  break;
235
292
  case "shift_to":
236
- e = await x(n);
293
+ e = await S(n);
237
294
  break;
238
295
  case "roll_to":
239
- e = await S(n);
296
+ e = await C(n);
240
297
  break;
241
298
  case "pitch_to":
242
- e = await C(n);
299
+ e = await w(n);
243
300
  break;
244
301
  case "rotate_to":
245
- e = await w(n);
302
+ e = await T(n);
246
303
  break;
247
304
  case "lift_to":
248
- e = await T(n);
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 E(n);
311
+ e = await O(n);
252
312
  break;
253
313
  case "get_state":
254
- e = await D();
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 k(e) {
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
- h(), process.exit(0);
346
+ g(), process.exit(0);
284
347
  });
285
348
  }
286
- async function A(e, t) {
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(), h(), process.exit(0);
361
+ await r.close(), i.close(), g(), process.exit(0);
299
362
  });
300
363
  }
301
- async function j() {
302
- let { Port: e, BaudRate: t, Transport: n, ListenPort: r } = u();
303
- d = e, f = t;
304
- let i = O();
305
- n === "http" ? await A(i, r) : await k(i);
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) && j().catch((e) => {
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 { _ as _destroyForTests, g as _setupForTests, O as createServer };
374
+ export { v as _destroyForTests, _ as _setupForTests, j 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.4",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "nova",