nova-control-mcp-server 0.0.3 → 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 +90 -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,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath as e } from "node:url";
|
|
3
3
|
import { realpathSync as t } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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";
|
|
9
11
|
//#region src/nova-control-mcp-server.ts
|
|
10
|
-
function
|
|
12
|
+
function u() {
|
|
11
13
|
try {
|
|
12
|
-
let { values: e } =
|
|
14
|
+
let { values: e } = r({
|
|
13
15
|
args: process.argv.slice(2),
|
|
14
16
|
options: {
|
|
15
17
|
port: {
|
|
@@ -19,33 +21,45 @@ function c() {
|
|
|
19
21
|
baud: {
|
|
20
22
|
type: "string",
|
|
21
23
|
short: "b"
|
|
24
|
+
},
|
|
25
|
+
transport: {
|
|
26
|
+
type: "string",
|
|
27
|
+
short: "t"
|
|
28
|
+
},
|
|
29
|
+
listen: {
|
|
30
|
+
type: "string",
|
|
31
|
+
short: "l"
|
|
22
32
|
}
|
|
23
33
|
},
|
|
24
34
|
strict: !0,
|
|
25
35
|
allowPositionals: !1
|
|
26
36
|
});
|
|
27
|
-
|
|
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)), {
|
|
28
40
|
Port: e.port,
|
|
29
|
-
BaudRate: Number(e.baud ?? "9600")
|
|
41
|
+
BaudRate: Number(e.baud ?? "9600"),
|
|
42
|
+
Transport: t,
|
|
43
|
+
ListenPort: Number(e.listen ?? "3000")
|
|
30
44
|
};
|
|
31
45
|
} catch (e) {
|
|
32
46
|
process.stderr.write(`nova-control-mcp: ${e.message ?? e}\n`), process.exit(1);
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
|
-
var
|
|
36
|
-
async function
|
|
37
|
-
return
|
|
49
|
+
var d = "", f = 9600, p;
|
|
50
|
+
async function m() {
|
|
51
|
+
return p ??= await l(d, f), p;
|
|
38
52
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
53
|
+
function h() {
|
|
54
|
+
p != null && (p.destroy(), p = void 0);
|
|
41
55
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
56
|
+
function g(e, t = 9600) {
|
|
57
|
+
d = e, f = t;
|
|
44
58
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
59
|
+
function _() {
|
|
60
|
+
h(), d = "", f = 9600;
|
|
47
61
|
}
|
|
48
|
-
var
|
|
62
|
+
var v = [
|
|
49
63
|
{
|
|
50
64
|
name: "home",
|
|
51
65
|
description: "send all servos to their home positions",
|
|
@@ -164,80 +178,80 @@ var g = [
|
|
|
164
178
|
}
|
|
165
179
|
}
|
|
166
180
|
];
|
|
167
|
-
async function
|
|
168
|
-
return await (await
|
|
181
|
+
async function y() {
|
|
182
|
+
return await (await m()).home(), "all servos moved to home positions";
|
|
169
183
|
}
|
|
170
|
-
async function
|
|
184
|
+
async function b(e) {
|
|
171
185
|
let t = {};
|
|
172
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");
|
|
173
|
-
let n = await
|
|
187
|
+
let n = await m();
|
|
174
188
|
return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
|
|
175
189
|
}
|
|
176
|
-
async function
|
|
177
|
-
let t = Number(e.deg), n = await
|
|
190
|
+
async function x(e) {
|
|
191
|
+
let t = Number(e.deg), n = await m();
|
|
178
192
|
return n.State = { s1: t }, await n.sendServoState(), `s1 (shift) → ${t}°`;
|
|
179
193
|
}
|
|
180
|
-
async function
|
|
181
|
-
let t = Number(e.deg), n = await
|
|
194
|
+
async function S(e) {
|
|
195
|
+
let t = Number(e.deg), n = await m();
|
|
182
196
|
return n.State = { s2: t }, await n.sendServoState(), `s2 (roll) → ${t}°`;
|
|
183
197
|
}
|
|
184
|
-
async function
|
|
185
|
-
let t = Number(e.deg), n = await
|
|
198
|
+
async function C(e) {
|
|
199
|
+
let t = Number(e.deg), n = await m();
|
|
186
200
|
return n.State = { s3: t }, await n.sendServoState(), `s3 (pitch) → ${t}°`;
|
|
187
201
|
}
|
|
188
|
-
async function
|
|
189
|
-
let t = Number(e.deg), n = await
|
|
202
|
+
async function w(e) {
|
|
203
|
+
let t = Number(e.deg), n = await m();
|
|
190
204
|
return n.State = { s4: t }, await n.sendServoState(), `s4 (rotate) → ${t}°`;
|
|
191
205
|
}
|
|
192
|
-
async function
|
|
193
|
-
let t = Number(e.deg), n = await
|
|
206
|
+
async function T(e) {
|
|
207
|
+
let t = Number(e.deg), n = await m();
|
|
194
208
|
return n.State = { s5: t }, await n.sendServoState(), `s5 (lift) → ${t}°`;
|
|
195
209
|
}
|
|
196
|
-
async function
|
|
210
|
+
async function E(e) {
|
|
197
211
|
let t = Number(e.ms);
|
|
198
212
|
if (isNaN(t) || t < 0) throw Error(`wait: invalid duration '${e.ms}' — expected a non-negative number`);
|
|
199
213
|
return await new Promise((e) => setTimeout(e, t)), `waited ${t} ms`;
|
|
200
214
|
}
|
|
201
|
-
async function
|
|
202
|
-
let e = await
|
|
215
|
+
async function D() {
|
|
216
|
+
let e = await m();
|
|
203
217
|
return JSON.stringify(e.State);
|
|
204
218
|
}
|
|
205
|
-
function
|
|
206
|
-
let e = new
|
|
219
|
+
function O() {
|
|
220
|
+
let e = new i({
|
|
207
221
|
name: "nova-control-mcp-server",
|
|
208
|
-
version: "0.0.
|
|
222
|
+
version: "0.0.4"
|
|
209
223
|
}, { capabilities: { tools: {} } });
|
|
210
|
-
return e.setRequestHandler(
|
|
224
|
+
return e.setRequestHandler(c, async () => ({ tools: v })), e.setRequestHandler(s, async (e) => {
|
|
211
225
|
let t = e.params.name, n = e.params.arguments ?? {};
|
|
212
226
|
try {
|
|
213
227
|
let e;
|
|
214
228
|
switch (t) {
|
|
215
229
|
case "home":
|
|
216
|
-
e = await
|
|
230
|
+
e = await y();
|
|
217
231
|
break;
|
|
218
232
|
case "move":
|
|
219
|
-
e = await
|
|
233
|
+
e = await b(n);
|
|
220
234
|
break;
|
|
221
235
|
case "shift_to":
|
|
222
|
-
e = await
|
|
236
|
+
e = await x(n);
|
|
223
237
|
break;
|
|
224
238
|
case "roll_to":
|
|
225
|
-
e = await
|
|
239
|
+
e = await S(n);
|
|
226
240
|
break;
|
|
227
241
|
case "pitch_to":
|
|
228
|
-
e = await
|
|
242
|
+
e = await C(n);
|
|
229
243
|
break;
|
|
230
244
|
case "rotate_to":
|
|
231
|
-
e = await
|
|
245
|
+
e = await w(n);
|
|
232
246
|
break;
|
|
233
247
|
case "lift_to":
|
|
234
|
-
e = await
|
|
248
|
+
e = await T(n);
|
|
235
249
|
break;
|
|
236
250
|
case "wait":
|
|
237
|
-
e = await
|
|
251
|
+
e = await E(n);
|
|
238
252
|
break;
|
|
239
253
|
case "get_state":
|
|
240
|
-
e = await
|
|
254
|
+
e = await D();
|
|
241
255
|
break;
|
|
242
256
|
default: return {
|
|
243
257
|
content: [{
|
|
@@ -262,17 +276,36 @@ function E() {
|
|
|
262
276
|
}
|
|
263
277
|
}), e;
|
|
264
278
|
}
|
|
265
|
-
async function
|
|
266
|
-
let
|
|
267
|
-
|
|
268
|
-
let n = E(), r = new i();
|
|
269
|
-
await n.connect(r);
|
|
279
|
+
async function k(e) {
|
|
280
|
+
let t = new a();
|
|
281
|
+
await e.connect(t);
|
|
270
282
|
for (let e of ["SIGINT", "SIGTERM"]) process.on(e, () => {
|
|
271
|
-
|
|
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"));
|
|
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);
|
|
272
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);
|
|
273
306
|
}
|
|
274
|
-
t(process.argv[1]) === e(import.meta.url) &&
|
|
307
|
+
t(process.argv[1]) === e(import.meta.url) && j().catch((e) => {
|
|
275
308
|
process.stderr.write(`nova-control-mcp: fatal: ${e.message ?? e}\n`), process.exit(1);
|
|
276
309
|
});
|
|
277
310
|
//#endregion
|
|
278
|
-
export {
|
|
311
|
+
export { _ as _destroyForTests, g as _setupForTests, O as createServer };
|