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.
- package/README.md +27 -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 +52 -11
- 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 +178 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +147 -100
- package/dist/controlplane/ui.js +1783 -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 +334 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +93 -0
- package/dist/ibmi/tn5250/ebcdicTables.js +27 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +1033 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +79 -24
- 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 +800 -0
- package/dist/mcp/modules/tn5250App.js +194 -0
- package/dist/mcp/modules/tn5250Font.js +8 -0
- package/dist/mcp/modules/tn5250Live.js +221 -0
- package/dist/mcp/modules/tn5250LivePage.js +480 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1719 -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 +264 -220
- package/docs/configuration.md +193 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +367 -188
- 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
|
+
}
|