@zoer7788/mcp-nexus-node 0.1.0
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 +35 -0
- package/cli.js +411 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# MCP Nexus Node
|
|
2
|
+
|
|
3
|
+
Remote node for MCP Nexus.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @zoer7788/mcp-nexus-node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Pair with local MCP Nexus
|
|
12
|
+
|
|
13
|
+
On `pub-118`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @zoer7788/mcp-nexus-node
|
|
17
|
+
MCP_NEXUS_ADMIN_URL=https://mcpnexus-pub118.omji.top \
|
|
18
|
+
mcp-nexus pair
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Enter the printed 6-digit code in the local MCP Nexus app.
|
|
22
|
+
|
|
23
|
+
After pairing, create a DevSpace project in MCP Nexus. Copy the returned
|
|
24
|
+
`https://.../mcp` URL into ChatGPT.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
mcp-nexus pair
|
|
30
|
+
mcp-nexus serve
|
|
31
|
+
mcp-nexus status
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
DevSpace is installed as a package dependency. `cloudflared` must be available on
|
|
35
|
+
the remote machine before creating public DevSpace projects.
|
package/cli.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, appendFileSync } from "node:fs";
|
|
6
|
+
import { homedir, hostname } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
|
|
11
|
+
const home = homedir();
|
|
12
|
+
const stateDir = process.env.MCP_NEXUS_NODE_DIR || join(home, ".mcp-nexus-node");
|
|
13
|
+
const configPath = join(stateDir, "config.json");
|
|
14
|
+
const projectsPath = join(stateDir, "projects.json");
|
|
15
|
+
const tokensPath = join(stateDir, "tokens.json");
|
|
16
|
+
const logDir = join(stateDir, "logs");
|
|
17
|
+
const nodeIdPath = join(stateDir, "node-id");
|
|
18
|
+
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
function ensureState() {
|
|
22
|
+
mkdirSync(stateDir, { recursive: true });
|
|
23
|
+
mkdirSync(logDir, { recursive: true });
|
|
24
|
+
if (!existsSync(projectsPath)) saveJson(projectsPath, []);
|
|
25
|
+
if (!existsSync(tokensPath)) saveJson(tokensPath, { adminToken: randomHex(32) });
|
|
26
|
+
if (!existsSync(configPath)) saveJson(configPath, {
|
|
27
|
+
name: hostname(),
|
|
28
|
+
host: "0.0.0.0",
|
|
29
|
+
port: Number(process.env.MCP_NEXUS_NODE_PORT || 9876),
|
|
30
|
+
baseDomain: process.env.MCP_BASE_DOMAIN || "omji.top",
|
|
31
|
+
relayUrl: process.env.MCP_NEXUS_RELAY_URL || "https://mcpnexus-relay-pub118.omji.top",
|
|
32
|
+
adminUrl: process.env.MCP_NEXUS_ADMIN_URL || "",
|
|
33
|
+
});
|
|
34
|
+
if (!existsSync(nodeIdPath)) writeFileSync(nodeIdPath, randomUUID() + "\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadJson(path, fallback) {
|
|
38
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return fallback; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveJson(path, data) {
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function randomHex(bytes) {
|
|
47
|
+
return randomBytes(bytes).toString("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function tokenHash(token) {
|
|
51
|
+
return createHash("sha256").update(token).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function config() {
|
|
55
|
+
ensureState();
|
|
56
|
+
const cfg = loadJson(configPath, {});
|
|
57
|
+
return {
|
|
58
|
+
...cfg,
|
|
59
|
+
port: Number(process.env.MCP_NEXUS_NODE_PORT || cfg.port || 9876),
|
|
60
|
+
baseDomain: process.env.MCP_BASE_DOMAIN || cfg.baseDomain || "omji.top",
|
|
61
|
+
relayUrl: process.env.MCP_NEXUS_RELAY_URL || cfg.relayUrl || "https://mcpnexus-relay-pub118.omji.top",
|
|
62
|
+
adminUrl: process.env.MCP_NEXUS_ADMIN_URL || cfg.adminUrl || "",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tokens() {
|
|
67
|
+
ensureState();
|
|
68
|
+
return loadJson(tokensPath, {});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function projects() {
|
|
72
|
+
ensureState();
|
|
73
|
+
return loadJson(projectsPath, []);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function saveProjects(items) {
|
|
77
|
+
saveJson(projectsPath, items);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function run(cmd, args, options = {}) {
|
|
81
|
+
const out = spawnSync(cmd, args, { encoding: "utf8", ...options });
|
|
82
|
+
if (out.status !== 0) throw new Error((out.stderr || out.stdout || `${cmd} failed`).trim());
|
|
83
|
+
return out.stdout.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function commandExists(cmd) {
|
|
87
|
+
return spawnSync("sh", ["-lc", `command -v ${quote(cmd)}`], { encoding: "utf8" }).status === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function quote(value) {
|
|
91
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sanitize(value) {
|
|
95
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]/g, "") || "project";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function freePort(start = 8787) {
|
|
99
|
+
for (let port = start; port < start + 200; port += 1) {
|
|
100
|
+
const out = spawnSync("sh", ["-lc", `lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1`]);
|
|
101
|
+
if (out.status !== 0) return port;
|
|
102
|
+
}
|
|
103
|
+
throw new Error("no free port found");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function localIp() {
|
|
107
|
+
const out = spawnSync("sh", ["-lc", "hostname -I 2>/dev/null | awk '{print $1}'"], { encoding: "utf8" });
|
|
108
|
+
return out.stdout.trim() || "127.0.0.1";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function adminUrl() {
|
|
112
|
+
const cfg = config();
|
|
113
|
+
return cfg.adminUrl || `http://${localIp()}:${cfg.port || 9876}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function json(res, status, body) {
|
|
117
|
+
res.writeHead(status, {
|
|
118
|
+
"content-type": "application/json; charset=utf-8",
|
|
119
|
+
"access-control-allow-origin": "*",
|
|
120
|
+
"access-control-allow-headers": "content-type, authorization",
|
|
121
|
+
"access-control-allow-methods": "GET,POST,PATCH,DELETE,OPTIONS",
|
|
122
|
+
});
|
|
123
|
+
res.end(JSON.stringify(body));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readJson(req) {
|
|
127
|
+
const chunks = [];
|
|
128
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
129
|
+
if (chunks.length === 0) return {};
|
|
130
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requireAuth(req) {
|
|
134
|
+
const header = req.headers.authorization || "";
|
|
135
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
136
|
+
return token && token === tokens().adminToken;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findProject(name) {
|
|
140
|
+
return projects().find((p) => p.name === name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function devspaceCommand() {
|
|
144
|
+
const explicit = process.env.DEVSPACE_CLI;
|
|
145
|
+
if (explicit && existsSync(explicit)) return ["node", explicit];
|
|
146
|
+
try { return ["node", require.resolve("@waishnav/devspace/dist/cli.js")]; } catch {}
|
|
147
|
+
const bundled = join(packageDir, "vendor", "devspace", "dist", "cli.js");
|
|
148
|
+
if (existsSync(bundled)) return ["node", bundled];
|
|
149
|
+
if (commandExists("devspace")) return ["devspace"];
|
|
150
|
+
throw new Error("devspace command not found");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureTooling() {
|
|
154
|
+
devspaceCommand();
|
|
155
|
+
if (!commandExists("cloudflared")) throw new Error("cloudflared command not found");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function projectStatus(project) {
|
|
159
|
+
let running = false;
|
|
160
|
+
if (project.devspacePid) {
|
|
161
|
+
try { process.kill(project.devspacePid, 0); running = true; } catch {}
|
|
162
|
+
}
|
|
163
|
+
return { ...project, running, token_label: "Owner password" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setupProject(input) {
|
|
167
|
+
ensureTooling();
|
|
168
|
+
const name = String(input.name || "").trim();
|
|
169
|
+
const root = String(input.root || input.path || "").trim();
|
|
170
|
+
if (!name || !root) throw new Error("name and root are required");
|
|
171
|
+
if (!existsSync(root)) throw new Error(`root not found: ${root}`);
|
|
172
|
+
const items = projects();
|
|
173
|
+
if (items.some((p) => p.name === name)) throw new Error(`project already exists: ${name}`);
|
|
174
|
+
|
|
175
|
+
const cfg = config();
|
|
176
|
+
const port = Number(input.port || 0) || freePort();
|
|
177
|
+
const base = sanitize(input.subdomain || name);
|
|
178
|
+
let subdomain = base;
|
|
179
|
+
let i = 1;
|
|
180
|
+
while (items.some((p) => p.subdomain === subdomain)) subdomain = `${base}${++i}`;
|
|
181
|
+
const hostnameValue = `${subdomain}.${cfg.baseDomain || "omji.top"}`;
|
|
182
|
+
const tunnelName = `devspace-${subdomain}`;
|
|
183
|
+
|
|
184
|
+
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];
|
|
188
|
+
else {
|
|
189
|
+
const created = run("cloudflared", ["tunnel", "create", tunnelName]);
|
|
190
|
+
tunnelId = (created.match(/[0-9a-f-]{36}/) || [])[0] || "";
|
|
191
|
+
if (!tunnelId) throw new Error(`could not read tunnel id: ${created}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
run("cloudflared", ["tunnel", "route", "dns", tunnelName, hostnameValue]);
|
|
195
|
+
const cfDir = join(home, ".cloudflared");
|
|
196
|
+
const cfConfig = join(cfDir, `${subdomain}-config.yml`);
|
|
197
|
+
const credFile = join(cfDir, `${tunnelId}.json`);
|
|
198
|
+
writeFileSync(cfConfig, [
|
|
199
|
+
`tunnel: ${tunnelId}`,
|
|
200
|
+
`credentials-file: ${credFile}`,
|
|
201
|
+
"",
|
|
202
|
+
"ingress:",
|
|
203
|
+
` - hostname: ${hostnameValue}`,
|
|
204
|
+
` service: http://127.0.0.1:${port}`,
|
|
205
|
+
` - hostname: "*.${cfg.baseDomain || "omji.top"}"`,
|
|
206
|
+
" service: http_status:404",
|
|
207
|
+
" - service: http_status:404",
|
|
208
|
+
"",
|
|
209
|
+
].join("\n"));
|
|
210
|
+
|
|
211
|
+
const ownerToken = randomHex(32);
|
|
212
|
+
const configDir = join(stateDir, "devspace-profiles", subdomain);
|
|
213
|
+
mkdirSync(configDir, { recursive: true });
|
|
214
|
+
saveJson(join(configDir, "config.json"), {
|
|
215
|
+
host: "127.0.0.1",
|
|
216
|
+
port,
|
|
217
|
+
allowedRoots: [root],
|
|
218
|
+
publicBaseUrl: `https://${hostnameValue}`,
|
|
219
|
+
allowedHosts: ["localhost", "127.0.0.1", "::1", hostnameValue],
|
|
220
|
+
minimalTools: false,
|
|
221
|
+
toolNaming: "short",
|
|
222
|
+
widgets: "full",
|
|
223
|
+
skillsEnabled: true,
|
|
224
|
+
});
|
|
225
|
+
saveJson(join(configDir, "auth.json"), { ownerToken });
|
|
226
|
+
chmodSync(join(configDir, "auth.json"), 0o600);
|
|
227
|
+
|
|
228
|
+
const project = {
|
|
229
|
+
name,
|
|
230
|
+
provider: "devspace",
|
|
231
|
+
root,
|
|
232
|
+
path: root,
|
|
233
|
+
port,
|
|
234
|
+
subdomain,
|
|
235
|
+
hostname: hostnameValue,
|
|
236
|
+
tunnel_name: tunnelName,
|
|
237
|
+
tunnel_id: tunnelId,
|
|
238
|
+
cloudflare_config: cfConfig,
|
|
239
|
+
config_dir: configDir,
|
|
240
|
+
token: ownerToken,
|
|
241
|
+
url: `https://${hostnameValue}/mcp`,
|
|
242
|
+
admin_url: `https://${hostnameValue}/dashboard`,
|
|
243
|
+
};
|
|
244
|
+
items.push(project);
|
|
245
|
+
saveProjects(items);
|
|
246
|
+
startProject(name);
|
|
247
|
+
return projectStatus(findProject(name));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function spawnLogged(project, cmd, args, env, logName) {
|
|
251
|
+
const logPath = join(logDir, logName);
|
|
252
|
+
const child = spawn(cmd, args, {
|
|
253
|
+
detached: true,
|
|
254
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
255
|
+
env: { ...process.env, ...env },
|
|
256
|
+
});
|
|
257
|
+
child.stdout.on("data", (buf) => appendFileSync(logPath, buf));
|
|
258
|
+
child.stderr.on("data", (buf) => appendFileSync(logPath, buf));
|
|
259
|
+
child.unref();
|
|
260
|
+
return child.pid;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function startProject(name) {
|
|
264
|
+
const items = projects();
|
|
265
|
+
const project = items.find((p) => p.name === name);
|
|
266
|
+
if (!project) throw new Error(`project not found: ${name}`);
|
|
267
|
+
if (project.devspacePid) {
|
|
268
|
+
try { process.kill(project.devspacePid, 0); return projectStatus(project); } catch {}
|
|
269
|
+
}
|
|
270
|
+
const [cmd, ...baseArgs] = devspaceCommand();
|
|
271
|
+
project.devspacePid = spawnLogged(project, cmd, [...baseArgs, "serve"], {
|
|
272
|
+
DEVSPACE_CONFIG_DIR: project.config_dir,
|
|
273
|
+
}, `${name}.out`);
|
|
274
|
+
if (project.cloudflare_config) {
|
|
275
|
+
project.cloudflaredPid = spawnLogged(project, "cloudflared", ["tunnel", "--protocol", "http2", "--config", project.cloudflare_config, "run"], {}, `${name}-cloudflared.out`);
|
|
276
|
+
}
|
|
277
|
+
saveProjects(items);
|
|
278
|
+
return projectStatus(project);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function stopProject(name) {
|
|
282
|
+
const items = projects();
|
|
283
|
+
const project = items.find((p) => p.name === name);
|
|
284
|
+
if (!project) throw new Error(`project not found: ${name}`);
|
|
285
|
+
for (const key of ["devspacePid", "cloudflaredPid"]) {
|
|
286
|
+
if (project[key]) {
|
|
287
|
+
try { process.kill(project[key]); } catch {}
|
|
288
|
+
delete project[key];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
saveProjects(items);
|
|
292
|
+
return projectStatus(project);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function patchProject(name, body) {
|
|
296
|
+
const items = projects();
|
|
297
|
+
const project = items.find((p) => p.name === name);
|
|
298
|
+
if (!project) throw new Error(`project not found: ${name}`);
|
|
299
|
+
const configFile = join(project.config_dir, "config.json");
|
|
300
|
+
const cfg = loadJson(configFile, {});
|
|
301
|
+
if (Array.isArray(body.allowedRoots)) cfg.allowedRoots = body.allowedRoots.map(String).filter(Boolean);
|
|
302
|
+
if (body.publicBaseUrl !== undefined) cfg.publicBaseUrl = body.publicBaseUrl || null;
|
|
303
|
+
if (Array.isArray(body.allowedHosts)) cfg.allowedHosts = body.allowedHosts.map(String).filter(Boolean);
|
|
304
|
+
saveJson(configFile, cfg);
|
|
305
|
+
return { ...projectStatus(project), config: cfg, restart_required: true };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function deleteProject(name) {
|
|
309
|
+
stopProject(name);
|
|
310
|
+
saveProjects(projects().filter((p) => p.name !== name));
|
|
311
|
+
return { ok: true, name };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function tail(name, lines = 80) {
|
|
315
|
+
const file = join(logDir, `${name}.out`);
|
|
316
|
+
if (!existsSync(file)) return "";
|
|
317
|
+
return readFileSync(file, "utf8").split("\n").slice(-lines).join("\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function serve() {
|
|
321
|
+
ensureState();
|
|
322
|
+
const cfg = config();
|
|
323
|
+
const server = http.createServer(async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
if (req.method === "OPTIONS") return json(res, 204, {});
|
|
326
|
+
if (req.method === "GET" && req.url === "/api/health") {
|
|
327
|
+
return json(res, 200, { ok: true, app: "mcp-nexus-node", nodeId: readFileSync(nodeIdPath, "utf8").trim(), name: cfg.name, version: "0.1.0" });
|
|
328
|
+
}
|
|
329
|
+
if (!req.url?.startsWith("/api/")) return json(res, 404, { ok: false, error: "not found" });
|
|
330
|
+
if (!requireAuth(req)) return json(res, 401, { ok: false, error: "unauthorized" });
|
|
331
|
+
if (req.method === "GET" && req.url === "/api/projects") return json(res, 200, { projects: projects().map(projectStatus) });
|
|
332
|
+
if (req.method === "POST" && req.url === "/api/projects") return json(res, 200, setupProject(await readJson(req)));
|
|
333
|
+
const match = req.url.match(/^\/api\/projects\/([^/]+)(?:\/([^?]+))?/);
|
|
334
|
+
if (match) {
|
|
335
|
+
const name = decodeURIComponent(match[1]);
|
|
336
|
+
const action = match[2] || "";
|
|
337
|
+
if (req.method === "PATCH" && action === "config") return json(res, 200, patchProject(name, await readJson(req)));
|
|
338
|
+
if (req.method === "POST" && action === "start") return json(res, 200, startProject(name));
|
|
339
|
+
if (req.method === "POST" && action === "stop") return json(res, 200, stopProject(name));
|
|
340
|
+
if (req.method === "POST" && action === "restart") { stopProject(name); return json(res, 200, startProject(name)); }
|
|
341
|
+
if (req.method === "GET" && action === "logs") return json(res, 200, { logs: tail(name) });
|
|
342
|
+
if (req.method === "DELETE" && !action) return json(res, 200, deleteProject(name));
|
|
343
|
+
}
|
|
344
|
+
json(res, 404, { ok: false, error: "not found" });
|
|
345
|
+
} catch (error) {
|
|
346
|
+
json(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
server.listen(cfg.port || 9876, cfg.host || "0.0.0.0", () => {
|
|
350
|
+
console.log(`mcp-nexus-node listening on http://${cfg.host || "0.0.0.0"}:${cfg.port || 9876}`);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function postJson(url, body) {
|
|
355
|
+
const response = await fetch(url, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "content-type": "application/json" },
|
|
358
|
+
body: JSON.stringify(body),
|
|
359
|
+
});
|
|
360
|
+
const jsonBody = await response.json().catch(() => ({}));
|
|
361
|
+
if (!response.ok) throw new Error(jsonBody.error || `HTTP ${response.status}`);
|
|
362
|
+
return jsonBody;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function startServeDetached() {
|
|
366
|
+
const child = spawn(process.argv[0], [fileURLToPath(import.meta.url), "serve"], {
|
|
367
|
+
detached: true,
|
|
368
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
369
|
+
env: process.env,
|
|
370
|
+
});
|
|
371
|
+
child.unref();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function pair() {
|
|
375
|
+
ensureState();
|
|
376
|
+
const cfg = config();
|
|
377
|
+
const pairCode = String(Math.floor(100000 + Math.random() * 900000));
|
|
378
|
+
const pairingSecret = randomHex(16);
|
|
379
|
+
startServeDetached();
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
381
|
+
const node = {
|
|
382
|
+
nodeId: readFileSync(nodeIdPath, "utf8").trim(),
|
|
383
|
+
name: cfg.name || hostname(),
|
|
384
|
+
adminUrl: adminUrl(),
|
|
385
|
+
token: tokens().adminToken,
|
|
386
|
+
tokenHash: tokenHash(tokens().adminToken),
|
|
387
|
+
};
|
|
388
|
+
await postJson(`${cfg.relayUrl.replace(/\/$/, "")}/pair/register`, { pairCode, pairingSecret, node });
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(`MCP Nexus pairing code: ${pairCode}`);
|
|
391
|
+
console.log(`Relay: ${cfg.relayUrl}`);
|
|
392
|
+
console.log(`Admin: ${node.adminUrl}`);
|
|
393
|
+
console.log("Enter this code in local MCP Nexus within 5 minutes.");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function status() {
|
|
397
|
+
ensureState();
|
|
398
|
+
console.log(JSON.stringify({
|
|
399
|
+
config: config(),
|
|
400
|
+
nodeId: readFileSync(nodeIdPath, "utf8").trim(),
|
|
401
|
+
projects: projects().map(projectStatus),
|
|
402
|
+
}, null, 2));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const command = process.argv[2] || "help";
|
|
406
|
+
if (command === "serve") serve();
|
|
407
|
+
else if (command === "pair") pair().catch((error) => { console.error(error.message); process.exit(1); });
|
|
408
|
+
else if (command === "status") status();
|
|
409
|
+
else {
|
|
410
|
+
console.log("Usage: mcp-nexus <pair|serve|status>");
|
|
411
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoer7788/mcp-nexus-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remote node for MCP Nexus pairing and DevSpace project control.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-nexus": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"check": "node --check cli.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@waishnav/devspace": "^1.0.3"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
}
|
|
23
|
+
}
|