mcp-for-i 0.1.7 → 0.1.9
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 +3 -0
- package/dist/ibmi/CompileTools.js +25 -7
- package/dist/ibmi/IBMiContent.js +56 -34
- package/dist/ibmi/Search.js +9 -1
- package/dist/ibmi/client.js +1 -1
- package/dist/index.js +58 -5
- package/dist/mcp/audit.js +339 -0
- package/dist/mcp/tools.js +2484 -167
- package/docs/capabilities.md +220 -0
- package/docs/overview.md +11 -10
- package/docs/tools.md +84 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -148,9 +148,12 @@ npm run release:patch -- --no-push
|
|
|
148
148
|
- Passwords are stored in keychain when available.
|
|
149
149
|
- Control plane is local-first (`127.0.0.1` by default).
|
|
150
150
|
- Guarded policy profile is the default for operational safety.
|
|
151
|
+
- Tool schemas are strict (`additionalProperties: false`) with runtime argument validation, including nested connection/action/filter/profile payloads.
|
|
152
|
+
- QSYS object names and source settings are validated before command execution to reduce interpolation and injection risk.
|
|
151
153
|
|
|
152
154
|
## Links
|
|
153
155
|
|
|
154
156
|
- Main repo: [ashishthomas2202/mcp-for-i](https://github.com/ashishthomas2202/mcp-for-i)
|
|
155
157
|
- Skills repo: [ashishthomas2202/mcp-for-i-skills](https://github.com/ashishthomas2202/mcp-for-i-skills)
|
|
156
158
|
- Docs: `docs/`
|
|
159
|
+
- Full capability guide: `docs/capabilities.md`
|
|
@@ -76,16 +76,34 @@ export var CompileTools;
|
|
|
76
76
|
}
|
|
77
77
|
CompileTools.runCommand = runCommand;
|
|
78
78
|
function buildLibraryList(config) {
|
|
79
|
-
return config.libraryList
|
|
79
|
+
return config.libraryList
|
|
80
|
+
.slice(0)
|
|
81
|
+
.map(lib => String(lib || "").trim().toUpperCase())
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.reverse();
|
|
80
84
|
}
|
|
81
85
|
function buildLiblistCommands(connection, config) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
const defaultLibNames = (connection.defaultUserLibraries || [])
|
|
87
|
+
.filter(l => l && l !== "QSYS")
|
|
88
|
+
.map(l => assertLibraryName(connection, l, "default library"));
|
|
89
|
+
const userLibNames = buildLibraryList(config).map(l => assertLibraryName(connection, l, "library list"));
|
|
90
|
+
const currentLib = assertLibraryName(connection, config.currentLibrary || "QGPL", "current library");
|
|
91
|
+
const defaultLibs = Tools.sanitizeObjNamesForPase(defaultLibNames);
|
|
92
|
+
const userLibs = Tools.sanitizeObjNamesForPase(userLibNames);
|
|
93
|
+
const commands = [
|
|
86
94
|
`liblist -d ${IBMiClient.escapeForShell(defaultLibs.join(` `))}`,
|
|
87
|
-
`liblist -c ${IBMiClient.escapeForShell(currentLib)}
|
|
88
|
-
`liblist -a ${IBMiClient.escapeForShell(userLibs.join(` `))}`
|
|
95
|
+
`liblist -c ${IBMiClient.escapeForShell(Tools.sanitizeObjNamesForPase([currentLib])[0])}`
|
|
89
96
|
];
|
|
97
|
+
if (userLibs.length > 0) {
|
|
98
|
+
commands.push(`liblist -a ${IBMiClient.escapeForShell(userLibs.join(` `))}`);
|
|
99
|
+
}
|
|
100
|
+
return commands;
|
|
90
101
|
}
|
|
91
102
|
})(CompileTools || (CompileTools = {}));
|
|
103
|
+
function assertLibraryName(connection, value, context) {
|
|
104
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
105
|
+
if (!normalized || !connection.validQsysName(normalized)) {
|
|
106
|
+
throw new Error(`Invalid ${context}: ${value}`);
|
|
107
|
+
}
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
package/dist/ibmi/IBMiContent.js
CHANGED
|
@@ -86,11 +86,11 @@ export class IBMiContent {
|
|
|
86
86
|
if (withDates !== undefined)
|
|
87
87
|
return withDates;
|
|
88
88
|
}
|
|
89
|
-
const lib = this.
|
|
90
|
-
const file = this.
|
|
91
|
-
const mbr = this.
|
|
89
|
+
const lib = this.requireQsysName(library, "library");
|
|
90
|
+
const file = this.requireQsysName(sourceFile, "sourceFile");
|
|
91
|
+
const mbr = this.requireQsysName(member, "member");
|
|
92
92
|
const tempRemote = this.ibmi.getTempRemote("mbr");
|
|
93
|
-
const ccsid = this.config.sourceFileCCSID
|
|
93
|
+
const ccsid = this.normalizeSourceFileCcsid(this.config.sourceFileCCSID);
|
|
94
94
|
const cmd = `QSYS/CPYTOSTMF FROMMBR('${Tools.qualifyPath(lib, file, mbr, undefined, true)}') ` +
|
|
95
95
|
`TOSTMF('${tempRemote}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${ccsid})`;
|
|
96
96
|
const result = await this.ibmi.sendQsh({ command: `system \"${IBMiClient.escapeForShell(cmd)}\"` });
|
|
@@ -114,12 +114,12 @@ export class IBMiContent {
|
|
|
114
114
|
}
|
|
115
115
|
async uploadMemberContentRaw(library, sourceFile, member, content) {
|
|
116
116
|
this.assertWritable();
|
|
117
|
-
const lib = this.
|
|
118
|
-
const file = this.
|
|
119
|
-
const mbr = this.
|
|
117
|
+
const lib = this.requireQsysName(library, "library");
|
|
118
|
+
const file = this.requireQsysName(sourceFile, "sourceFile");
|
|
119
|
+
const mbr = this.requireQsysName(member, "member");
|
|
120
120
|
const tempRemote = this.ibmi.getTempRemote("mbr");
|
|
121
121
|
await this.writeStreamfileRaw(tempRemote, content);
|
|
122
|
-
const ccsid = this.config.sourceFileCCSID
|
|
122
|
+
const ccsid = this.normalizeSourceFileCcsid(this.config.sourceFileCCSID);
|
|
123
123
|
const cmd = `QSYS/CPYFRMSTMF FROMSTMF('${tempRemote}') ` +
|
|
124
124
|
`TOMBR('${Tools.qualifyPath(lib, file, mbr, undefined, true)}') MBROPT(*REPLACE) ` +
|
|
125
125
|
`STMFCCSID(1208) DBFCCSID(${ccsid})`;
|
|
@@ -181,7 +181,7 @@ export class IBMiContent {
|
|
|
181
181
|
return badLibs;
|
|
182
182
|
}
|
|
183
183
|
async getObjectList(library, types = ["*ALL"]) {
|
|
184
|
-
const lib = this.
|
|
184
|
+
const lib = this.requireQsysName(library, "library");
|
|
185
185
|
const safeTypes = types.map(t => this.ibmi.upperCaseName(t));
|
|
186
186
|
const hasInvalidType = safeTypes.some(t => !/^[*A-Z0-9]+$/.test(t));
|
|
187
187
|
if (hasInvalidType)
|
|
@@ -197,8 +197,8 @@ export class IBMiContent {
|
|
|
197
197
|
}));
|
|
198
198
|
}
|
|
199
199
|
async getMemberList(options) {
|
|
200
|
-
const lib = this.
|
|
201
|
-
const file = this.
|
|
200
|
+
const lib = this.requireQsysName(options.library, "library");
|
|
201
|
+
const file = this.requireQsysName(options.sourceFile, "sourceFile");
|
|
202
202
|
const rows = await this.ibmi.runSQL(`select SYSTEM_TABLE_MEMBER, SOURCE_TYPE, PARTITION_TEXT from QSYS2.SYSPARTITIONSTAT where SYSTEM_TABLE_SCHEMA=${Tools.sqlString(lib)} and SYSTEM_TABLE_NAME=${Tools.sqlString(file)}`);
|
|
203
203
|
return rows.map(r => ({
|
|
204
204
|
library: lib,
|
|
@@ -209,9 +209,9 @@ export class IBMiContent {
|
|
|
209
209
|
}));
|
|
210
210
|
}
|
|
211
211
|
async getMemberInfo(library, sourceFile, member) {
|
|
212
|
-
const lib = this.
|
|
213
|
-
const file = this.
|
|
214
|
-
const mbr = this.
|
|
212
|
+
const lib = this.requireQsysName(library, "library");
|
|
213
|
+
const file = this.requireQsysName(sourceFile, "sourceFile");
|
|
214
|
+
const mbr = this.requireQsysName(member, "member");
|
|
215
215
|
const rows = await this.ibmi.runSQL(`select SYSTEM_TABLE_MEMBER, SOURCE_TYPE, PARTITION_TEXT from QSYS2.SYSPARTITIONSTAT where SYSTEM_TABLE_SCHEMA=${Tools.sqlString(lib)} and SYSTEM_TABLE_NAME=${Tools.sqlString(file)} and SYSTEM_TABLE_MEMBER=${Tools.sqlString(mbr)}`);
|
|
216
216
|
const row = rows[0];
|
|
217
217
|
if (!row)
|
|
@@ -225,9 +225,9 @@ export class IBMiContent {
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
async downloadMemberContentWithDates(library, sourceFile, member) {
|
|
228
|
-
const lib = this.
|
|
229
|
-
const file = this.
|
|
230
|
-
const mbr = this.
|
|
228
|
+
const lib = this.requireQsysName(library, "library");
|
|
229
|
+
const file = this.requireQsysName(sourceFile, "sourceFile");
|
|
230
|
+
const mbr = this.requireQsysName(member, "member");
|
|
231
231
|
const tempLib = await this.getTempLibrary();
|
|
232
232
|
if (!tempLib)
|
|
233
233
|
return undefined;
|
|
@@ -251,9 +251,9 @@ export class IBMiContent {
|
|
|
251
251
|
}
|
|
252
252
|
async uploadMemberContentWithDates(library, sourceFile, member, body) {
|
|
253
253
|
this.assertWritable();
|
|
254
|
-
const lib = this.
|
|
255
|
-
const file = this.
|
|
256
|
-
const mbr = this.
|
|
254
|
+
const lib = this.requireQsysName(library, "library");
|
|
255
|
+
const file = this.requireQsysName(sourceFile, "sourceFile");
|
|
256
|
+
const mbr = this.requireQsysName(member, "member");
|
|
257
257
|
const tempLib = await this.getTempLibrary();
|
|
258
258
|
if (!tempLib) {
|
|
259
259
|
throw new Error(`Temp library is not available for source-date updates.`);
|
|
@@ -322,42 +322,64 @@ export class IBMiContent {
|
|
|
322
322
|
}
|
|
323
323
|
async createLibrary(library) {
|
|
324
324
|
this.assertWritable();
|
|
325
|
-
const lib = this.
|
|
325
|
+
const lib = this.requireQsysName(library, "library");
|
|
326
326
|
return this.ibmi.sendQsh({ command: `system \"CRTLIB LIB(${lib})\"` });
|
|
327
327
|
}
|
|
328
328
|
async createSourceFile(library, file, rcdlen = 112) {
|
|
329
329
|
this.assertWritable();
|
|
330
|
-
const lib = this.
|
|
331
|
-
const src = this.
|
|
330
|
+
const lib = this.requireQsysName(library, "library");
|
|
331
|
+
const src = this.requireQsysName(file, "sourceFile");
|
|
332
332
|
return this.ibmi.sendQsh({ command: `system \"CRTSRCPF FILE(${lib}/${src}) RCDLEN(${rcdlen})\"` });
|
|
333
333
|
}
|
|
334
334
|
async createMember(library, file, member, srctype) {
|
|
335
335
|
this.assertWritable();
|
|
336
|
-
const lib = this.
|
|
337
|
-
const src = this.
|
|
338
|
-
const mbr = this.
|
|
339
|
-
const type =
|
|
336
|
+
const lib = this.requireQsysName(library, "library");
|
|
337
|
+
const src = this.requireQsysName(file, "sourceFile");
|
|
338
|
+
const mbr = this.requireQsysName(member, "member");
|
|
339
|
+
const type = this.normalizeSourceType(srctype);
|
|
340
340
|
return this.ibmi.sendQsh({ command: `system \"ADDPFM FILE(${lib}/${src}) MBR(${mbr}) SRCTYPE(${type})\"` });
|
|
341
341
|
}
|
|
342
342
|
async renameMember(library, file, member, newMember) {
|
|
343
343
|
this.assertWritable();
|
|
344
|
-
const lib = this.
|
|
345
|
-
const src = this.
|
|
346
|
-
const mbr = this.
|
|
347
|
-
const newMbr = this.
|
|
344
|
+
const lib = this.requireQsysName(library, "library");
|
|
345
|
+
const src = this.requireQsysName(file, "sourceFile");
|
|
346
|
+
const mbr = this.requireQsysName(member, "member");
|
|
347
|
+
const newMbr = this.requireQsysName(newMember, "newMember");
|
|
348
348
|
return this.ibmi.sendQsh({ command: `system \"RNMM FILE(${lib}/${src}) MBR(${mbr}) NEWMBR(${newMbr})\"` });
|
|
349
349
|
}
|
|
350
350
|
async deleteMember(library, file, member) {
|
|
351
351
|
this.assertWritable();
|
|
352
|
-
const lib = this.
|
|
353
|
-
const src = this.
|
|
354
|
-
const mbr = this.
|
|
352
|
+
const lib = this.requireQsysName(library, "library");
|
|
353
|
+
const src = this.requireQsysName(file, "sourceFile");
|
|
354
|
+
const mbr = this.requireQsysName(member, "member");
|
|
355
355
|
return this.ibmi.sendQsh({ command: `system \"RMVM FILE(${lib}/${src}) MBR(${mbr})\"` });
|
|
356
356
|
}
|
|
357
357
|
async createTempFile() {
|
|
358
358
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mcpfori-"));
|
|
359
359
|
return path.join(dir, "tmp");
|
|
360
360
|
}
|
|
361
|
+
requireQsysName(value, label) {
|
|
362
|
+
const normalized = this.ibmi.upperCaseName(String(value || "").trim());
|
|
363
|
+
if (!normalized || !this.ibmi.validQsysName(normalized)) {
|
|
364
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
365
|
+
}
|
|
366
|
+
return normalized;
|
|
367
|
+
}
|
|
368
|
+
normalizeSourceType(value) {
|
|
369
|
+
const normalized = this.ibmi.upperCaseName(String(value || "*NONE").trim());
|
|
370
|
+
if (!/^\*?[A-Z0-9#@$._]{1,10}$/.test(normalized)) {
|
|
371
|
+
throw new Error(`Invalid source type: ${value}`);
|
|
372
|
+
}
|
|
373
|
+
return normalized;
|
|
374
|
+
}
|
|
375
|
+
normalizeSourceFileCcsid(value) {
|
|
376
|
+
const normalized = String(value || "*FILE").trim().toUpperCase();
|
|
377
|
+
if (/^\d{1,5}$/.test(normalized))
|
|
378
|
+
return normalized;
|
|
379
|
+
if (/^\*[A-Z0-9]{2,10}$/.test(normalized))
|
|
380
|
+
return normalized;
|
|
381
|
+
throw new Error(`Invalid sourceFileCCSID setting: ${value}`);
|
|
382
|
+
}
|
|
361
383
|
assertWritable() {
|
|
362
384
|
if (this.config.readOnlyMode) {
|
|
363
385
|
throw new Error("Connection is in read-only mode");
|
package/dist/ibmi/Search.js
CHANGED
|
@@ -14,6 +14,14 @@ export var Search;
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
async function searchMembers(connection, library, sourceFile, searchTerm, members, readOnly) {
|
|
17
|
+
const lib = String(library || "").trim().toUpperCase();
|
|
18
|
+
const src = String(sourceFile || "").trim().toUpperCase();
|
|
19
|
+
if (!connection.validQsysName(lib)) {
|
|
20
|
+
throw new Error(`Invalid library: ${library}`);
|
|
21
|
+
}
|
|
22
|
+
if (!connection.validQsysName(src)) {
|
|
23
|
+
throw new Error(`Invalid source file: ${sourceFile}`);
|
|
24
|
+
}
|
|
17
25
|
let detailedMembers;
|
|
18
26
|
let memberFilter;
|
|
19
27
|
const pfgrep = connection.remoteFeatures.pfgrep;
|
|
@@ -42,7 +50,7 @@ export var Search;
|
|
|
42
50
|
: `/usr/bin/grep -inHR -F "${sanitizeSearchTerm(searchTerm)}" ${memberFilter}`;
|
|
43
51
|
const result = await connection.sendCommand({
|
|
44
52
|
command,
|
|
45
|
-
directory: `/QSYS.LIB/${
|
|
53
|
+
directory: `/QSYS.LIB/${lib}.LIB/${src}.FILE`
|
|
46
54
|
});
|
|
47
55
|
if (!result.stderr) {
|
|
48
56
|
let hits = parseGrepOutput(result.stdout || '', readOnly);
|
package/dist/ibmi/client.js
CHANGED
|
@@ -39,7 +39,7 @@ export class IBMiClient {
|
|
|
39
39
|
if (name.length > 10)
|
|
40
40
|
return false;
|
|
41
41
|
const upper = this.upperCaseName(name);
|
|
42
|
-
return /^[A-Z][A-Z0-
|
|
42
|
+
return /^[A-Z#@$][A-Z0-9#@$._]{0,9}$/.test(upper);
|
|
43
43
|
}
|
|
44
44
|
async connect(connection) {
|
|
45
45
|
this.currentHost = connection.host;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from "crypto";
|
|
2
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -6,7 +7,8 @@ import { getTools, handleTool } from "./mcp/tools.js";
|
|
|
6
7
|
import { McpContext } from "./mcp/context.js";
|
|
7
8
|
import { log } from "./mcp/logger.js";
|
|
8
9
|
import { validateToolInput } from "./mcp/validation.js";
|
|
9
|
-
|
|
10
|
+
import { appendAuditRecord } from "./mcp/audit.js";
|
|
11
|
+
const server = new Server({ name: "mcp-for-i", version: "0.1.8" }, { capabilities: { tools: {} } });
|
|
10
12
|
const ctx = new McpContext();
|
|
11
13
|
const tools = getTools();
|
|
12
14
|
const toolMap = new Map(tools.map(tool => [tool.name, tool]));
|
|
@@ -15,6 +17,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
15
17
|
}));
|
|
16
18
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
17
19
|
const { name, arguments: args } = request.params;
|
|
20
|
+
const correlationId = crypto.randomUUID();
|
|
21
|
+
const startedAt = Date.now();
|
|
18
22
|
try {
|
|
19
23
|
const toolDef = toolMap.get(name);
|
|
20
24
|
if (!toolDef)
|
|
@@ -23,16 +27,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
23
27
|
if (errors.length > 0) {
|
|
24
28
|
throw new Error(`Invalid arguments: ${errors.join("; ")}`);
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
const redactedArgs = redactSensitive(args || {});
|
|
31
|
+
log("info", "tool.call", { name, correlationId, args: redactedArgs });
|
|
27
32
|
const result = await handleTool(ctx, name, args || {});
|
|
28
|
-
|
|
33
|
+
const durationMs = Date.now() - startedAt;
|
|
34
|
+
log("debug", "tool.result", { name, correlationId, durationMs });
|
|
35
|
+
await safeAppendAudit({
|
|
36
|
+
tool: name,
|
|
37
|
+
status: "ok",
|
|
38
|
+
args: redactedArgs,
|
|
39
|
+
connectionName: resolveConnectionName(name, args || {}, ctx.activeName),
|
|
40
|
+
approve: Boolean(args && typeof args === "object" && args.approve),
|
|
41
|
+
durationMs,
|
|
42
|
+
correlationId,
|
|
43
|
+
resultSummary: summarizeResult(result)
|
|
44
|
+
});
|
|
29
45
|
return result;
|
|
30
46
|
}
|
|
31
47
|
catch (err) {
|
|
32
|
-
|
|
48
|
+
const durationMs = Date.now() - startedAt;
|
|
49
|
+
const errorMessage = err?.message || String(err);
|
|
50
|
+
log("error", "tool.error", { name, correlationId, durationMs, error: errorMessage });
|
|
51
|
+
await safeAppendAudit({
|
|
52
|
+
tool: name,
|
|
53
|
+
status: "error",
|
|
54
|
+
args: redactSensitive(args || {}),
|
|
55
|
+
connectionName: resolveConnectionName(name, args || {}, ctx.activeName),
|
|
56
|
+
approve: Boolean(args && typeof args === "object" && args.approve),
|
|
57
|
+
durationMs,
|
|
58
|
+
correlationId,
|
|
59
|
+
error: errorMessage
|
|
60
|
+
});
|
|
33
61
|
return {
|
|
34
62
|
isError: true,
|
|
35
|
-
content: [{ type: "text", text:
|
|
63
|
+
content: [{ type: "text", text: errorMessage }]
|
|
36
64
|
};
|
|
37
65
|
}
|
|
38
66
|
});
|
|
@@ -57,3 +85,28 @@ function redactSensitive(input) {
|
|
|
57
85
|
}
|
|
58
86
|
return input;
|
|
59
87
|
}
|
|
88
|
+
function summarizeResult(result) {
|
|
89
|
+
const contentCount = Array.isArray(result?.content) ? result.content.length : 0;
|
|
90
|
+
return {
|
|
91
|
+
isError: Boolean(result?.isError),
|
|
92
|
+
contentCount,
|
|
93
|
+
hasStructuredContent: typeof result?.structuredContent !== "undefined"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function resolveConnectionName(tool, args, activeName) {
|
|
97
|
+
if (args?.connectionName)
|
|
98
|
+
return String(args.connectionName);
|
|
99
|
+
if (tool === "ibmi.connect" && args?.name)
|
|
100
|
+
return String(args.name);
|
|
101
|
+
return activeName;
|
|
102
|
+
}
|
|
103
|
+
async function safeAppendAudit(input) {
|
|
104
|
+
if (process.env.MCP_FOR_I_AUDIT_ENABLED === "0")
|
|
105
|
+
return;
|
|
106
|
+
try {
|
|
107
|
+
await appendAuditRecord(input);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
log("warn", "audit.append.failed", { error: err?.message || String(err) });
|
|
111
|
+
}
|
|
112
|
+
}
|