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,221 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import http from "http";
4
+ import path from "path";
5
+ import { getConfigDir } from "../../config/store.js";
6
+ import { renderLivePage } from "./tn5250LivePage.js";
7
+ export const LIVE_TOKEN_HEADER = "x-mcp-for-i-live-token";
8
+ const MAX_INPUT_BYTES = 16 * 1024;
9
+ /** Folder where each MCP process lists its live view, for the control plane. */
10
+ export function liveRegistryDir(configDir = getConfigDir()) {
11
+ return path.join(configDir, "tn5250-live");
12
+ }
13
+ export async function startLiveServer(provider, preferredPort = 3981) {
14
+ const token = crypto.randomBytes(24).toString("hex");
15
+ const streams = new Set();
16
+ let port = 0;
17
+ const server = http.createServer((req, res) => {
18
+ handle(req, res).catch(err => {
19
+ if (!res.headersSent)
20
+ sendJson(res, 500, { error: err?.message || String(err) });
21
+ else
22
+ res.destroy();
23
+ });
24
+ });
25
+ async function handle(req, res) {
26
+ const url = new URL(req.url || "/", "http://127.0.0.1");
27
+ if (!allowedHost(req.headers.host, port))
28
+ return sendJson(res, 421, { error: "Host not allowed" });
29
+ const origin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
30
+ if (origin && !allowedOrigin(origin, port))
31
+ return sendJson(res, 403, { error: "Cross-origin requests are not allowed" });
32
+ const provided = url.searchParams.get("t") || req.headers[LIVE_TOKEN_HEADER];
33
+ if (!tokensMatch(token, provided))
34
+ return sendJson(res, 401, { error: "Missing or invalid live view token" });
35
+ const name = url.searchParams.get("s") || "";
36
+ if (req.method === "GET" && url.pathname === "/") {
37
+ res.writeHead(200, {
38
+ "Content-Type": "text/html; charset=utf-8",
39
+ "Cache-Control": "no-store",
40
+ "Referrer-Policy": "no-referrer",
41
+ "X-Content-Type-Options": "nosniff",
42
+ "Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'self'"
43
+ });
44
+ res.end(renderLivePage());
45
+ return;
46
+ }
47
+ if (req.method === "GET" && url.pathname === "/api/sessions") {
48
+ return sendJson(res, 200, { sessions: provider.list() });
49
+ }
50
+ if (req.method === "GET" && url.pathname === "/api/screen") {
51
+ const screen = provider.screen(name);
52
+ return screen ? sendJson(res, 200, screen) : sendJson(res, 404, { error: `No 5250 session for ${name}` });
53
+ }
54
+ if (req.method === "GET" && url.pathname === "/api/events") {
55
+ res.writeHead(200, {
56
+ "Content-Type": "text/event-stream",
57
+ "Cache-Control": "no-store",
58
+ Connection: "keep-alive"
59
+ });
60
+ streams.add(res);
61
+ let timer;
62
+ const push = () => {
63
+ timer = undefined;
64
+ const screen = provider.screen(name);
65
+ res.write(`event: screen\ndata: ${JSON.stringify(screen || { missing: true, connectionName: name })}\n\n`);
66
+ };
67
+ const onChanged = (changed) => {
68
+ if (changed === name && !timer)
69
+ timer = setTimeout(push, 40);
70
+ res.write(`event: sessions\ndata: ${JSON.stringify(provider.list())}\n\n`);
71
+ };
72
+ const keepAlive = setInterval(() => res.write(": keep-alive\n\n"), 15000);
73
+ provider.events.on("changed", onChanged);
74
+ push();
75
+ req.on("close", () => {
76
+ clearInterval(keepAlive);
77
+ if (timer)
78
+ clearTimeout(timer);
79
+ provider.events.off("changed", onChanged);
80
+ streams.delete(res);
81
+ });
82
+ return;
83
+ }
84
+ if (req.method === "POST" && url.pathname === "/api/input") {
85
+ const contentType = String(req.headers["content-type"] || "").split(";")[0].trim().toLowerCase();
86
+ if (contentType !== "application/json")
87
+ return sendJson(res, 415, { error: "Content-Type must be application/json" });
88
+ const body = await readBody(req);
89
+ let input;
90
+ try {
91
+ input = JSON.parse(body || "{}");
92
+ }
93
+ catch {
94
+ return sendJson(res, 400, { error: "Invalid JSON" });
95
+ }
96
+ try {
97
+ const result = await provider.input(name, sanitizeInput(input));
98
+ return sendJson(res, 200, { ok: true, sent: result.sent, note: result.note, settled: result.settled });
99
+ }
100
+ catch (err) {
101
+ return sendJson(res, 200, { ok: false, error: err?.message || String(err) });
102
+ }
103
+ }
104
+ sendJson(res, 404, { error: "Not found" });
105
+ }
106
+ await new Promise((resolve, reject) => {
107
+ const tryListen = (candidate, fallback) => {
108
+ server.once("error", (err) => {
109
+ if (fallback && err?.code === "EADDRINUSE")
110
+ tryListen(0, false);
111
+ else
112
+ reject(err);
113
+ });
114
+ server.listen(candidate, "127.0.0.1", () => resolve());
115
+ };
116
+ tryListen(preferredPort, preferredPort !== 0);
117
+ });
118
+ server.unref();
119
+ port = server.address().port;
120
+ const registryFile = path.join(liveRegistryDir(), `${process.pid}.json`);
121
+ const writeRegistry = () => {
122
+ try {
123
+ fs.mkdirSync(path.dirname(registryFile), { recursive: true });
124
+ const entry = { pid: process.pid, port, url: urlFor(), sessions: provider.list(), updatedAt: new Date().toISOString() };
125
+ fs.writeFileSync(registryFile, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 0o600 });
126
+ }
127
+ catch {
128
+ // the control plane listing is a convenience
129
+ }
130
+ };
131
+ const removeRegistry = () => {
132
+ try {
133
+ fs.rmSync(registryFile, { force: true });
134
+ }
135
+ catch { /* ignore */ }
136
+ };
137
+ let registryTimer;
138
+ const onAnyChange = () => {
139
+ if (registryTimer)
140
+ return;
141
+ registryTimer = setTimeout(() => { registryTimer = undefined; writeRegistry(); }, 250);
142
+ registryTimer.unref();
143
+ };
144
+ provider.events.on("changed", onAnyChange);
145
+ process.once("exit", removeRegistry);
146
+ writeRegistry();
147
+ function urlFor(connectionName) {
148
+ const params = new URLSearchParams({ t: token });
149
+ if (connectionName)
150
+ params.set("s", connectionName);
151
+ return `http://127.0.0.1:${port}/?${params.toString()}`;
152
+ }
153
+ return {
154
+ port,
155
+ token,
156
+ urlFor,
157
+ close: () => new Promise(resolve => {
158
+ provider.events.off("changed", onAnyChange);
159
+ if (registryTimer)
160
+ clearTimeout(registryTimer);
161
+ removeRegistry();
162
+ for (const stream of streams)
163
+ stream.end();
164
+ server.close(() => resolve());
165
+ server.closeAllConnections();
166
+ })
167
+ };
168
+ }
169
+ function sanitizeInput(input) {
170
+ const out = {};
171
+ if (typeof input?.text === "string")
172
+ out.text = input.text.slice(0, 2000);
173
+ if (typeof input?.keys === "string")
174
+ out.keys = input.keys.slice(0, 500);
175
+ if (Number.isFinite(input?.cursorRow))
176
+ out.cursorRow = Number(input.cursorRow);
177
+ if (Number.isFinite(input?.cursorCol))
178
+ out.cursorCol = Number(input.cursorCol);
179
+ if (input?.click && Number.isFinite(input.click.row) && Number.isFinite(input.click.col)) {
180
+ out.click = { row: Number(input.click.row), col: Number(input.click.col), doubleClick: input.click.doubleClick === true };
181
+ }
182
+ return out;
183
+ }
184
+ function readBody(req) {
185
+ return new Promise((resolve, reject) => {
186
+ let size = 0;
187
+ const chunks = [];
188
+ req.on("data", (chunk) => {
189
+ size += chunk.length;
190
+ if (size > MAX_INPUT_BYTES) {
191
+ reject(new Error("Request body too large"));
192
+ req.destroy();
193
+ return;
194
+ }
195
+ chunks.push(chunk);
196
+ });
197
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
198
+ req.on("error", reject);
199
+ });
200
+ }
201
+ function sendJson(res, status, body) {
202
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
203
+ res.end(JSON.stringify(body));
204
+ }
205
+ function allowedHost(host, port) {
206
+ if (!host)
207
+ return false;
208
+ const lower = host.toLowerCase();
209
+ return lower === `127.0.0.1:${port}` || lower === `localhost:${port}`;
210
+ }
211
+ function allowedOrigin(origin, port) {
212
+ const lower = origin.toLowerCase();
213
+ return lower === `http://127.0.0.1:${port}` || lower === `http://localhost:${port}`;
214
+ }
215
+ function tokensMatch(expected, provided) {
216
+ if (typeof provided !== "string")
217
+ return false;
218
+ const a = Buffer.from(expected);
219
+ const b = Buffer.from(provided);
220
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
221
+ }