mcp-for-i 0.1.13 → 0.2.0

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.
Files changed (67) hide show
  1. package/README.md +17 -0
  2. package/dist/config/store.js +229 -109
  3. package/dist/controlplane/auth.js +128 -0
  4. package/dist/controlplane/clientIntegrationService.js +83 -4
  5. package/dist/controlplane/commands.js +14 -9
  6. package/dist/controlplane/connectionService.js +110 -5
  7. package/dist/controlplane/connectionTest.js +329 -0
  8. package/dist/controlplane/errors.js +12 -0
  9. package/dist/controlplane/runtimeService.js +98 -26
  10. package/dist/controlplane/server.js +122 -11
  11. package/dist/controlplane/serverShared.js +25 -6
  12. package/dist/controlplane/serviceManager.js +113 -100
  13. package/dist/controlplane/ui.js +1742 -920
  14. package/dist/ibmi/CompileTools.js +29 -1
  15. package/dist/ibmi/IBMiContent.js +435 -63
  16. package/dist/ibmi/IbmDocs.js +685 -0
  17. package/dist/ibmi/LocalLanguageActions.js +129 -3
  18. package/dist/ibmi/Search.js +158 -52
  19. package/dist/ibmi/Tools.js +7 -10
  20. package/dist/ibmi/client.js +220 -31
  21. package/dist/ibmi/evfevent.js +256 -0
  22. package/dist/ibmi/knownHosts.js +190 -0
  23. package/dist/ibmi/libraryList.js +93 -0
  24. package/dist/ibmi/sourceInspect.js +815 -0
  25. package/dist/ibmi/sqlJob.js +888 -0
  26. package/dist/ibmi/sqlParse.js +317 -0
  27. package/dist/ibmi/tn5250/certTrust.js +59 -0
  28. package/dist/ibmi/tn5250/client.js +310 -0
  29. package/dist/ibmi/tn5250/datastream.js +109 -0
  30. package/dist/ibmi/tn5250/ebcdic.js +74 -0
  31. package/dist/ibmi/tn5250/index.js +6 -0
  32. package/dist/ibmi/tn5250/screen.js +827 -0
  33. package/dist/ibmi/tn5250/telnet.js +186 -0
  34. package/dist/ibmi/variables.js +18 -7
  35. package/dist/index.js +43 -22
  36. package/dist/mcp/annotations.js +155 -0
  37. package/dist/mcp/audit.js +490 -133
  38. package/dist/mcp/confirm.js +48 -0
  39. package/dist/mcp/context.js +43 -5
  40. package/dist/mcp/deployMethods.js +192 -0
  41. package/dist/mcp/logger.js +5 -10
  42. package/dist/mcp/modules/bulkMembers.js +476 -0
  43. package/dist/mcp/modules/changedLines.js +189 -0
  44. package/dist/mcp/modules/compare.js +198 -0
  45. package/dist/mcp/modules/compile.js +515 -0
  46. package/dist/mcp/modules/ifsOps.js +123 -0
  47. package/dist/mcp/modules/index.js +38 -0
  48. package/dist/mcp/modules/inspect.js +811 -0
  49. package/dist/mcp/modules/libl.js +246 -0
  50. package/dist/mcp/modules/members.js +171 -0
  51. package/dist/mcp/modules/objects.js +339 -0
  52. package/dist/mcp/modules/system.js +266 -0
  53. package/dist/mcp/modules/tn5250.js +515 -0
  54. package/dist/mcp/policy.js +412 -0
  55. package/dist/mcp/tools.js +1700 -698
  56. package/dist/security/credentialStore.js +62 -13
  57. package/dist/security/localPaths.js +138 -0
  58. package/dist/security/redact.js +79 -0
  59. package/dist/util/atomicWrite.js +101 -0
  60. package/dist/util/fileLock.js +206 -0
  61. package/docs/capabilities.md +50 -7
  62. package/docs/configuration.md +189 -60
  63. package/docs/examples.md +109 -73
  64. package/docs/overview.md +15 -15
  65. package/docs/security.md +101 -16
  66. package/docs/tools.md +363 -188
  67. package/package.json +56 -53
