nova-control-mcp-server 0.0.5 → 0.0.7
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 +375 -93
- 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
|
|
|
@@ -7,9 +7,239 @@ 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 {
|
|
10
|
+
import { SerialPort as l } from "serialport";
|
|
11
|
+
//#region ../nova-control-node/dist/nova-control-node.js
|
|
12
|
+
var u = 9600, d = Object.freeze({
|
|
13
|
+
s1: 90,
|
|
14
|
+
s2: 90,
|
|
15
|
+
s3: 110,
|
|
16
|
+
s4: 90,
|
|
17
|
+
s5: 95
|
|
18
|
+
}), f = Object.freeze({
|
|
19
|
+
s1: [45, 135],
|
|
20
|
+
s2: [10, 170],
|
|
21
|
+
s3: [40, 150],
|
|
22
|
+
s4: [30, 180],
|
|
23
|
+
s5: [20, 150]
|
|
24
|
+
}), p = Object.freeze({
|
|
25
|
+
s1: (f.s1[1] - f.s1[0]) / 1e3,
|
|
26
|
+
s2: (f.s2[1] - f.s2[0]) / 1e3,
|
|
27
|
+
s3: (f.s3[1] - f.s3[0]) / 1e3,
|
|
28
|
+
s4: (f.s4[1] - f.s4[0]) / 1e3,
|
|
29
|
+
s5: (f.s5[1] - f.s5[0]) / 1e3
|
|
30
|
+
});
|
|
31
|
+
function m(e, t) {
|
|
32
|
+
let [n, r] = f[t];
|
|
33
|
+
return Math.max(n, Math.min(r, Math.round(e)));
|
|
34
|
+
}
|
|
35
|
+
function h(e) {
|
|
36
|
+
return new Uint8Array([
|
|
37
|
+
m(e.s4, "s4"),
|
|
38
|
+
m(e.s3, "s3"),
|
|
39
|
+
m(e.s2, "s2"),
|
|
40
|
+
m(e.s1, "s1"),
|
|
41
|
+
m(e.s5, "s5")
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
async function g(e, t) {
|
|
45
|
+
let n = new l({
|
|
46
|
+
path: e,
|
|
47
|
+
baudRate: t,
|
|
48
|
+
autoOpen: !1
|
|
49
|
+
});
|
|
50
|
+
return await new Promise((e, t) => {
|
|
51
|
+
n.open((n) => {
|
|
52
|
+
n == null ? e() : t(n);
|
|
53
|
+
});
|
|
54
|
+
}), await new Promise((e) => setTimeout(e, 2e3)), {
|
|
55
|
+
async write(e) {
|
|
56
|
+
await new Promise((t, r) => {
|
|
57
|
+
n.write(Buffer.from(e), (e) => {
|
|
58
|
+
e == null ? t() : r(e);
|
|
59
|
+
});
|
|
60
|
+
}), await new Promise((e, t) => {
|
|
61
|
+
n.drain((n) => {
|
|
62
|
+
n == null ? e() : t(n);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
destroy() {
|
|
67
|
+
n.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function _(e, t) {
|
|
72
|
+
let n = Math.min(.499, Math.max(0, t)), r = 1 / (1 - n);
|
|
73
|
+
if (e <= n) return r * e * e / (2 * n);
|
|
74
|
+
if (e <= 1 - n) return r * (e - n / 2);
|
|
75
|
+
let i = 1 - e;
|
|
76
|
+
return 1 - r * i * i / (2 * n);
|
|
77
|
+
}
|
|
78
|
+
async function v(e, t = u, n) {
|
|
79
|
+
let r = n?.StepIntervalMs ?? 20, i = n?.RampRatio ?? .25, a = await g(e, t), o = { ...d }, s, c = Promise.resolve();
|
|
80
|
+
function l(e) {
|
|
81
|
+
s = {
|
|
82
|
+
...s ?? o,
|
|
83
|
+
...e
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function f() {
|
|
87
|
+
let e = c;
|
|
88
|
+
c = (async () => {
|
|
89
|
+
try {
|
|
90
|
+
await e;
|
|
91
|
+
} catch {}
|
|
92
|
+
for (; s != null;) {
|
|
93
|
+
let e = s, t = !0, n = { ...o };
|
|
94
|
+
for (let i of [
|
|
95
|
+
"s1",
|
|
96
|
+
"s2",
|
|
97
|
+
"s3",
|
|
98
|
+
"s4",
|
|
99
|
+
"s5"
|
|
100
|
+
]) {
|
|
101
|
+
let a = e[i] - o[i], s = r > 0 ? p[i] * r : Infinity;
|
|
102
|
+
Math.abs(a) > s ? (n[i] = o[i] + Math.sign(a) * s, t = !1) : n[i] = e[i];
|
|
103
|
+
}
|
|
104
|
+
t && (s = void 0), o = { ...n }, await a.write(h(n)), t || await new Promise((e) => setTimeout(e, r));
|
|
105
|
+
}
|
|
106
|
+
})(), await c;
|
|
107
|
+
}
|
|
108
|
+
async function m(e, t) {
|
|
109
|
+
let n = c;
|
|
110
|
+
c = (async () => {
|
|
111
|
+
try {
|
|
112
|
+
await n;
|
|
113
|
+
} catch {}
|
|
114
|
+
let c = { ...o }, l = r > 0 ? Math.max(1, Math.round(t / r)) : 1;
|
|
115
|
+
s = void 0;
|
|
116
|
+
for (let t = 1; t <= l; t++) {
|
|
117
|
+
let n = _(t / l, i), s = { ...o };
|
|
118
|
+
for (let t of Object.keys(e)) s[t] = Math.round(c[t] + (e[t] - c[t]) * n);
|
|
119
|
+
o = s, await a.write(h(s)), t < l && await new Promise((e) => setTimeout(e, r));
|
|
120
|
+
}
|
|
121
|
+
})(), await c;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
async home(e) {
|
|
125
|
+
e != null && e > 0 ? await m({ ...d }, e) : (l({ ...d }), await f());
|
|
126
|
+
},
|
|
127
|
+
async shiftHeadTo(e, t) {
|
|
128
|
+
t != null && t > 0 ? await m({ s1: e }, t) : (l({ s1: e }), await f());
|
|
129
|
+
},
|
|
130
|
+
async rollHeadTo(e, t) {
|
|
131
|
+
t != null && t > 0 ? await m({ s2: e }, t) : (l({ s2: e }), await f());
|
|
132
|
+
},
|
|
133
|
+
async pitchHeadTo(e, t) {
|
|
134
|
+
t != null && t > 0 ? await m({ s3: e }, t) : (l({ s3: e }), await f());
|
|
135
|
+
},
|
|
136
|
+
async liftHeadTo(e, t) {
|
|
137
|
+
t != null && t > 0 ? await m({ s5: e }, t) : (l({ s5: e }), await f());
|
|
138
|
+
},
|
|
139
|
+
async rotateBodyTo(e, t) {
|
|
140
|
+
t != null && t > 0 ? await m({ s4: e }, t) : (l({ s4: e }), await f());
|
|
141
|
+
},
|
|
142
|
+
async moveTo(e, t) {
|
|
143
|
+
t != null && t > 0 ? await m(e, t) : (l(e), await f());
|
|
144
|
+
},
|
|
145
|
+
get State() {
|
|
146
|
+
return structuredClone(s ?? o);
|
|
147
|
+
},
|
|
148
|
+
set State(e) {
|
|
149
|
+
s = {
|
|
150
|
+
...o,
|
|
151
|
+
...e
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
async sendServoState() {
|
|
155
|
+
await f();
|
|
156
|
+
},
|
|
157
|
+
destroy() {
|
|
158
|
+
a.destroy();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function y(e, t) {
|
|
163
|
+
let n = t.split("\n");
|
|
164
|
+
for (let t = 0; t < n.length; t++) {
|
|
165
|
+
let r = n[t].trim(), i = t + 1;
|
|
166
|
+
if (r === "" || r.startsWith("#")) continue;
|
|
167
|
+
let a = r.split(/\s+/), o = a[0].toLowerCase();
|
|
168
|
+
switch (!0) {
|
|
169
|
+
case o === "home":
|
|
170
|
+
await e.home();
|
|
171
|
+
break;
|
|
172
|
+
case o === "shift-to": {
|
|
173
|
+
let t = Number(a[1]);
|
|
174
|
+
if (isNaN(t)) throw Error(`line ${i}: shift-to requires a numeric angle, got '${a[1]}'`);
|
|
175
|
+
await e.shiftHeadTo(t);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case o === "roll-to": {
|
|
179
|
+
let t = Number(a[1]);
|
|
180
|
+
if (isNaN(t)) throw Error(`line ${i}: roll-to requires a numeric angle, got '${a[1]}'`);
|
|
181
|
+
await e.rollHeadTo(t);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case o === "pitch-to": {
|
|
185
|
+
let t = Number(a[1]);
|
|
186
|
+
if (isNaN(t)) throw Error(`line ${i}: pitch-to requires a numeric angle, got '${a[1]}'`);
|
|
187
|
+
await e.pitchHeadTo(t);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case o === "rotate-to": {
|
|
191
|
+
let t = Number(a[1]);
|
|
192
|
+
if (isNaN(t)) throw Error(`line ${i}: rotate-to requires a numeric angle, got '${a[1]}'`);
|
|
193
|
+
await e.rotateBodyTo(t);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case o === "lift-to": {
|
|
197
|
+
let t = Number(a[1]);
|
|
198
|
+
if (isNaN(t)) throw Error(`line ${i}: lift-to requires a numeric angle, got '${a[1]}'`);
|
|
199
|
+
await e.liftHeadTo(t);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case o === "move": {
|
|
203
|
+
let t = {};
|
|
204
|
+
for (let e = 1; e < a.length; e += 2) {
|
|
205
|
+
let n = a[e].toLowerCase(), r = Number(a[e + 1]);
|
|
206
|
+
if (isNaN(r)) throw Error(`line ${i}: '${n}' requires a numeric angle, got '${a[e + 1]}'`);
|
|
207
|
+
switch (n) {
|
|
208
|
+
case "shift-to":
|
|
209
|
+
t.s1 = r;
|
|
210
|
+
break;
|
|
211
|
+
case "roll-to":
|
|
212
|
+
t.s2 = r;
|
|
213
|
+
break;
|
|
214
|
+
case "pitch-to":
|
|
215
|
+
t.s3 = r;
|
|
216
|
+
break;
|
|
217
|
+
case "rotate-to":
|
|
218
|
+
t.s4 = r;
|
|
219
|
+
break;
|
|
220
|
+
case "lift-to":
|
|
221
|
+
t.s5 = r;
|
|
222
|
+
break;
|
|
223
|
+
default: throw Error(`line ${i}: unknown move argument '${n}'`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (Object.keys(t).length === 0) throw Error(`line ${i}: move requires at least one servo argument`);
|
|
227
|
+
e.State = t, await e.sendServoState();
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case o === "wait": {
|
|
231
|
+
let e = Number(a[1]);
|
|
232
|
+
if (isNaN(e) || e < 0) throw Error(`line ${i}: wait requires a non-negative number in ms, got '${a[1]}'`);
|
|
233
|
+
await new Promise((t) => setTimeout(t, e));
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
default: throw Error(`line ${i}: unknown command '${o}'`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
11
241
|
//#region src/nova-control-mcp-server.ts
|
|
12
|
-
function
|
|
242
|
+
function b() {
|
|
13
243
|
try {
|
|
14
244
|
let { values: e } = r({
|
|
15
245
|
args: process.argv.slice(2),
|
|
@@ -46,34 +276,41 @@ function d() {
|
|
|
46
276
|
process.stderr.write(`nova-control-mcp: ${e.message ?? e}\n`), process.exit(1);
|
|
47
277
|
}
|
|
48
278
|
}
|
|
49
|
-
var
|
|
50
|
-
async function
|
|
51
|
-
return
|
|
279
|
+
var x = "", S = 9600, C;
|
|
280
|
+
async function w() {
|
|
281
|
+
return C ??= await v(x, S), C;
|
|
52
282
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
283
|
+
function T() {
|
|
284
|
+
C != null && (C.destroy(), C = void 0);
|
|
55
285
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
286
|
+
function E(e, t = 9600) {
|
|
287
|
+
x = e, S = t;
|
|
58
288
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
289
|
+
function D() {
|
|
290
|
+
T(), x = "", S = 9600;
|
|
61
291
|
}
|
|
62
|
-
var
|
|
292
|
+
var O = [
|
|
63
293
|
{
|
|
64
294
|
name: "home",
|
|
65
|
-
description: "send all servos to their home positions",
|
|
295
|
+
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
296
|
inputSchema: {
|
|
67
297
|
type: "object",
|
|
68
|
-
properties: {
|
|
298
|
+
properties: { within_ms: {
|
|
299
|
+
type: "number",
|
|
300
|
+
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"
|
|
301
|
+
} }
|
|
69
302
|
}
|
|
70
303
|
},
|
|
71
304
|
{
|
|
72
305
|
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",
|
|
306
|
+
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
307
|
inputSchema: {
|
|
75
308
|
type: "object",
|
|
76
309
|
properties: {
|
|
310
|
+
within_ms: {
|
|
311
|
+
type: "number",
|
|
312
|
+
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"
|
|
313
|
+
},
|
|
77
314
|
shift_to: {
|
|
78
315
|
type: "number",
|
|
79
316
|
description: "shift head forward (>90°) or back (<90°) — s1"
|
|
@@ -99,61 +336,91 @@ var y = [
|
|
|
99
336
|
},
|
|
100
337
|
{
|
|
101
338
|
name: "shift_to",
|
|
102
|
-
description: "shift head forward (>90°) or back (<90°) — s1",
|
|
339
|
+
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
340
|
inputSchema: {
|
|
104
341
|
type: "object",
|
|
105
|
-
properties: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
342
|
+
properties: {
|
|
343
|
+
angle: {
|
|
344
|
+
type: "number",
|
|
345
|
+
description: "target angle in degrees"
|
|
346
|
+
},
|
|
347
|
+
within_ms: {
|
|
348
|
+
type: "number",
|
|
349
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
109
352
|
required: ["angle"]
|
|
110
353
|
}
|
|
111
354
|
},
|
|
112
355
|
{
|
|
113
356
|
name: "roll_to",
|
|
114
|
-
description: "roll head clockwise (>90°) or counter-clockwise (<90°) — s2",
|
|
357
|
+
description: "roll head clockwise (>90°) or counter-clockwise (<90°) — s2; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
115
358
|
inputSchema: {
|
|
116
359
|
type: "object",
|
|
117
|
-
properties: {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
360
|
+
properties: {
|
|
361
|
+
angle: {
|
|
362
|
+
type: "number",
|
|
363
|
+
description: "target angle in degrees"
|
|
364
|
+
},
|
|
365
|
+
within_ms: {
|
|
366
|
+
type: "number",
|
|
367
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
368
|
+
}
|
|
369
|
+
},
|
|
121
370
|
required: ["angle"]
|
|
122
371
|
}
|
|
123
372
|
},
|
|
124
373
|
{
|
|
125
374
|
name: "pitch_to",
|
|
126
|
-
description: "pitch head up (>110°) or down (<110°) — s3",
|
|
375
|
+
description: "pitch head up (>110°) or down (<110°) — s3; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
127
376
|
inputSchema: {
|
|
128
377
|
type: "object",
|
|
129
|
-
properties: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
378
|
+
properties: {
|
|
379
|
+
angle: {
|
|
380
|
+
type: "number",
|
|
381
|
+
description: "target angle in degrees"
|
|
382
|
+
},
|
|
383
|
+
within_ms: {
|
|
384
|
+
type: "number",
|
|
385
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
133
388
|
required: ["angle"]
|
|
134
389
|
}
|
|
135
390
|
},
|
|
136
391
|
{
|
|
137
392
|
name: "rotate_to",
|
|
138
|
-
description: "rotate body around Z-axis — s4",
|
|
393
|
+
description: "rotate body around Z-axis — s4; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
139
394
|
inputSchema: {
|
|
140
395
|
type: "object",
|
|
141
|
-
properties: {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
396
|
+
properties: {
|
|
397
|
+
angle: {
|
|
398
|
+
type: "number",
|
|
399
|
+
description: "target angle in degrees"
|
|
400
|
+
},
|
|
401
|
+
within_ms: {
|
|
402
|
+
type: "number",
|
|
403
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
404
|
+
}
|
|
405
|
+
},
|
|
145
406
|
required: ["angle"]
|
|
146
407
|
}
|
|
147
408
|
},
|
|
148
409
|
{
|
|
149
410
|
name: "lift_to",
|
|
150
|
-
description: "lift head on secondary axis, range 20°–150° — s5",
|
|
411
|
+
description: "lift head on secondary axis, range 20°–150° — s5; pass within_ms for smooth motion with trapezoidal velocity profile",
|
|
151
412
|
inputSchema: {
|
|
152
413
|
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
414
|
+
properties: {
|
|
415
|
+
angle: {
|
|
416
|
+
type: "number",
|
|
417
|
+
description: "target angle in degrees"
|
|
418
|
+
},
|
|
419
|
+
within_ms: {
|
|
420
|
+
type: "number",
|
|
421
|
+
description: "duration in milliseconds for smooth motion with trapezoidal ramp-up and ramp-down; omit for constant-speed movement"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
157
424
|
required: ["angle"]
|
|
158
425
|
}
|
|
159
426
|
},
|
|
@@ -213,7 +480,7 @@ var y = [
|
|
|
213
480
|
},
|
|
214
481
|
{
|
|
215
482
|
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 <
|
|
483
|
+
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
484
|
inputSchema: {
|
|
218
485
|
type: "object",
|
|
219
486
|
properties: { script: {
|
|
@@ -222,99 +489,114 @@ var y = [
|
|
|
222
489
|
} },
|
|
223
490
|
required: ["script"]
|
|
224
491
|
}
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "disconnect",
|
|
495
|
+
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",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {}
|
|
499
|
+
}
|
|
225
500
|
}
|
|
226
501
|
];
|
|
227
|
-
async function
|
|
228
|
-
|
|
502
|
+
async function k(e) {
|
|
503
|
+
let t = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
504
|
+
return await (await w()).home(t), "all servos moved to home positions";
|
|
229
505
|
}
|
|
230
|
-
async function
|
|
506
|
+
async function A(e) {
|
|
231
507
|
let t = {};
|
|
232
508
|
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
|
|
509
|
+
let n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
510
|
+
return await (await w()).moveTo(t, n), `servos updated: ${JSON.stringify(t)}`;
|
|
511
|
+
}
|
|
512
|
+
async function j(e) {
|
|
513
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
514
|
+
return await (await w()).shiftHeadTo(t, n), `s1 (shift) → ${t}°`;
|
|
235
515
|
}
|
|
236
|
-
async function
|
|
237
|
-
let t = Number(e.angle), n =
|
|
238
|
-
return
|
|
516
|
+
async function M(e) {
|
|
517
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
518
|
+
return await (await w()).rollHeadTo(t, n), `s2 (roll) → ${t}°`;
|
|
239
519
|
}
|
|
240
|
-
async function
|
|
241
|
-
let t = Number(e.angle), n =
|
|
242
|
-
return
|
|
520
|
+
async function N(e) {
|
|
521
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
522
|
+
return await (await w()).pitchHeadTo(t, n), `s3 (pitch) → ${t}°`;
|
|
243
523
|
}
|
|
244
|
-
async function
|
|
245
|
-
let t = Number(e.angle), n =
|
|
246
|
-
return
|
|
524
|
+
async function P(e) {
|
|
525
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
526
|
+
return await (await w()).rotateBodyTo(t, n), `s4 (rotate) → ${t}°`;
|
|
247
527
|
}
|
|
248
|
-
async function
|
|
249
|
-
let t = Number(e.angle), n =
|
|
250
|
-
return
|
|
528
|
+
async function F(e) {
|
|
529
|
+
let t = Number(e.angle), n = e.within_ms == null ? void 0 : Number(e.within_ms);
|
|
530
|
+
return await (await w()).liftHeadTo(t, n), `s5 (lift) → ${t}°`;
|
|
251
531
|
}
|
|
252
|
-
async function
|
|
253
|
-
|
|
254
|
-
return n.State = { s5: t }, await n.sendServoState(), `s5 (lift) → ${t}°`;
|
|
532
|
+
async function I() {
|
|
533
|
+
return C == null ? "not connected" : (T(), "disconnected");
|
|
255
534
|
}
|
|
256
|
-
async function
|
|
535
|
+
async function L(e) {
|
|
257
536
|
let t = Number(e.within_ms);
|
|
258
537
|
if (isNaN(t) || t <= 0) throw Error("move_to: within_ms must be a positive number");
|
|
259
538
|
let n = {};
|
|
260
539
|
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
|
|
540
|
+
return await (await w()).moveTo(n, t), "move completed";
|
|
262
541
|
}
|
|
263
|
-
async function
|
|
542
|
+
async function R(e) {
|
|
264
543
|
let t = Number(e.ms);
|
|
265
544
|
if (isNaN(t) || t < 0) throw Error(`wait: invalid duration '${e.ms}' — expected a non-negative number`);
|
|
266
545
|
return await new Promise((e) => setTimeout(e, t)), `waited ${t} ms`;
|
|
267
546
|
}
|
|
268
|
-
async function
|
|
269
|
-
let e = await
|
|
547
|
+
async function z() {
|
|
548
|
+
let e = await w();
|
|
270
549
|
return JSON.stringify(e.State);
|
|
271
550
|
}
|
|
272
|
-
async function
|
|
551
|
+
async function B(e) {
|
|
273
552
|
let t = String(e.script ?? "");
|
|
274
|
-
return await
|
|
553
|
+
return await y(await w(), t), "script executed successfully";
|
|
275
554
|
}
|
|
276
|
-
function
|
|
555
|
+
function V() {
|
|
277
556
|
let e = new i({
|
|
278
557
|
name: "nova-control-mcp-server",
|
|
279
|
-
version: "0.0.
|
|
558
|
+
version: "0.0.7"
|
|
280
559
|
}, { capabilities: { tools: {} } });
|
|
281
|
-
return e.setRequestHandler(c, async () => ({ tools:
|
|
560
|
+
return e.setRequestHandler(c, async () => ({ tools: O })), e.setRequestHandler(s, async (e) => {
|
|
282
561
|
let t = e.params.name, n = e.params.arguments ?? {};
|
|
283
562
|
try {
|
|
284
563
|
let e;
|
|
285
564
|
switch (t) {
|
|
286
565
|
case "home":
|
|
287
|
-
e = await
|
|
566
|
+
e = await k(n);
|
|
288
567
|
break;
|
|
289
568
|
case "move":
|
|
290
|
-
e = await
|
|
569
|
+
e = await A(n);
|
|
291
570
|
break;
|
|
292
571
|
case "shift_to":
|
|
293
|
-
e = await
|
|
572
|
+
e = await j(n);
|
|
294
573
|
break;
|
|
295
574
|
case "roll_to":
|
|
296
|
-
e = await
|
|
575
|
+
e = await M(n);
|
|
297
576
|
break;
|
|
298
577
|
case "pitch_to":
|
|
299
|
-
e = await
|
|
578
|
+
e = await N(n);
|
|
300
579
|
break;
|
|
301
580
|
case "rotate_to":
|
|
302
|
-
e = await
|
|
581
|
+
e = await P(n);
|
|
303
582
|
break;
|
|
304
583
|
case "lift_to":
|
|
305
|
-
e = await
|
|
584
|
+
e = await F(n);
|
|
306
585
|
break;
|
|
307
586
|
case "move_to":
|
|
308
|
-
e = await
|
|
587
|
+
e = await L(n);
|
|
309
588
|
break;
|
|
310
589
|
case "wait":
|
|
311
|
-
e = await
|
|
590
|
+
e = await R(n);
|
|
312
591
|
break;
|
|
313
592
|
case "get_state":
|
|
314
|
-
e = await
|
|
593
|
+
e = await z();
|
|
315
594
|
break;
|
|
316
595
|
case "run_script":
|
|
317
|
-
e = await
|
|
596
|
+
e = await B(n);
|
|
597
|
+
break;
|
|
598
|
+
case "disconnect":
|
|
599
|
+
e = await I();
|
|
318
600
|
break;
|
|
319
601
|
default: return {
|
|
320
602
|
content: [{
|
|
@@ -339,14 +621,14 @@ function j() {
|
|
|
339
621
|
}
|
|
340
622
|
}), e;
|
|
341
623
|
}
|
|
342
|
-
async function
|
|
624
|
+
async function H(e) {
|
|
343
625
|
let t = new a();
|
|
344
626
|
await e.connect(t);
|
|
345
627
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, () => {
|
|
346
|
-
|
|
628
|
+
T(), process.exit(0);
|
|
347
629
|
});
|
|
348
630
|
}
|
|
349
|
-
async function
|
|
631
|
+
async function U(e, t) {
|
|
350
632
|
let r = new o({ sessionIdGenerator: void 0 });
|
|
351
633
|
await e.connect(r);
|
|
352
634
|
let i = n(async (e, t) => {
|
|
@@ -358,17 +640,17 @@ async function N(e, t) {
|
|
|
358
640
|
}), i.once("error", n);
|
|
359
641
|
});
|
|
360
642
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, async () => {
|
|
361
|
-
await r.close(), i.close(),
|
|
643
|
+
await r.close(), i.close(), T(), process.exit(0);
|
|
362
644
|
});
|
|
363
645
|
}
|
|
364
|
-
async function
|
|
365
|
-
let { Port: e, BaudRate: t, Transport: n, ListenPort: r } =
|
|
366
|
-
|
|
367
|
-
let i =
|
|
368
|
-
n === "http" ? await
|
|
646
|
+
async function W() {
|
|
647
|
+
let { Port: e, BaudRate: t, Transport: n, ListenPort: r } = b();
|
|
648
|
+
x = e, S = t;
|
|
649
|
+
let i = V();
|
|
650
|
+
n === "http" ? await U(i, r) : await H(i);
|
|
369
651
|
}
|
|
370
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
652
|
+
t(process.argv[1]) === e(import.meta.url) && W().catch((e) => {
|
|
371
653
|
process.stderr.write(`nova-control-mcp: fatal: ${e.message ?? e}\n`), process.exit(1);
|
|
372
654
|
});
|
|
373
655
|
//#endregion
|
|
374
|
-
export {
|
|
656
|
+
export { D as _destroyForTests, E as _setupForTests, V as createServer };
|