'use strict'; /** * 对线程级 gitea-mcp stdio 配置做最小 MCP initialize 预检。 * * 预检只负责判断“命令能否启动并完成协议握手”,不调用 Gitea 工具,也不把 * stderr、Token 或响应正文写入日志。真正的 Agent 工作仍由 Codex App 按线程 * 配置启动另一份 gitea-mcp 进程完成。 */ const { spawn: defaultSpawn } = require('child_process'); const readline = require('readline'); const DEFAULT_TIMEOUT_MS = 8000; const MAX_TIMEOUT_MS = 30000; const MCP_PROTOCOL_VERSION = '2024-11-05'; function safeText(value, max = 512) { return String(value ?? '').trim().slice(0, max); } function probeError(code, message, details = {}) { return Object.assign(new Error(message), { code, ...details }); } function normalizeTimeout(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) return DEFAULT_TIMEOUT_MS; return Math.min(MAX_TIMEOUT_MS, Math.max(250, Math.floor(numeric))); } function initializeRequest() { return { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'cc-web-gitea-preflight', version: '1.0.0' }, }, }; } function isInitializeResponse(message) { return message && message.jsonrpc === '2.0' && (message.id === 1 || message.id === '1') && (Object.prototype.hasOwnProperty.call(message, 'result') || Object.prototype.hasOwnProperty.call(message, 'error')); } function sendJsonLine(stdin, value) { stdin.write(`${JSON.stringify(value)}\n`); } /** * @param {object} options * @param {string} options.command MCP 命令或绝对路径 * @param {string[]} [options.args] 命令参数 * @param {object} [options.env] 线程级环境变量 * @param {string} [options.cwd] 子进程工作目录 * @param {number} [options.timeoutMs] 握手超时,默认 8 秒 * @param {Function} [options.spawnImpl] 测试替身,签名同 child_process.spawn * @returns {Promise<{ok:true,protocolVersion:string}>} */ function probeGiteaMcp(options = {}) { const command = safeText(options.command, 2048); if (!command) return Promise.reject(probeError('gitea_mcp_command_empty', 'Gitea MCP 命令为空。')); const args = Array.isArray(options.args) ? options.args.map((item) => String(item)) : []; const timeoutMs = normalizeTimeout(options.timeoutMs); const spawnImpl = typeof options.spawnImpl === 'function' ? options.spawnImpl : defaultSpawn; const env = { ...process.env, ...(options.env && typeof options.env === 'object' ? options.env : {}) }; const cwd = safeText(options.cwd, 4096) || process.cwd(); return new Promise((resolve, reject) => { let child; let settled = false; let timer = null; let sawInvalidLine = false; const finish = (error, result) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); try { if (child?.stdin && !child.stdin.destroyed) child.stdin.end(); } catch {} try { if (child && !child.killed) child.kill('SIGTERM'); } catch {} if (error) reject(error); else resolve(result); }; try { child = spawnImpl(command, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, }); } catch (error) { finish(probeError('gitea_mcp_spawn_failed', 'Gitea MCP 进程无法启动。', { cause: error })); return; } const handleLine = (line) => { const text = String(line || '').trim(); if (!text) return; let message; try { message = JSON.parse(text); } catch { // stdio 协议要求 JSONL;兼容少数实现的启动提示,最终仍以超时/退出分类。 sawInvalidLine = true; return; } if (!isInitializeResponse(message)) return; if (message.error) { finish(probeError('gitea_mcp_handshake_failed', 'Gitea MCP initialize 握手失败。', { rpcCode: message.error.code ?? null, })); return; } const result = message.result && typeof message.result === 'object' ? message.result : {}; try { // MCP 客户端在收到 initialize 响应后必须发送 initialized 通知。 sendJsonLine(child.stdin, { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }); } catch (error) { finish(probeError('gitea_mcp_handshake_failed', 'Gitea MCP initialized 通知发送失败。', { cause: error })); return; } finish(null, { ok: true, protocolVersion: safeText(result.protocolVersion, 64) || MCP_PROTOCOL_VERSION, }); }; if (child?.stdout) { const lineReader = readline.createInterface({ input: child.stdout }); lineReader.on('line', handleLine); child.once?.('close', () => lineReader.close()); } child?.once?.('error', (error) => { finish(probeError('gitea_mcp_spawn_failed', 'Gitea MCP 进程启动失败。', { cause: error })); }); child?.once?.('exit', (code, signal) => { if (settled) return; finish(probeError('gitea_mcp_start_failed', 'Gitea MCP 进程在握手前退出。', { exitCode: code ?? null, signal: signal || null, invalidOutput: sawInvalidLine, })); }); timer = setTimeout(() => { finish(probeError('gitea_mcp_handshake_timeout', `Gitea MCP initialize 握手超时(${timeoutMs}ms)。`)); }, timeoutMs); try { sendJsonLine(child.stdin, initializeRequest()); } catch (error) { finish(probeError('gitea_mcp_handshake_failed', 'Gitea MCP initialize 请求发送失败。', { cause: error })); } }); } module.exports = { DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, MCP_PROTOCOL_VERSION, probeGiteaMcp, };