mcp-for-i 0.1.2 → 0.1.4

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.
@@ -274,14 +274,13 @@ export class RuntimeService {
274
274
  return normalizedRoot.includes(marker);
275
275
  }
276
276
  async scheduleWindowsSelfUpdate(push, action) {
277
- const npm = await resolveNpmLaunch();
278
277
  const controlPlaneScript = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
279
278
  const jobId = `${Date.now()}-${process.pid}`;
280
279
  const logPath = path.join(os.tmpdir(), `${PACKAGE_NAME}-${action}-${jobId}.log`);
281
280
  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`;
281
+ const npmCmdPath = path.join(path.dirname(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
282
+ const npmCommand = await pathExists(npmCmdPath) ? `"${npmCmdPath}"` : "npm.cmd";
283
+ const installCommand = `${npmCommand} install -g ${PACKAGE_NAME}@latest`;
285
284
  const script = [
286
285
  "@echo off",
287
286
  "setlocal",
@@ -4,6 +4,7 @@ import path from "path";
4
4
  import { Tools } from "./Tools.js";
5
5
  import { IBMiContent } from "./IBMiContent.js";
6
6
  const LOCALE = "LC_ALL=EN_US.UTF-8";
7
+ const SQL_HEREDOC_TAG = "__MCP_FOR_I_SQL__";
7
8
  export class IBMiClient {
8
9
  client;
9
10
  content;
@@ -110,16 +111,26 @@ export class IBMiClient {
110
111
  };
111
112
  }
112
113
  async runSQL(statements) {
113
- const input = Tools.fixSQL(statements, true);
114
+ const input = Tools.fixSQL(statements, true).trim();
115
+ if (!input)
116
+ return [];
114
117
  const ccsid = this.config.sqlJobCcsid ?? 1208;
115
- const command = `${LOCALE} system "CHGJOB CCSID(${ccsid})" ; ${LOCALE} system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`;
116
- const output = await this.sendCommand({ command, stdin: input, env: { QIBM_PASE_CCSID: String(ccsid) } });
117
- if (output.stderr && output.stderr.trim()) {
118
- // QZDFMDB2 tends to emit errors in stdout, but keep stderr for debugging
119
- }
120
- if (output.stdout) {
118
+ const command = [
119
+ `system "CHGJOB CCSID(${ccsid})"`,
120
+ `cat <<'${SQL_HEREDOC_TAG}' | system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`,
121
+ input,
122
+ SQL_HEREDOC_TAG
123
+ ].join("\n");
124
+ const output = await this.sendQsh({
125
+ command,
126
+ env: { QIBM_PASE_CCSID: String(ccsid) }
127
+ });
128
+ if (output.stdout && output.stdout.trim()) {
121
129
  return Tools.db2Parse(output.stdout, input);
122
130
  }
131
+ if (hasSqlError(output.stderr || "")) {
132
+ throw new Tools.SqlError(normalizeSqlError(output.stderr || ""));
133
+ }
123
134
  return [];
124
135
  }
125
136
  async loadDefaultLibraryList() {
@@ -211,3 +222,13 @@ export class IBMiClient {
211
222
  this.remoteFeatures["QZDFMDB2.PGM"] = "/QSYS.LIB/QZDFMDB2.PGM";
212
223
  }
213
224
  }
225
+ function hasSqlError(stderr) {
226
+ return /(SQL\d{4}|SQLSTATE|CLI ERROR)/i.test(stderr);
227
+ }
228
+ function normalizeSqlError(stderr) {
229
+ return stderr
230
+ .split(/\r?\n/)
231
+ .map(line => line.trim())
232
+ .filter(line => line && !line.includes("Object is EN_US.UTF-8"))
233
+ .join(" ");
234
+ }
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.2" }, { capabilities: { tools: {} } });
9
+ const server = new Server({ name: "mcp-for-i", version: "0.1.4" }, { 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/dist/mcp/tools.js CHANGED
@@ -2,13 +2,12 @@ import { LocalLanguageActions } from "../ibmi/LocalLanguageActions.js";
2
2
  import { CompileTools } from "../ibmi/CompileTools.js";
3
3
  import { Search } from "../ibmi/Search.js";
4
4
  import { Tools } from "../ibmi/Tools.js";
5
- import { IBMiClient } from "../ibmi/client.js";
6
5
  import { ConnectionService } from "../controlplane/connectionService.js";
7
6
  import { log } from "./logger.js";
8
7
  import fs from "fs/promises";
9
8
  import path from "path";
10
9
  export function getTools() {
11
- return [
10
+ const tools = [
12
11
  {
13
12
  name: "ibmi.connect",
14
13
  description: "Connect to a saved IBM i connection by name.",
@@ -563,6 +562,7 @@ export function getTools() {
563
562
  inputSchema: { type: "object", properties: {} }
564
563
  }
565
564
  ];
565
+ return tools.map(withGuardedApprovalHint);
566
566
  }
567
567
  export async function handleTool(ctx, name, args) {
568
568
  const connections = new ConnectionService(ctx.store);
@@ -685,13 +685,15 @@ export async function handleTool(ctx, name, args) {
685
685
  }
686
686
  case "ibmi.ifs.write": {
687
687
  const conn = ctx.ensureActive(args?.connectionName);
688
- enforceWritable(ctx, conn, "ifs.write", args);
688
+ const op = resolveIfsWriteOperation(args.path);
689
+ enforceWritable(ctx, conn, op, args);
689
690
  await conn.content.writeStreamfileRaw(args.path, args.content);
690
691
  return result("OK");
691
692
  }
692
693
  case "ibmi.ifs.mkdir": {
693
694
  const conn = ctx.ensureActive(args?.connectionName);
694
- enforceWritable(ctx, conn, "ifs.write", args);
695
+ const op = resolveIfsWriteOperation(args.path);
696
+ enforceWritable(ctx, conn, op, args);
695
697
  await conn.sendCommand({ command: `mkdir -p ${Tools.escapePath(args.path)}` });
696
698
  return result("OK");
697
699
  }
@@ -704,7 +706,8 @@ export async function handleTool(ctx, name, args) {
704
706
  }
705
707
  case "ibmi.ifs.upload": {
706
708
  const conn = ctx.ensureActive(args?.connectionName);
707
- enforceWritable(ctx, conn, "ifs.write", args);
709
+ const op = resolveIfsWriteOperation(args.remotePath);
710
+ enforceWritable(ctx, conn, op, args);
708
711
  await conn.client.putFile(args.localPath, args.remotePath);
709
712
  return result("OK");
710
713
  }
@@ -1125,7 +1128,13 @@ export async function handleTool(ctx, name, args) {
1125
1128
  cmdResult = await conn.sendQsh({ command, directory: cwd });
1126
1129
  }
1127
1130
  else {
1128
- cmdResult = await conn.sendQsh({ command: `system \"${IBMiClient.escapeForShell(command)}\"`, directory: cwd });
1131
+ try {
1132
+ await conn.runSQL(`CALL QSYS2.QCMDEXC(${Tools.sqlString(command)})`);
1133
+ cmdResult = { code: 0, signal: null, stdout: "", stderr: "", command };
1134
+ }
1135
+ catch (err) {
1136
+ cmdResult = { code: 1, signal: null, stdout: "", stderr: err?.message || String(err), command };
1137
+ }
1129
1138
  }
