@zoer7788/mcp-nexus-node 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/cli.js +75 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,7 +14,6 @@ On `pub-118`:
14
14
 
15
15
  ```bash
16
16
  npm install -g @zoer7788/mcp-nexus-node
17
- MCP_NEXUS_ADMIN_URL=https://mcpnexus-pub118.omji.top \
18
17
  mcp-nexus pair
19
18
  ```
20
19
 
@@ -32,4 +31,5 @@ mcp-nexus status
32
31
  ```
33
32
 
34
33
  DevSpace is installed as a package dependency. `cloudflared` must be available on
35
- the remote machine before creating public DevSpace projects.
34
+ the remote machine. `mcp-nexus pair` creates the control tunnel automatically
35
+ when Cloudflare auth is present at `~/.cloudflared/cert.pem`.
package/cli.js CHANGED
@@ -63,6 +63,11 @@ function config() {
63
63
  };
64
64
  }
65
65
 
66
+ function saveConfigPatch(patch) {
67
+ ensureState();
68
+ saveJson(configPath, { ...loadJson(configPath, {}), ...patch });
69
+ }
70
+
66
71
  function tokens() {
67
72
  ensureState();
68
73
  return loadJson(tokensPath, {});
@@ -113,6 +118,17 @@ function adminUrl() {
113
118
  return cfg.adminUrl || `http://${localIp()}:${cfg.port || 9876}`;
114
119
  }
115
120
 
121
+ function defaultControlHost() {
122
+ const cfg = config();
123
+ const id = readFileSync(nodeIdPath, "utf8").trim().slice(0, 8);
124
+ return `mcpnexus-${sanitize(hostname()).slice(0, 24)}-${id}.${cfg.baseDomain || "omji.top"}`;
125
+ }
126
+
127
+ function findCloudflareTunnel(name) {
128
+ const list = spawnSync("cloudflared", ["tunnel", "list"], { encoding: "utf8" }).stdout || "";
129
+ return list.split("\n").map((line) => line.trim().split(/\s+/)).find((cols) => cols[1] === name)?.[0] || "";
130
+ }
131
+
116
132
  function json(res, status, body) {
117
133
  res.writeHead(status, {
118
134
  "content-type": "application/json; charset=utf-8",
@@ -168,7 +184,10 @@ function setupProject(input) {
168
184
  const name = String(input.name || "").trim();
169
185
  const root = String(input.root || input.path || "").trim();
170
186
  if (!name || !root) throw new Error("name and root are required");
171
- if (!existsSync(root)) throw new Error(`root not found: ${root}`);
187
+ if (!existsSync(root)) {
188
+ if (input.createRoot) mkdirSync(root, { recursive: true });
189
+ else throw new Error(`root not found: ${root}`);
190
+ }
172
191
  const items = projects();
173
192
  if (items.some((p) => p.name === name)) throw new Error(`project already exists: ${name}`);
174
193
 
@@ -182,9 +201,8 @@ function setupProject(input) {
182
201
  const tunnelName = `devspace-${subdomain}`;
183
202
 
184
203
  let tunnelId = "";
185
- const list = spawnSync("cloudflared", ["tunnel", "list"], { encoding: "utf8" }).stdout || "";
186
- const existing = list.split("\n").map((line) => line.trim().split(/\s+/)).find((cols) => cols[1] === tunnelName);
187
- if (existing) tunnelId = existing[0];
204
+ const existing = findCloudflareTunnel(tunnelName);
205
+ if (existing) tunnelId = existing;
188
206
  else {
189
207
  const created = run("cloudflared", ["tunnel", "create", tunnelName]);
190
208
  tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
@@ -243,10 +261,59 @@ function setupProject(input) {
243
261
  };
244
262
  items.push(project);
245
263
  saveProjects(items);
246
- startProject(name);
264
+ if (input.autostart !== false) startProject(name);
247
265
  return projectStatus(findProject(name));
248
266
  }
249
267
 
268
+ function setupControl(hostnameValue) {
269
+ ensureState();
270
+ if (!commandExists("cloudflared")) throw new Error("cloudflared command not found");
271
+ const cfDir = join(home, ".cloudflared");
272
+ const certFile = join(cfDir, "cert.pem");
273
+ if (!existsSync(certFile)) throw new Error(`cloudflared is not logged in: ${certFile} not found`);
274
+ const host = String(hostnameValue || "").trim().toLowerCase();
275
+ if (!/^[a-z0-9.-]+$/.test(host) || !host.includes(".")) throw new Error("usage: mcp-nexus setup-control <hostname>");
276
+
277
+ const cfg = config();
278
+ const tunnelName = `mcp-nexus-${sanitize(host)}-control`;
279
+ let tunnelId = findCloudflareTunnel(tunnelName);
280
+ if (!tunnelId) {
281
+ const created = run("cloudflared", ["tunnel", "create", tunnelName]);
282
+ tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
283
+ if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
284
+ }
285
+ run("cloudflared", ["tunnel", "route", "dns", tunnelName, host]);
286
+
287
+ const cfConfig = join(cfDir, `${tunnelName}.yml`);
288
+ const credFile = join(cfDir, `${tunnelId}.json`);
289
+ writeFileSync(cfConfig, [
290
+ `tunnel: ${tunnelId}`,
291
+ `credentials-file: ${credFile}`,
292
+ "",
293
+ "ingress:",
294
+ ` - hostname: ${host}`,
295
+ ` service: http://127.0.0.1:${cfg.port || 9876}`,
296
+ " - service: http_status:404",
297
+ "",
298
+ ].join("\n"));
299
+
300
+ const child = spawn("cloudflared", ["tunnel", "--protocol", "http2", "--config", cfConfig, "run"], {
301
+ detached: true,
302
+ stdio: ["ignore", "ignore", "ignore"],
303
+ });
304
+ child.unref();
305
+ saveConfigPatch({ adminUrl: `https://${host}` });
306
+ console.log(`Admin URL: https://${host}`);
307
+ console.log(`Control tunnel: ${tunnelName}`);
308
+ console.log("Next: mcp-nexus pair");
309
+ }
310
+
311
+ function ensureControl() {
312
+ const cfg = config();
313
+ if (cfg.adminUrl || process.env.MCP_NEXUS_ADMIN_URL) return;
314
+ setupControl(defaultControlHost());
315
+ }
316
+
250
317
  function spawnLogged(project, cmd, args, env, logName) {
251
318
  const logPath = join(logDir, logName);
252
319
  const child = spawn(cmd, args, {
@@ -373,6 +440,7 @@ function startServeDetached() {
373
440
 
374
441
  async function pair() {
375
442
  ensureState();
443
+ ensureControl();
376
444
  const cfg = config();
377
445
  const pairCode = String(Math.floor(100000 + Math.random() * 900000));
378
446
  const pairingSecret = randomHex(16);
@@ -405,7 +473,8 @@ function status() {
405
473
  const command = process.argv[2] || "help";
406
474
  if (command === "serve") serve();
407
475
  else if (command === "pair") pair().catch((error) => { console.error(error.message); process.exit(1); });
476
+ else if (command === "setup-control") { try { setupControl(process.argv[3]); } catch (error) { console.error(error.message); process.exit(1); } }
408
477
  else if (command === "status") status();
409
478
  else {
410
- console.log("Usage: mcp-nexus <pair|serve|status>");
479
+ console.log("Usage: mcp-nexus <setup-control|pair|serve|status>");
411
480
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoer7788/mcp-nexus-node",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Remote node for MCP Nexus pairing and DevSpace project control.",
5
5
  "license": "MIT",
6
6
  "type": "module",