@zoer7788/mcp-nexus-node 0.1.0 → 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.
- package/README.md +3 -3
- package/cli.js +111 -11
- 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
|
|
|
@@ -31,5 +30,6 @@ mcp-nexus serve
|
|
|
31
30
|
mcp-nexus status
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
DevSpace is installed as a package dependency. `
|
|
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, {
|
|
@@ -63,6 +65,11 @@ function config() {
|
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
function saveConfigPatch(patch) {
|
|
69
|
+
ensureState();
|
|
70
|
+
saveJson(configPath, { ...loadJson(configPath, {}), ...patch });
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
function tokens() {
|
|
67
74
|
ensureState();
|
|
68
75
|
return loadJson(tokensPath, {});
|
|
@@ -84,7 +91,11 @@ function run(cmd, args, options = {}) {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
function commandExists(cmd) {
|
|
87
|
-
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();
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
function quote(value) {
|
|
@@ -113,6 +124,43 @@ function adminUrl() {
|
|
|
113
124
|
return cfg.adminUrl || `http://${localIp()}:${cfg.port || 9876}`;
|
|
114
125
|
}
|
|
115
126
|
|
|
127
|
+
function defaultControlHost() {
|
|
128
|
+
const cfg = config();
|
|
129
|
+
const id = readFileSync(nodeIdPath, "utf8").trim().slice(0, 8);
|
|
130
|
+
return `mcpnexus-${sanitize(hostname()).slice(0, 24)}-${id}.${cfg.baseDomain || "omji.top"}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findCloudflareTunnel(name) {
|
|
134
|
+
const list = spawnSync(cloudflaredCommand(), ["tunnel", "list"], { encoding: "utf8" }).stdout || "";
|
|
135
|
+
return list.split("\n").map((line) => line.trim().split(/\s+/)).find((cols) => cols[1] === name)?.[0] || "";
|
|
136
|
+
}
|
|
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
|
+
|
|
116
164
|
function json(res, status, body) {
|
|
117
165
|
res.writeHead(status, {
|
|
118
166
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -152,7 +200,8 @@ function devspaceCommand() {
|
|
|
152
200
|
|
|
153
201
|
function ensureTooling() {
|
|
154
202
|
devspaceCommand();
|
|
155
|
-
|
|
203
|
+
cloudflaredCommand();
|
|
204
|
+
ensureCloudflareAuth();
|
|
156
205
|
}
|
|
157
206
|
|
|
158
207
|
function projectStatus(project) {
|
|
@@ -168,7 +217,10 @@ function setupProject(input) {
|
|
|
168
217
|
const name = String(input.name || "").trim();
|
|
169
218
|
const root = String(input.root || input.path || "").trim();
|
|
170
219
|
if (!name || !root) throw new Error("name and root are required");
|
|
171
|
-
if (!existsSync(root))
|
|
220
|
+
if (!existsSync(root)) {
|
|
221
|
+
if (input.createRoot) mkdirSync(root, { recursive: true });
|
|
222
|
+
else throw new Error(`root not found: ${root}`);
|
|
223
|
+
}
|
|
172
224
|
const items = projects();
|
|
173
225
|
if (items.some((p) => p.name === name)) throw new Error(`project already exists: ${name}`);
|
|
174
226
|
|
|
@@ -182,16 +234,15 @@ function setupProject(input) {
|
|
|
182
234
|
const tunnelName = `devspace-${subdomain}`;
|
|
183
235
|
|
|
184
236
|
let tunnelId = "";
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (existing) tunnelId = existing[0];
|
|
237
|
+
const existing = findCloudflareTunnel(tunnelName);
|
|
238
|
+
if (existing) tunnelId = existing;
|
|
188
239
|
else {
|
|
189
|
-
const created = run(
|
|
240
|
+
const created = run(cloudflaredCommand(), ["tunnel", "create", tunnelName]);
|
|
190
241
|
tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
|
|
191
242
|
if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
|
|
192
243
|
}
|
|
193
244
|
|
|
194
|
-
run(
|
|
245
|
+
run(cloudflaredCommand(), ["tunnel", "route", "dns", tunnelName, hostnameValue]);
|
|
195
246
|
const cfDir = join(home, ".cloudflared");
|
|
196
247
|
const cfConfig = join(cfDir, `${subdomain}-config.yml`);
|
|
197
248
|
const credFile = join(cfDir, `${tunnelId}.json`);
|
|
@@ -243,10 +294,57 @@ function setupProject(input) {
|
|
|
243
294
|
};
|
|
244
295
|
items.push(project);
|
|
245
296
|
saveProjects(items);
|
|
246
|
-
startProject(name);
|
|
297
|
+
if (input.autostart !== false) startProject(name);
|
|
247
298
|
return projectStatus(findProject(name));
|
|
248
299
|
}
|
|
249
300
|
|
|
301
|
+
function setupControl(hostnameValue) {
|
|
302
|
+
ensureState();
|
|
303
|
+
const cfDir = join(home, ".cloudflared");
|
|
304
|
+
ensureCloudflareAuth();
|
|
305
|
+
const host = String(hostnameValue || "").trim().toLowerCase();
|
|
306
|
+
if (!/^[a-z0-9.-]+$/.test(host) || !host.includes(".")) throw new Error("usage: mcp-nexus setup-control <hostname>");
|
|
307
|
+
|
|
308
|
+
const cfg = config();
|
|
309
|
+
const tunnelName = `mcp-nexus-${sanitize(host)}-control`;
|
|
310
|
+
let tunnelId = findCloudflareTunnel(tunnelName);
|
|
311
|
+
if (!tunnelId) {
|
|
312
|
+
const created = run(cloudflaredCommand(), ["tunnel", "create", tunnelName]);
|
|
313
|
+
tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
|
|
314
|
+
if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
|
|
315
|
+
}
|
|
316
|
+
run(cloudflaredCommand(), ["tunnel", "route", "dns", tunnelName, host]);
|
|
317
|
+
|
|
318
|
+
const cfConfig = join(cfDir, `${tunnelName}.yml`);
|
|
319
|
+
const credFile = join(cfDir, `${tunnelId}.json`);
|
|
320
|
+
writeFileSync(cfConfig, [
|
|
321
|
+
`tunnel: ${tunnelId}`,
|
|
322
|
+
`credentials-file: ${credFile}`,
|
|
323
|
+
"",
|
|
324
|
+
"ingress:",
|
|
325
|
+
` - hostname: ${host}`,
|
|
326
|
+
` service: http://127.0.0.1:${cfg.port || 9876}`,
|
|
327
|
+
" - service: http_status:404",
|
|
328
|
+
"",
|
|
329
|
+
].join("\n"));
|
|
330
|
+
|
|
331
|
+
const child = spawn(cloudflaredCommand(), ["tunnel", "--protocol", "http2", "--config", cfConfig, "run"], {
|
|
332
|
+
detached: true,
|
|
333
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
334
|
+
});
|
|
335
|
+
child.unref();
|
|
336
|
+
saveConfigPatch({ adminUrl: `https://${host}` });
|
|
337
|
+
console.log(`Admin URL: https://${host}`);
|
|
338
|
+
console.log(`Control tunnel: ${tunnelName}`);
|
|
339
|
+
console.log("Next: mcp-nexus pair");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function ensureControl() {
|
|
343
|
+
const cfg = config();
|
|
344
|
+
if (cfg.adminUrl || process.env.MCP_NEXUS_ADMIN_URL) return;
|
|
345
|
+
setupControl(defaultControlHost());
|
|
346
|
+
}
|
|
347
|
+
|
|
250
348
|
function spawnLogged(project, cmd, args, env, logName) {
|
|
251
349
|
const logPath = join(logDir, logName);
|
|
252
350
|
const child = spawn(cmd, args, {
|
|
@@ -272,7 +370,7 @@ function startProject(name) {
|
|
|
272
370
|
DEVSPACE_CONFIG_DIR: project.config_dir,
|
|
273
371
|
}, `${name}.out`);
|
|
274
372
|
if (project.cloudflare_config) {
|
|
275
|
-
project.cloudflaredPid = spawnLogged(project,
|
|
373
|
+
project.cloudflaredPid = spawnLogged(project, cloudflaredCommand(), ["tunnel", "--protocol", "http2", "--config", project.cloudflare_config, "run"], {}, `${name}-cloudflared.out`);
|
|
276
374
|
}
|
|
277
375
|
saveProjects(items);
|
|
278
376
|
return projectStatus(project);
|
|
@@ -373,6 +471,7 @@ function startServeDetached() {
|
|
|
373
471
|
|
|
374
472
|
async function pair() {
|
|
375
473
|
ensureState();
|
|
474
|
+
ensureControl();
|
|
376
475
|
const cfg = config();
|
|
377
476
|
const pairCode = String(Math.floor(100000 + Math.random() * 900000));
|
|
378
477
|
const pairingSecret = randomHex(16);
|
|
@@ -405,7 +504,8 @@ function status() {
|
|
|
405
504
|
const command = process.argv[2] || "help";
|
|
406
505
|
if (command === "serve") serve();
|
|
407
506
|
else if (command === "pair") pair().catch((error) => { console.error(error.message); process.exit(1); });
|
|
507
|
+
else if (command === "setup-control") { try { setupControl(process.argv[3]); } catch (error) { console.error(error.message); process.exit(1); } }
|
|
408
508
|
else if (command === "status") status();
|
|
409
509
|
else {
|
|
410
|
-
console.log("Usage: mcp-nexus <pair|serve|status>");
|
|
510
|
+
console.log("Usage: mcp-nexus <setup-control|pair|serve|status>");
|
|
411
511
|
}
|