@@ -0,0 +1,339 @@
1
+ import { checkClCommand, checkResourcePolicy } from "../policy.js";
2
+ import { clampNumber, commandOutcome, enforceWritable, quoteClString, requireDangerousApproval, runClCommand, toolError } from "../tools.js";
3
+ /**
4
+ * Object and library operations (copy, move, rename, delete, describe).
5
+ * Every tool changes the IBM i, so each one passes the write gate, checks the
6
+ * built CL command against the command policy and every library it touches
7
+ * against the library policy, and reports a failing command as a tool error.
8
+ */
9
+ /** Object types these tools accept, with the command and keyword that delete each one. */
10
+ export const OBJECT_DELETE_COMMANDS = {
11
+ "*PGM": { command: "DLTPGM", keyword: "PGM" },
12
+ "*SRVPGM": { command: "DLTSRVPGM", keyword: "SRVPGM" },
13
+ "*MODULE": { command: "DLTMOD", keyword: "MODULE" },
14
+ "*FILE": { command: "DLTF", keyword: "FILE" },
15
+ "*DTAARA": { command: "DLTDTAARA", keyword: "DTAARA" },
16
+ "*DTAQ": { command: "DLTDTAQ", keyword: "DTAQ" },
17
+ "*CMD": { command: "DLTCMD", keyword: "CMD" },
18
+ "*MSGF": { command: "DLTMSGF", keyword: "MSGF" },
19
+ "*MENU": { command: "DLTMNU", keyword: "MENU" },
20
+ "*PNLGRP": { command: "DLTPNLGRP", keyword: "PNLGRP" },
21
+ "*BNDDIR": { command: "DLTBNDDIR", keyword: "BNDDIR" },
22
+ "*JOBD": { command: "DLTJOBD", keyword: "JOBD" },
23
+ "*OUTQ": { command: "DLTOUTQ", keyword: "OUTQ" },
24
+ "*USRSPC": { command: "DLTUSRSPC", keyword: "USRSPC" },
25
+ "*USRIDX": { command: "DLTUSRIDX", keyword: "USRIDX" },
26
+ "*USRQ": { command: "DLTUSRQ", keyword: "USRQ" },
27
+ "*SQLPKG": { command: "DLTSQLPKG", keyword: "SQLPKG" },
28
+ "*QRYDFN": { command: "DLTQRY", keyword: "QRY" },
29
+ "*JRN": { command: "DLTJRN", keyword: "JRN" },
30
+ "*JRNRCV": { command: "DLTJRNRCV", keyword: "JRNRCV" },
31
+ "*SBSD": { command: "DLTSBSD", keyword: "SBSD" },
32
+ "*CLS": { command: "DLTCLS", keyword: "CLS" },
33
+ "*TBL": { command: "DLTTBL", keyword: "TBL" }
34
+ };
35
+ export const OBJECT_TYPES = Object.keys(OBJECT_DELETE_COMMANDS);
36
+ /** Libraries whose objects these tools never change, move, delete or copy into. */
37
+ const PROTECTED_LIBRARIES = new Set([
38
+ "QSYS", "QSYS2", "QSYSINC", "QUSRSYS", "QRECOVERY", "QSPL", "QRPLOBJ", "QHLPSYS", "QDOC", "QSRV",
39
+ "SYSIBM", "SYSIBMADM", "SYSPROC", "SYSTOOLS", "SYSCAT", "SYSFUN", "SYSSTAT"
40
+ ]);
41
+ /** True for IBM system libraries whose contents these tools must not change. */
42
+ export function isProtectedLibrary(name) {
43
+ const upper = String(name || "").toUpperCase();
44
+ return PROTECTED_LIBRARIES.has(upper) || upper.startsWith("QSYS") || upper.startsWith("#");
45
+ }
46
+ /**
47
+ * True for library names reserved for the system: the protected libraries
48
+ * plus every name starting with Q (QGPL, QTEMP, QUSRSYS, ...), which IBM
49
+ * reserves for its own libraries. Used for deleting or creating whole libraries.
50
+ */
51
+ export function isReservedLibraryName(name) {
52
+ const upper = String(name || "").toUpperCase();
53
+ return isProtectedLibrary(upper) || upper.startsWith("Q");
54
+ }
55
+ const NAME_PATTERN = /^[A-Z#@$][A-Z0-9#@$_.]{0,9}$/;
56
+ /** Validate and uppercase an IBM i object, library, file or member name. */
57
+ export function requireName(value, label) {
58
+ const name = String(value ?? "").trim().toUpperCase();
59
+ if (!name)
60
+ throw new Error(`${label} is required`);
61
+ if (!NAME_PATTERN.test(name))
62
+ throw new Error(`Invalid ${label}: ${value}`);
63
+ return name;
64
+ }
65
+ export function optionalName(value, label, fallback) {
66
+ return value === undefined || value === null || String(value).trim() === "" ? fallback : requireName(value, label);
67
+ }
68
+ export function requireObjectType(value) {
69
+ const objectType = String(value ?? "").trim().toUpperCase();
70
+ if (!objectType)
71
+ throw new Error("objectType is required");
72
+ if (!OBJECT_DELETE_COMMANDS[objectType]) {
73
+ throw new Error(`Unsupported objectType ${value}. Use one of: ${OBJECT_TYPES.join(", ")}`);
74
+ }
75
+ return objectType;
76
+ }
77
+ /**
78
+ * Validate a TEXT value (at most 50 characters, no control characters) and
79
+ * return it as a CL parameter value: a quoted string, or *BLANK when empty.
80
+ */
81
+ export function clTextValue(value, label = "text") {
82
+ if (value === undefined || value === null)
83
+ throw new Error(`${label} is required`);
84
+ const text = String(value);
85
+ if (/[\x00-\x1f\x7f]/.test(text))
86
+ throw new Error(`Invalid ${label}: control characters are not allowed`);
87
+ if (text.length > 50)
88
+ throw new Error(`Invalid ${label}: at most 50 characters (got ${text.length})`);
89
+ return text.trim() === "" ? "*BLANK" : quoteClString(text);
90
+ }
91
+ export function refuseProtectedLibrary(library, action) {
92
+ if (isProtectedLibrary(library)) {
93
+ throw new Error(`Refused: ${action} in system library ${library} is not allowed`);
94
+ }
95
+ }
96
+ /** Apply the library policy (allowLibraries/denyLibraries) to every library a call touches. */
97
+ export function enforceLibraryPolicy(ctx, toolName, args, libraries) {
98
+ const policy = ctx.getPolicy(args?.connectionName);
99
+ for (const library of libraries) {
100
+ const reason = checkResourcePolicy(policy, toolName, { library });
101
+ if (reason)
102
+ throw new Error(`Blocked by policy: ${toolName} (${reason})`);
103
+ }
104
+ }
105
+ /** Check a built CL command against the command policy, run it, and return the outcome. */
106
+ export async function runCheckedCl(ctx, conn, args, command, timeoutMs = 120000) {
107
+ const reason = checkClCommand(ctx.getPolicy(args?.connectionName), command);
108
+ if (reason)
109
+ throw new Error(`Blocked by policy: ${reason}`);
110
+ const res = await runClCommand(conn, command, "ile", undefined, timeoutMs);
111
+ return { ...res, ok: res.code === 0, command };
112
+ }
113
+ const CONNECTION_NAME = { type: "string", description: "Saved IBM i connection name." };
114
+ const APPROVE = { type: "boolean", description: "Set true to approve guarded-policy operations." };
115
+ const OBJECT_TYPE = { type: "string", enum: OBJECT_TYPES, description: "Object type, for example *PGM or *FILE." };
116
+ function annotations(destructive, idempotent = false) {
117
+ return { readOnlyHint: false, destructiveHint: destructive, idempotentHint: idempotent, openWorldHint: false };
118
+ }
119
+ const tools = [
120
+ {
121
+ name: "ibmi.objects.copy",
122
+ description: "Duplicate an object with CRTDUPOBJ. Copies to toLibrary (default: same library) as newObject (default: same name). data=true also copies file data.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ library: { type: "string" },
127
+ object: { type: "string" },
128
+ objectType: OBJECT_TYPE,
129
+ toLibrary: { type: "string" },
130
+ newObject: { type: "string" },
131
+ data: { type: "boolean", description: "Copy the data of a *FILE (DATA(*YES)). Default false." },
132
+ connectionName: CONNECTION_NAME,
133
+ approve: APPROVE
134
+ },
135
+ required: ["library", "object", "objectType"],
136
+ additionalProperties: false
137
+ },
138
+ annotations: annotations(false)
139
+ },
140
+ {
141
+ name: "ibmi.objects.move",
142
+ description: "Move an object to another library with MOVOBJ.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ library: { type: "string" },
147
+ object: { type: "string" },
148
+ objectType: OBJECT_TYPE,
149
+ toLibrary: { type: "string" },
150
+ connectionName: CONNECTION_NAME,
151
+ approve: APPROVE
152
+ },
153
+ required: ["library", "object", "objectType", "toLibrary"],
154
+ additionalProperties: false
155
+ },
156
+ annotations: annotations(true)
157
+ },
158
+ {
159
+ name: "ibmi.objects.rename",
160
+ description: "Rename an object in its library with RNMOBJ.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ library: { type: "string" },
165
+ object: { type: "string" },
166
+ objectType: OBJECT_TYPE,
167
+ newObject: { type: "string" },
168
+ connectionName: CONNECTION_NAME,
169
+ approve: APPROVE
170
+ },
171
+ required: ["library", "object", "objectType", "newObject"],
172
+ additionalProperties: false
173
+ },
174
+ annotations: annotations(true)
175
+ },
176
+ {
177
+ name: "ibmi.objects.delete",
178
+ description: "Delete an object with the delete command for its type (DLTPGM, DLTF, ...). To delete a whole library, " +
179
+ "set objectType=*LIB, put the library in `library`, and pass approve=true and confirmName equal to the library name. " +
180
+ "System libraries (Q*, #*, SYS*) are refused.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ library: { type: "string", description: "Library holding the object, or the library to delete when objectType is *LIB." },
185
+ object: { type: "string", description: "Object name. Not used when objectType is *LIB." },
186
+ objectType: { type: "string", enum: [...OBJECT_TYPES, "*LIB"] },
187
+ confirmName: { type: "string", description: "Required for *LIB: repeat the library name exactly." },
188
+ connectionName: CONNECTION_NAME,
189
+ approve: APPROVE
190
+ },
191
+ required: ["library", "objectType"],
192
+ additionalProperties: false
193
+ },
194
+ annotations: annotations(true)
195
+ },
196
+ {
197
+ name: "ibmi.objects.changeDescription",
198
+ description: "Change an object's text description with CHGOBJD TEXT(...). An empty text sets *BLANK.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ library: { type: "string" },
203
+ object: { type: "string" },
204
+ objectType: OBJECT_TYPE,
205
+ text: { type: "string", description: "Up to 50 characters." },
206
+ connectionName: CONNECTION_NAME,
207
+ approve: APPROVE
208
+ },
209
+ required: ["library", "object", "objectType", "text"],
210
+ additionalProperties: false
211
+ },
212
+ annotations: annotations(false, true)
213
+ },
214
+ {
215
+ name: "ibmi.libraries.copy",
216
+ description: "Copy a library with CPYLIB. createLibrary (default true) creates toLibrary; data (default true) copies file data.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ fromLibrary: { type: "string" },
221
+ toLibrary: { type: "string" },
222
+ createLibrary: { type: "boolean", description: "CRTLIB(*YES) when true (default), CRTLIB(*NO) to copy into an existing library." },
223
+ data: { type: "boolean", description: "DATA(*YES) when true (default)." },
224
+ timeoutMs: { type: "number", description: "How long to wait for CPYLIB (default 600000, max 3600000)." },
225
+ connectionName: CONNECTION_NAME,
226
+ approve: APPROVE
227
+ },
228
+ required: ["fromLibrary", "toLibrary"],
229
+ additionalProperties: false
230
+ },
231
+ annotations: annotations(false)
232
+ }
233
+ ];
234
+ async function handle(ctx, name, args) {
235
+ args = args || {};
236
+ const conn = await ctx.ensureActive(args.connectionName);
237
+ await enforceWritable(ctx, conn, "qsys.write", args);
238
+ switch (name) {
239
+ case "ibmi.objects.copy": {
240
+ const library = requireName(args.library, "library");
241
+ const object = requireName(args.object, "object");
242
+ const objectType = requireObjectType(args.objectType);
243
+ const toLibrary = optionalName(args.toLibrary, "toLibrary", library);
244
+ const newObject = optionalName(args.newObject, "newObject", object);
245
+ if (toLibrary === library && newObject === object) {
246
+ throw new Error("Copy target is the same as the source: set toLibrary or newObject");
247
+ }
248
+ refuseProtectedLibrary(toLibrary, "copying objects");
249
+ enforceLibraryPolicy(ctx, name, args, [library, toLibrary]);
250
+ let command = `CRTDUPOBJ OBJ(${object}) FROMLIB(${library}) OBJTYPE(${objectType}) TOLIB(${toLibrary}) NEWOBJ(${newObject})`;
251
+ if (objectType === "*FILE")
252
+ command += ` DATA(${args.data ? "*YES" : "*NO"})`;
253
+ return commandOutcome(await runCheckedCl(ctx, conn, args, command));
254
+ }
255
+ case "ibmi.objects.move": {
256
+ const library = requireName(args.library, "library");
257
+ const object = requireName(args.object, "object");
258
+ const objectType = requireObjectType(args.objectType);
259
+ const toLibrary = requireName(args.toLibrary, "toLibrary");
260
+ if (toLibrary === library)
261
+ throw new Error("toLibrary must differ from library");
262
+ refuseProtectedLibrary(library, "moving objects");
263
+ refuseProtectedLibrary(toLibrary, "moving objects");
264
+ enforceLibraryPolicy(ctx, name, args, [library, toLibrary]);
265
+ const command = `MOVOBJ OBJ(${library}/${object}) OBJTYPE(${objectType}) TOLIB(${toLibrary})`;
266
+ return commandOutcome(await runCheckedCl(ctx, conn, args, command));
267
+ }
268
+ case "ibmi.objects.rename": {
269
+ const library = requireName(args.library, "library");
270
+ const object = requireName(args.object, "object");
271
+ const objectType = requireObjectType(args.objectType);
272
+ const newObject = requireName(args.newObject, "newObject");
273
+ if (newObject === object)
274
+ throw new Error("newObject must differ from object");
275
+ refuseProtectedLibrary(library, "renaming objects");
276
+ enforceLibraryPolicy(ctx, name, args, [library]);
277
+ const command = `RNMOBJ OBJ(${library}/${object}) OBJTYPE(${objectType}) NEWOBJ(${newObject})`;
278
+ return commandOutcome(await runCheckedCl(ctx, conn, args, command));
279
+ }
280
+ case "ibmi.objects.delete": {
281
+ const objectType = String(args.objectType ?? "").trim().toUpperCase();
282
+ const library = requireName(args.library, "library");
283
+ if (objectType === "*LIB") {
284
+ if (args.object !== undefined && requireName(args.object, "object") !== library) {
285
+ throw new Error("For objectType *LIB put the library to delete in `library` and leave `object` out");
286
+ }
287
+ if (isReservedLibraryName(library)) {
288
+ throw new Error(`Refused: ${library} is a system library and cannot be deleted`);
289
+ }
290
+ const guarded = ctx.getPolicy(args?.connectionName).profile === "guarded";
291
+ if (!guarded && args.approve !== true) {
292
+ return toolError(`Deleting library ${library} needs approve=true and confirmName="${library}".`, { library });
293
+ }
294
+ if (String(args.confirmName ?? "").trim().toUpperCase() !== library) {
295
+ return toolError(`confirmName must repeat the library name exactly (${library}) to delete it.`, { library });
296
+ }
297
+ // Under guarded the person confirms this delete on its own, every time.
298
+ if (guarded)
299
+ await requireDangerousApproval(ctx, "qsys.deleteLibrary", args, "");
300
+ enforceLibraryPolicy(ctx, name, args, [library]);
301
+ return commandOutcome(await runCheckedCl(ctx, conn, args, `DLTLIB LIB(${library})`, 600000));
302
+ }
303
+ const type = requireObjectType(objectType);
304
+ const object = requireName(args.object, "object");
305
+ refuseProtectedLibrary(library, "deleting objects");
306
+ enforceLibraryPolicy(ctx, name, args, [library]);
307
+ const { command: dlt, keyword } = OBJECT_DELETE_COMMANDS[type];
308
+ return commandOutcome(await runCheckedCl(ctx, conn, args, `${dlt} ${keyword}(${library}/${object})`));
309
+ }
310
+ case "ibmi.objects.changeDescription": {
311
+ const library = requireName(args.library, "library");
312
+ const object = requireName(args.object, "object");
313
+ const objectType = requireObjectType(args.objectType);
314
+ const text = clTextValue(args.text);
315
+ refuseProtectedLibrary(library, "changing objects");
316
+ enforceLibraryPolicy(ctx, name, args, [library]);
317
+ const command = `CHGOBJD OBJ(${library}/${object}) OBJTYPE(${objectType}) TEXT(${text})`;
318
+ return commandOutcome(await runCheckedCl(ctx, conn, args, command));
319
+ }
320
+ case "ibmi.libraries.copy": {
321
+ const fromLibrary = requireName(args.fromLibrary, "fromLibrary");
322
+ const toLibrary = requireName(args.toLibrary, "toLibrary");
323
+ if (fromLibrary === toLibrary)
324
+ throw new Error("toLibrary must differ from fromLibrary");
325
+ if (isReservedLibraryName(toLibrary)) {
326
+ throw new Error(`Refused: cannot copy into system library ${toLibrary}`);
327
+ }
328
+ enforceLibraryPolicy(ctx, name, args, [fromLibrary, toLibrary]);
329
+ const createLibrary = args.createLibrary === undefined ? true : Boolean(args.createLibrary);
330
+ const data = args.data === undefined ? true : Boolean(args.data);
331
+ const timeoutMs = clampNumber(args.timeoutMs, 600000, 1000, 3600000);
332
+ const command = `CPYLIB FROMLIB(${fromLibrary}) TOLIB(${toLibrary}) CRTLIB(${createLibrary ? "*YES" : "*NO"}) DATA(${data ? "*YES" : "*NO"})`;
333
+ return commandOutcome(await runCheckedCl(ctx, conn, args, command, timeoutMs));
334
+ }
335
+ default:
336
+ throw new Error(`Unknown tool: ${name}`);
337
+ }
338
+ }
339
+ export const objectsModule = { tools, handle };
@@ -0,0 +1,266 @@
1
+ import { clampInteger, json } from "../tools.js";
2
+ const SECTIONS = ["os", "ptfGroups", "authorities", "systemValues", "pase", "tools"];
3
+ /**
4
+ * System values an agent may read through ibmi.system.info. Kept to values
5
+ * that describe the system's behaviour; nothing here is a secret.
6
+ */
7
+ export const SYSTEM_VALUE_ALLOW_LIST = [
8
+ "QSECURITY", "QPWDLVL", "QALWOBJRST", "QAUDCTL", "QAUDLVL", "QCRTAUT", "QMAXSIGN", "QINACTITV",
9
+ "QCCSID", "QCHRID", "QLANGID", "QCNTRYID", "QKBDTYPE", "QCURSYM",
10
+ "QDATFMT", "QDATSEP", "QTIMSEP", "QDECFMT", "QTIMZON", "QUTCOFFSET", "QLEAPADJ",
11
+ "QSYSLIBL", "QUSRLIBL", "QMODEL", "QPRCFEAT", "QSRLNBR", "QIPLTYPE", "QAUTOVRT", "QJOBMSGQFL"
12
+ ];
13
+ /** Helper tools looked up in PASE: on PATH first, then in the IBM i open source location. */
14
+ const HELPER_TOOLS = ["pfgrep", "openssl", "git", "bash", "python3", "node", "java"];
15
+ const PASE_ENV_VARS = ["PATH", "HOME", "SHELL", "LANG", "LC_ALL", "TZ", "QIBM_CCSID", "QIBM_PASE_CCSID"];
16
+ const MAPEPIRE_PATHS = [
17
+ "/QOpenSys/pkgs/bin/mapepire",
18
+ "/QOpenSys/pkgs/lib/mapepire/mapepire-server.jar",
19
+ "/opt/mapepire/bin/mapepire"
20
+ ];
21
+ const PTF_GROUP_DEFAULT_LIMIT = 100;
22
+ const PTF_GROUP_MAX_LIMIT = 1000;
23
+ /** The single PASE script used for the `pase` and `tools` sections. It takes no user input. */
24
+ export function buildPaseProbeScript() {
25
+ return [
26
+ `printf 'uname=%s\\n' "$(uname -srv 2>/dev/null)"`,
27
+ ...PASE_ENV_VARS.map(name => `printf 'env.${name}=%s\\n' "\${${name}:-}"`),
28
+ `for t in ${HELPER_TOOLS.join(" ")}; do ` +
29
+ `p=$(command -v "$t" 2>/dev/null); ` +
30
+ `if [ -z "$p" ] && [ -x "/QOpenSys/pkgs/bin/$t" ]; then p="/QOpenSys/pkgs/bin/$t"; fi; ` +
31
+ `printf 'tool.%s=%s\\n' "$t" "$p"; done`,
32
+ `m=$(command -v mapepire 2>/dev/null); ` +
33
+ `if [ -z "$m" ]; then for c in ${MAPEPIRE_PATHS.join(" ")}; do if [ -e "$c" ]; then m="$c"; break; fi; done; fi; ` +
34
+ `printf 'tool.mapepire-server=%s\\n' "$m"`
35
+ ].join("; ");
36
+ }
37
+ export function parsePaseProbe(stdout) {
38
+ const env = {};
39
+ const tools = {};
40
+ let uname = null;
41
+ for (const line of String(stdout || "").split(/\r?\n/)) {
42
+ const separator = line.indexOf("=");
43
+ if (separator <= 0)
44
+ continue;
45
+ const key = line.slice(0, separator);
46
+ const value = line.slice(separator + 1).trim();
47
+ if (key === "uname")
48
+ uname = value || null;
49
+ else if (key.startsWith("env."))
50
+ env[key.slice(4)] = value || null;
51
+ else if (key.startsWith("tool."))
52
+ tools[key.slice(5)] = value || null;
53
+ }
54
+ return { uname, env, tools };
55
+ }
56
+ function sqlList(values) {
57
+ // Values come from the fixed allow-list above, never from the caller.
58
+ return values.map(value => `'${value.replace(/'/g, "''")}'`).join(", ");
59
+ }
60
+ function text(value) {
61
+ if (value === null || value === undefined)
62
+ return null;
63
+ const str = String(value).trim();
64
+ return str === "" || str === "-" ? null : str;
65
+ }
66
+ function num(value) {
67
+ if (value === null || value === undefined || value === "" || value === "-")
68
+ return null;
69
+ const parsed = Number(value);
70
+ return Number.isFinite(parsed) ? parsed : null;
71
+ }
72
+ async function osSection(conn) {
73
+ const rows = await conn.runSQL("select OS_NAME, OS_VERSION, OS_RELEASE, HOST_NAME, TOTAL_CPUS, CONFIGURED_CPUS, TOTAL_MEMORY " +
74
+ "from SYSIBMADM.ENV_SYS_INFO fetch first 1 row only");
75
+ const row = rows[0] || {};
76
+ const version = String(row.OS_VERSION ?? "").replace(/\D/g, "");
77
+ const release = String(row.OS_RELEASE ?? "").replace(/\D/g, "");
78
+ const out = {
79
+ osName: text(row.OS_NAME),
80
+ release: version && release ? `${Number(version)}.${Number(release)}` : null,
81
+ hostName: text(row.HOST_NAME),
82
+ totalCpus: num(row.TOTAL_CPUS),
83
+ configuredCpus: num(row.CONFIGURED_CPUS),
84
+ totalMemoryMb: num(row.TOTAL_MEMORY)
85
+ };
86
+ try {
87
+ const status = await conn.runSQL("select PARTITION_ID, MAIN_STORAGE_SIZE, SYSTEM_ASP_STORAGE, SYSTEM_ASP_USED, TOTAL_JOBS_IN_SYSTEM, ELAPSED_CPU_USED " +
88
+ "from QSYS2.SYSTEM_STATUS_INFO fetch first 1 row only");
89
+ const s = status[0] || {};
90
+ out.status = {
91
+ partitionId: num(s.PARTITION_ID),
92
+ mainStorageKb: num(s.MAIN_STORAGE_SIZE),
93
+ systemAspStorageMb: num(s.SYSTEM_ASP_STORAGE),
94
+ systemAspUsedPercent: num(s.SYSTEM_ASP_USED),
95
+ totalJobs: num(s.TOTAL_JOBS_IN_SYSTEM),
96
+ elapsedCpuUsedPercent: num(s.ELAPSED_CPU_USED)
97
+ };
98
+ }
99
+ catch (error) {
100
+ out.statusError = error?.message || String(error);
101
+ }
102
+ return out;
103
+ }
104
+ async function ptfGroupsSection(conn, limit) {
105
+ // One row per group: the newest level known and the newest level installed.
106
+ const rows = await conn.runSQL("with L as (select PTF_GROUP_NAME, max(PTF_GROUP_DESCRIPTION) as DESCRIPTION, " +
107
+ "max(PTF_GROUP_LEVEL) as LATEST_LEVEL, " +
108
+ "max(case when PTF_GROUP_STATUS = 'INSTALLED' then PTF_GROUP_LEVEL end) as INSTALLED_LEVEL " +
109
+ "from QSYS2.GROUP_PTF_INFO group by PTF_GROUP_NAME) " +
110
+ "select L.PTF_GROUP_NAME, L.DESCRIPTION, L.LATEST_LEVEL, L.INSTALLED_LEVEL, " +
111
+ "(select max(G.PTF_GROUP_STATUS) from QSYS2.GROUP_PTF_INFO G where G.PTF_GROUP_NAME = L.PTF_GROUP_NAME " +
112
+ "and G.PTF_GROUP_LEVEL = L.LATEST_LEVEL) as LATEST_STATUS " +
113
+ `from L order by L.PTF_GROUP_NAME fetch first ${limit + 1} rows only`);
114
+ const truncated = rows.length > limit;
115
+ return {
116
+ groups: rows.slice(0, limit).map((row) => ({
117
+ group: text(row.PTF_GROUP_NAME),
118
+ description: text(row.DESCRIPTION),
119
+ latestLevel: num(row.LATEST_LEVEL),
120
+ latestStatus: text(row.LATEST_STATUS),
121
+ installedLevel: num(row.INSTALLED_LEVEL)
122
+ })),
123
+ returned: Math.min(rows.length, limit),
124
+ truncated,
125
+ ...(truncated ? { truncatedReason: `More than ${limit} PTF groups; raise ptfGroupLimit (max ${PTF_GROUP_MAX_LIMIT}).` } : {})
126
+ };
127
+ }
128
+ async function authoritiesSection(conn) {
129
+ const rows = await conn.runSQL("select USER as RUNTIME_USER, SYSTEM_USER as SYSTEM_USER_NAME, U.USER_CLASS_NAME, U.SPECIAL_AUTHORITIES, " +
130
+ "U.GROUP_PROFILE_NAME, U.SUPPLEMENTAL_GROUP_LIST, U.LIMIT_CAPABILITIES, U.STATUS " +
131
+ "from QSYS2.USER_INFO U where U.AUTHORIZATION_NAME = USER fetch first 1 row only");
132
+ const row = rows[0];
133
+ if (!row)
134
+ return { user: null, specialAuthorities: [], note: "No QSYS2.USER_INFO row for the current user." };
135
+ const split = (value) => String(value ?? "").split(/\s+/).map(item => item.trim()).filter(item => item && item !== "*NONE" && item !== "-");
136
+ return {
137
+ user: text(row.RUNTIME_USER),
138
+ systemUser: text(row.SYSTEM_USER_NAME),
139
+ userClass: text(row.USER_CLASS_NAME),
140
+ specialAuthorities: split(row.SPECIAL_AUTHORITIES),
141
+ groupProfile: text(row.GROUP_PROFILE_NAME),
142
+ supplementalGroups: split(row.SUPPLEMENTAL_GROUP_LIST),
143
+ limitCapabilities: text(row.LIMIT_CAPABILITIES),
144
+ status: text(row.STATUS)
145
+ };
146
+ }
147
+ async function systemValuesSection(conn, names) {
148
+ const rows = await conn.runSQL("select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE, CURRENT_CHARACTER_VALUE from QSYS2.SYSTEM_VALUE_INFO " +
149
+ `where SYSTEM_VALUE_NAME in (${sqlList(names)}) order by SYSTEM_VALUE_NAME`);
150
+ const values = {};
151
+ for (const row of rows) {
152
+ const name = text(row.SYSTEM_VALUE_NAME);
153
+ if (!name)
154
+ continue;
155
+ const character = text(row.CURRENT_CHARACTER_VALUE);
156
+ values[name] = character !== null ? character : num(row.CURRENT_NUMERIC_VALUE);
157
+ }
158
+ const missing = names.filter(name => !(name in values));
159
+ return { values, ...(missing.length ? { notReturned: missing } : {}) };
160
+ }
161
+ function resolveSections(value) {
162
+ if (value === undefined || value === null)
163
+ return [...SECTIONS];
164
+ if (!Array.isArray(value) || value.length === 0)
165
+ throw new Error("sections must be a non-empty array");
166
+ const out = [];
167
+ for (const item of value) {
168
+ const name = String(item);
169
+ if (!SECTIONS.includes(name))
170
+ throw new Error(`Unknown section: ${item}. Use ${SECTIONS.join(", ")}`);
171
+ if (!out.includes(name))
172
+ out.push(name);
173
+ }
174
+ return out;
175
+ }
176
+ function resolveSystemValues(value) {
177
+ if (value === undefined || value === null)
178
+ return [...SYSTEM_VALUE_ALLOW_LIST];
179
+ if (!Array.isArray(value) || value.length === 0)
180
+ throw new Error("systemValues must be a non-empty array");
181
+ const names = value.map(item => String(item).trim().toUpperCase());
182
+ const blocked = names.filter(name => !SYSTEM_VALUE_ALLOW_LIST.includes(name));
183
+ if (blocked.length) {
184
+ throw new Error(`System value(s) not in the allow-list: ${blocked.join(", ")}. Allowed: ${SYSTEM_VALUE_ALLOW_LIST.join(", ")}`);
185
+ }
186
+ return [...new Set(names)];
187
+ }
188
+ async function systemInfo(ctx, args) {
189
+ const sections = resolveSections(args?.sections);
190
+ const systemValues = resolveSystemValues(args?.systemValues);
191
+ const ptfGroupLimit = clampInteger(args?.ptfGroupLimit, 1, PTF_GROUP_MAX_LIMIT, PTF_GROUP_DEFAULT_LIMIT);
192
+ const conn = await ctx.ensureActive(args?.connectionName);
193
+ const output = { sections };
194
+ const errors = {};
195
+ const run = async (section, loader) => {
196
+ if (!sections.includes(section))
197
+ return;
198
+ try {
199
+ output[section] = await loader();
200
+ }
201
+ catch (error) {
202
+ errors[section] = error?.message || String(error);
203
+ }
204
+ };
205
+ await run("os", () => osSection(conn));
206
+ await run("ptfGroups", () => ptfGroupsSection(conn, ptfGroupLimit));
207
+ await run("authorities", () => authoritiesSection(conn));
208
+ await run("systemValues", () => systemValuesSection(conn, systemValues));
209
+ if (sections.includes("pase") || sections.includes("tools")) {
210
+ try {
211
+ const res = await conn.sendCommand({ command: buildPaseProbeScript() });
212
+ if (!res || (res.code !== 0 && !String(res.stdout || "").trim())) {
213
+ throw new Error(`PASE probe failed (exit ${res?.code ?? "unknown"})${res?.stderr ? `: ${String(res.stderr).trim()}` : ""}`);
214
+ }
215
+ const probe = parsePaseProbe(res.stdout);
216
+ if (sections.includes("pase"))
217
+ output.pase = { uname: probe.uname, env: probe.env };
218
+ if (sections.includes("tools"))
219
+ output.tools = probe.tools;
220
+ }
221
+ catch (error) {
222
+ const message = error?.message || String(error);
223
+ if (sections.includes("pase"))
224
+ errors.pase = message;
225
+ if (sections.includes("tools"))
226
+ errors.tools = message;
227
+ }
228
+ }
229
+ if (Object.keys(errors).length)
230
+ output.errors = errors;
231
+ return json(output);
232
+ }
233
+ export const systemModule = {
234
+ tools: [
235
+ {
236
+ name: "ibmi.system.info",
237
+ description: "Read-only summary of the IBM i system: OS release, PTF group levels, the current user's special authorities, " +
238
+ "selected system values, PASE environment and which helper tools (pfgrep, openssl, git, bash, python3, node, java, " +
239
+ "mapepire-server) are installed. Use sections to pick parts; a failing section is reported under errors.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ connectionName: { type: "string", description: "Saved IBM i connection name." },
244
+ sections: {
245
+ type: "array",
246
+ items: { type: "string", enum: [...SECTIONS] },
247
+ description: "Sections to return (default: all)."
248
+ },
249
+ systemValues: {
250
+ type: "array",
251
+ items: { type: "string" },
252
+ description: `System values to read (default: all allowed). Allowed: ${SYSTEM_VALUE_ALLOW_LIST.join(", ")}.`
253
+ },
254
+ ptfGroupLimit: { type: "number", description: `Maximum PTF groups returned (default ${PTF_GROUP_DEFAULT_LIMIT}, max ${PTF_GROUP_MAX_LIMIT}).` }
255
+ },
256
+ additionalProperties: false
257
+ },
258
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
259
+ }
260
+ ],
261
+ handle: async (ctx, name, args) => {
262
+ if (name === "ibmi.system.info")
263
+ return systemInfo(ctx, args);
264
+ throw new Error(`Unknown tool: ${name}`);
265
+ }
266
+ };