@zoer7788/mcp-nexus-node 0.1.2 → 0.1.3

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 +3 -3
  2. package/cli.js +43 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,6 @@ mcp-nexus serve
30
30
  mcp-nexus status
31
31
  ```
32
32
 
33
- DevSpace is installed as a package dependency. `cloudflared` must be available on
34
- the remote machine. `mcp-nexus pair` creates the control tunnel automatically
35
- when Cloudflare auth is present at `~/.cloudflared/cert.pem`.
33
+ DevSpace is installed as a package dependency. `mcp-nexus pair` auto-downloads
34
+ `cloudflared` on Linux x64/arm64 when missing. If Cloudflare is not authorized
35
+ yet, `pair` starts the login flow first.
package/cli.js CHANGED
@@ -14,6 +14,7 @@ const configPath = join(stateDir, "config.json");
14
14
  const projectsPath = join(stateDir, "projects.json");
15
15
  const tokensPath = join(stateDir, "tokens.json");
16
16
  const logDir = join(stateDir, "logs");
17
+ const binDir = join(stateDir, "bin");
17
18
  const nodeIdPath = join(stateDir, "node-id");
18
19
  const packageDir = dirname(fileURLToPath(import.meta.url));
19
20
  const require = createRequire(import.meta.url);
@@ -21,6 +22,7 @@ const require = createRequire(import.meta.url);
21
22
  function ensureState() {
22
23
  mkdirSync(stateDir, { recursive: true });
23
24
  mkdirSync(logDir, { recursive: true });
25
+ mkdirSync(binDir, { recursive: true });
24
26
  if (!existsSync(projectsPath)) saveJson(projectsPath, []);
25
27
  if (!existsSync(tokensPath)) saveJson(tokensPath, { adminToken: randomHex(32) });
26
28
  if (!existsSync(configPath)) saveJson(configPath, {
@@ -89,7 +91,11 @@ function run(cmd, args, options = {}) {
89
91
  }
90
92
 
91
93
  function commandExists(cmd) {
92
- return spawnSync("sh", ["-lc", `command -v ${quote(cmd)}`], { encoding: "utf8" }).status === 0;
94
+ return Boolean(commandPath(cmd));
95
+ }
96
+
97
+ function commandPath(cmd) {
98
+ return spawnSync("sh", ["-lc", `command -v ${quote(cmd)}`], { encoding: "utf8" }).stdout.trim();
93
99
  }
94
100
 
95
101
  function quote(value) {
@@ -125,10 +131,36 @@ function defaultControlHost() {
125
131
  }
126
132
 
127
133
  function findCloudflareTunnel(name) {
128
- const list = spawnSync("cloudflared", ["tunnel", "list"], { encoding: "utf8" }).stdout || "";
134
+ const list = spawnSync(cloudflaredCommand(), ["tunnel", "list"], { encoding: "utf8" }).stdout || "";
129
135
  return list.split("\n").map((line) => line.trim().split(/\s+/)).find((cols) => cols[1] === name)?.[0] || "";
130
136
  }
131
137
 
138
+ function cloudflaredDownloadUrl() {
139
+ if (process.platform === "linux" && process.arch === "x64") return "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64";
140
+ if (process.platform === "linux" && process.arch === "arm64") return "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64";
141
+ throw new Error(`cloudflared auto-install is only supported on Linux x64/arm64, got ${process.platform}/${process.arch}`);
142
+ }
143
+
144
+ function cloudflaredCommand() {
145
+ const installed = commandPath("cloudflared");
146
+ if (installed) return installed;
147
+ const local = join(binDir, "cloudflared");
148
+ if (existsSync(local)) return local;
149
+ ensureState();
150
+ console.log("Installing cloudflared...");
151
+ run("sh", ["-lc", `curl -L --fail -o ${quote(local)} ${quote(cloudflaredDownloadUrl())} && chmod +x ${quote(local)}`]);
152
+ return local;
153
+ }
154
+
155
+ function ensureCloudflareAuth() {
156
+ const certFile = join(home, ".cloudflared", "cert.pem");
157
+ if (existsSync(certFile)) return;
158
+ console.log("Cloudflare authorization required. Opening login flow...");
159
+ const out = spawnSync(cloudflaredCommand(), ["tunnel", "login"], { stdio: "inherit" });
160
+ if (out.status !== 0) throw new Error("cloudflared tunnel login failed");
161
+ if (!existsSync(certFile)) throw new Error(`cloudflared login did not create ${certFile}`);
162
+ }
163
+
132
164
  function json(res, status, body) {
133
165
  res.writeHead(status, {
134
166
  "content-type": "application/json; charset=utf-8",
@@ -168,7 +200,8 @@ function devspaceCommand() {
168
200
 
169
201
  function ensureTooling() {
170
202
  devspaceCommand();
171
- if (!commandExists("cloudflared")) throw new Error("cloudflared command not found");
203
+ cloudflaredCommand();
204
+ ensureCloudflareAuth();
172
205
  }
173
206
 
174
207
  function projectStatus(project) {
@@ -204,12 +237,12 @@ function setupProject(input) {
204
237
  const existing = findCloudflareTunnel(tunnelName);
205
238
  if (existing) tunnelId = existing;
206
239
  else {
207
- const created = run("cloudflared", ["tunnel", "create", tunnelName]);
240
+ const created = run(cloudflaredCommand(), ["tunnel", "create", tunnelName]);
208
241
  tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
209
242
  if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
210
243
  }
211
244
 
212
- run("cloudflared", ["tunnel", "route", "dns", tunnelName, hostnameValue]);
245
+ run(cloudflaredCommand(), ["tunnel", "route", "dns", tunnelName, hostnameValue]);
213
246
  const cfDir = join(home, ".cloudflared");
214
247
  const cfConfig = join(cfDir, `${subdomain}-config.yml`);
215
248
  const credFile = join(cfDir, `${tunnelId}.json`);
@@ -267,10 +300,8 @@ function setupProject(input) {
267
300
 
268
301
  function setupControl(hostnameValue) {
269
302
  ensureState();
270
- if (!commandExists("cloudflared")) throw new Error("cloudflared command not found");
271
303
  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`);
