mcp-for-i 0.1.11 → 0.1.13
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 +207 -159
- package/dist/controlplane/clientIntegrationService.js +292 -0
- package/dist/controlplane/commands.js +109 -0
- package/dist/controlplane/index.js +5 -118
- package/dist/controlplane/platform.js +79 -0
- package/dist/controlplane/runtimeService.js +57 -70
- package/dist/controlplane/server.js +39 -20
- package/dist/controlplane/serverShared.js +18 -0
- package/dist/controlplane/serviceManager.js +381 -0
- package/dist/controlplane/ui.js +121 -36
- package/dist/index.js +41 -1
- package/dist/security/credentialStore.js +134 -7
- package/docs/configuration.md +3 -0
- package/docs/installation.md +48 -6
- package/docs/security.md +9 -1
- package/package.json +2 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { startControlPlaneServer } from "./server.js";
|
|
2
|
+
import { openUrl } from "./platform.js";
|
|
3
|
+
import { getControlPlaneUrl } from "./serverShared.js";
|
|
4
|
+
import { disableBackgroundService, enableBackgroundService, ensureControlPlaneRunning, getServiceStatus } from "./serviceManager.js";
|
|
5
|
+
export async function runControlCommand(args, options) {
|
|
6
|
+
const stderr = options.stderr || process.stderr;
|
|
7
|
+
const command = normalizeCommand(args, options.defaultCommand || "serve");
|
|
8
|
+
const scriptPath = options.scriptPath;
|
|
9
|
+
const nodePath = options.nodePath || process.execPath;
|
|
10
|
+
switch (command) {
|
|
11
|
+
case "serve":
|
|
12
|
+
await serve(stderr);
|
|
13
|
+
return;
|
|
14
|
+
case "status":
|
|
15
|
+
await printStatus(stderr, scriptPath, nodePath);
|
|
16
|
+
return;
|
|
17
|
+
case "open":
|
|
18
|
+
await openControlUi(stderr, scriptPath, nodePath);
|
|
19
|
+
return;
|
|
20
|
+
case "enable":
|
|
21
|
+
await enableService(stderr, scriptPath, nodePath);
|
|
22
|
+
return;
|
|
23
|
+
case "disable":
|
|
24
|
+
await disableService(stderr, scriptPath, nodePath);
|
|
25
|
+
return;
|
|
26
|
+
case "help":
|
|
27
|
+
default:
|
|
28
|
+
printHelp(stderr);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function serve(stderr) {
|
|
33
|
+
try {
|
|
34
|
+
const server = await startControlPlaneServer();
|
|
35
|
+
stderr.write(`mcp-for-i control plane running at http://${server.host}:${server.port}\n`);
|
|
36
|
+
const shutdown = async () => {
|
|
37
|
+
await server.close();
|
|
38
|
+
process.exit(0);
|
|
39
|
+
};
|
|
40
|
+
process.on("SIGINT", shutdown);
|
|
41
|
+
process.on("SIGTERM", shutdown);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err?.code === "EADDRINUSE") {
|
|
45
|
+
const url = getControlPlaneUrl();
|
|
46
|
+
stderr.write(`mcp-for-i control plane already running at ${url}\n`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function printStatus(stderr, scriptPath, nodePath) {
|
|
53
|
+
const url = getControlPlaneUrl();
|
|
54
|
+
const status = await getServiceStatus(scriptPath, nodePath);
|
|
55
|
+
stderr.write(`Control plane URL: ${url}\n`);
|
|
56
|
+
stderr.write(`Platform: ${status.label} (${status.arch})\n`);
|
|
57
|
+
stderr.write(`Running: ${status.running ? "yes" : "no"}\n`);
|
|
58
|
+
stderr.write(`Background service: ${status.enabled ? "enabled" : "disabled"} via ${status.managedBy}\n`);
|
|
59
|
+
if (status.target)
|
|
60
|
+
stderr.write(`Target: ${status.target}\n`);
|
|
61
|
+
if (status.detail)
|
|
62
|
+
stderr.write(`Detail: ${status.detail}\n`);
|
|
63
|
+
}
|
|
64
|
+
async function openControlUi(stderr, scriptPath, nodePath) {
|
|
65
|
+
const url = getControlPlaneUrl();
|
|
66
|
+
const result = await ensureControlPlaneRunning(scriptPath, nodePath);
|
|
67
|
+
if (result.started) {
|
|
68
|
+
stderr.write(`Started control plane at ${url}\n`);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await openUrl(url);
|
|
72
|
+
stderr.write(`Opening ${url}\n`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
stderr.write(`Control plane is running at ${url}\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function enableService(stderr, scriptPath, nodePath) {
|
|
79
|
+
const status = await enableBackgroundService(scriptPath, nodePath);
|
|
80
|
+
await ensureControlPlaneRunning(scriptPath, nodePath);
|
|
81
|
+
stderr.write(`Enabled background service via ${status.managedBy}\n`);
|
|
82
|
+
if (status.target)
|
|
83
|
+
stderr.write(`Target: ${status.target}\n`);
|
|
84
|
+
if (status.detail)
|
|
85
|
+
stderr.write(`Detail: ${status.detail}\n`);
|
|
86
|
+
}
|
|
87
|
+
async function disableService(stderr, scriptPath, nodePath) {
|
|
88
|
+
const status = await disableBackgroundService(scriptPath, nodePath);
|
|
89
|
+
stderr.write(`Disabled background service (${status.managedBy})\n`);
|
|
90
|
+
if (status.target)
|
|
91
|
+
stderr.write(`Target: ${status.target}\n`);
|
|
92
|
+
}
|
|
93
|
+
function printHelp(stderr) {
|
|
94
|
+
stderr.write(`mcp-for-i-control commands:\n`);
|
|
95
|
+
stderr.write(` serve Start control plane server in the foreground\n`);
|
|
96
|
+
stderr.write(` open Start control plane in the background if needed, then open UI\n`);
|
|
97
|
+
stderr.write(` status Show running and background-service status\n`);
|
|
98
|
+
stderr.write(` enable Enable background service for this platform\n`);
|
|
99
|
+
stderr.write(` disable Disable background service for this platform\n`);
|
|
100
|
+
stderr.write(` help Show this help text\n`);
|
|
101
|
+
}
|
|
102
|
+
function normalizeCommand(args, fallback) {
|
|
103
|
+
const first = (args[0] || fallback).toLowerCase();
|
|
104
|
+
if (!first || first === "start")
|
|
105
|
+
return fallback;
|
|
106
|
+
if (first === "--help" || first === "-h")
|
|
107
|
+
return "help";
|
|
108
|
+
return first;
|
|
109
|
+
}
|
|
@@ -1,120 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startControlPlaneServer } from "./server.js";
|
|
3
|
-
import { spawn } from "child_process";
|
|
4
2
|
import { fileURLToPath } from "url";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
break;
|
|
11
|
-
case "setup":
|
|
12
|
-
await setupAutostart();
|
|
13
|
-
break;
|
|
14
|
-
case "status":
|
|
15
|
-
await printStatus();
|
|
16
|
-
break;
|
|
17
|
-
case "remove":
|
|
18
|
-
case "uninstall":
|
|
19
|
-
await removeAutostart();
|
|
20
|
-
break;
|
|
21
|
-
case "open":
|
|
22
|
-
await openUi();
|
|
23
|
-
break;
|
|
24
|
-
case "help":
|
|
25
|
-
default:
|
|
26
|
-
printHelp();
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
async function serve() {
|
|
30
|
-
try {
|
|
31
|
-
const server = await startControlPlaneServer();
|
|
32
|
-
process.stderr.write(`mcp-for-i control plane running at http://${server.host}:${server.port}\n`);
|
|
33
|
-
const shutdown = async () => {
|
|
34
|
-
await server.close();
|
|
35
|
-
process.exit(0);
|
|
36
|
-
};
|
|
37
|
-
process.on("SIGINT", shutdown);
|
|
38
|
-
process.on("SIGTERM", shutdown);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
if (err?.code === "EADDRINUSE") {
|
|
42
|
-
const url = getControlPlaneUrl();
|
|
43
|
-
process.stderr.write(`mcp-for-i control plane already running at ${url}\n`);
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function setupAutostart() {
|
|
50
|
-
if (process.platform !== "win32") {
|
|
51
|
-
process.stderr.write("Autostart setup is currently implemented for Windows only.\n");
|
|
52
|
-
process.stderr.write(`Run manually with: mcp-for-i-control serve\n`);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const scriptPath = fileURLToPath(import.meta.url);
|
|
56
|
-
await configureWindowsAutostart(scriptPath, process.execPath);
|
|
57
|
-
process.stderr.write(`Autostart configured (${AUTOSTART_TASK_NAME}).\n`);
|
|
58
|
-
process.stderr.write(`Control plane URL: ${getControlPlaneUrl()}\n`);
|
|
59
|
-
}
|
|
60
|
-
async function removeAutostart() {
|
|
61
|
-
if (process.platform !== "win32") {
|
|
62
|
-
process.stderr.write("Autostart removal is currently implemented for Windows only.\n");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
await removeWindowsAutostart();
|
|
67
|
-
process.stderr.write(`Autostart removed (${AUTOSTART_TASK_NAME}).\n`);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
const msg = err?.message || String(err);
|
|
71
|
-
if (msg.includes("cannot find the file specified") || msg.includes("ERROR: The system cannot find")) {
|
|
72
|
-
process.stderr.write(`Autostart task not found (${AUTOSTART_TASK_NAME}).\n`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
throw err;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function printStatus() {
|
|
79
|
-
const url = getControlPlaneUrl();
|
|
80
|
-
const running = await isControlPlaneRunning(url);
|
|
81
|
-
process.stderr.write(`Control plane URL: ${url}\n`);
|
|
82
|
-
process.stderr.write(`Running: ${running ? "yes" : "no"}\n`);
|
|
83
|
-
if (process.platform === "win32") {
|
|
84
|
-
const auto = await getWindowsAutostartStatus();
|
|
85
|
-
process.stderr.write(`Autostart task: ${auto.installed ? "installed" : "not installed"}\n`);
|
|
86
|
-
if (auto.state) {
|
|
87
|
-
process.stderr.write(`Autostart state: ${auto.state}\n`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
process.stderr.write("Autostart task: platform not managed by this command\n");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
async function openUi() {
|
|
95
|
-
const url = getControlPlaneUrl();
|
|
96
|
-
if (process.platform === "win32") {
|
|
97
|
-
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
98
|
-
}
|
|
99
|
-
else if (process.platform === "darwin") {
|
|
100
|
-
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
104
|
-
}
|
|
105
|
-
process.stderr.write(`Opening ${url}\n`);
|
|
106
|
-
}
|
|
107
|
-
function printHelp() {
|
|
108
|
-
process.stderr.write(`mcp-for-i-control commands:\n`);
|
|
109
|
-
process.stderr.write(` serve Start control plane server (foreground)\n`);
|
|
110
|
-
process.stderr.write(` setup Configure autostart at login and launch now\n`);
|
|
111
|
-
process.stderr.write(` status Show running/autostart status\n`);
|
|
112
|
-
process.stderr.write(` remove Remove autostart configuration\n`);
|
|
113
|
-
process.stderr.write(` open Open UI URL in default browser\n`);
|
|
114
|
-
}
|
|
115
|
-
function normalizeCommand(args) {
|
|
116
|
-
const first = (args[0] || "serve").toLowerCase();
|
|
117
|
-
if (!first || first === "start")
|
|
118
|
-
return "serve";
|
|
119
|
-
return first;
|
|
120
|
-
}
|
|
3
|
+
import { runControlCommand } from "./commands.js";
|
|
4
|
+
await runControlCommand(process.argv.slice(2), {
|
|
5
|
+
defaultCommand: "serve",
|
|
6
|
+
scriptPath: fileURLToPath(import.meta.url)
|
|
7
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
export function detectWsl(platform = process.platform, release = os.release(), env = process.env) {
|
|
4
|
+
if (platform !== "linux")
|
|
5
|
+
return false;
|
|
6
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP)
|
|
7
|
+
return true;
|
|
8
|
+
return /microsoft/i.test(release);
|
|
9
|
+
}
|
|
10
|
+
export function getPlatformDescriptor(platform = process.platform, arch = process.arch, release = os.release(), env = process.env) {
|
|
11
|
+
const isWsl = detectWsl(platform, release, env);
|
|
12
|
+
return {
|
|
13
|
+
platform,
|
|
14
|
+
arch,
|
|
15
|
+
isWsl,
|
|
16
|
+
label: describePlatform(platform, isWsl)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function getOpenUrlCommands(url, descriptor = getPlatformDescriptor()) {
|
|
20
|
+
if (descriptor.platform === "win32") {
|
|
21
|
+
return [
|
|
22
|
+
{ command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] },
|
|
23
|
+
{ command: "explorer.exe", args: [url] }
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
if (descriptor.platform === "darwin") {
|
|
27
|
+
return [{ command: "open", args: [url] }];
|
|
28
|
+
}
|
|
29
|
+
if (descriptor.isWsl) {
|
|
30
|
+
return [
|
|
31
|
+
{ command: "wslview", args: [url] },
|
|
32
|
+
{ command: "explorer.exe", args: [url] },
|
|
33
|
+
{ command: "xdg-open", args: [url] }
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
{ command: "xdg-open", args: [url] },
|
|
38
|
+
{ command: "gio", args: ["open", url] }
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export async function openUrl(url, descriptor = getPlatformDescriptor()) {
|
|
42
|
+
const candidates = getOpenUrlCommands(url, descriptor);
|
|
43
|
+
let lastError;
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
await spawnDetached(candidate.command, candidate.args);
|
|
47
|
+
return candidate.command;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw lastError || new Error(`No browser launcher available for ${descriptor.label}`);
|
|
54
|
+
}
|
|
55
|
+
function spawnDetached(command, args) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn(command, args, {
|
|
58
|
+
detached: true,
|
|
59
|
+
stdio: "ignore",
|
|
60
|
+
windowsHide: true
|
|
61
|
+
});
|
|
62
|
+
child.once("error", reject);
|
|
63
|
+
child.once("spawn", () => {
|
|
64
|
+
child.unref();
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function describePlatform(platform, isWsl) {
|
|
70
|
+
if (platform === "win32")
|
|
71
|
+
return "Windows";
|
|
72
|
+
if (platform === "darwin")
|
|
73
|
+
return "macOS";
|
|
74
|
+
if (isWsl)
|
|
75
|
+
return "Linux (WSL)";
|
|
76
|
+
if (platform === "linux")
|
|
77
|
+
return "Linux";
|
|
78
|
+
return platform;
|
|
79
|
+
}
|
|
@@ -3,15 +3,15 @@ import fs from "fs/promises";
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import {
|
|
6
|
+
import { getPlatformDescriptor } from "./platform.js";
|
|
7
|
+
import { disableBackgroundService, enableBackgroundService, getServiceStatus } from "./serviceManager.js";
|
|
7
8
|
export class RuntimeService {
|
|
8
9
|
rootDir;
|
|
9
10
|
state = {
|
|
10
|
-
install: makeJob("install"),
|
|
11
11
|
updateMcp: makeJob("updateMcp"),
|
|
12
12
|
updateSkills: makeJob("updateSkills"),
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
enableService: makeJob("enableService"),
|
|
14
|
+
disableService: makeJob("disableService")
|
|
15
15
|
};
|
|
16
16
|
constructor(rootDir) {
|
|
17
17
|
this.rootDir = rootDir;
|
|
@@ -68,17 +68,6 @@ export class RuntimeService {
|
|
|
68
68
|
skills
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
async installOrRepair() {
|
|
72
|
-
return this.runJob("install", async (push) => {
|
|
73
|
-
const mode = await this.detectExecutionMode(push);
|
|
74
|
-
if (mode === "git-checkout") {
|
|
75
|
-
await this.runNpm(["install"], this.rootDir, push);
|
|
76
|
-
await this.buildIfPresent(push);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
await this.runGlobalPackageUpdate(push, "install");
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
71
|
async updateMcp() {
|
|
83
72
|
return this.runJob("updateMcp", async (push) => {
|
|
84
73
|
const mode = await this.detectExecutionMode(push);
|
|
@@ -135,57 +124,33 @@ export class RuntimeService {
|
|
|
135
124
|
}
|
|
136
125
|
});
|
|
137
126
|
}
|
|
138
|
-
async
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await configureWindowsAutostart(scriptPath, process.execPath);
|
|
145
|
-
push(`Autostart configured (${AUTOSTART_TASK_NAME}).`);
|
|
146
|
-
push(`Control plane URL: ${getControlPlaneUrl()}`);
|
|
147
|
-
});
|
|
127
|
+
async getEnvironmentInfo() {
|
|
128
|
+
return {
|
|
129
|
+
...getPlatformDescriptor(),
|
|
130
|
+
commandMode: "portable",
|
|
131
|
+
updateMode: await this.detectExecutionModeQuiet()
|
|
132
|
+
};
|
|
148
133
|
}
|
|
149
|
-
async
|
|
150
|
-
return this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (msg.includes("cannot find the file specified") || msg.includes("ERROR: The system cannot find")) {
|
|
161
|
-
push(`Autostart task not found (${AUTOSTART_TASK_NAME}).`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
throw err;
|
|
165
|
-
}
|
|
134
|
+
async getServiceStatus() {
|
|
135
|
+
return getServiceStatus(this.getControlPlaneScriptPath(), process.execPath);
|
|
136
|
+
}
|
|
137
|
+
async enableService() {
|
|
138
|
+
return this.runJob("enableService", async (push) => {
|
|
139
|
+
const status = await enableBackgroundService(this.getControlPlaneScriptPath(), process.execPath);
|
|
140
|
+
push(`Enabled background service via ${status.managedBy}.`);
|
|
141
|
+
if (status.target)
|
|
142
|
+
push(`Target: ${status.target}`);
|
|
143
|
+
if (status.detail)
|
|
144
|
+
push(status.detail);
|
|
166
145
|
});
|
|
167
146
|
}
|
|
168
|
-
async
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
controlPlaneUrl: url,
|
|
176
|
-
supported: false,
|
|
177
|
-
installed: false
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const task = await getWindowsAutostartStatus();
|
|
181
|
-
return {
|
|
182
|
-
platform: process.platform,
|
|
183
|
-
running,
|
|
184
|
-
controlPlaneUrl: url,
|
|
185
|
-
supported: true,
|
|
186
|
-
installed: task.installed,
|
|
187
|
-
state: task.state
|
|
188
|
-
};
|
|
147
|
+
async disableService() {
|
|
148
|
+
return this.runJob("disableService", async (push) => {
|
|
149
|
+
const status = await disableBackgroundService(this.getControlPlaneScriptPath(), process.execPath);
|
|
150
|
+
push(`Disabled background service (${status.managedBy}).`);
|
|
151
|
+
if (status.target)
|
|
152
|
+
push(`Target: ${status.target}`);
|
|
153
|
+
});
|
|
189
154
|
}
|
|
190
155
|
async runJob(kind, fn) {
|
|
191
156
|
const current = this.state[kind];
|
|
@@ -221,7 +186,8 @@ export class RuntimeService {
|
|
|
221
186
|
push(`$ ${command} ${args.join(" ")}`);
|
|
222
187
|
const child = spawn(command, args, {
|
|
223
188
|
cwd,
|
|
224
|
-
shell:
|
|
189
|
+
shell: shouldUseShell(command),
|
|
190
|
+
windowsHide: true
|
|
225
191
|
});
|
|
226
192
|
child.stdout.on("data", chunk => push(String(chunk).trimEnd()));
|
|
227
193
|
child.stderr.on("data", chunk => push(String(chunk).trimEnd()));
|
|
@@ -258,7 +224,8 @@ export class RuntimeService {
|
|
|
258
224
|
push(`$ ${command} ${args.join(" ")}`);
|
|
259
225
|
const child = spawn(command, args, {
|
|
260
226
|
cwd,
|
|
261
|
-
shell:
|
|
227
|
+
shell: shouldUseShell(command),
|
|
228
|
+
windowsHide: true
|
|
262
229
|
});
|
|
263
230
|
const lines = [];
|
|
264
231
|
child.stdout.on("data", chunk => {
|
|
@@ -327,7 +294,7 @@ export class RuntimeService {
|
|
|
327
294
|
return normalizedRoot.includes(marker);
|
|
328
295
|
}
|
|
329
296
|
async scheduleWindowsSelfUpdate(push, action) {
|
|
330
|
-
const controlPlaneScript =
|
|
297
|
+
const controlPlaneScript = this.getControlPlaneScriptPath();
|
|
331
298
|
const jobId = `${Date.now()}-${process.pid}`;
|
|
332
299
|
const logPath = path.join(os.tmpdir(), `${PACKAGE_NAME}-${action}-${jobId}.log`);
|
|
333
300
|
const scriptPath = path.join(os.tmpdir(), `${PACKAGE_NAME}-${action}-${jobId}.cmd`);
|
|
@@ -436,7 +403,8 @@ export class RuntimeService {
|
|
|
436
403
|
return new Promise((resolve, reject) => {
|
|
437
404
|
const child = spawn(command, args, {
|
|
438
405
|
cwd,
|
|
439
|
-
shell:
|
|
406
|
+
shell: shouldUseShell(command),
|
|
407
|
+
windowsHide: true
|
|
440
408
|
});
|
|
441
409
|
const stdout = [];
|
|
442
410
|
const stderr = [];
|
|
@@ -454,6 +422,9 @@ export class RuntimeService {
|
|
|
454
422
|
});
|
|
455
423
|
});
|
|
456
424
|
}
|
|
425
|
+
getControlPlaneScriptPath() {
|
|
426
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
427
|
+
}
|
|
457
428
|
}
|
|
458
429
|
function makeJob(id) {
|
|
459
430
|
return {
|
|
@@ -466,9 +437,20 @@ async function resolveNpmLaunch() {
|
|
|
466
437
|
if (process.env.npm_execpath && await pathExists(process.env.npm_execpath)) {
|
|
467
438
|
return { mode: "node-cli", path: process.env.npm_execpath };
|
|
468
439
|
}
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
440
|
+
const npmCliCandidates = process.platform === "win32"
|
|
441
|
+
? [
|
|
442
|
+
path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
443
|
+
path.join(path.dirname(process.execPath), "..", "node_modules", "npm", "bin", "npm-cli.js")
|
|
444
|
+
]
|
|
445
|
+
: [
|
|
446
|
+
path.resolve(process.execPath, "..", "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
447
|
+
path.resolve(process.execPath, "..", "..", "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
|
|
448
|
+
];
|
|
449
|
+
for (const candidate of npmCliCandidates) {
|
|
450
|
+
if (await pathExists(candidate)) {
|
|
451
|
+
return { mode: "node-cli", path: candidate };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
472
454
|
return { mode: "command", path: process.platform === "win32" ? "npm.cmd" : "npm" };
|
|
473
455
|
}
|
|
474
456
|
function sanitizeRepoUrl(value) {
|
|
@@ -506,3 +488,8 @@ async function pathExists(target) {
|
|
|
506
488
|
return false;
|
|
507
489
|
}
|
|
508
490
|
}
|
|
491
|
+
function shouldUseShell(command) {
|
|
492
|
+
if (process.platform !== "win32")
|
|
493
|
+
return false;
|
|
494
|
+
return /\.(cmd|bat)$/i.test(path.basename(command));
|
|
495
|
+
}
|
|
@@ -5,9 +5,10 @@ import path from "path";
|
|
|
5
5
|
import { URL } from "url";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { ConfigStore, getConfigDir } from "../config/store.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getCredentialStoreStatus } from "../security/credentialStore.js";
|
|
9
9
|
import { ConnectionService } from "./connectionService.js";
|
|
10
10
|
import { RuntimeService } from "./runtimeService.js";
|
|
11
|
+
import { ClientIntegrationService } from "./clientIntegrationService.js";
|
|
11
12
|
import { renderControlPlaneHtml } from "./ui.js";
|
|
12
13
|
export async function startControlPlaneServer(opts) {
|
|
13
14
|
const host = opts?.host || process.env.MCP_FOR_I_CONTROL_HOST || "127.0.0.1";
|
|
@@ -16,9 +17,10 @@ export async function startControlPlaneServer(opts) {
|
|
|
16
17
|
const store = new ConfigStore();
|
|
17
18
|
const connections = new ConnectionService(store);
|
|
18
19
|
const runtime = new RuntimeService(rootDir);
|
|
20
|
+
const clients = new ClientIntegrationService(rootDir);
|
|
19
21
|
const server = http.createServer(async (req, res) => {
|
|
20
22
|
try {
|
|
21
|
-
await routeRequest(req, res, { store, connections, runtime });
|
|
23
|
+
await routeRequest(req, res, { store, connections, runtime, clients });
|
|
22
24
|
}
|
|
23
25
|
catch (err) {
|
|
24
26
|
sendJson(res, 500, { error: err?.message || String(err) });
|
|
@@ -46,7 +48,8 @@ async function routeRequest(req, res, services) {
|
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
if (method === "GET" && pathname === "/api/health") {
|
|
49
|
-
|
|
51
|
+
const credentialStore = await getCredentialStoreStatus();
|
|
52
|
+
sendJson(res, 200, { ok: true, keychainAvailable: credentialStore.keychainAvailable, credentialStore });
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
52
55
|
if (method === "GET" && pathname === "/api/settings") {
|
|
@@ -97,8 +100,39 @@ async function routeRequest(req, res, services) {
|
|
|
97
100
|
sendJson(res, 200, versions);
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
|
-
if (method === "
|
|
101
|
-
const
|
|
103
|
+
if (method === "GET" && pathname === "/api/runtime/environment") {
|
|
104
|
+
const environment = await services.runtime.getEnvironmentInfo();
|
|
105
|
+
sendJson(res, 200, { environment });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (method === "GET" && pathname === "/api/service/status") {
|
|
109
|
+
const status = await services.runtime.getServiceStatus();
|
|
110
|
+
sendJson(res, 200, { status });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (method === "GET" && pathname === "/api/clients") {
|
|
114
|
+
const targets = await services.clients.listTargets();
|
|
115
|
+
sendJson(res, 200, { targets });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (method === "POST" && pathname === "/api/clients/install") {
|
|
119
|
+
const body = await readJson(req);
|
|
120
|
+
const id = String(body.id || "").trim();
|
|
121
|
+
if (!id) {
|
|
122
|
+
sendJson(res, 400, { error: "id is required" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const result = await services.clients.installTarget(id);
|
|
126
|
+
sendJson(res, 200, { result });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (method === "POST" && pathname === "/api/service/enable") {
|
|
130
|
+
const job = await services.runtime.enableService();
|
|
131
|
+
sendJson(res, 202, { job });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (method === "POST" && pathname === "/api/service/disable") {
|
|
135
|
+
const job = await services.runtime.disableService();
|
|
102
136
|
sendJson(res, 202, { job });
|
|
103
137
|
return;
|
|
104
138
|
}
|
|
@@ -116,21 +150,6 @@ async function routeRequest(req, res, services) {
|
|
|
116
150
|
sendJson(res, 202, { job });
|
|
117
151
|
return;
|
|
118
152
|
}
|
|
119
|
-
if (method === "GET" && pathname === "/api/runtime/autostart/status") {
|
|
120
|
-
const status = await services.runtime.getAutostartStatus();
|
|
121
|
-
sendJson(res, 200, { status });
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (method === "POST" && pathname === "/api/runtime/autostart/setup") {
|
|
125
|
-
const job = await services.runtime.setupAutostart();
|
|
126
|
-
sendJson(res, 202, { job });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (method === "POST" && pathname === "/api/runtime/autostart/remove") {
|
|
130
|
-
const job = await services.runtime.removeAutostart();
|
|
131
|
-
sendJson(res, 202, { job });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
153
|
if (method === "GET" && pathname === "/api/connections") {
|
|
135
154
|
const list = await services.connections.list();
|
|
136
155
|
sendJson(res, 200, { connections: list });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function getControlPlaneUrl() {
|
|
2
|
+
const host = process.env.MCP_FOR_I_CONTROL_HOST || "127.0.0.1";
|
|
3
|
+
const portRaw = Number(process.env.MCP_FOR_I_CONTROL_PORT || 3980);
|
|
4
|
+
const port = Number.isFinite(portRaw) && portRaw > 0 && portRaw <= 65535 ? Math.floor(portRaw) : 3980;
|
|
5
|
+
return `http://${host}:${port}`;
|
|
6
|
+
}
|
|
7
|
+
export async function isControlPlaneRunning(baseUrl = getControlPlaneUrl()) {
|
|
8
|
+
try {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
11
|
+
const res = await fetch(`${baseUrl}/api/health`, { signal: controller.signal });
|
|
12
|
+
clearTimeout(timeout);
|
|
13
|
+
return res.ok;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|