mcp-for-i 0.1.13 → 0.3.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 (72) hide show
  1. package/README.md +27 -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 +52 -11
  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 +178 -11
  11. package/dist/controlplane/serverShared.js +25 -6
  12. package/dist/controlplane/serviceManager.js +147 -100
  13. package/dist/controlplane/ui.js +1783 -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 +334 -0
  29. package/dist/ibmi/tn5250/datastream.js +109 -0
  30. package/dist/ibmi/tn5250/ebcdic.js +93 -0
  31. package/dist/ibmi/tn5250/ebcdicTables.js +27 -0
  32. package/dist/ibmi/tn5250/index.js +6 -0
  33. package/dist/ibmi/tn5250/screen.js +1033 -0
  34. package/dist/ibmi/tn5250/telnet.js +186 -0
  35. package/dist/ibmi/variables.js +18 -7
  36. package/dist/index.js +79 -24
  37. package/dist/mcp/annotations.js +155 -0
  38. package/dist/mcp/audit.js +490 -133
  39. package/dist/mcp/confirm.js +48 -0
  40. package/dist/mcp/context.js +43 -5
  41. package/dist/mcp/deployMethods.js +192 -0
  42. package/dist/mcp/logger.js +5 -10
  43. package/dist/mcp/modules/bulkMembers.js +476 -0
  44. package/dist/mcp/modules/changedLines.js +189 -0
  45. package/dist/mcp/modules/compare.js +198 -0
  46. package/dist/mcp/modules/compile.js +515 -0
  47. package/dist/mcp/modules/ifsOps.js +123 -0
  48. package/dist/mcp/modules/index.js +38 -0
  49. package/dist/mcp/modules/inspect.js +811 -0
  50. package/dist/mcp/modules/libl.js +246 -0
  51. package/dist/mcp/modules/members.js +171 -0
  52. package/dist/mcp/modules/objects.js +339 -0
  53. package/dist/mcp/modules/system.js +266 -0
  54. package/dist/mcp/modules/tn5250.js +800 -0
  55. package/dist/mcp/modules/tn5250App.js +194 -0
  56. package/dist/mcp/modules/tn5250Font.js +8 -0
  57. package/dist/mcp/modules/tn5250Live.js +221 -0
  58. package/dist/mcp/modules/tn5250LivePage.js +480 -0
  59. package/dist/mcp/policy.js +412 -0
  60. package/dist/mcp/tools.js +1719 -698
  61. package/dist/security/credentialStore.js +62 -13
  62. package/dist/security/localPaths.js +138 -0
  63. package/dist/security/redact.js +79 -0
  64. package/dist/util/atomicWrite.js +101 -0
  65. package/dist/util/fileLock.js +206 -0
  66. package/docs/capabilities.md +264 -220
  67. package/docs/configuration.md +193 -60
  68. package/docs/examples.md +109 -73
  69. package/docs/overview.md +15 -15
  70. package/docs/security.md +101 -16
  71. package/docs/tools.md +367 -188
  72. package/package.json +5 -2
@@ -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
+ }
@@ -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
- for (const [key, value] of this.entries()) {
36
- if (!keysToOmit.includes(key)) {
37
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38
- input = input.replace(new RegExp(escapedKey, `g`), value);
39
- }
40
- }
41
- return input;
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,18 @@ 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";
18
+ import { MCP_APP_MIME_TYPE, TN5250_APP_TOOLS, TN5250_APP_URI, renderTn5250App } from "./mcp/modules/tn5250App.js";
14
19
  const argv = process.argv.slice(2);
15
20
  const firstArg = (argv[0] || "").toLowerCase();
16
21
  const packageVersion = readPackageVersion();
@@ -29,17 +34,64 @@ if (firstArg === "--help" || firstArg === "-h" || firstArg === "help") {
29
34
  printHelp();
30
35
  process.exit(0);
31
36
  }
32
- const server = new Server({ name: "mcp-for-i", version: packageVersion }, { capabilities: { tools: {} } });
37
+ const server = new Server({ name: "mcp-for-i", version: packageVersion }, { capabilities: { tools: {}, resources: {} } });
33
38
  const ctx = new McpContext();
34
- const tools = getTools();
39
+ ctx.confirm = createElicitationConfirm(server);
40
+ // 5250 tools point MCP Apps clients at the screen view; other clients ignore _meta.
41
+ const tools = getTools().map(tool => (TN5250_APP_TOOLS.has(tool.name)
42
+ ? { ...tool, _meta: { ...(tool._meta || {}), ui: { resourceUri: TN5250_APP_URI } } }
43
+ : tool));
35
44
  const toolMap = new Map(tools.map(tool => [tool.name, tool]));
36
45
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
46
  tools
38
47
  }));
