@zoer7788/mcp-nexus-node 0.1.2 → 0.1.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 +3 -3
- package/cli.js +63 -12
- 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. `
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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);
|
|
@@ -372,6 +403,25 @@ function patchProject(name, body) {
|
|
|
372
403
|
return { ...projectStatus(project), config: cfg, restart_required: true };
|
|
373
404
|
}
|
|
374
405
|
|
|
406
|
+
function renameProject(name, body) {
|
|
407
|
+
const newName = String(body.name || "").trim();
|
|
408
|
+
if (!newName) throw new Error("new name is required");
|
|
409
|
+
const items = projects();
|
|
410
|
+
const project = items.find((p) => p.name === name);
|
|
411
|
+
if (!project) throw new Error(`project not found: ${name}`);
|
|
412
|
+
if (items.some((p) => p.name === newName)) throw new Error(`project already exists: ${newName}`);
|
|
413
|
+
project.name = newName;
|
|
414
|
+
saveProjects(items);
|
|
415
|
+
for (const suffix of [".out", "-cloudflared.out"]) {
|
|
416
|
+
const oldPath = join(logDir, `${name}${suffix}`);
|
|
417
|
+
const newPath = join(logDir, `${newName}${suffix}`);
|
|
418
|
+
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
419
|
+
try { writeFileSync(newPath, readFileSync(oldPath)); } catch {}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return projectStatus(project);
|
|
423
|
+
}
|
|
424
|
+
|
|
375
425
|
function deleteProject(name) {
|
|
376
426
|
stopProject(name);
|
|
377
427
|
saveProjects(projects().filter((p) => p.name !== name));
|
|
@@ -402,6 +452,7 @@ function serve() {
|
|
|
402
452
|
const name = decodeURIComponent(match[1]);
|
|
403
453
|
const action = match[2] || "";
|
|
404
454
|
if (req.method === "PATCH" && action === "config") return json(res, 200, patchProject(name, await readJson(req)));
|
|
455
|
+
if (req.method === "PATCH" && action === "name") return json(res, 200, renameProject(name, await readJson(req)));
|
|
405
456
|
if (req.method === "POST" && action === "start") return json(res, 200, startProject(name));
|
|
406
457
|
if (req.method === "POST" && action === "stop") return json(res, 200, stopProject(name));
|
|
407
458
|
if (req.method === "POST" && action === "restart") { stopProject(name); return json(res, 200, startProject(name)); }
|