bayuMIR/.codex/mcp_996_gui_proxy.js

263 lines
7.3 KiB
JavaScript

const URL = process.env.MCP_996_GUI_URL;
const AUTH = process.env.MCP_996_GUI_AUTH;
const fs = require("fs");
const LOG = process.env.MCP_996_GUI_LOG || "C:\\Users\\Administrator\\.codex\\mcp_996_gui_proxy.log";
function log(message) {
try {
fs.appendFileSync(LOG, `${new Date().toISOString()} ${message}\n`, "utf8");
} catch {
// Logging must never break the MCP server.
}
}
log(`start pid=${process.pid}`);
const tools = [
{
name: "list_rule_files",
description: "List all client GUI API rule files.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
{
name: "list_versions",
description: "List available client GUI API versions.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
{
name: "list_apis",
description: "Search client GUI APIs by keyword.",
inputSchema: {
type: "object",
properties: {
keyword: { type: "string", default: "" },
limit: { type: "integer", default: 50 },
version: { type: "string", default: "" },
},
additionalProperties: false,
},
},
{
name: "generate_lua",
description: "Generate client Lua code from the original natural language query.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
top_k: { type: "integer", default: 3 },
version: { type: "string", default: "" },
},
required: ["query"],
additionalProperties: false,
},
},
{
name: "generate_lua_996",
description: "Alias of generate_lua for 996 API client GUI code generation.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
top_k: { type: "integer", default: 3 },
version: { type: "string", default: "" },
},
required: ["query"],
additionalProperties: false,
},
},
];
let input = Buffer.alloc(0);
let responseMode = "lsp";
process.stdin.on("data", (chunk) => {
log(`stdin bytes=${chunk.length}`);
log(`stdin utf8=${JSON.stringify(chunk.toString("utf8"))}`);
log(`stdin hex=${chunk.toString("hex")}`);
input = Buffer.concat([input, chunk]);
readMessages();
});
process.stdin.on("end", () => log("stdin end"));
process.on("uncaughtException", (error) => log(`uncaught ${error.stack || error.message || error}`));
function readMessages() {
while (true) {
let firstNonWhitespace = 0;
while (
firstNonWhitespace < input.length &&
[9, 10, 13, 32].includes(input[firstNonWhitespace])
) {
firstNonWhitespace += 1;
}
if (firstNonWhitespace > 0) input = input.slice(firstNonWhitespace);
if (input.length === 0) return;
if (input[0] === 123) {
responseMode = "ndjson";
const lineEnd = input.indexOf("\n");
if (lineEnd < 0) return;
const body = input.slice(0, lineEnd).toString("utf8").trim();
input = input.slice(lineEnd + 1);
if (!body) continue;
log(`message ${body}`);
handle(JSON.parse(body)).catch((error) => {
respond(JSON.parse(body).id, null, {
code: -32603,
message: error && error.message ? error.message : String(error),
});
});
continue;
}
let headerEnd = input.indexOf("\r\n\r\n");
let separatorLength = 4;
if (headerEnd < 0) {
headerEnd = input.indexOf("\n\n");
separatorLength = 2;
}
if (headerEnd < 0) return;
const header = input.slice(0, headerEnd).toString("ascii");
const match = /Content-Length:\s*(\d+)/i.exec(header);
if (!match) {
input = input.slice(headerEnd + separatorLength);
continue;
}
const length = Number(match[1]);
const start = headerEnd + separatorLength;
const end = start + length;
if (input.length < end) return;
const body = input.slice(start, end).toString("utf8");
input = input.slice(end);
log(`message ${body}`);
handle(JSON.parse(body)).catch((error) => {
respond(JSON.parse(body).id, null, {
code: -32603,
message: error && error.message ? error.message : String(error),
});
});
}
}
async function handle(message) {
if (message.id === undefined) return;
log(`handle ${message.method}`);
if (message.method === "initialize") {
respond(message.id, {
protocolVersion: message.params?.protocolVersion || "2025-03-26",
capabilities: { tools: { listChanged: false } },
serverInfo: { name: "996 Client GUI Proxy", version: "1.0.0" },
});
return;
}
if (message.method === "tools/list") {
respond(message.id, { tools });
return;
}
if (message.method === "resources/list") {
respond(message.id, { resources: [] });
return;
}
if (message.method === "resources/templates/list") {
respond(message.id, { resourceTemplates: [] });
return;
}
if (message.method === "prompts/list") {
respond(message.id, { prompts: [] });
return;
}
if (message.method === "tools/call") {
const name = message.params?.name;
const args = message.params?.arguments || {};
const result = await callRemote(name, args);
respond(message.id, { content: [{ type: "text", text: stringifyResult(result) }] });
return;
}
respond(message.id, null, { code: -32601, message: `Unknown method: ${message.method}` });
}
async function callRemote(name, args) {
if (!URL || !AUTH) throw new Error("MCP_996_GUI_URL or MCP_996_GUI_AUTH is missing");
const init = await post(null, {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "codex-996-gui-proxy", version: "1.0.0" },
},
});
const session = init.session;
await post(session, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
const response = await post(session, {
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name, arguments: args },
});
return response.data?.result ?? response.data;
}
async function post(session, payload) {
const headers = {
Authorization: AUTH,
Accept: "application/json, text/event-stream",
"Content-Type": "application/json",
};
if (session) headers["mcp-session-id"] = session;
const response = await fetch(URL, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
const text = await response.text();
if (!response.ok) throw new Error(`Remote MCP HTTP ${response.status}: ${text}`);
return {
session: response.headers.get("mcp-session-id"),
data: parseMcpBody(text),
};
}
function parseMcpBody(text) {
if (!text || !text.trim()) return null;
const dataLine = text
.split(/\r?\n/)
.find((line) => line.startsWith("data:"));
return JSON.parse(dataLine ? dataLine.slice(5).trim() : text);
}
function stringifyResult(result) {
if (typeof result === "string") return result;
return JSON.stringify(result, null, 2);
}
function respond(id, result, error) {
const payload = error
? { jsonrpc: "2.0", id, error }
: { jsonrpc: "2.0", id, result };
const body = JSON.stringify(payload);
log(`respond id=${id} bytes=${Buffer.byteLength(body, "utf8")} error=${Boolean(error)}`);
if (responseMode === "ndjson") {
process.stdout.write(`${body}\n`);
} else {
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
}
}