mcp-for-i 0.1.0 → 0.1.2

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 CHANGED
@@ -1,30 +1,41 @@
1
1
  # MCP-for-i
2
2
 
3
- Local MCP server for IBM i, mirroring core Code for IBM i capabilities. See `docs/` for setup and usage.
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 As CLI Package
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
- Run control-plane UI server:
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
- Then open:
28
+ 2. Open the UI at:
20
29
 
21
30
  ```text
22
31
  http://127.0.0.1:3980
23
32
  ```
24
33
 
25
- ### Background Startup (Windows)
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
- Configure autostart at login and launch now:
38
+ Enable startup at login:
28
39
 
29
40
  ```bash
30
41
  mcp-for-i-control setup
@@ -36,18 +47,72 @@ Check status:
36
47
  mcp-for-i-control status
37
48
  ```
38
49
 
39
- Disable autostart:
50
+ Disable startup:
40
51
 
41
52
  ```bash
42
53
  mcp-for-i-control remove
43
54
  ```
44
55
 
45
- ## Control Plane UI
56
+ ## Update and Version Management
46
57
 
47
- Run the local onboarding/control UI:
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
+ - On Windows global installs, update runs in a detached updater and briefly restarts control plane to avoid file-lock (`EBUSY`) errors.
93
+ - `Update Skills`
94
+ - Pulls/clones the configured skills repository and branch into the local `skills` directory.
95
+
96
+ ## Development (Git Checkout)
97
+
98
+ For contributors working from source:
99
+
100
+ ```bash
101
+ npm install
102
+ npm run build
103
+ npm test
50
104
  npm run start:controlplane
51
105
  ```
52
106
 
