@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.
- package/README.md +2 -2
- package/cli.js +75 -6
- 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
|
|
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))
|
|
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
|
|
186
|
-
|
|
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
|
}
|