#!/usr/bin/env bun import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { homedir } from "node:os"; import { createMcpHttpHeaders } from "./mcp-http-headers.mjs"; const CONFIG_PATH = process.env.EDGEEVER_CONFIG || join(homedir(), ".edgeever", "config.json"); const DEFAULT_URL = "http://127.0.0.1:8787"; const options = parseOptions(process.argv.slice(2)); const usage = `EdgeEver MCP stdio adapter Usage: EDGEEVER_URL=https://your.edgeever.host EDGEEVER_TOKEN=... bun scripts/edgeever-mcp-stdio.mjs bun scripts/edgeever-mcp-stdio.mjs --profile prod The adapter bridges local stdio MCP clients to the remote EdgeEver /mcp endpoint. `; if (options.help || options.h) { process.stdout.write(usage); process.exit(0); } let client; try { client = await createClient(options.profile); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exit(1); } let inputBuffer = Buffer.alloc(0); let transportMode = null; let negotiatedProtocolVersion = null; process.stdin.on("data", (chunk) => { inputBuffer = Buffer.concat([inputBuffer, chunk]); drainInput(); }); process.stdin.on("error", (error) => { console.error(error instanceof Error ? error.message : error); process.exit(1); }); process.stdin.resume(); function drainInput() { while (inputBuffer.length > 0) { const framed = readFramedMessage(inputBuffer); if (framed) { transportMode = "framed"; inputBuffer = inputBuffer.subarray(framed.consumedBytes); void handleMessage(framed.payload); continue; } if (looksLikeFramedMessage(inputBuffer)) { return; } const newlineIndex = inputBuffer.indexOf(0x0a); if (newlineIndex === -1) { return; } const line = inputBuffer.subarray(0, newlineIndex).toString("utf8").trim(); inputBuffer = inputBuffer.subarray(newlineIndex + 1); if (!line) { continue; } transportMode = transportMode || "line"; void handleMessage(line); } } async function handleMessage(payload) { let request; try { request = JSON.parse(payload); } catch { writeMessage(jsonRpcError(null, -32700, "Parse error")); return; } try { const response = await forwardToEdgeEver(request); if (response !== null) { writeMessage(response); } } catch (error) { writeMessage( jsonRpcError(getJsonRpcId(request), -32000, error instanceof Error ? error.message : "MCP bridge failed") ); } } async function forwardToEdgeEver(request) { if (request?.method === "initialize") { negotiatedProtocolVersion = request?.params?.protocolVersion || "2025-11-25"; } const requestHeaders = createMcpHttpHeaders(request, negotiatedProtocolVersion); const response = await fetch(`${client.baseUrl}/mcp`, { method: "POST", headers: { Authorization: `Bearer ${client.token}`, Accept: "application/json, text/event-stream", "Content-Type": "application/json", ...requestHeaders, }, body: JSON.stringify(request), }); if (response.status === 202 || response.status === 204) { return null; } const body = await response.json().catch(() => null); if (!body) { throw new Error(`${response.status} ${response.statusText}`); } if (request?.method === "initialize" && body?.result?.protocolVersion) { negotiatedProtocolVersion = body.result.protocolVersion; } return body; } async function createClient(profileName) { const directUrl = process.env.EDGEEVER_URL; const directToken = process.env.EDGEEVER_TOKEN; // ACP supplies both values for its short-lived local bridge. In that mode, // do not read an unrelated MCP profile from the user's home directory. const config = directUrl && directToken ? {} : await readConfig(); const profile = profileName ? config.profiles?.[profileName] : undefined; const baseUrl = (directUrl || profile?.url || DEFAULT_URL).replace(/\/+$/, ""); const token = directToken || profile?.token; if (!token) { throw new Error("EDGEEVER_TOKEN is required, or configure a profile with `bun run cli -- profile set`."); } return { baseUrl, token }; } async function readConfig() { try { const value = JSON.parse(await readFile(CONFIG_PATH, "utf8")); return value && typeof value === "object" ? value : {}; } catch { return {}; } } function readFramedMessage(buffer) { const headerEnd = findHeaderEnd(buffer); if (!headerEnd) { return null; } const header = buffer.subarray(0, headerEnd.index).toString("utf8"); const match = /^content-length:\s*(\d+)\s*$/im.exec(header); if (!match) { return null; } const contentLength = Number(match[1]); const bodyStart = headerEnd.index + headerEnd.length; const bodyEnd = bodyStart + contentLength; if (!Number.isInteger(contentLength) || contentLength < 0 || buffer.length < bodyEnd) { return null; } return { payload: buffer.subarray(bodyStart, bodyEnd).toString("utf8"), consumedBytes: bodyEnd, }; } function findHeaderEnd(buffer) { const crlf = buffer.indexOf("\r\n\r\n"); if (crlf !== -1) { return { index: crlf, length: 4 }; } const lf = buffer.indexOf("\n\n"); return lf === -1 ? null : { index: lf, length: 2 }; } function looksLikeFramedMessage(buffer) { return buffer.subarray(0, Math.min(buffer.length, 32)).toString("utf8").toLowerCase().startsWith("content-length:"); } function writeMessage(message) { const payload = `${JSON.stringify(message)}\n`; if (transportMode === "framed") { const content = payload.trimEnd(); process.stdout.write(`Content-Length: ${Buffer.byteLength(content, "utf8")}\r\n\r\n${content}`); return; } process.stdout.write(payload); } function parseOptions(args) { const parsed = {}; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (!arg.startsWith("--")) { continue; } const key = arg.slice(2); const next = args[index + 1]; if (!next || next.startsWith("--")) { parsed[key] = "true"; continue; } parsed[key] = next; index += 1; } return parsed; } function getJsonRpcId(request) { if (!request || typeof request !== "object" || !("id" in request)) { return null; } const id = request.id; return typeof id === "string" || typeof id === "number" || id === null ? id : null; } function jsonRpcError(id, code, message) { return { jsonrpc: "2.0", id, error: { code, message, }, }; }