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 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,71 @@ 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
+ - `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
- Then open `http://127.0.0.1:3980`.
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 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
+ }
@@ -20,15 +20,35 @@ export class RuntimeService {
20
20
  }
21
21
  async installOrRepair() {
22
22
  return this.runJob("install", async (push) => {
23
- await this.runNpm(["install"], this.rootDir, push);
24
- await this.runNpm(["run", "build"], this.rootDir, push);
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
- 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
+ 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 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();
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
- child.stdout.on("data", chunk => push(String(chunk).trimEnd()));
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/ashishthomas-pcr/mcp-for-i-skills.git";
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.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,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">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">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/ashishthomas-pcr/mcp-for-i-skills.git";
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.0",
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/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",