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,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telnet layer for TN5250 (RFC 854, RFC 1205, RFC 2877, RFC 4777).
|
|
3
|
+
*
|
|
4
|
+
* TelnetParser turns raw socket bytes into negotiation events and complete
|
|
5
|
+
* 5250 records (data between IAC EOR markers, with IAC IAC unescaped).
|
|
6
|
+
*/
|
|
7
|
+
export const IAC = 255;
|
|
8
|
+
export const DONT = 254;
|
|
9
|
+
export const DO = 253;
|
|
10
|
+
export const WONT = 252;
|
|
11
|
+
export const WILL = 251;
|
|
12
|
+
export const SB = 250;
|
|
13
|
+
export const SE = 240;
|
|
14
|
+
export const EOR = 239;
|
|
15
|
+
export const OPT_BINARY = 0;
|
|
16
|
+
export const OPT_TERMINAL_TYPE = 24;
|
|
17
|
+
export const OPT_EOR = 25;
|
|
18
|
+
export const OPT_NEW_ENVIRON = 39;
|
|
19
|
+
// Subnegotiation codes
|
|
20
|
+
export const TT_IS = 0;
|
|
21
|
+
export const TT_SEND = 1;
|
|
22
|
+
export const ENV_IS = 0;
|
|
23
|
+
export const ENV_SEND = 1;
|
|
24
|
+
export const ENV_INFO = 2;
|
|
25
|
+
export const ENV_VAR = 0;
|
|
26
|
+
export const ENV_VALUE = 1;
|
|
27
|
+
export const ENV_ESC = 2;
|
|
28
|
+
export const ENV_USERVAR = 3;
|
|
29
|
+
export class TelnetParser {
|
|
30
|
+
state = "data";
|
|
31
|
+
command = 0;
|
|
32
|
+
record = [];
|
|
33
|
+
sb = [];
|
|
34
|
+
/** Feed socket bytes; returns the events completed by this chunk. */
|
|
35
|
+
push(chunk) {
|
|
36
|
+
const events = [];
|
|
37
|
+
for (const byte of chunk) {
|
|
38
|
+
switch (this.state) {
|
|
39
|
+
case "data":
|
|
40
|
+
if (byte === IAC)
|
|
41
|
+
this.state = "iac";
|
|
42
|
+
else
|
|
43
|
+
this.record.push(byte);
|
|
44
|
+
break;
|
|
45
|
+
case "iac":
|
|
46
|
+
if (byte === IAC) {
|
|
47
|
+
this.record.push(IAC);
|
|
48
|
+
this.state = "data";
|
|
49
|
+
}
|
|
50
|
+
else if (byte === EOR) {
|
|
51
|
+
events.push({ type: "record", data: Buffer.from(this.record) });
|
|
52
|
+
this.record = [];
|
|
53
|
+
this.state = "data";
|
|
54
|
+
}
|
|
55
|
+
else if (byte === DO || byte === DONT || byte === WILL || byte === WONT) {
|
|
56
|
+
this.command = byte;
|
|
57
|
+
this.state = "option";
|
|
58
|
+
}
|
|
59
|
+
else if (byte === SB) {
|
|
60
|
+
this.sb = [];
|
|
61
|
+
this.state = "sb";
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// NOP, GA and other two-byte commands carry nothing we need.
|
|
65
|
+
this.state = "data";
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case "option":
|
|
69
|
+
events.push({ type: "command", command: this.command, option: byte });
|
|
70
|
+
this.state = "data";
|
|
71
|
+
break;
|
|
72
|
+
case "sb":
|
|
73
|
+
if (byte === IAC)
|
|
74
|
+
this.state = "sb-iac";
|
|
75
|
+
else
|
|
76
|
+
this.sb.push(byte);
|
|
77
|
+
break;
|
|
78
|
+
case "sb-iac":
|
|
79
|
+
if (byte === SE) {
|
|
80
|
+
const [option, ...rest] = this.sb;
|
|
81
|
+
events.push({ type: "subnegotiation", option: option ?? -1, data: Buffer.from(rest) });
|
|
82
|
+
this.sb = [];
|
|
83
|
+
this.state = "data";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// IAC IAC inside a subnegotiation is a literal 0xFF.
|
|
87
|
+
this.sb.push(byte);
|
|
88
|
+
this.state = "sb";
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function telnetCommand(command, option) {
|
|
97
|
+
return Buffer.from([IAC, command, option]);
|
|
98
|
+
}
|
|
99
|
+
/** Wrap a 5250 record for sending: double IAC bytes and append IAC EOR. */
|
|
100
|
+
export function frameRecord(record) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const byte of record) {
|
|
103
|
+
out.push(byte);
|
|
104
|
+
if (byte === IAC)
|
|
105
|
+
out.push(IAC);
|
|
106
|
+
}
|
|
107
|
+
out.push(IAC, EOR);
|
|
108
|
+
return Buffer.from(out);
|
|
109
|
+
}
|
|
110
|
+
export function subnegotiation(option, payload) {
|
|
111
|
+
const out = [IAC, SB, option];
|
|
112
|
+
for (const byte of payload) {
|
|
113
|
+
out.push(byte);
|
|
114
|
+
if (byte === IAC)
|
|
115
|
+
out.push(IAC);
|
|
116
|
+
}
|
|
117
|
+
out.push(IAC, SE);
|
|
118
|
+
return Buffer.from(out);
|
|
119
|
+
}
|
|
120
|
+
export function terminalTypeIs(terminalType) {
|
|
121
|
+
return subnegotiation(OPT_TERMINAL_TYPE, Buffer.concat([Buffer.from([TT_IS]), Buffer.from(terminalType, "ascii")]));
|
|
122
|
+
}
|
|
123
|
+
/** Parse the body of a NEW-ENVIRON SEND (after the SEND byte). */
|
|
124
|
+
export function parseEnvSend(data) {
|
|
125
|
+
const requested = [];
|
|
126
|
+
let serverSeed;
|
|
127
|
+
let i = 0;
|
|
128
|
+
while (i < data.length) {
|
|
129
|
+
const kindByte = data[i];
|
|
130
|
+
if (kindByte !== ENV_VAR && kindByte !== ENV_USERVAR) {
|
|
131
|
+
i += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
i += 1;
|
|
135
|
+
// RFC 4777: the server's seed follows the IBMRSEED name directly
|
|
136
|
+
// (optionally after VALUE) and is 8 raw bytes.
|
|
137
|
+
if (data.subarray(i, i + 8).toString("ascii") === "IBMRSEED") {
|
|
138
|
+
i += 8;
|
|
139
|
+
if (data[i] === ENV_VALUE)
|
|
140
|
+
i += 1;
|
|
141
|
+
const seed = data.subarray(i, Math.min(i + 8, data.length));
|
|
142
|
+
if (seed.length > 0)
|
|
143
|
+
serverSeed = Buffer.from(seed);
|
|
144
|
+
i += seed.length;
|
|
145
|
+
requested.push({ kind: kindByte === ENV_VAR ? "var" : "uservar", name: "IBMRSEED" });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const name = [];
|
|
149
|
+
let inValue = false;
|
|
150
|
+
while (i < data.length && data[i] !== ENV_VAR && data[i] !== ENV_USERVAR) {
|
|
151
|
+
if (data[i] === ENV_VALUE) {
|
|
152
|
+
inValue = true;
|
|
153
|
+
i += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (data[i] === ENV_ESC && i + 1 < data.length)
|
|
157
|
+
i += 1;
|
|
158
|
+
if (!inValue)
|
|
159
|
+
name.push(data[i]);
|
|
160
|
+
i += 1;
|
|
161
|
+
}
|
|
162
|
+
// An empty name means "send all variables of this kind".
|
|
163
|
+
requested.push({ kind: kindByte === ENV_VAR ? "var" : "uservar", name: Buffer.from(name).toString("ascii") });
|
|
164
|
+
}
|
|
165
|
+
return { requested, serverSeed };
|
|
166
|
+
}
|
|
167
|
+
function escapeEnv(bytes) {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const byte of bytes) {
|
|
170
|
+
if (byte === ENV_VAR || byte === ENV_VALUE || byte === ENV_ESC || byte === ENV_USERVAR)
|
|
171
|
+
out.push(ENV_ESC);
|
|
172
|
+
out.push(byte);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/** Build IAC SB NEW-ENVIRON IS ... IAC SE. */
|
|
177
|
+
export function envIs(values) {
|
|
178
|
+
const out = [ENV_IS];
|
|
179
|
+
for (const entry of values) {
|
|
180
|
+
out.push(entry.kind === "var" ? ENV_VAR : ENV_USERVAR);
|
|
181
|
+
out.push(...escapeEnv(Buffer.from(entry.name, "ascii")));
|
|
182
|
+
out.push(ENV_VALUE);
|
|
183
|
+
out.push(...escapeEnv(entry.value));
|
|
184
|
+
}
|
|
185
|
+
return subnegotiation(OPT_NEW_ENVIRON, Buffer.from(out));
|
|
186
|
+
}
|
package/dist/ibmi/variables.js
CHANGED
|
@@ -31,14 +31,25 @@ export class Variables extends Map {
|
|
|
31
31
|
super.set(key, this.expand(value, [key]));
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Replace every variable in one pass. Longer names are tried first and a
|
|
36
|
+
* name that ends in a letter or digit only matches when the next character
|
|
37
|
+
* is not part of a name, so `&EXT` never replaces the start of `&EXTL` and
|
|
38
|
+
* `&OPENLIB` never replaces the start of `&OPENLIBL`.
|
|
39
|
+
*/
|
|
34
40
|
expand(input, keysToOmit = []) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const keys = Array.from(this.keys())
|
|
42
|
+
.filter(key => key.length > 0 && !keysToOmit.includes(key))
|
|
43
|
+
.sort((a, b) => b.length - a.length);
|
|
44
|
+
if (keys.length === 0)
|
|
45
|
+
return input;
|
|
46
|
+
const pattern = keys
|
|
47
|
+
.map(key => {
|
|
48
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
return /[A-Za-z0-9_#@$]$/.test(key) ? `${escaped}(?![A-Za-z0-9_#@$])` : escaped;
|
|
50
|
+
})
|
|
51
|
+
.join("|");
|
|
52
|
+
return input.replace(new RegExp(pattern, "g"), match => this.get(match) ?? match);
|
|
42
53
|
}
|
|
43
54
|
toPaseVariables() {
|
|
44
55
|
const variables = {};
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,17 @@ import fs from "fs";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { CallToolRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { getTools, handleTool } from "./mcp/tools.js";
|
|
9
9
|
import { McpContext } from "./mcp/context.js";
|
|
10
|
+
import { createElicitationConfirm } from "./mcp/confirm.js";
|
|
10
11
|
import { log } from "./mcp/logger.js";
|
|
12
|
+
import { redactSecrets, redactSecretsInText } from "./security/redact.js";
|
|
11
13
|
import { validateToolInput } from "./mcp/validation.js";
|
|
12
14
|
import { appendAuditRecord } from "./mcp/audit.js";
|
|
15
|
+
import { redactModuleArgs } from "./mcp/modules/index.js";
|
|
13
16
|
import { runControlCommand } from "./controlplane/commands.js";
|
|
17
|
+
import { ibmDocs } from "./ibmi/IbmDocs.js";
|
|
14
18
|
const argv = process.argv.slice(2);
|
|
15
19
|
const firstArg = (argv[0] || "").toLowerCase();
|
|
16
20
|
const packageVersion = readPackageVersion();
|
|
@@ -29,17 +33,50 @@ if (firstArg === "--help" || firstArg === "-h" || firstArg === "help") {
|
|
|
29
33
|
printHelp();
|
|
30
34
|
process.exit(0);
|
|
31
35
|
}
|
|
32
|
-
const server = new Server({ name: "mcp-for-i", version: packageVersion }, { capabilities: { tools: {} } });
|
|
36
|
+
const server = new Server({ name: "mcp-for-i", version: packageVersion }, { capabilities: { tools: {}, resources: {} } });
|
|
33
37
|
const ctx = new McpContext();
|
|
38
|
+
ctx.confirm = createElicitationConfirm(server);
|
|
34
39
|
const tools = getTools();
|
|
35
40
|
const toolMap = new Map(tools.map(tool => [tool.name, tool]));
|
|
36
41
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
37
42
|
tools
|
|
38
43
|
}));
|
|
44
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
45
|
+
resources: [
|
|
46
|
+
{
|
|
47
|
+
uri: "ibmi-docs://catalog",
|
|
48
|
+
name: "IBM i documentation catalog",
|
|
49
|
+
description: "Release-aware allowlist of official IBM i documentation available through mcp-for-i.",
|
|
50
|
+
mimeType: "application/json"
|
|
51
|
+
},
|
|
52
|
+
...ibmDocs.listResources()
|
|
53
|
+
]
|
|
54
|
+
}));
|
|
55
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
56
|
+
resourceTemplates: [{
|
|
57
|
+
uriTemplate: "ibmi-docs://topic/{ref}",
|
|
58
|
+
name: "Official IBM i documentation topic",
|
|
59
|
+
description: "Open a versioned official IBM i documentation topic using a ref from ibmi.docs.search.",
|
|
60
|
+
mimeType: "text/markdown"
|
|
61
|
+
}]
|
|
62
|
+
}));
|
|
63
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
64
|
+
const uri = request.params.uri;
|
|
65
|
+
const text = await ibmDocs.readResource(uri);
|
|
66
|
+
return {
|
|
67
|
+
contents: [{
|
|
68
|
+
uri,
|
|
69
|
+
mimeType: uri === "ibmi-docs://catalog" ? "application/json" : "text/markdown",
|
|
70
|
+
text
|
|
71
|
+
}]
|
|
72
|
+
};
|
|
73
|
+
});
|
|
39
74
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
40
75
|
const { name, arguments: args } = request.params;
|
|
41
76
|
const correlationId = crypto.randomUUID();
|
|
42
77
|
const startedAt = Date.now();
|
|
78
|
+
// Computed before the call: module redaction depends on the state the call starts from.
|
|
79
|
+
let redactedArgs;
|
|
43
80
|
try {
|
|
44
81
|
const toolDef = toolMap.get(name);
|
|
45
82
|
if (!toolDef)
|
|
@@ -48,7 +85,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
48
85
|
if (errors.length > 0) {
|
|
49
86
|
throw new Error(`Invalid arguments: ${errors.join("; ")}`);
|
|
50
87
|
}
|
|
51
|
-
|
|
88
|
+
redactedArgs = redactSensitive(redactModuleArgs(ctx, name, args || {}));
|
|
52
89
|
log("info", "tool.call", { name, correlationId, args: redactedArgs });
|
|
53
90
|
const result = await handleTool(ctx, name, args || {});
|
|
54
91
|
const durationMs = Date.now() - startedAt;
|
|
@@ -72,12 +109,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
72
109
|
await safeAppendAudit({
|
|
73
110
|
tool: name,
|
|
74
111
|
status: "error",
|
|
75
|
-
args: redactSensitive(args || {}),
|
|
112
|
+
args: redactedArgs ?? redactSensitive(redactModuleArgs(ctx, name, args || {})),
|
|
76
113
|
connectionName: resolveConnectionName(name, args || {}, ctx.activeName),
|
|
77
114
|
approve: Boolean(args && typeof args === "object" && args.approve),
|
|
78
115
|
durationMs,
|
|
79
116
|
correlationId,
|
|
80
|
-
error: errorMessage
|
|
117
|
+
error: redactSecretsInText(errorMessage)
|
|
81
118
|
});
|
|
82
119
|
return {
|
|
83
120
|
isError: true,
|
|
@@ -88,23 +125,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
88
125
|
const transport = new StdioServerTransport();
|
|
89
126
|
await server.connect(transport);
|
|
90
127
|
function redactSensitive(input) {
|
|
91
|
-
|
|
92
|
-
if (Array.isArray(input)) {
|
|
93
|
-
return input.map(redactSensitive);
|
|
94
|
-
}
|
|
95
|
-
if (typeof input === "object" && input !== null) {
|
|
96
|
-
const out = {};
|
|
97
|
-
for (const [key, value] of Object.entries(input)) {
|
|
98
|
-
if (secrets.has(key)) {
|
|
99
|
-
out[key] = "***REDACTED***";
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
out[key] = redactSensitive(value);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
return input;
|
|
128
|
+
return redactSecrets(input);
|
|
108
129
|
}
|
|
109
130
|
function summarizeResult(result) {
|
|
110
131
|
const contentCount = Array.isArray(result?.content) ? result.content.length : 0;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Tools that only read from the IBM i, the local config or the audit log.
|
|
2
|
+
const READ_ONLY = new Set([
|
|
3
|
+
"ibmi.session.list",
|
|
4
|
+
"ibmi.session.status",
|
|
5
|
+
"ibmi.session.keepalive",
|
|
6
|
+
"ibmi.session.bootstrap",
|
|
7
|
+
"ibmi.connections.list",
|
|
8
|
+
"ibmi.qsys.libraries.list",
|
|
9
|
+
"ibmi.qsys.objects.list",
|
|
10
|
+
"ibmi.qsys.sourcefiles.list",
|
|
11
|
+
"ibmi.qsys.members.list",
|
|
12
|
+
"ibmi.qsys.members.read",
|
|
13
|
+
"ibmi.ifs.list",
|
|
14
|
+
"ibmi.ifs.read",
|
|
15
|
+
"ibmi.search.members",
|
|
16
|
+
"ibmi.source.resolve",
|
|
17
|
+
"ibmi.source.inspect",
|
|
18
|
+
"ibmi.search.ifs",
|
|
19
|
+
"ibmi.find.ifs",
|
|
20
|
+
"ibmi.actions.list",
|
|
21
|
+
"ibmi.libl.get",
|
|
22
|
+
"ibmi.libl.validate",
|
|
23
|
+
"ibmi.profiles.list",
|
|
24
|
+
"ibmi.resolve.path",
|
|
25
|
+
"ibmi.filters.list",
|
|
26
|
+
"ibmi.ifs.shortcuts.list",
|
|
27
|
+
"ibmi.debug.status",
|
|
28
|
+
"ibmi.deploy.compare",
|
|
29
|
+
"ibmi.sql.query",
|
|
30
|
+
"ibmi.diagnostics.parseEvfevent",
|
|
31
|
+
"ibmi.joblog.get",
|
|
32
|
+
"ibmi.spool.list",
|
|
33
|
+
"ibmi.spool.read",
|
|
34
|
+
"ibmi.jobs.list",
|
|
35
|
+
"ibmi.subsystems.list",
|
|
36
|
+
"ibmi.subsystems.status",
|
|
37
|
+
"ibmi.msgq.read",
|
|
38
|
+
"ibmi.locks.list",
|
|
39
|
+
"ibmi.authority.object.get",
|
|
40
|
+
"ibmi.dataarea.read",
|
|
41
|
+
"ibmi.audit.list",
|
|
42
|
+
"ibmi.audit.verify",
|
|
43
|
+
"ibmi.journal.objects.list",
|
|
44
|
+
"ibmi.journal.entries.query",
|
|
45
|
+
"ibmi.qaudjrn.events.query",
|
|
46
|
+
"ibmi.tn5250.readScreen",
|
|
47
|
+
"ibmi.tn5250.waitFor",
|
|
48
|
+
"ibmi.tn5250.snapshot"
|
|
49
|
+
]);
|
|
50
|
+
// Read-only tools that reach outside the IBM i and the local machine.
|
|
51
|
+
const OPEN_WORLD = new Set(["ibmi.docs.search", "ibmi.docs.open"]);
|
|
52
|
+
// Tools that can delete or overwrite data, end work, or run arbitrary commands.
|
|
53
|
+
const DESTRUCTIVE = new Set([
|
|
54
|
+
"ibmi.connections.delete",
|
|
55
|
+
"ibmi.qsys.members.write",
|
|
56
|
+
"ibmi.qsys.members.delete",
|
|
57
|
+
"ibmi.qsys.members.rename",
|
|
58
|
+
"ibmi.ifs.write",
|
|
59
|
+
"ibmi.ifs.delete",
|
|
60
|
+
"ibmi.ifs.upload",
|
|
61
|
+
"ibmi.ifs.download",
|
|
62
|
+
"ibmi.actions.run",
|
|
63
|
+
"ibmi.actions.delete",
|
|
64
|
+
"ibmi.profiles.delete",
|
|
65
|
+
"ibmi.filters.delete",
|
|
66
|
+
"ibmi.ifs.shortcuts.delete",
|
|
67
|
+
"ibmi.deploy.uploadDirectory",
|
|
68
|
+
"ibmi.deploy.uploadFiles",
|
|
69
|
+
"ibmi.deploy.sync",
|
|
70
|
+
"ibmi.sql.execute",
|
|
71
|
+
"ibmi.cl.run",
|
|
72
|
+
"ibmi.spool.delete",
|
|
73
|
+
"ibmi.jobs.end",
|
|
74
|
+
"ibmi.subsystems.end",
|
|
75
|
+
"ibmi.dataqueue.receive",
|
|
76
|
+
"ibmi.dataarea.write",
|
|
77
|
+
"ibmi.audit.export",
|
|
78
|
+
"ibmi.audit.purge",
|
|
79
|
+
"ibmi.journal.endPf",
|
|
80
|
+
"ibmi.journal.endIfs",
|
|
81
|
+
"ibmi.journal.receivers.retention",
|
|
82
|
+
"ibmi.compliance.report.generate",
|
|
83
|
+
"ibmi.tn5250.sendKeys"
|
|
84
|
+
]);
|
|
85
|
+
// Writes that leave the same end state when repeated with the same arguments.
|
|
86
|
+
const IDEMPOTENT_WRITES = new Set([
|
|
87
|
+
"ibmi.connect",
|
|
88
|
+
"ibmi.disconnect",
|
|
89
|
+
"ibmi.session.terminate",
|
|
90
|
+
"ibmi.connections.update",
|
|
91
|
+
"ibmi.qsys.members.write",
|
|
92
|
+
"ibmi.ifs.write",
|
|
93
|
+
"ibmi.ifs.mkdir",
|
|
94
|
+
"ibmi.ifs.upload",
|
|
95
|
+
"ibmi.ifs.download",
|
|
96
|
+
"ibmi.libl.set",
|
|
97
|
+
"ibmi.libl.setCurrent",
|
|
98
|
+
"ibmi.profiles.save",
|
|
99
|
+
"ibmi.profiles.activate",
|
|
100
|
+
"ibmi.actions.save",
|
|
101
|
+
"ibmi.filters.save",
|
|
102
|
+
"ibmi.deploy.setCcsid",
|
|
103
|
+
"ibmi.spool.hold",
|
|
104
|
+
"ibmi.spool.release",
|
|
105
|
+
"ibmi.jobs.hold",
|
|
106
|
+
"ibmi.jobs.release",
|
|
107
|
+
"ibmi.dataarea.write",
|
|
108
|
+
"ibmi.audit.export",
|
|
109
|
+
"ibmi.tn5250.connect",
|
|
110
|
+
"ibmi.tn5250.setField",
|
|
111
|
+
"ibmi.tn5250.disconnect"
|
|
112
|
+
]);
|
|
113
|
+
export function annotationsFor(name) {
|
|
114
|
+
if (READ_ONLY.has(name)) {
|
|
115
|
+
return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
116
|
+
}
|
|
117
|
+
if (OPEN_WORLD.has(name)) {
|
|
118
|
+
return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
119
|
+
}
|
|
120
|
+
// Anything not listed is treated as a write; unknown tools default to destructive.
|
|
121
|
+
return {
|
|
122
|
+
readOnlyHint: false,
|
|
123
|
+
destructiveHint: DESTRUCTIVE.has(name) || !IDEMPOTENT_WRITES.has(name) && !isKnownNonDestructiveWrite(name),
|
|
124
|
+
idempotentHint: IDEMPOTENT_WRITES.has(name),
|
|
125
|
+
openWorldHint: false
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function isKnownNonDestructiveWrite(name) {
|
|
129
|
+
return [
|
|
130
|
+
"ibmi.connections.add",
|
|
131
|
+
"ibmi.qsys.members.create",
|
|
132
|
+
"ibmi.qsys.sourcefiles.create",
|
|
133
|
+
"ibmi.qsys.libraries.create",
|
|
134
|
+
"ibmi.libl.add",
|
|
135
|
+
"ibmi.libl.remove",
|
|
136
|
+
"ibmi.ifs.shortcuts.add",
|
|
137
|
+
"ibmi.debug.startService",
|
|
138
|
+
"ibmi.debug.stopService",
|
|
139
|
+
"ibmi.spool.move",
|
|
140
|
+
"ibmi.subsystems.start",
|
|
141
|
+
"ibmi.msgq.send",
|
|
142
|
+
"ibmi.msgq.reply",
|
|
143
|
+
"ibmi.dataqueue.send",
|
|
144
|
+
"ibmi.journal.receiver.create",
|
|
145
|
+
"ibmi.journal.create",
|
|
146
|
+
"ibmi.journal.receiver.change",
|
|
147
|
+
"ibmi.journal.startPf",
|
|
148
|
+
"ibmi.journal.startIfs"
|
|
149
|
+
].includes(name);
|
|
150
|
+
}
|
|
151
|
+
export function withAnnotations(tool) {
|
|
152
|
+
if (tool.annotations)
|
|
153
|
+
return tool;
|
|
154
|
+
return { ...tool, annotations: annotationsFor(tool.name) };
|
|
155
|
+
}
|