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.
- package/README.md +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { structuredPatch } from "diff";
|
|
4
|
+
import { checkResourcePolicy } from "../policy.js";
|
|
5
|
+
import { clampInteger, decodeUtf8Text, guardLocalPath, json, requireIfsPath, requireQsysName, statIfsFileSize } from "../tools.js";
|
|
6
|
+
// tools.ts imports this module, so nothing from tools.ts may be read at load time.
|
|
7
|
+
const HARD_MAX_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_CONTEXT = 3;
|
|
10
|
+
const DEFAULT_MAX_DIFF_LINES = 2000;
|
|
11
|
+
const HARD_MAX_DIFF_LINES = 50000;
|
|
12
|
+
const SOURCE_SCHEMA = {
|
|
13
|
+
type: "object",
|
|
14
|
+
description: "One side of the comparison: a source member, an IFS stream file or a local file.",
|
|
15
|
+
properties: {
|
|
16
|
+
type: { type: "string", enum: ["member", "ifs", "local"] },
|
|
17
|
+
library: { type: "string", description: "member: library name." },
|
|
18
|
+
sourceFile: { type: "string", description: "member: source physical file name." },
|
|
19
|
+
member: { type: "string", description: "member: member name." },
|
|
20
|
+
path: { type: "string", description: "ifs: absolute IFS path. local: local file path inside the allowed local roots." },
|
|
21
|
+
connectionName: { type: "string", description: "member/ifs: connection to read from (defaults to the top-level connectionName or the active connection)." }
|
|
22
|
+
},
|
|
23
|
+
required: ["type"],
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
};
|
|
26
|
+
const tools = [
|
|
27
|
+
{
|
|
28
|
+
name: "ibmi.compare.text",
|
|
29
|
+
description: "Compare two text sources (source member, IFS stream file or local file, in any combination) and return a unified diff " +
|
|
30
|
+
"with configurable context lines plus added/removed counts. Members are read as source text only, so sequence numbers " +
|
|
31
|
+
"and source dates never show up as changes. Read-only.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
left: SOURCE_SCHEMA,
|
|
36
|
+
right: SOURCE_SCHEMA,
|
|
37
|
+
context: { type: "number", description: `Context lines around each change (default ${DEFAULT_CONTEXT}, max 1000).` },
|
|
38
|
+
ignoreTrailingWhitespace: { type: "boolean", description: "Ignore trailing blanks and tabs on every line (default true; member lines are always right-trimmed)." },
|
|
39
|
+
ignoreCase: { type: "boolean", description: "Compare lines case-insensitively (default false)." },
|
|
40
|
+
maxBytes: { type: "number", description: `Refuse a side larger than this many bytes (default ${DEFAULT_MAX_BYTES}, max ${HARD_MAX_BYTES}).` },
|
|
41
|
+
maxDiffLines: { type: "number", description: `Truncate the unified diff after this many lines (default ${DEFAULT_MAX_DIFF_LINES}, max ${HARD_MAX_DIFF_LINES}). Counts always cover the whole diff.` },
|
|
42
|
+
connectionName: { type: "string", description: "Saved IBM i connection name used for member/ifs sides that don't name their own." }
|
|
43
|
+
},
|
|
44
|
+
required: ["left", "right"],
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
},
|
|
47
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
async function handle(ctx, name, args) {
|
|
51
|
+
if (name !== "ibmi.compare.text")
|
|
52
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
53
|
+
const maxBytes = clampInteger(args?.maxBytes, 1, HARD_MAX_BYTES, DEFAULT_MAX_BYTES);
|
|
54
|
+
const context = clampInteger(args?.context, 0, 1000, DEFAULT_CONTEXT);
|
|
55
|
+
const maxDiffLines = clampInteger(args?.maxDiffLines, 1, HARD_MAX_DIFF_LINES, DEFAULT_MAX_DIFF_LINES);
|
|
56
|
+
const ignoreTrailingWhitespace = args?.ignoreTrailingWhitespace !== false;
|
|
57
|
+
const ignoreCase = Boolean(args?.ignoreCase);
|
|
58
|
+
const left = await readSide(ctx, args?.left, "left", args?.connectionName, maxBytes);
|
|
59
|
+
const right = await readSide(ctx, args?.right, "right", args?.connectionName, maxBytes);
|
|
60
|
+
const comparison = compareTexts(left, right, { context, ignoreTrailingWhitespace, ignoreCase, maxDiffLines });
|
|
61
|
+
return json({
|
|
62
|
+
left: describeSide(left),
|
|
63
|
+
right: describeSide(right),
|
|
64
|
+
identical: comparison.identical,
|
|
65
|
+
stats: comparison.stats,
|
|
66
|
+
options: { context, ignoreTrailingWhitespace, ignoreCase, maxBytes, maxDiffLines },
|
|
67
|
+
truncated: comparison.truncated,
|
|
68
|
+
diff: comparison.diff
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function describeSide(side) {
|
|
72
|
+
return {
|
|
73
|
+
type: side.type,
|
|
74
|
+
label: side.label,
|
|
75
|
+
...(side.connectionName ? { connectionName: side.connectionName } : {}),
|
|
76
|
+
bytes: side.bytes,
|
|
77
|
+
lines: side.lines,
|
|
78
|
+
sha256: side.sha256
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function readSide(ctx, spec, label, defaultConnection, maxBytes) {
|
|
82
|
+
if (!spec || typeof spec !== "object")
|
|
83
|
+
throw new Error(`${label} is required`);
|
|
84
|
+
const type = String(spec.type || "").trim().toLowerCase();
|
|
85
|
+
if (type === "local") {
|
|
86
|
+
const localPath = await guardLocalPath(ctx, spec.path, "read");
|
|
87
|
+
const stat = await fs.stat(localPath);
|
|
88
|
+
if (!stat.isFile())
|
|
89
|
+
throw new Error(`${label}: ${localPath} is not a file`);
|
|
90
|
+
if (stat.size > maxBytes)
|
|
91
|
+
throw new Error(`${label}: ${localPath} is ${stat.size} bytes, more than maxBytes (${maxBytes})`);
|
|
92
|
+
const data = await fs.readFile(localPath);
|
|
93
|
+
return makeSide("local", localPath, undefined, data, label);
|
|
94
|
+
}
|
|
95
|
+
if (type !== "member" && type !== "ifs")
|
|
96
|
+
throw new Error(`${label}.type must be member, ifs or local`);
|
|
97
|
+
const connectionName = String(spec.connectionName || defaultConnection || "").trim() || undefined;
|
|
98
|
+
const conn = await ctx.ensureActive(connectionName);
|
|
99
|
+
if (type === "member") {
|
|
100
|
+
const library = requireQsysName(conn, spec.library, `${label}.library`);
|
|
101
|
+
const sourceFile = requireQsysName(conn, spec.sourceFile, `${label}.sourceFile`);
|
|
102
|
+
const member = requireQsysName(conn, spec.member, `${label}.member`);
|
|
103
|
+
await enforceSidePolicy(ctx, connectionName, { library });
|
|
104
|
+
const snapshot = await conn.content.getMemberSnapshot(library, sourceFile, member);
|
|
105
|
+
// Source records are blank padded to the record length; the padding is
|
|
106
|
+
// never meaningful, so member lines are always right-trimmed.
|
|
107
|
+
const text = String(snapshot.content ?? "").split("\n").map((line) => line.replace(/[ \t]+$/, "")).join("\n");
|
|
108
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
109
|
+
if (bytes > maxBytes)
|
|
110
|
+
throw new Error(`${label}: ${library}/${sourceFile}(${member}) is ${bytes} bytes, more than maxBytes (${maxBytes})`);
|
|
111
|
+
return makeSide("member", `${library}/${sourceFile}(${member})`, connectionName, Buffer.from(text, "utf8"), label);
|
|
112
|
+
}
|
|
113
|
+
const remotePath = requireIfsPath(spec.path, `${label}.path`);
|
|
114
|
+
await enforceSidePolicy(ctx, connectionName, { path: remotePath });
|
|
115
|
+
const knownSize = await statIfsFileSize(conn, remotePath);
|
|
116
|
+
if (knownSize !== undefined && knownSize > maxBytes) {
|
|
117
|
+
throw new Error(`${label}: ${remotePath} is ${knownSize} bytes, more than maxBytes (${maxBytes})`);
|
|
118
|
+
}
|
|
119
|
+
const data = Buffer.from(await conn.content.downloadStreamfileRaw(remotePath));
|
|
120
|
+
if (data.length > maxBytes)
|
|
121
|
+
throw new Error(`${label}: ${remotePath} is ${data.length} bytes, more than maxBytes (${maxBytes})`);
|
|
122
|
+
return makeSide("ifs", remotePath, connectionName, data, label);
|
|
123
|
+
}
|
|
124
|
+
function makeSide(type, name, connectionName, data, label) {
|
|
125
|
+
const text = decodeUtf8Text(data);
|
|
126
|
+
if (text === undefined)
|
|
127
|
+
throw new Error(`${label}: ${name} is not UTF-8 text and can't be compared`);
|
|
128
|
+
const normalized = text.replace(/^/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
129
|
+
return {
|
|
130
|
+
type,
|
|
131
|
+
label: connectionName && type !== "local" ? `${connectionName}:${name}` : name,
|
|
132
|
+
connectionName,
|
|
133
|
+
bytes: data.length,
|
|
134
|
+
lines: splitLines(normalized).length,
|
|
135
|
+
sha256: crypto.createHash("sha256").update(data).digest("hex"),
|
|
136
|
+
text: normalized
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** The tool's own top-level policy check can't see nested sides, so each side is checked here. */
|
|
140
|
+
async function enforceSidePolicy(ctx, connectionName, resource) {
|
|
141
|
+
const resolver = ctx;
|
|
142
|
+
const policy = typeof resolver.resolvePolicy === "function"
|
|
143
|
+
? await resolver.resolvePolicy(connectionName)
|
|
144
|
+
: typeof resolver.getPolicy === "function" ? resolver.getPolicy(connectionName) : undefined;
|
|
145
|
+
if (!policy)
|
|
146
|
+
return;
|
|
147
|
+
const reason = checkResourcePolicy(policy, "ibmi.compare.text", resource);
|
|
148
|
+
if (reason)
|
|
149
|
+
throw new Error(`Blocked by policy: ibmi.compare.text (${reason})`);
|
|
150
|
+
}
|
|
151
|
+
function splitLines(text) {
|
|
152
|
+
if (text.length === 0)
|
|
153
|
+
return [];
|
|
154
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
155
|
+
if (lines[lines.length - 1] === "")
|
|
156
|
+
lines.pop();
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
export function compareTexts(left, right, options) {
|
|
160
|
+
const prepare = (text) => {
|
|
161
|
+
let lines = splitLines(text);
|
|
162
|
+
if (options.ignoreTrailingWhitespace)
|
|
163
|
+
lines = lines.map(line => line.replace(/[ \t]+$/, ""));
|
|
164
|
+
// A missing final newline is not treated as a difference.
|
|
165
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
166
|
+
};
|
|
167
|
+
const patch = structuredPatch(left.label, right.label, prepare(left.text), prepare(right.text), "", "", {
|
|
168
|
+
context: options.context,
|
|
169
|
+
ignoreCase: options.ignoreCase
|
|
170
|
+
});
|
|
171
|
+
let added = 0;
|
|
172
|
+
let removed = 0;
|
|
173
|
+
const out = [`--- ${left.label}`, `+++ ${right.label}`];
|
|
174
|
+
for (const hunk of patch.hunks) {
|
|
175
|
+
out.push(`@@ -${rangeText(hunk.oldStart, hunk.oldLines)} +${rangeText(hunk.newStart, hunk.newLines)} @@`);
|
|
176
|
+
for (const line of hunk.lines) {
|
|
177
|
+
if (line.startsWith("\\"))
|
|
178
|
+
continue;
|
|
179
|
+
if (line.startsWith("+"))
|
|
180
|
+
added++;
|
|
181
|
+
else if (line.startsWith("-"))
|
|
182
|
+
removed++;
|
|
183
|
+
out.push(line);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const identical = patch.hunks.length === 0;
|
|
187
|
+
const truncated = out.length > options.maxDiffLines;
|
|
188
|
+
return {
|
|
189
|
+
identical,
|
|
190
|
+
stats: { hunks: patch.hunks.length, added, removed },
|
|
191
|
+
truncated,
|
|
192
|
+
diff: identical ? "" : (truncated ? out.slice(0, options.maxDiffLines) : out).join("\n")
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function rangeText(start, count) {
|
|
196
|
+
return count === 1 ? String(start) : `${start},${count}`;
|
|
197
|
+
}
|
|
198
|
+
export const compareModule = { tools, handle };
|