48
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
49
+ resources: [
50
+ {
51
+ uri: "ibmi-docs://catalog",
52
+ name: "IBM i documentation catalog",
53
+ description: "Release-aware allowlist of official IBM i documentation available through mcp-for-i.",
54
+ mimeType: "application/json"
55
+ },
56
+ {
57
+ uri: TN5250_APP_URI,
58
+ name: "5250 screen view",
59
+ description: "Interactive 5250 screen for MCP Apps clients (used by the ibmi.tn5250 tools).",
60
+ mimeType: MCP_APP_MIME_TYPE
61
+ },
62
+ ...ibmDocs.listResources()
63
+ ]
64
+ }));
65
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
66
+ resourceTemplates: [{
67
+ uriTemplate: "ibmi-docs://topic/{ref}",
68
+ name: "Official IBM i documentation topic",
69
+ description: "Open a versioned official IBM i documentation topic using a ref from ibmi.docs.search.",
70
+ mimeType: "text/markdown"
71
+ }]
72
+ }));
73
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
74
+ const uri = request.params.uri;
75
+ if (uri === TN5250_APP_URI) {
76
+ return {
77
+ contents: [{ uri, mimeType: MCP_APP_MIME_TYPE, text: renderTn5250App(packageVersion), _meta: { ui: { prefersBorder: false } } }]
78
+ };
79
+ }
80
+ const text = await ibmDocs.readResource(uri);
81
+ return {
82
+ contents: [{
83
+ uri,
84
+ mimeType: uri === "ibmi-docs://catalog" ? "application/json" : "text/markdown",
85
+ text
86
+ }]
87
+ };
88
+ });
39
89
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
40
90
  const { name, arguments: args } = request.params;
41
91
  const correlationId = crypto.randomUUID();
42
92
  const startedAt = Date.now();
93
+ // Computed before the call: module redaction depends on the state the call starts from.
94
+ let redactedArgs;
43
95
  try {
44
96
  const toolDef = toolMap.get(name);
45
97
  if (!toolDef)
@@ -48,7 +100,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
48
100
  if (errors.length > 0) {
49
101
  throw new Error(`Invalid arguments: ${errors.join("; ")}`);
50
102
  }
51
- const redactedArgs = redactSensitive(args || {});
103
+ redactedArgs = redactSensitive(redactModuleArgs(ctx, name, args || {}));
52
104
  log("info", "tool.call", { name, correlationId, args: redactedArgs });
53
105
  const result = await handleTool(ctx, name, args || {});
54
106
  const durationMs = Date.now() - startedAt;
@@ -72,12 +124,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
124
  await safeAppendAudit({
73
125
  tool: name,
74
126
  status: "error",
75
- args: redactSensitive(args || {}),
127
+ args: redactedArgs ?? redactSensitive(redactModuleArgs(ctx, name, args || {})),
76
128
  connectionName: resolveConnectionName(name, args || {}, ctx.activeName),
77
129
  approve: Boolean(args && typeof args === "object" && args.approve),
78
130
  durationMs,
79
131
  correlationId,
80
- error: errorMessage
132
+ error: redactSecretsInText(errorMessage)
81
133
  });
82
134
  return {
83
135
  isError: true,
@@ -88,30 +140,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
88
140
  const transport = new StdioServerTransport();
89
141
  await server.connect(transport);
90
142
  function redactSensitive(input) {
91
- const secrets = new Set(["password", "passphrase", "secret", "token", "apikey", "apiKey", "authorization"]);
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;
143
+ return redactSecrets(input);
108
144
  }
109
145
  function summarizeResult(result) {
110
146
  const contentCount = Array.isArray(result?.content) ? result.content.length : 0;
111
147
  return {
112
148
  isError: Boolean(result?.isError),
113
149
  contentCount,
114
- hasStructuredContent: typeof result?.structuredContent !== "undefined"
150
+ hasStructuredContent: typeof result?.structuredContent !== "undefined",
151
+ ...summarizeScreen(result?.structuredContent)
152
+ };
153
+ }
154
+ /** For 5250 results: the screen's first text line and state, so the audit shows where a session went. */
155
+ function summarizeScreen(structured) {
156
+ const content = structured?.data ?? structured;
157
+ const screen = content && Array.isArray(content.text) ? content : content?.screen;
158
+ if (!screen || !Array.isArray(screen.text) || typeof screen.version !== "number")
159
+ return {};
160
+ const title = String(screen.text.find((line) => typeof line === "string" && line.trim()) || "").trim().substring(0, 80);
161
+ return {
162
+ screen: {
163
+ title: redactSecretsInText(title),
164
+ keyboardLocked: Boolean(screen.keyboardLocked),
165
+ ...(screen.errorLine ? { errorLine: redactSecretsInText(String(screen.errorLine).substring(0, 80)) } : {}),
166
+ ...(typeof content.sent === "string" ? { sent: content.sent } : {})
167
+ }
115
168
  };
116
169
  }
117
170
  function resolveConnectionName(tool, args, activeName) {
@@ -136,6 +189,8 @@ function printHelp() {
136
189
  process.stderr.write(` mcp-for-i Start MCP stdio server\n`);
137
190
  process.stderr.write(` mcp-for-i control Start/open control plane UI\n`);
138
191
  process.stderr.write(` mcp-for-i control status Show control plane and background-service status\n`);
192
+ process.stderr.write(` mcp-for-i control stop Stop the running control plane\n`);
193
+ process.stderr.write(` mcp-for-i control restart Stop the control plane and start it again\n`);
139
194
  process.stderr.write(` mcp-for-i control enable Enable background service for this platform\n`);
140
195
  process.stderr.write(` mcp-for-i control disable Disable background service for this platform\n`);
141
196
  process.stderr.write(` mcp-for-i --version Print package version\n`);
@@ -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
+ }