mcp-for-i 0.1.11 → 0.1.12
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 +51 -20
- 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 +36 -18
- package/dist/controlplane/serverShared.js +18 -0
- package/dist/controlplane/serviceManager.js +381 -0
- package/dist/controlplane/ui.js +93 -36
- package/dist/index.js +23 -0
- package/docs/installation.md +44 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MCP-for-i
|
|
2
2
|
|
|
3
|
-
`mcp-for-i` is a local MCP server + control plane for IBM i workflows. It provides secure connection onboarding, keychain-backed credentials, runtime update controls, and IBM i tools for agent-driven operations.
|
|
3
|
+
`mcp-for-i` is a local MCP server + control plane for IBM i workflows. It provides secure connection onboarding, keychain-backed credentials, cross-platform runtime update controls, and IBM i tools for agent-driven operations.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -17,15 +17,19 @@ mcp-for-i --help
|
|
|
17
17
|
mcp-for-i-control help
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
Supported shells and terminals include PowerShell, `cmd`, Git Bash, Bash, Zsh, and WSL shells, across Windows, macOS, and Linux.
|
|
21
|
+
|
|
20
22
|
## Quick Start
|
|
21
23
|
|
|
22
|
-
1.
|
|
24
|
+
1. Open the local control plane:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
mcp-for-i
|
|
27
|
+
mcp-for-i control
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
`mcp-for-i control` will start the control server in the background if needed, then open the UI. If a browser cannot be opened in the current environment, it prints the local URL instead.
|
|
31
|
+
|
|
32
|
+
2. The default local UI URL is:
|
|
29
33
|
|
|
30
34
|
```text
|
|
31
35
|
http://127.0.0.1:3980
|
|
@@ -33,25 +37,45 @@ http://127.0.0.1:3980
|
|
|
33
37
|
|
|
34
38
|
3. Add IBM i connection profiles in the UI. Passwords are stored in the OS keychain, not plain config.
|
|
35
39
|
|
|
36
|
-
##
|
|
40
|
+
## Control Commands
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
From an npm install or a git checkout build, these commands are the supported control entrypoints:
|
|
39
43
|
|
|
40
44
|
```bash
|
|
41
|
-
mcp-for-i
|
|
45
|
+
mcp-for-i control
|
|
46
|
+
mcp-for-i control status
|
|
47
|
+
mcp-for-i control enable
|
|
48
|
+
mcp-for-i control disable
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
Equivalent direct control-plane binary commands:
|
|
45
52
|
|
|
46
53
|
```bash
|
|
54
|
+
mcp-for-i-control open
|
|
47
55
|
mcp-for-i-control status
|
|
56
|
+
mcp-for-i-control enable
|
|
57
|
+
mcp-for-i-control disable
|
|
58
|
+
mcp-for-i-control serve
|
|
48
59
|
```
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
Behavior by environment:
|
|
62
|
+
- Windows: `enable` registers a Startup entry.
|
|
63
|
+
- macOS: `enable` registers a LaunchAgent.
|
|
64
|
+
- Linux: `enable` uses a user `systemd` service when available.
|
|
65
|
+
- WSL: `enable` installs a guarded `.bashrc` hook that starts the control plane on interactive Bash shell launch.
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
## Add To Coding Clients
|
|
68
|
+
|
|
69
|
+
The control-plane UI now includes a `Clients` tab with one-click setup for:
|
|
70
|
+
- `Codex Desktop / CLI`
|
|
71
|
+
- `Claude Code` when the `claude` CLI is installed
|
|
72
|
+
- `VS Code / GitHub Copilot` when the `code` CLI is installed
|
|
73
|
+
|
|
74
|
+
It also includes manual guidance for:
|
|
75
|
+
- `Project Bob`
|
|
76
|
+
- other MCP clients that use generic JSON/TOML stdio server configuration
|
|
77
|
+
|
|
78
|
+
The UI writes or invokes the local client-specific setup on the same machine where the control plane is running. If a client CLI is not installed, the UI falls back to showing the exact launch command/snippet to use manually.
|
|
55
79
|
|
|
56
80
|
## Update and Version Management
|
|
57
81
|
|
|
@@ -79,19 +103,18 @@ Check current installed version:
|
|
|
79
103
|
mcp-for-i --version
|
|
80
104
|
```
|
|
81
105
|
|
|
82
|
-
## Control Plane
|
|
106
|
+
## Control Plane Updates
|
|
83
107
|
|
|
84
|
-
|
|
108
|
+
The control plane intentionally stays narrow:
|
|
85
109
|
|
|
86
|
-
- `Install/Repair MCP`
|
|
87
|
-
- If running from a git checkout: runs local dependency repair/build.
|
|
88
|
-
- If running as npm-installed package: runs global npm install/repair for latest package.
|
|
89
110
|
- `Update MCP`
|
|
90
111
|
- If running from a git checkout: pulls latest changes from `origin/<current-branch>`, then installs/builds.
|
|
91
|
-
- If running as npm-installed package: upgrades global npm package to latest.
|
|
92
|
-
- On Windows global installs, update runs in a detached updater
|
|
112
|
+
- If running as an npm-installed package: upgrades the global npm package to latest.
|
|
113
|
+
- On Windows global installs, update still runs in a detached updater to avoid file-lock (`EBUSY`) errors during self-update.
|
|
93
114
|
- `Update Skills`
|
|
94
|
-
- Pulls
|
|
115
|
+
- Pulls or clones the configured skills repository and branch into the local `skills` directory.
|
|
116
|
+
|
|
117
|
+
For npm installs and manual upgrades, prefer standard npm commands such as `npm i -g mcp-for-i@latest`.
|
|
95
118
|
|
|
96
119
|
## Development (Git Checkout)
|
|
97
120
|
|
|
@@ -104,6 +127,14 @@ npm test
|
|
|
104
127
|
npm run start:controlplane
|
|
105
128
|
```
|
|
106
129
|
|
|
130
|
+
You can also use the built artifacts directly after build:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
node dist/index.js
|
|
134
|
+
node dist/index.js control
|
|
135
|
+
node dist/controlplane/index.js open
|
|
136
|
+
```
|
|
137
|
+
|
|
107
138
|
## Automated Release (No Manual Version Bump)
|
|
108
139
|
|
|
109
140
|
Use the release script instead of raw `npm publish`.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
const SERVER_NAME = "mcp-for-i";
|
|
6
|
+
export class ClientIntegrationService {
|
|
7
|
+
rootDir;
|
|
8
|
+
constructor(rootDir) {
|
|
9
|
+
this.rootDir = rootDir;
|
|
10
|
+
}
|
|
11
|
+
async listTargets() {
|
|
12
|
+
const launchSpec = this.getLaunchSpec();
|
|
13
|
+
const [hasCodex, hasClaude, hasCode] = await Promise.all([
|
|
14
|
+
commandExists("codex"),
|
|
15
|
+
commandExists("claude"),
|
|
16
|
+
commandExists("code")
|
|
17
|
+
]);
|
|
18
|
+
const [codexConfigured, claudeConfigured, codeConfigured] = await Promise.all([
|
|
19
|
+
hasCodex ? this.isCodexConfigured() : false,
|
|
20
|
+
hasClaude ? this.isClaudeConfigured() : false,
|
|
21
|
+
hasCode ? this.isCodeConfigured() : false
|
|
22
|
+
]);
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: "codex",
|
|
26
|
+
label: "Codex Desktop / CLI",
|
|
27
|
+
description: "Adds mcp-for-i to Codex using the local Codex MCP command.",
|
|
28
|
+
available: hasCodex,
|
|
29
|
+
autoInstall: hasCodex,
|
|
30
|
+
configured: codexConfigured,
|
|
31
|
+
details: hasCodex ? undefined : "codex command not found on PATH."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "claude-code",
|
|
35
|
+
label: "Claude Code",
|
|
36
|
+
description: "Adds mcp-for-i to Claude Code at user scope.",
|
|
37
|
+
available: hasClaude,
|
|
38
|
+
autoInstall: hasClaude,
|
|
39
|
+
configured: claudeConfigured,
|
|
40
|
+
details: hasClaude ? undefined : "claude command not found on PATH."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "vscode-copilot",
|
|
44
|
+
label: "VS Code / GitHub Copilot",
|
|
45
|
+
description: "Adds mcp-for-i to the current VS Code user profile via the code CLI.",
|
|
46
|
+
available: hasCode,
|
|
47
|
+
autoInstall: hasCode,
|
|
48
|
+
configured: codeConfigured,
|
|
49
|
+
details: hasCode ? undefined : "code command not found on PATH."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "project-bob",
|
|
53
|
+
label: "Project Bob",
|
|
54
|
+
description: "Manual instructions only. No stable local Project Bob automation path is configured here.",
|
|
55
|
+
available: true,
|
|
56
|
+
autoInstall: false,
|
|
57
|
+
configured: false,
|
|
58
|
+
details: "Use manual instructions until a documented local config or CLI flow is available."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "manual",
|
|
62
|
+
label: "Other MCP Clients",
|
|
63
|
+
description: "Show ready-to-copy config snippets for clients not listed above.",
|
|
64
|
+
available: true,
|
|
65
|
+
autoInstall: false,
|
|
66
|
+
configured: false,
|
|
67
|
+
details: `Launch command: ${launchSpec.command} ${launchSpec.args.join(" ")}`
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
async installTarget(id) {
|
|
72
|
+
switch (id) {
|
|
73
|
+
case "codex":
|
|
74
|
+
return this.installCodex();
|
|
75
|
+
case "claude-code":
|
|
76
|
+
return this.installClaudeCode();
|
|
77
|
+
case "vscode-copilot":
|
|
78
|
+
return this.installVsCode();
|
|
79
|
+
case "project-bob":
|
|
80
|
+
return this.manualOnly("project-bob", "Project Bob");
|
|
81
|
+
case "manual":
|
|
82
|
+
return this.manualOnly("manual", "Other MCP Clients");
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unknown client target: ${id}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async installCodex() {
|
|
88
|
+
if (!await commandExists("codex")) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
id: "codex",
|
|
92
|
+
label: "Codex Desktop / CLI",
|
|
93
|
+
message: "codex command was not found. Use the manual instructions below."
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (await this.isCodexConfigured()) {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
id: "codex",
|
|
100
|
+
label: "Codex Desktop / CLI",
|
|
101
|
+
message: "mcp-for-i is already configured in Codex."
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const spec = this.getLaunchSpec();
|
|
105
|
+
const result = await runCommand("codex", ["mcp", "add", SERVER_NAME, "--", spec.command, ...spec.args]);
|
|
106
|
+
if (!result.ok)
|
|
107
|
+
throw new Error(result.stderr || result.stdout || "Failed to add mcp-for-i to Codex.");
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
id: "codex",
|
|
111
|
+
label: "Codex Desktop / CLI",
|
|
112
|
+
message: "Added mcp-for-i to Codex.",
|
|
113
|
+
output: compactOutput(result)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async installClaudeCode() {
|
|
117
|
+
if (!await commandExists("claude")) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
id: "claude-code",
|
|
121
|
+
label: "Claude Code",
|
|
122
|
+
message: "claude command was not found. Use the manual instructions below."
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (await this.isClaudeConfigured()) {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
id: "claude-code",
|
|
129
|
+
label: "Claude Code",
|
|
130
|
+
message: "mcp-for-i is already configured in Claude Code."
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const spec = this.getLaunchSpec();
|
|
134
|
+
const result = await runCommand("claude", ["mcp", "add", "--scope", "user", "--transport", "stdio", SERVER_NAME, "--", spec.command, ...spec.args]);
|
|
135
|
+
if (!result.ok)
|
|
136
|
+
throw new Error(result.stderr || result.stdout || "Failed to add mcp-for-i to Claude Code.");
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
id: "claude-code",
|
|
140
|
+
label: "Claude Code",
|
|
141
|
+
message: "Added mcp-for-i to Claude Code.",
|
|
142
|
+
output: compactOutput(result)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async installVsCode() {
|
|
146
|
+
if (!await commandExists("code")) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
id: "vscode-copilot",
|
|
150
|
+
label: "VS Code / GitHub Copilot",
|
|
151
|
+
message: "code command was not found. Use the manual instructions below."
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const spec = this.getLaunchSpec();
|
|
155
|
+
const payload = JSON.stringify({
|
|
156
|
+
name: SERVER_NAME,
|
|
157
|
+
command: spec.command,
|
|
158
|
+
args: spec.args
|
|
159
|
+
});
|
|
160
|
+
const result = await runCommand("code", ["--add-mcp", payload]);
|
|
161
|
+
if (!result.ok)
|
|
162
|
+
throw new Error(result.stderr || result.stdout || "Failed to add mcp-for-i to VS Code.");
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
id: "vscode-copilot",
|
|
166
|
+
label: "VS Code / GitHub Copilot",
|
|
167
|
+
message: "Added mcp-for-i to VS Code / GitHub Copilot.",
|
|
168
|
+
output: compactOutput(result)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async manualOnly(id, label) {
|
|
172
|
+
const spec = this.getLaunchSpec();
|
|
173
|
+
const genericJson = JSON.stringify({
|
|
174
|
+
mcpServers: {
|
|
175
|
+
[SERVER_NAME]: {
|
|
176
|
+
command: spec.command,
|
|
177
|
+
args: spec.args
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}, null, 2);
|
|
181
|
+
const vscodeJson = JSON.stringify({
|
|
182
|
+
name: SERVER_NAME,
|
|
183
|
+
command: spec.command,
|
|
184
|
+
args: spec.args
|
|
185
|
+
}, null, 2);
|
|
186
|
+
const codexToml = [
|
|
187
|
+
`[mcp_servers.${quoteTomlKey(SERVER_NAME)}]`,
|
|
188
|
+
`command = ${quoteToml(spec.command)}`,
|
|
189
|
+
`args = [${spec.args.map(quoteToml).join(", ")}]`
|
|
190
|
+
].join("\n");
|
|
191
|
+
const claudeCommand = `claude mcp add --scope user --transport stdio ${SERVER_NAME} -- ${spec.command} ${spec.args.join(" ")}`;
|
|
192
|
+
const codexCommand = `codex mcp add ${SERVER_NAME} -- ${spec.command} ${spec.args.join(" ")}`;
|
|
193
|
+
const vscodeCommand = `code --add-mcp ${JSON.stringify(vscodeJson)}`;
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
id,
|
|
197
|
+
label,
|
|
198
|
+
message: "Manual setup instructions",
|
|
199
|
+
output: [
|
|
200
|
+
"Codex:",
|
|
201
|
+
codexCommand,
|
|
202
|
+
"",
|
|
203
|
+
codexToml,
|
|
204
|
+
"",
|
|
205
|
+
"Claude Code:",
|
|
206
|
+
claudeCommand,
|
|
207
|
+
"",
|
|
208
|
+
"VS Code / GitHub Copilot:",
|
|
209
|
+
vscodeCommand,
|
|
210
|
+
"",
|
|
211
|
+
vscodeJson,
|
|
212
|
+
"",
|
|
213
|
+
"Generic JSON clients:",
|
|
214
|
+
genericJson,
|
|
215
|
+
"",
|
|
216
|
+
"Project Bob:",
|
|
217
|
+
"Use the generic JSON command/args above unless Project Bob publishes a stable local MCP config or CLI flow."
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async isCodexConfigured() {
|
|
222
|
+
const result = await runCommand("codex", ["mcp", "get", SERVER_NAME]);
|
|
223
|
+
return result.ok;
|
|
224
|
+
}
|
|
225
|
+
async isClaudeConfigured() {
|
|
226
|
+
const result = await runCommand("claude", ["mcp", "get", SERVER_NAME]);
|
|
227
|
+
return result.ok;
|
|
228
|
+
}
|
|
229
|
+
async isCodeConfigured() {
|
|
230
|
+
const userConfigPath = getVsCodeUserMcpPath();
|
|
231
|
+
try {
|
|
232
|
+
const raw = await fs.readFile(userConfigPath, "utf8");
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
if (Array.isArray(parsed?.servers)) {
|
|
235
|
+
return parsed.servers.some((entry) => entry?.name === SERVER_NAME);
|
|
236
|
+
}
|
|
237
|
+
if (parsed?.servers && typeof parsed.servers === "object") {
|
|
238
|
+
return Object.prototype.hasOwnProperty.call(parsed.servers, SERVER_NAME);
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
getLaunchSpec() {
|
|
247
|
+
return {
|
|
248
|
+
command: process.execPath,
|
|
249
|
+
args: [path.join(this.rootDir, "dist", "index.js")]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function getVsCodeUserMcpPath() {
|
|
254
|
+
if (process.platform === "win32") {
|
|
255
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
256
|
+
}
|
|
257
|
+
if (process.platform === "darwin") {
|
|
258
|
+
return path.join(os.homedir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
259
|
+
}
|
|
260
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "Code", "User", "mcp.json");
|
|
261
|
+
}
|
|
262
|
+
async function commandExists(command) {
|
|
263
|
+
const result = await runCommand(command, ["--help"]);
|
|
264
|
+
return result.ok;
|
|
265
|
+
}
|
|
266
|
+
async function runCommand(command, args) {
|
|
267
|
+
return new Promise(resolve => {
|
|
268
|
+
const child = spawn(command, args, {
|
|
269
|
+
windowsHide: true,
|
|
270
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
271
|
+
});
|
|
272
|
+
const stdout = [];
|
|
273
|
+
const stderr = [];
|
|
274
|
+
child.stdout.on("data", chunk => stdout.push(String(chunk)));
|
|
275
|
+
child.stderr.on("data", chunk => stderr.push(String(chunk)));
|
|
276
|
+
child.on("error", () => resolve({ ok: false, stdout: "", stderr: "" }));
|
|
277
|
+
child.on("exit", code => resolve({
|
|
278
|
+
ok: code === 0,
|
|
279
|
+
stdout: stdout.join("").trim(),
|
|
280
|
+
stderr: stderr.join("").trim()
|
|
281
|
+
}));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function compactOutput(result) {
|
|
285
|
+
return [result.stdout, result.stderr].filter(Boolean).flatMap(value => value.split(/\r?\n/)).filter(Boolean);
|
|
286
|
+
}
|
|
287
|
+
function quoteToml(value) {
|
|
288
|
+
return JSON.stringify(value);
|
|
289
|
+
}
|
|
290
|
+
function quoteTomlKey(value) {
|
|
291
|
+
return JSON.stringify(value);
|
|
292
|
+
}
|
|
@@ -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
|
+
});
|