nova-control-mcp-server 0.0.2 → 0.0.4
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 +43 -0
- package/dist/nova-control-mcp-server.js +91 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,49 @@ Add the same block under `mcpServers` in `~/.cursor/mcp.json`.
|
|
|
67
67
|
|---|---|---|---|
|
|
68
68
|
| `--port <path>` | `-p` | *(required)* | serial port path (e.g. `/dev/ttyACM0`, `COM3`) |
|
|
69
69
|
| `--baud <rate>` | `-b` | `9600` | baud rate |
|
|
70
|
+
| `--transport <mode>` | `-t` | `stdio` | transport: `stdio` or `http` |
|
|
71
|
+
| `--listen <port>` | `-l` | `3000` | HTTP listen port (only used when `--transport http`) |
|
|
72
|
+
|
|
73
|
+
## HTTP transport (Docker / remote)
|
|
74
|
+
|
|
75
|
+
Use `--transport http` to expose the MCP server over HTTP instead of stdio. This is the recommended mode when the server runs in a Docker container or on a remote machine.
|
|
76
|
+
|
|
77
|
+
The server listens on the given port and accepts MCP requests at `POST /mcp`.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
nova-control-mcp-server --port /dev/ttyACM0 --transport http --listen 3000
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Docker example
|
|
84
|
+
|
|
85
|
+
```dockerfile
|
|
86
|
+
FROM node:22-slim
|
|
87
|
+
RUN npm install -g nova-control-mcp-server
|
|
88
|
+
EXPOSE 3000
|
|
89
|
+
CMD ["nova-control-mcp-server", "--port", "/dev/ttyACM0", \
|
|
90
|
+
"--transport", "http", "--listen", "3000"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run the container with the serial device passed through:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
docker run --device /dev/ttyACM0 -p 3000:3000 nova-mcp
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Connecting an MCP client to the HTTP transport
|
|
100
|
+
|
|
101
|
+
Point your MCP client at `http://<host>:3000/mcp`. For Claude Desktop with a remote server, use the `url` transport type:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"nova-control": {
|
|
107
|
+
"type": "http",
|
|
108
|
+
"url": "http://localhost:3000/mcp"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
70
113
|
|
|
71
114
|
## Tools
|
|
72
115
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath as e } from "node:url";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { realpathSync as t } from "node:fs";
|
|
4
|
+
import { createServer as n } from "node:http";
|
|
5
|
+
import { parseArgs as r } from "node:util";
|
|
6
|
+
import { Server as i } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport as a } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { StreamableHTTPServerTransport as o } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
+
import { CallToolRequestSchema as s, ListToolsRequestSchema as c } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { openNova as l } from "nova-control-node";
|
|
8
11
|
//#region src/nova-control-mcp-server.ts
|
|
9
|
-
function
|
|
12
|
+
function u() {
|
|
10
13
|
try {
|
|
11
|
-
let { values: e } =
|
|
14
|
+
let { values: e } = r({
|
|
12
15
|
args: process.argv.slice(2),
|
|
13
16
|
options: {
|
|
14
17
|
port: {
|
|
@@ -18,33 +21,45 @@ function s() {
|
|
|
18
21
|
baud: {
|
|
19
22
|
type: "string",
|
|
20
23
|
short: "b"
|
|
24
|
+
},
|
|
25
|
+
transport: {
|
|
26
|
+
type: "string",
|
|
27
|
+
short: "t"
|
|
28
|
+
},
|
|
29
|
+
listen: {
|
|
30
|
+
type: "string",
|
|
31
|
+
short: "l"
|
|
21
32
|
}
|
|
22
33
|
},
|
|
23
34
|
strict: !0,
|
|
24
35
|
allowPositionals: !1
|
|
25
36
|
});
|
|
26
|
-
|
|
37
|
+
e.port ?? (process.stderr.write("nova-control-mcp: --port is required\n"), process.exit(1));
|
|
38
|
+
let t = e.transport ?? "stdio";
|
|
39
|
+
return t !== "stdio" && t !== "http" && (process.stderr.write(`nova-control-mcp: --transport must be 'stdio' or 'http', got '${t}'\n`), process.exit(1)), {
|
|
27
40
|
Port: e.port,
|
|
28
|
-
BaudRate: Number(e.baud ?? "9600")
|
|
41
|
+
BaudRate: Number(e.baud ?? "9600"),
|
|
42
|
+
Transport: t,
|
|
43
|
+
ListenPort: Number(e.listen ?? "3000")
|
|
29
44
|
};
|
|
30
45
|
} catch (e) {
|
|
31
46
|
process.stderr.write(`nova-control-mcp: ${e.message ?? e}\n`), process.exit(1);
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
|
-
var
|
|
35
|
-
async function
|
|
36
|
-
return
|
|
49
|
+
var d = "", f = 9600, p;
|
|
50
|
+
async function m() {
|
|
51
|
+
return p ??= await l(d, f), p;
|
|
37
52
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
53
|
+
function h() {
|
|
54
|
+
p != null && (p.destroy(), p = void 0);
|
|
40
55
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
56
|
+
function g(e, t = 9600) {
|
|
57
|
+
d = e, f = t;
|
|
43
58
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
59
|
+
function _() {
|
|
60
|
+
h(), d = "", f = 9600;
|
|
46
61
|
}
|
|
47
|
-
var
|
|
62
|
+
var v = [
|
|
48
63
|
{
|
|
49
64
|
name: "home",
|
|
50
65
|
description: "send all servos to their home positions",
|
|
@@ -163,80 +178,80 @@ var h = [
|
|
|
163
178
|
}
|
|
164
179
|
}
|
|
165
180
|
];
|
|
166
|
-
async function
|
|
167
|
-
return await (await
|
|
181
|
+
async function y() {
|
|
182
|
+
return await (await m()).home(), "all servos moved to home positions";
|
|
168
183
|
}
|
|
169
|
-
async function
|
|
184
|
+
async function b(e) {
|
|
170
185
|
let t = {};
|
|
171
186
|
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");
|
|
172
|
-
let n = await
|
|
187
|
+
let n = await m();
|
|
173
188
|
return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
|
|
174
189
|
}
|
|
175
|
-
async function
|
|
176
|
-
let t = Number(e.deg), n = await
|
|
190
|
+
async function x(e) {
|
|
191
|
+
let t = Number(e.deg), n = await m();
|
|
177
192
|
return n.State = { s1: t }, await n.sendServoState(), `s1 (shift) → ${t}°`;
|
|
178
193
|
}
|
|
179
|
-
async function
|
|
180
|
-
let t = Number(e.deg), n = await
|
|
194
|
+
async function S(e) {
|
|
195
|
+
let t = Number(e.deg), n = await m();
|
|
181
196
|
return n.State = { s2: t }, await n.sendServoState(), `s2 (roll) → ${t}°`;
|
|
182
197
|
}
|
|
183
|
-
async function
|
|
184
|
-
let t = Number(e.deg), n = await
|
|
198
|
+
async function C(e) {
|
|
199
|
+
let t = Number(e.deg), n = await m();
|
|
185
200
|
return n.State = { s3: t }, await n.sendServoState(), `s3 (pitch) → ${t}°`;
|
|
186
201
|
}
|
|
187
|
-
async function
|
|
188
|
-
let t = Number(e.deg), n = await
|
|
202
|
+
async function w(e) {
|
|
203
|
+
let t = Number(e.deg), n = await m();
|
|
189
204
|
return n.State = { s4: t }, await n.sendServoState(), `s4 (rotate) → ${t}°`;
|
|
190
205
|
}
|
|
191
|
-
async function
|
|
192
|
-
let t = Number(e.deg), n = await
|
|
206
|
+
async function T(e) {
|
|
207
|
+
let t = Number(e.deg), n = await m();
|
|
193
208
|
return n.State = { s5: t }, await n.sendServoState(), `s5 (lift) → ${t}°`;
|
|
194
209
|
}
|
|
195
|
-
async function
|
|
210
|
+
async function E(e) {
|
|
196
211
|
let t = Number(e.ms);
|
|
197
212
|
if (isNaN(t) || t < 0) throw Error(`wait: invalid duration '${e.ms}' — expected a non-negative number`);
|
|
198
213
|
return await new Promise((e) => setTimeout(e, t)), `waited ${t} ms`;
|
|
199
214
|
}
|
|
200
|
-
async function
|
|
201
|
-
let e = await
|
|
215
|
+
async function D() {
|
|
216
|
+
let e = await m();
|
|
202
217
|
return JSON.stringify(e.State);
|
|
203
218
|
}
|
|
204
|
-
function
|
|
205
|
-
let e = new
|
|
219
|
+
function O() {
|
|
220
|
+
let e = new i({
|
|
206
221
|
name: "nova-control-mcp-server",
|
|
207
|
-
version: "0.0.
|
|
222
|
+
version: "0.0.4"
|
|
208
223
|
}, { capabilities: { tools: {} } });
|
|
209
|
-
return e.setRequestHandler(
|
|
224
|
+
return e.setRequestHandler(c, async () => ({ tools: v })), e.setRequestHandler(s, async (e) => {
|
|
210
225
|
let t = e.params.name, n = e.params.arguments ?? {};
|
|
211
226
|
try {
|
|
212
227
|
let e;
|
|
213
228
|
switch (t) {
|
|
214
229
|
case "home":
|
|
215
|
-
e = await
|
|
230
|
+
e = await y();
|
|
216
231
|
break;
|
|
217
232
|
case "move":
|
|
218
|
-
e = await
|
|
233
|
+
e = await b(n);
|
|
219
234
|
break;
|
|
220
235
|
case "shift_to":
|
|
221
|
-
e = await
|
|
236
|
+
e = await x(n);
|
|
222
237
|
break;
|
|
223
238
|
case "roll_to":
|
|
224
|
-
e = await
|
|
239
|
+
e = await S(n);
|
|
225
240
|
break;
|
|
226
241
|
case "pitch_to":
|
|
227
|
-
e = await
|
|
242
|
+
e = await C(n);
|
|
228
243
|
break;
|
|
229
244
|
case "rotate_to":
|
|
230
|
-
e = await
|
|
245
|
+
e = await w(n);
|
|
231
246
|
break;
|
|
232
247
|
case "lift_to":
|
|
233
|
-
e = await
|
|
248
|
+
e = await T(n);
|
|
234
249
|
break;
|
|
235
250
|
case "wait":
|
|
236
|
-
e = await
|
|
251
|
+
e = await E(n);
|
|
237
252
|
break;
|
|
238
253
|
case "get_state":
|
|
239
|
-
e = await
|
|
254
|
+
e = await D();
|
|
240
255
|
break;
|
|
241
256
|
default: return {
|
|
242
257
|
content: [{
|
|
@@ -261,17 +276,36 @@ function T() {
|
|
|
261
276
|
}
|
|
262
277
|
}), e;
|
|
263
278
|
}
|
|
264
|
-
async function
|
|
265
|
-
let
|
|
266
|
-
|
|
267
|
-
let n = T(), i = new r();
|
|
268
|
-
await n.connect(i);
|
|
279
|
+
async function k(e) {
|
|
280
|
+
let t = new a();
|
|
281
|
+
await e.connect(t);
|
|
269
282
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, () => {
|
|
270
|
-
|
|
283
|
+
h(), process.exit(0);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async function A(e, t) {
|
|
287
|
+
let r = new o({ sessionIdGenerator: void 0 });
|
|
288
|
+
await e.connect(r);
|
|
289
|
+
let i = n(async (e, t) => {
|
|
290
|
+
e.url === "/mcp" ? await r.handleRequest(e, t) : (t.writeHead(404, { "Content-Type": "text/plain" }), t.end("not found"));
|
|
271
291
|
});
|
|
292
|
+
await new Promise((e, n) => {
|
|
293
|
+
i.listen(t, () => {
|
|
294
|
+
process.stderr.write(`nova-control-mcp: HTTP transport listening on port ${t} — POST /mcp\n`), e();
|
|
295
|
+
}), i.once("error", n);
|
|
296
|
+
});
|
|
297
|
+
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, async () => {
|
|
298
|
+
await r.close(), i.close(), h(), process.exit(0);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
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);
|
|
272
306
|
}
|
|
273
|
-
process.argv[1] === e(import.meta.url) &&
|
|
307
|
+
t(process.argv[1]) === e(import.meta.url) && j().catch((e) => {
|
|
274
308
|
process.stderr.write(`nova-control-mcp: fatal: ${e.message ?? e}\n`), process.exit(1);
|
|
275
309
|
});
|
|
276
310
|
//#endregion
|
|
277
|
-
export {
|
|
311
|
+
export { _ as _destroyForTests, g as _setupForTests, O as createServer };
|