53
- Then open `http://127.0.0.1:3980`.
107
+ ## Security Model
108
+
109
+ - Credential-bearing direct tool arguments are blocked for connection creation/update flows.
110
+ - Passwords are stored in keychain when available.
111
+ - Control plane is local-first (`127.0.0.1` by default).
112
+ - Guarded policy profile is the default for operational safety.
113
+
114
+ ## Links
115
+
116
+ - Main repo: [ashishthomas2202/mcp-for-i](https://github.com/ashishthomas2202/mcp-for-i)
117
+ - Skills repo: [ashishthomas2202/mcp-for-i-skills](https://github.com/ashishthomas2202/mcp-for-i-skills)
118
+ - 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 taskCmd = `"${nodePath}" "${scriptPath}" serve`;
24
- await runCommand("schtasks", [
25
- "/Create",
26
- "/F",
27
- "/SC",
28
- "ONLOGON",
29
- "/RL",
30
- "LIMITED",
31
- "/TN",
32
- AUTOSTART_TASK_NAME,
33
- "/TR",
34
- taskCmd
35
- ]);
36
- await runCommand("schtasks", ["/Run", "/TN", AUTOSTART_TASK_NAME]);
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
- await runCommand("schtasks", ["/Delete", "/TN", AUTOSTART_TASK_NAME, "/F"]);
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 output = await runCommandCapture("schtasks", ["/Query", "/TN", AUTOSTART_TASK_NAME, "/FO", "LIST"]);
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: hasTask,
53
- state: stateLine ? stateLine.split(":").slice(1).join(":").trim() : undefined,
54
- raw: output
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
- export async function runCommand(command, args) {
66
- await runCommandCapture(command, args);
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
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import fs from "fs/promises";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  import { fileURLToPath } from "url";
5
6
  import { AUTOSTART_TASK_NAME, configureWindowsAutostart, getControlPlaneUrl, getWindowsAutostartStatus, isControlPlaneRunning, removeWindowsAutostart } from "./autostart.js";
@@ -20,15 +21,33 @@ export class RuntimeService {
20
21
  }
21
22
  async installOrRepair() {
22
23
  return this.runJob("install", async (push) => {
23
- await this.runNpm(["install"], this.rootDir, push);
24
- await this.runNpm(["run", "build"], this.rootDir, push);
24
+ const mode = await this.detectExecutionMode(push);
25
+ if (mode === "git-checkout") {
26
+ await this.runNpm(["install"], this.rootDir, push);
27
+ await this.buildIfPresent(push);
28
+ return;
29
+ }
30
+ await this.runGlobalPackageUpdate(push, "install");
25
31
  });
26
32
  }
27
33
  async updateMcp() {
28
34
  return this.runJob("updateMcp", async (push) => {
29
- await this.runCommand("git", ["pull", "--ff-only"], this.rootDir, push);
30
- await this.runNpm(["install"], this.rootDir, push);
31
- await this.runNpm(["run", "build"], this.rootDir, push);
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
+ await this.runGlobalPackageUpdate(push, "update");
32
51
  });
33
52
  }
34
53
  async updateSkills(options) {
@@ -178,12 +197,7 @@ export class RuntimeService {
178
197
  }
179
198
  async getGitRemoteUrl(cwd, push) {
180
199
  try {
181
- const lines = [];
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();
200
+ const value = await this.runCommandCapture("git", ["remote", "get-url", "origin"], cwd, push);
187
201
  return value || undefined;
188
202
  }
189
203
  catch {
@@ -197,12 +211,19 @@ export class RuntimeService {
197
211
  cwd,
198
212
  shell: process.platform === "win32"
199
213
  });
200
- child.stdout.on("data", chunk => push(String(chunk).trimEnd()));
214
+ const lines = [];
215
+ child.stdout.on("data", chunk => {
216
+ const line = String(chunk).trimEnd();
217
+ if (!line)
218
+ return;
219
+ lines.push(line);
220
+ push(line);
221
+ });
201
222
  child.stderr.on("data", chunk => push(String(chunk).trimEnd()));
202
223
  child.on("error", reject);
203
224
  child.on("exit", code => {
204
225
  if (code === 0) {
205
- resolve();
226
+ resolve(lines.join("\n").trim());
206
227
  }
207
228
  else {
208
229
  reject(new Error(`Command failed (${code}): ${command} ${args.join(" ")}`));
@@ -210,6 +231,80 @@ export class RuntimeService {
210
231
  });
211
232
  });
212
233
  }
234
+ async detectExecutionMode(push) {
235
+ const gitDir = path.join(this.rootDir, ".git");
236
+ if (await pathExists(gitDir)) {
237
+ return "git-checkout";
238
+ }
239
+ push("No git checkout found. Falling back to npm-based package management.");
240
+ return "package-install";
241
+ }
242
+ async buildIfPresent(push) {
243
+ const tsconfigPath = path.join(this.rootDir, "tsconfig.json");
244
+ if (!await pathExists(tsconfigPath)) {
245
+ push("Skipping build: tsconfig.json not found.");
246
+ return;
247
+ }
248
+ await this.runNpm(["run", "build"], this.rootDir, push);
249
+ }
250
+ async getGitCurrentBranch(cwd, push) {
251
+ try {
252
+ const name = await this.runCommandCapture("git", ["rev-parse", "--abbrev-ref", "HEAD"], cwd, push);
253
+ const branch = name.trim();
254
+ if (!branch || branch === "HEAD") {
255
+ return undefined;
256
+ }
257
+ return branch;
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ }
263
+ async runGlobalPackageUpdate(push, action) {
264
+ if (process.platform === "win32" && this.isLikelyGlobalInstall()) {
265
+ await this.scheduleWindowsSelfUpdate(push, action);
266
+ return;
267
+ }
268
+ push(`Running global ${action} for ${PACKAGE_NAME}@latest`);
269
+ await this.runNpm(["install", "-g", `${PACKAGE_NAME}@latest`], this.rootDir, push);
270
+ }
271
+ isLikelyGlobalInstall() {
272
+ const normalizedRoot = path.normalize(this.rootDir).toLowerCase();
273
+ const marker = `${path.sep}node_modules${path.sep}${PACKAGE_NAME}`.toLowerCase();
274
+ return normalizedRoot.includes(marker);
275
+ }
276
+ async scheduleWindowsSelfUpdate(push, action) {
277
+ const npm = await resolveNpmLaunch();
278
+ const controlPlaneScript = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
279
+ const jobId = `${Date.now()}-${process.pid}`;
280
+ const logPath = path.join(os.tmpdir(), `${PACKAGE_NAME}-${action}-${jobId}.log`);
281
+ const scriptPath = path.join(os.tmpdir(), `${PACKAGE_NAME}-${action}-${jobId}.cmd`);
282
+ const installCommand = npm.mode === "node-cli"
283
+ ? `"${process.execPath}" "${npm.path}" install -g ${PACKAGE_NAME}@latest`
284
+ : `"${npm.path}" install -g ${PACKAGE_NAME}@latest`;
285
+ const script = [
286
+ "@echo off",
287
+ "setlocal",
288
+ `set \"LOG_PATH=${logPath}\"`,
289
+ `echo [${new Date().toISOString()}] Scheduled ${action}>>\"%LOG_PATH%\"`,
290
+ "timeout /t 2 /nobreak >nul",
291
+ `taskkill /PID ${process.pid} /F >nul 2>nul`,
292
+ `${installCommand} >>\"%LOG_PATH%\" 2>&1`,
293
+ "if errorlevel 1 goto :end",
294
+ `start \"\" \"${process.execPath}\" \"${controlPlaneScript}\" serve`,
295
+ ":end",
296
+ `echo [${new Date().toISOString()}] Finished ${action}>>\"%LOG_PATH%\"`,
297
+ "del \"%~f0\" >nul 2>nul"
298
+ ].join("\r\n");
299
+ await fs.writeFile(scriptPath, script, "utf8");
300
+ spawn("cmd.exe", ["/d", "/s", "/c", scriptPath], {
301
+ detached: true,
302
+ stdio: "ignore"
303
+ }).unref();
304
+ push(`Scheduled self-${action} in background to avoid Windows file-lock errors.`);
305
+ push("Control plane will stop briefly, then restart automatically.");
306
+ push(`Background update log: ${logPath}`);
307
+ }
213
308
  }
214
309
  function makeJob(id) {
215
310
  return {
@@ -251,7 +346,8 @@ async function safeRename(from, to) {
251
346
  // best-effort rollback
252
347
  }
253
348
  }
254
- const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas-pcr/mcp-for-i-skills.git";
349
+ const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas2202/mcp-for-i-skills.git";
350
+ const PACKAGE_NAME = "mcp-for-i";
255
351
  async function pathExists(target) {
256
352
  try {
257
353
  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.cwd();
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
+ }
@@ -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/ashishthomas-pcr/mcp-for-i-skills.git" />
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 from GitHub</button>
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,9 @@ 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">Actions stream logs live. Skills update can clone or pull from the repo and branch shown above.</div>
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">Windows global updates may briefly restart the local control-plane service.</div>
403
+ <div class="hint">Skills update can clone or pull from the repo and branch shown above.</div>
402
404
  </section>
403
405
 
404
406
  <section class="card" style="grid-column: 1 / -1;">
@@ -424,7 +426,7 @@ export function renderControlPlaneHtml() {
424
426
  const THEME_KEY = "mcp_for_i_theme";
425
427
  const SKILLS_REPO_KEY = "mcp_for_i_skills_repo";
426
428
  const SKILLS_BRANCH_KEY = "mcp_for_i_skills_branch";
427
- const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas-pcr/mcp-for-i-skills.git";
429
+ const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas2202/mcp-for-i-skills.git";
428
430
  const DEFAULT_SKILLS_BRANCH = "main";
429
431
 
430
432
  const state = {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { getTools, handleTool } from "./mcp/tools.js";
6
6
  import { McpContext } from "./mcp/context.js";
7
7
  import { log } from "./mcp/logger.js";
8
8
  import { validateToolInput } from "./mcp/validation.js";
9
- const server = new Server({ name: "mcp-for-i", version: "0.1.0" }, { capabilities: { tools: {} } });
9
+ const server = new Server({ name: "mcp-for-i", version: "0.1.2" }, { capabilities: { tools: {} } });
10
10
  const ctx = new McpContext();
11
11
  const tools = getTools();
12
12
  const toolMap = new Map(tools.map(tool => [tool.name, tool]));
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "mcp-for-i",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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/ashishthomas-pcr/mcp-for-i",
7
+ "homepage": "https://github.com/ashishthomas2202/mcp-for-i",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/ashishthomas-pcr/mcp-for-i.git"
10
+ "url": "git+https://github.com/ashishthomas2202/mcp-for-i.git"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/ashishthomas-pcr/mcp-for-i/issues"
13
+ "url": "https://github.com/ashishthomas2202/mcp-for-i/issues"
14
14
  },
15
15
  "files": [
16
16
  "dist",