Ctrl K
ComunidadGuías…

scripts/mcp-http-client.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// Host-only stdio bridge. The server still enforces every scope and project boundary.
import { open } from "node:fs/promises";
import { constants } from "node:fs";
import { createHash, randomUUID } from "node:crypto";
import { resolve } from "node:path";
import { isIP } from "node:net";
import { fileURLToPath } from "node:url";
const MAX_REQUEST = 1024 * 1024, MAX_IMAGE_REQUEST = 3 * 1024 * 1024, MAX_RESPONSE = 8 * MAX_REQUEST;
const fail = () => { throw new Error("Private MCP configuration is missing, unsafe or invalid."); };
export async function readClientConfig(path) {
  let handle;
  try {
    handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
    const stat = await handle.stat();
    if (!stat.isFile() || stat.size < 1 || stat.size > 8192 || stat.mode & 0o077 || (process.getuid && stat.uid !== process.getuid())) fail();
    const config = JSON.parse(await handle.readFile("utf8"));
    if (!config || typeof config !== "object" || Array.isArray(config) || Object.keys(config).some(key => !["endpoint", "token", "allowInsecureLan"].includes(key))) fail();
    const endpoint = new URL(config.endpoint);
    if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !endpoint.pathname.endsWith("/mcp")) fail();
    const trustedLan = isIP(endpoint.hostname) === 4 && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(endpoint.hostname) || ["localhost", "[::1]"].includes(endpoint.hostname);
    if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && config.allowInsecureLan === true && trustedLan)) fail();
    if (typeof config.token !== "string" || config.token.length < 16 || config.token.length > 4096 || /[\s\u0000-\u001f]/u.test(config.token)) fail();
    return { endpoint: endpoint.href, token: config.token };
  } catch { fail(); } finally { await handle?.close(); }
}
const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
export function createForwarder(config, request = fetch) {
  const unconfirmed = new Map();
  return async message => {
    const id = message && (typeof message.id === "string" || Number.isSafeInteger(message.id)) ? message.id : null;
    const error = (code, text) => id === null ? null : { jsonrpc: "2.0", id, error: { code, message: text } };
    if (!message || Array.isArray(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || Buffer.byteLength(JSON.stringify(message)) > (message.method === "tools/call" && message.params?.name === "add_inventory_image" ? MAX_IMAGE_REQUEST : MAX_REQUEST)) return error(-32600, "Invalid or oversized MCP request.");
    const operation = message.method === "tools/call" ? createHash("sha256").update(canonical({ name: message.params?.name, arguments: message.params?.arguments ?? {} })).digest("hex") : undefined;
    const key = operation ? unconfirmed.get(operation) ?? `host-${randomUUID()}` : undefined;
    if (operation && !unconfirmed.has(operation) && unconfirmed.size >= 64) return error(-32098, "Too many unconfirmed commands. Resolve unchanged requests before sending new commands.");
    if (operation) unconfirmed.set(operation, key);
    try {
      const response = await request(config.endpoint, { method: "POST", redirect: "error", signal: AbortSignal.timeout(60_000), headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${config.token}`, ...(key ? { "idempotency-key": key } : {}) }, body: JSON.stringify(message) });
      if (!response.ok) {
        await response.body?.cancel();
        if (operation && response.status < 500) unconfirmed.delete(operation);
        return error(response.status >= 500 ? -32098 : -32001, response.status >= 500 ? "Server response is not confirmed. Retry unchanged in this session or read current state before a new command." : `MCP request rejected (HTTP ${response.status}). Check the connection scope and request.`);
      }
      if (id === null) { await response.body?.cancel(); return null; }
      if (response.status === 202) throw new Error("Request acknowledgement is missing");
      if (!response.headers.get("content-type")?.includes("application/json")) throw new Error("Invalid response");
      let length = 0; const chunks = [];
      for await (const chunk of response.body) { length += chunk.byteLength; if (length > MAX_RESPONSE) throw new Error("Response too large"); chunks.push(chunk); }
      const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
      if (body?.jsonrpc !== "2.0" || body.id !== id || (!body.error && !body.result)) throw new Error("Unconfirmed response");
      if (operation && !["BACKEND_ERROR", "INTERNAL_ERROR"].includes(body.result?.structuredContent?.error?.code)) unconfirmed.delete(operation);
      return body;
    } catch { return error(-32098, "Connection or acknowledgement failed. Do not assume the command failed. Retry unchanged in this session or read current state before a new command."); }
  };
}
export async function runStdioBridge(config, input = process.stdin, output = process.stdout) {
  const forward = createForwarder(config); let pending = Buffer.alloc(0);
  for await (const chunk of input) {
    pending = Buffer.concat([pending, Buffer.from(chunk)]);
    let newline;
    while ((newline = pending.indexOf(10)) >= 0) {
      const line = pending.subarray(0, newline); pending = pending.subarray(newline + 1);
      if (line.byteLength > MAX_IMAGE_REQUEST) throw new Error("MCP request exceeds the local limit.");
      if (!line.toString("utf8").trim()) continue;
      let message;
      try { message = JSON.parse(line.toString("utf8")); }
      catch { output.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Invalid JSON request." } }) + "\n"); continue; }
      const response = await forward(message);
      if (response) output.write(JSON.stringify(response) + "\n");
    }
    if (pending.byteLength > MAX_IMAGE_REQUEST) throw new Error("MCP request exceeds the local limit.");
  }
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  if (process.argv[2] === "--help") console.log("Host MCP connection: node scripts/mcp-http-client.mjs --config <private-file>\nConfiguration: endpoint, token, optional allowInsecureLan. The file must be owned by this user with mode 0600. Use a project-restricted server token. No automatic retries; re-read state after restarting a connection with an unresolved write.");
  else {
    try {
      if (process.argv.length !== 4 || process.argv[2] !== "--config") fail();
      await runStdioBridge(await readClientConfig(process.argv[3]));
    } catch { console.error("BenchLedger MCP connection stopped. Check the private configuration, access permissions and server status. No automatic retries were made."); process.exitCode = 1; }
  }
}