1130
1139
  log("info", "audit.cl.run", { connectionName: args?.connectionName || ctx.activeName, environment });
1131
1140
  return json(cmdResult);
@@ -1311,6 +1320,47 @@ function isDeniedByPolicy(policy, operation, args) {
1311
1320
  }
1312
1321
  return false;
1313
1322
  }
1323
+ const APPROVAL_HINT_TOOLS = new Set([
1324
+ "ibmi.qsys.members.write",
1325
+ "ibmi.qsys.members.create",
1326
+ "ibmi.qsys.members.rename",
1327
+ "ibmi.qsys.members.delete",
1328
+ "ibmi.qsys.sourcefiles.create",
1329
+ "ibmi.qsys.libraries.create",
1330
+ "ibmi.ifs.write",
1331
+ "ibmi.ifs.mkdir",
1332
+ "ibmi.ifs.delete",
1333
+ "ibmi.ifs.upload",
1334
+ "ibmi.actions.run",
1335
+ "ibmi.profiles.activate",
1336
+ "ibmi.deploy.uploadDirectory",
1337
+ "ibmi.deploy.uploadFiles",
1338
+ "ibmi.deploy.setCcsid",
1339
+ "ibmi.deploy.sync",
1340
+ "ibmi.sql.execute",
1341
+ "ibmi.cl.run"
1342
+ ]);
1343
+ function withGuardedApprovalHint(tool) {
1344
+ if (!APPROVAL_HINT_TOOLS.has(tool.name))
1345
+ return tool;
1346
+ const schema = tool.inputSchema || { type: "object", properties: {} };
1347
+ if (!schema.properties)
1348
+ schema.properties = {};
1349
+ if (!schema.properties.approve) {
1350
+ schema.properties.approve = {
1351
+ type: "boolean",
1352
+ description: "Set true to approve guarded-policy operations."
1353
+ };
1354
+ }
1355
+ return { ...tool, inputSchema: schema };
1356
+ }
1357
+ function resolveIfsWriteOperation(pathValue) {
1358
+ const target = String(pathValue || "");
1359
+ if (/^\/qsys\.lib\//i.test(target)) {
1360
+ return "qsys.write";
1361
+ }
1362
+ return "ifs.write";
1363
+ }
1314
1364
  function parseEvfevent(content) {
1315
1365
  const diagnostics = [];
1316
1366
  for (const rawLine of content.split(/\r?\n/)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-for-i",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Local MCP server and control plane for IBM i workflows",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "start:controlplane": "node dist/controlplane/index.js",
29
29
  "dev": "ts-node src/index.ts",
30
30
  "dev:controlplane": "ts-node src/controlplane/index.ts",
31
- "test": "npm run build && node --test test/tools.test.mjs test/variables.test.mjs test/validation.test.mjs test/config.migration.test.mjs",
31
+ "test": "npm run build && node --test test/tools.test.mjs test/variables.test.mjs test/validation.test.mjs test/config.migration.test.mjs test/sql.parse.test.mjs",
32
32
  "test:all": "npm run build && node --test test/*.mjs",
33
33
  "test:ibmi": "npm run build && node --test test/ibmi.integration.test.mjs",
34
34
  "test:mcp": "npm run build && node --test test/mcp.integration.test.mjs"