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 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 { parseArgs as n } from "node:util";
5
- import { Server as r } from "@modelcontextprotocol/sdk/server/index.js";
6
- import { StdioServerTransport as i } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { CallToolRequestSchema as a, ListToolsRequestSchema as o } from "@modelcontextprotocol/sdk/types.js";
8
- import { openNova as s } from "nova-control-node";
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 c() {
12
+ function u() {
11
13
  try {
12
- let { values: e } = n({
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
- return e.port ?? (process.stderr.write("nova-control-mcp: --port is required\n"), process.exit(1)), {
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 l = "", u = 9600, d;
36
- async function f() {
37
- return d ??= await s(l, u), d;
49
+ var d = "", f = 9600, p;
50
+ async function m() {
51
+ return p ??= await l(d, f), p;
38
52
  }
39
- function p() {
40
- d != null && (d.destroy(), d = void 0);
53
+ function h() {
54
+ p != null && (p.destroy(), p = void 0);
41
55
  }
42
- function m(e, t = 9600) {
43
- l = e, u = t;
56
+ function g(e, t = 9600) {
57
+ d = e, f = t;
44
58
  }
45
- function h() {
46
- p(), l = "", u = 9600;
59
+ function _() {
60
+ h(), d = "", f = 9600;
47
61
  }
48
- var g = [
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 f()).home(), "all servos moved to home positions";
181
+ async function y() {
182
+ return await (await m()).home(), "all servos moved to home positions";
169
183
  }
170
- async function v(e) {
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 f();
187
+ let n = await m();
174
188
  return n.State = t, await n.sendServoState(), `servos updated: ${JSON.stringify(t)}`;
175
189
  }
176
- async function y(e) {
177
- let t = Number(e.deg), n = await f();
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 b(e) {
181
- let t = Number(e.deg), n = await f();
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 x(e) {
185
- let t = Number(e.deg), n = await f();
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 S(e) {
189
- let t = Number(e.deg), n = await f();
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 C(e) {
193
- let t = Number(e.deg), n = await f();
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 w(e) {
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 T() {
202
- let e = await f();
215
+ async function D() {
216
+ let e = await m();
203
217
  return JSON.stringify(e.State);
204
218
  }
205
- function E() {
206
- let e = new r({
219
+ function O() {
220
+ let e = new i({
207
221
  name: "nova-control-mcp-server",
208
- version: "0.0.1"
222
+ version: "0.0.4"
209
223
  }, { capabilities: { tools: {} } });
210
- return e.setRequestHandler(o, async () => ({ tools: g })), e.setRequestHandler(a, async (e) => {
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 v(n);
233
+ e = await b(n);
220
234
  break;
221
235
  case "shift_to":
222
- e = await y(n);
236
+ e = await x(n);
223
237
  break;
224
238
  case "roll_to":
225
- e = await b(n);
239
+ e = await S(n);
226
240
  break;
227
241
  case "pitch_to":
228
- e = await x(n);
242
+ e = await C(n);
229
243
  break;
230
244
  case "rotate_to":
231
- e = await S(n);
245
+ e = await w(n);
232
246
  break;
233
247
  case "lift_to":
234
- e = await C(n);
248
+ e = await T(n);
235
249
  break;
236
250
  case "wait":
237
- e = await w(n);
251
+ e = await E(n);
238
252
  break;
239
253
  case "get_state":
240
- e = await T();
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 D() {
266
- let { Port: e, BaudRate: t } = c();
267
- l = e, u = t;
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
- p(), process.exit(0);
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) && D().catch((e) => {
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 { h as _destroyForTests, m as _setupForTests, E as createServer };
311
+ export { _ as _destroyForTests, g as _setupForTests, O 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.3",
4
+ "version": "0.0.4",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "nova",