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