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.
@@ -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 { AUTOSTART_TASK_NAME, configureWindowsAutostart, getControlPlaneUrl, getWindowsAutostartStatus, isControlPlaneRunning, removeWindowsAutostart } from "./autostart.js";
6
- const command = normalizeCommand(process.argv.slice(2));
7
- switch (command) {
8
- case "serve":
9
- await serve();
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 { AUTOSTART_TASK_NAME, configureWindowsAutostart, getControlPlaneUrl, getWindowsAutostartStatus, isControlPlaneRunning, removeWindowsAutostart } from "./autostart.js";
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
- setupAutostart: makeJob("setupAutostart"),
14
- removeAutostart: makeJob("removeAutostart")
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 setupAutostart() {
139
- return this.runJob("setupAutostart", async (push) => {
140
- if (process.platform !== "win32") {
141
- throw new Error("Autostart setup is currently supported on Windows only.");
142
- }
143
- const scriptPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
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 removeAutostart() {
150
- return this.runJob("removeAutostart", async (push) => {
151
- if (process.platform !== "win32") {
152
- throw new Error("Autostart removal is currently supported on Windows only.");
153
- }
154
- try {
155
- await removeWindowsAutostart();
156
- push(`Autostart removed (${AUTOSTART_TASK_NAME}).`);
157
- }
158
- catch (err) {
159
- const msg = err?.message || String(err);
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 getAutostartStatus() {
169
- const url = getControlPlaneUrl();
170
- const running = await isControlPlaneRunning(url);
171
- if (process.platform !== "win32") {
172
- return {
173
- platform: process.platform,
174
- running,
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: process.platform === "win32"
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: process.platform === "win32"
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 = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
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: process.platform === "win32"
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 guessed = path.resolve(process.execPath, "..", "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
470
- if (await pathExists(guessed))
471
- return { mode: "node-cli", path: guessed };
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 { isKeychainAvailable } from "../security/credentialStore.js";
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
- sendJson(res, 200, { ok: true, keychainAvailable: await isKeychainAvailable() });
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 === "POST" && pathname === "/api/runtime/install") {
101
- const job = await services.runtime.installOrRepair();
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
+ }