304
+ ensureCloudflareAuth();
274
305
  const host = String(hostnameValue || "").trim().toLowerCase();
275
306
  if (!/^[a-z0-9.-]+$/.test(host) || !host.includes(".")) throw new Error("usage: mcp-nexus setup-control <hostname>");
276
307
 
@@ -278,11 +309,11 @@ function setupControl(hostnameValue) {
278
309
  const tunnelName = `mcp-nexus-${sanitize(host)}-control`;
279
310
  let tunnelId = findCloudflareTunnel(tunnelName);
280
311
  if (!tunnelId) {
281
- const created = run("cloudflared", ["tunnel", "create", tunnelName]);
312
+ const created = run(cloudflaredCommand(), ["tunnel", "create", tunnelName]);
282
313
  tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
283
314
  if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
284
315
  }
285
- run("cloudflared", ["tunnel", "route", "dns", tunnelName, host]);
316
+ run(cloudflaredCommand(), ["tunnel", "route", "dns", tunnelName, host]);
286
317
 
287
318
  const cfConfig = join(cfDir, `${tunnelName}.yml`);
288
319
  const credFile = join(cfDir, `${tunnelId}.json`);
@@ -297,7 +328,7 @@ function setupControl(hostnameValue) {
297
328
  "",
298
329
  ].join("\n"));
299
330
 
300
- const child = spawn("cloudflared", ["tunnel", "--protocol", "http2", "--config", cfConfig, "run"], {
331
+ const child = spawn(cloudflaredCommand(), ["tunnel", "--protocol", "http2", "--config", cfConfig, "run"], {
301
332
  detached: true,
302
333
  stdio: ["ignore", "ignore", "ignore"],
303
334
  });
@@ -339,7 +370,7 @@ function startProject(name) {
339
370
  DEVSPACE_CONFIG_DIR: project.config_dir,
340
371
  }, `${name}.out`);
341
372
  if (project.cloudflare_config) {
342
- project.cloudflaredPid = spawnLogged(project, "cloudflared", ["tunnel", "--protocol", "http2", "--config", project.cloudflare_config, "run"], {}, `${name}-cloudflared.out`);
373
+ project.cloudflaredPid = spawnLogged(project, cloudflaredCommand(), ["tunnel", "--protocol", "http2", "--config", project.cloudflare_config, "run"], {}, `${name}-cloudflared.out`);
343
374
  }
344
375
  saveProjects(items);
345
376
  return projectStatus(project);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoer7788/mcp-nexus-node",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Remote node for MCP Nexus pairing and DevSpace project control.",
5
5
  "license": "MIT",
6
6
  "type": "module",