mcp-for-i 0.1.0 → 0.1.1
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 +75 -11
- package/dist/controlplane/autostart.js +35 -48
- package/dist/controlplane/runtimeService.js +66 -14
- package/dist/controlplane/server.js +12 -1
- package/dist/controlplane/ui.js +5 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
# MCP-for-i
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
4
|
|
|
5
|
-
## Install
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
Install globally:
|
|
7
|
+
Install globally from npm:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm i -g mcp-for-i
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Verify commands:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mcp-for-i --help
|
|
17
|
+
mcp-for-i-control help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
1. Start the local control plane:
|
|
14
23
|
|
|
15
24
|
```bash
|
|
16
25
|
mcp-for-i-control serve
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
2. Open the UI at:
|
|
20
29
|
|
|
21
30
|
```text
|
|
22
31
|
http://127.0.0.1:3980
|
|
23
32
|
```
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
3. Add IBM i connection profiles in the UI. Passwords are stored in the OS keychain, not plain config.
|
|
35
|
+
|
|
36
|
+
## Background Startup (Windows)
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
Enable startup at login:
|
|
28
39
|
|
|
29
40
|
```bash
|
|
30
41
|
mcp-for-i-control setup
|
|
@@ -36,18 +47,71 @@ Check status:
|
|
|
36
47
|
mcp-for-i-control status
|
|
37
48
|
```
|
|
38
49
|
|
|
39
|
-
Disable
|
|
50
|
+
Disable startup:
|
|
40
51
|
|
|
41
52
|
```bash
|
|
42
53
|
mcp-for-i-control remove
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
##
|
|
56
|
+
## Update and Version Management
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
Update to latest globally:
|
|
48
59
|
|
|
49
60
|
```bash
|
|
61
|
+
npm i -g mcp-for-i@latest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Install a specific version:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm i -g mcp-for-i@0.1.1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Rollback example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm i -g mcp-for-i@0.1.0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Check current installed version:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mcp-for-i --version
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Control Plane Runtime Actions
|
|
83
|
+
|
|
84
|
+
In the UI, runtime buttons behave as follows:
|
|
85
|
+
|
|
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
|
+
- `Update MCP`
|
|
90
|
+
- 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
|
+
- `Update Skills`
|
|
93
|
+
- Pulls/clones the configured skills repository and branch into the local `skills` directory.
|
|
94
|
+
|
|
95
|
+
## Development (Git Checkout)
|
|
96
|
+
|
|
97
|
+
For contributors working from source:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install
|
|
101
|
+
npm run build
|
|
102
|
+
npm test
|
|
50
103
|
npm run start:controlplane
|
|
51
104
|
```
|
|
52
105
|
|
|
53
|
-
|
|
106
|
+
## Security Model
|
|
107
|
+
|
|
108
|
+
- Credential-bearing direct tool arguments are blocked for connection creation/update flows.
|
|
109
|
+
- Passwords are stored in keychain when available.
|
|
110
|
+
- Control plane is local-first (`127.0.0.1` by default).
|
|
111
|
+
- Guarded policy profile is the default for operational safety.
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- Main repo: [ashishthomas2202/mcp-for-i](https://github.com/ashishthomas2202/mcp-for-i)
|
|
116
|
+
- Skills repo: [ashishthomas2202/mcp-for-i-skills](https://github.com/ashishthomas2202/mcp-for-i-skills)
|
|
117
|
+
- Docs: `docs/`
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
2
5
|
export const AUTOSTART_TASK_NAME = "mcp-for-i-controlplane";
|
|
3
6
|
export function getControlPlaneUrl() {
|
|
4
7
|
const host = process.env.MCP_FOR_I_CONTROL_HOST || "127.0.0.1";
|
|
@@ -20,74 +23,58 @@ export async function isControlPlaneRunning(baseUrl = getControlPlaneUrl()) {
|
|
|
20
23
|
}
|
|
21
24
|
export async function configureWindowsAutostart(scriptPath, nodePath = process.execPath) {
|
|
22
25
|
assertWindowsPlatform();
|
|
23
|
-
const
|
|
24
|
-
await
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"/
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
const startupScriptPath = getWindowsStartupScriptPath();
|
|
27
|
+
await fs.mkdir(path.dirname(startupScriptPath), { recursive: true });
|
|
28
|
+
const command = `${quoteForWindowsCommand(nodePath)} ${quoteForWindowsCommand(scriptPath)} serve`;
|
|
29
|
+
const startupScript = [
|
|
30
|
+
"@echo off",
|
|
31
|
+
"setlocal",
|
|
32
|
+
`start \"\" /MIN ${command}`,
|
|
33
|
+
"exit /b 0"
|
|
34
|
+
].join("\r\n");
|
|
35
|
+
await fs.writeFile(startupScriptPath, startupScript, "utf8");
|
|
36
|
+
spawn(nodePath, [scriptPath, "serve"], {
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
windowsHide: true
|
|
40
|
+
}).unref();
|
|
37
41
|
}
|
|
38
42
|
export async function removeWindowsAutostart() {
|
|
39
43
|
assertWindowsPlatform();
|
|
40
|
-
|
|
44
|
+
const startupScriptPath = getWindowsStartupScriptPath();
|
|
45
|
+
await fs.rm(startupScriptPath, { force: true });
|
|
41
46
|
}
|
|
42
47
|
export async function getWindowsAutostartStatus() {
|
|
43
48
|
assertWindowsPlatform();
|
|
49
|
+
const startupScriptPath = getWindowsStartupScriptPath();
|
|
44
50
|
try {
|
|
45
|
-
const
|
|
46
|
-
const lower = output.toLowerCase();
|
|
47
|
-
const hasTask = lower.includes("taskname:");
|
|
48
|
-
const stateLine = output
|
|
49
|
-
.split(/\r?\n/)
|
|
50
|
-
.find(line => line.trim().toLowerCase().startsWith("status:"));
|
|
51
|
+
const stat = await fs.stat(startupScriptPath);
|
|
51
52
|
return {
|
|
52
|
-
installed:
|
|
53
|
-
state:
|
|
54
|
-
raw:
|
|
53
|
+
installed: true,
|
|
54
|
+
state: "startup-script",
|
|
55
|
+
raw: startupScriptPath,
|
|
56
|
+
updatedAt: stat.mtime.toISOString()
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
catch {
|
|
58
60
|
return {
|
|
59
61
|
installed: false,
|
|
60
62
|
state: undefined,
|
|
61
|
-
raw:
|
|
63
|
+
raw: startupScriptPath,
|
|
64
|
+
updatedAt: undefined
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
export async function runCommandCapture(command, args) {
|
|
69
|
-
return await new Promise((resolve, reject) => {
|
|
70
|
-
const child = spawn(command, args, { shell: process.platform === "win32" });
|
|
71
|
-
let output = "";
|
|
72
|
-
child.stdout.on("data", chunk => {
|
|
73
|
-
output += String(chunk);
|
|
74
|
-
});
|
|
75
|
-
child.stderr.on("data", chunk => {
|
|
76
|
-
output += String(chunk);
|
|
77
|
-
});
|
|
78
|
-
child.on("error", reject);
|
|
79
|
-
child.on("exit", code => {
|
|
80
|
-
if (code === 0) {
|
|
81
|
-
resolve(output.trim());
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
reject(new Error(output.trim() || `Command failed (${code}): ${command} ${args.join(" ")}`));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
68
|
+
function getWindowsStartupScriptPath() {
|
|
69
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
70
|
+
return path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", `${AUTOSTART_TASK_NAME}.cmd`);
|
|
88
71
|
}
|
|
89
72
|
function assertWindowsPlatform() {
|
|
90
73
|
if (process.platform !== "win32") {
|
|
91
74
|
throw new Error("This autostart operation is currently supported on Windows only.");
|
|
92
75
|
}
|
|
93
76
|
}
|
|
77
|
+
function quoteForWindowsCommand(value) {
|
|
78
|
+
const escaped = value.replaceAll(`"`, `""`);
|
|
79
|
+
return `"${escaped}"`;
|
|
80
|
+
}
|
|
@@ -20,15 +20,35 @@ export class RuntimeService {
|
|
|
20
20
|
}
|
|
21
21
|
async installOrRepair() {
|
|
22
22
|
return this.runJob("install", async (push) => {
|
|
23
|
-
await this.
|
|
24
|
-
|
|
23
|
+
const mode = await this.detectExecutionMode(push);
|
|
24
|
+
if (mode === "git-checkout") {
|
|
25
|
+
await this.runNpm(["install"], this.rootDir, push);
|
|
26
|
+
await this.buildIfPresent(push);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
push(`Running global install for ${PACKAGE_NAME}@latest`);
|
|
30
|
+
await this.runNpm(["install", "-g", `${PACKAGE_NAME}@latest`], this.rootDir, push);
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
async updateMcp() {
|
|
28
34
|
return this.runJob("updateMcp", async (push) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const mode = await this.detectExecutionMode(push);
|
|
36
|
+
if (mode === "git-checkout") {
|
|
37
|
+
const branch = await this.getGitCurrentBranch(this.rootDir, push);
|
|
38
|
+
if (branch) {
|
|
39
|
+
await this.runCommand("git", ["fetch", "origin", branch], this.rootDir, push);
|
|
40
|
+
await this.runCommand("git", ["pull", "--ff-only", "origin", branch], this.rootDir, push);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
push("Could not determine current branch. Using plain fast-forward pull.");
|
|
44
|
+
await this.runCommand("git", ["pull", "--ff-only"], this.rootDir, push);
|
|
45
|
+
}
|
|
46
|
+
await this.runNpm(["install"], this.rootDir, push);
|
|
47
|
+
await this.buildIfPresent(push);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
push(`Running global update for ${PACKAGE_NAME}@latest`);
|
|
51
|
+
await this.runNpm(["install", "-g", `${PACKAGE_NAME}@latest`], this.rootDir, push);
|
|
32
52
|
});
|
|
33
53
|
}
|
|
34
54
|
async updateSkills(options) {
|
|
@@ -178,12 +198,7 @@ export class RuntimeService {
|
|
|
178
198
|
}
|
|
179
199
|
async getGitRemoteUrl(cwd, push) {
|
|
180
200
|
try {
|
|
181
|
-
const
|
|
182
|
-
await this.runCommandCapture("git", ["remote", "get-url", "origin"], cwd, line => {
|
|
183
|
-
push(line);
|
|
184
|
-
lines.push(line);
|
|
185
|
-
});
|
|
186
|
-
const value = lines.join("\n").trim();
|
|
201
|
+
const value = await this.runCommandCapture("git", ["remote", "get-url", "origin"], cwd, push);
|
|
187
202
|
return value || undefined;
|
|
188
203
|
}
|
|
189
204
|
catch {
|
|
@@ -197,12 +212,19 @@ export class RuntimeService {
|
|
|
197
212
|
cwd,
|
|
198
213
|
shell: process.platform === "win32"
|
|
199
214
|
});
|
|
200
|
-
|
|
215
|
+
const lines = [];
|
|
216
|
+
child.stdout.on("data", chunk => {
|
|
217
|
+
const line = String(chunk).trimEnd();
|
|
218
|
+
if (!line)
|
|
219
|
+
return;
|
|
220
|
+
lines.push(line);
|
|
221
|
+
push(line);
|
|
222
|
+
});
|
|
201
223
|
child.stderr.on("data", chunk => push(String(chunk).trimEnd()));
|
|
202
224
|
child.on("error", reject);
|
|
203
225
|
child.on("exit", code => {
|
|
204
226
|
if (code === 0) {
|
|
205
|
-
resolve();
|
|
227
|
+
resolve(lines.join("\n").trim());
|
|
206
228
|
}
|
|
207
229
|
else {
|
|
208
230
|
reject(new Error(`Command failed (${code}): ${command} ${args.join(" ")}`));
|
|
@@ -210,6 +232,35 @@ export class RuntimeService {
|
|
|
210
232
|
});
|
|
211
233
|
});
|
|
212
234
|
}
|
|
235
|
+
async detectExecutionMode(push) {
|
|
236
|
+
const gitDir = path.join(this.rootDir, ".git");
|
|
237
|
+
if (await pathExists(gitDir)) {
|
|
238
|
+
return "git-checkout";
|
|
239
|
+
}
|
|
240
|
+
push("No git checkout found. Falling back to npm-based package management.");
|
|
241
|
+
return "package-install";
|
|
242
|
+
}
|
|
243
|
+
async buildIfPresent(push) {
|
|
244
|
+
const tsconfigPath = path.join(this.rootDir, "tsconfig.json");
|
|
245
|
+
if (!await pathExists(tsconfigPath)) {
|
|
246
|
+
push("Skipping build: tsconfig.json not found.");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await this.runNpm(["run", "build"], this.rootDir, push);
|
|
250
|
+
}
|
|
251
|
+
async getGitCurrentBranch(cwd, push) {
|
|
252
|
+
try {
|
|
253
|
+
const name = await this.runCommandCapture("git", ["rev-parse", "--abbrev-ref", "HEAD"], cwd, push);
|
|
254
|
+
const branch = name.trim();
|
|
255
|
+
if (!branch || branch === "HEAD") {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
return branch;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
213
264
|
}
|
|
214
265
|
function makeJob(id) {
|
|
215
266
|
return {
|
|
@@ -251,7 +302,8 @@ async function safeRename(from, to) {
|
|
|
251
302
|
// best-effort rollback
|
|
252
303
|
}
|
|
253
304
|
}
|
|
254
|
-
const DEFAULT_SKILLS_REPO = "https://github.com/
|
|
305
|
+
const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas2202/mcp-for-i-skills.git";
|
|
306
|
+
const PACKAGE_NAME = "mcp-for-i";
|
|
255
307
|
async function pathExists(target) {
|
|
256
308
|
try {
|
|
257
309
|
await fs.access(target);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
2
4
|
import { URL } from "url";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
3
6
|
import { ConfigStore } from "../config/store.js";
|
|
4
7
|
import { isKeychainAvailable } from "../security/credentialStore.js";
|
|
5
8
|
import { ConnectionService } from "./connectionService.js";
|
|
@@ -8,7 +11,7 @@ import { renderControlPlaneHtml } from "./ui.js";
|
|
|
8
11
|
export async function startControlPlaneServer(opts) {
|
|
9
12
|
const host = opts?.host || process.env.MCP_FOR_I_CONTROL_HOST || "127.0.0.1";
|
|
10
13
|
const port = normalizePort(opts?.port || Number(process.env.MCP_FOR_I_CONTROL_PORT || 3980));
|
|
11
|
-
const rootDir = opts?.rootDir || process.
|
|
14
|
+
const rootDir = opts?.rootDir || process.env.MCP_FOR_I_ROOT_DIR || getDefaultRootDir();
|
|
12
15
|
const store = new ConfigStore();
|
|
13
16
|
const connections = new ConnectionService(store);
|
|
14
17
|
const runtime = new RuntimeService(rootDir);
|
|
@@ -178,3 +181,11 @@ function normalizePort(port) {
|
|
|
178
181
|
return 3980;
|
|
179
182
|
return Math.floor(port);
|
|
180
183
|
}
|
|
184
|
+
function getDefaultRootDir() {
|
|
185
|
+
const runtimeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
186
|
+
const packageJsonPath = path.join(runtimeRoot, "package.json");
|
|
187
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
188
|
+
return runtimeRoot;
|
|
189
|
+
}
|
|
190
|
+
return process.cwd();
|
|
191
|
+
}
|
package/dist/controlplane/ui.js
CHANGED
|
@@ -380,7 +380,7 @@ export function renderControlPlaneHtml() {
|
|
|
380
380
|
<div class="grid-2">
|
|
381
381
|
<div class="field">
|
|
382
382
|
<label>Skills Repo URL</label>
|
|
383
|
-
<input id="skillsRepoUrl" placeholder="https://github.com/
|
|
383
|
+
<input id="skillsRepoUrl" placeholder="https://github.com/ashishthomas2202/mcp-for-i-skills.git" />
|
|
384
384
|
</div>
|
|
385
385
|
<div class="field">
|
|
386
386
|
<label>Skills Branch</label>
|
|
@@ -389,7 +389,7 @@ export function renderControlPlaneHtml() {
|
|
|
389
389
|
</div>
|
|
390
390
|
<div class="actions">
|
|
391
391
|
<button id="installBtn" type="button">Install/Repair MCP</button>
|
|
392
|
-
<button id="updateMcpBtn" class="secondary" type="button">Update MCP
|
|
392
|
+
<button id="updateMcpBtn" class="secondary" type="button">Update MCP</button>
|
|
393
393
|
<button id="updateSkillsBtn" class="secondary" type="button">Update Skills</button>
|
|
394
394
|
<button id="refreshStatusBtn" class="secondary" type="button">Refresh Status</button>
|
|
395
395
|
<button id="setupAutostartBtn" class="secondary" type="button">Enable Background Startup</button>
|
|
@@ -398,7 +398,8 @@ export function renderControlPlaneHtml() {
|
|
|
398
398
|
</div>
|
|
399
399
|
<div id="autostartInfo" class="hint">Startup status: checking...</div>
|
|
400
400
|
<div id="logs" class="logs">No jobs yet.</div>
|
|
401
|
-
<div class="hint">
|
|
401
|
+
<div class="hint">Install/Repair uses local git checkout when available, otherwise global npm install. Update MCP follows the same mode.</div>
|
|
402
|
+
<div class="hint">Skills update can clone or pull from the repo and branch shown above.</div>
|
|
402
403
|
</section>
|
|
403
404
|
|
|
404
405
|
<section class="card" style="grid-column: 1 / -1;">
|
|
@@ -424,7 +425,7 @@ export function renderControlPlaneHtml() {
|
|
|
424
425
|
const THEME_KEY = "mcp_for_i_theme";
|
|
425
426
|
const SKILLS_REPO_KEY = "mcp_for_i_skills_repo";
|
|
426
427
|
const SKILLS_BRANCH_KEY = "mcp_for_i_skills_branch";
|
|
427
|
-
const DEFAULT_SKILLS_REPO = "https://github.com/
|
|
428
|
+
const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas2202/mcp-for-i-skills.git";
|
|
428
429
|
const DEFAULT_SKILLS_BRANCH = "main";
|
|
429
430
|
|
|
430
431
|
const state = {
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-for-i",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local MCP server and control plane for IBM i workflows",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/ashishthomas2202/mcp-for-i",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/ashishthomas2202/mcp-for-i.git"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/ashishthomas2202/mcp-for-i/issues"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|