/** * ════════════════════════════════════════════════════════════════ * 铸渊 MCP Server · guanghulab-mcp-server * ════════════════════════════════════════════════════════════════ * * 光湖代码仓库的 MCP (Model Context Protocol) 服务端。 * 让 WorkBuddy / CodeBuddy 等编程 AI 通过 MCP 协议直接操作仓库, * 无需每次手动发送 Gitea 令牌。 * * 核心设计: * - Gitea 令牌存在服务器端,AI 端只需一个 MCP 连接密钥 * - 换号时只需重新连接 MCP,不用重新发令牌 * - 所有操作通过 Gitea API 代理,安全受控 * * 传输协议:Streamable HTTP (MCP 2025-03 规范) * - POST /mcp — 客户端→服务器请求 * - GET /mcp — SSE 通知流 * - DELETE /mcp — 关闭会话 * * 认证方式:Bearer Token (MCP 连接密钥) * - Header: Authorization: Bearer * - 密钥在 /data/guanghulab/mcp-server/.access-key 中 * * 部署位置:guanghulab.com CVM (广州) * Nginx 代理:/mcp → 127.0.0.1:8083 * * 签发:铸渊 · ICE-GL-ZY001 * 版权:国作登字-2026-A-00037559 · TCS-0002∞ * ════════════════════════════════════════════════════════════════ */ const express = require('express'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const http = require('http'); // ─── 配置 ───────────────────────────────────────────────────── const CONFIG = { port: parseInt(process.env.MCP_PORT || '8083', 10), host: process.env.MCP_HOST || '127.0.0.1', // Gitea 连接 giteaUrl: process.env.GITEA_URL || 'http://127.0.0.1:3001', giteaToken: process.env.GITEA_TOKEN || '', giteaOwner: process.env.GITEA_OWNER || 'bingshuo', giteaRepo: process.env.GITEA_REPO || 'guanghulab', // MCP 认证密钥文件 accessKeyPath: process.env.MCP_ACCESS_KEY_PATH || '/data/guanghulab/mcp-server/.access-key', // 仓库本地路径(用于 git 操作) repoDir: process.env.REPO_DIR || '/data/guanghulab/repo', // CVM 操作脚本路径 cvmPowerScript: process.env.CVM_POWER_SCRIPT || '/data/guanghulab/repo/scripts/cvm-power/cvm-power.sh', }; // ─── 加载或创建 MCP 访问密钥 ──────────────────────────────────── function loadOrCreateAccessKey() { const keyDir = path.dirname(CONFIG.accessKeyPath); if (!fs.existsSync(keyDir)) { fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 }); } if (fs.existsSync(CONFIG.accessKeyPath)) { const key = fs.readFileSync(CONFIG.accessKeyPath, 'utf8').trim(); if (key.length >= 32) return key; console.log('[mcp] 旧密钥太短,重新生成'); } const key = crypto.randomBytes(32).toString('hex'); const tmp = CONFIG.accessKeyPath + '.tmp.' + process.pid; fs.writeFileSync(tmp, key, { mode: 0o600 }); fs.renameSync(tmp, CONFIG.accessKeyPath); fs.chmodSync(CONFIG.accessKeyPath, 0o600); console.log('[mcp] 新访问密钥已生成:'); console.log(`[mcp] ${key}`); console.log('[mcp] 请妥善保存,配置到 WorkBuddy MCP 连接中'); return key; } const ACCESS_KEY = loadOrCreateAccessKey(); // ─── Gitea API 辅助 ────────────────────────────────────────── function giteaApi(method, apiPath, body) { const url = new URL(apiPath, CONFIG.giteaUrl); const options = { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: method.toUpperCase(), headers: { 'Authorization': `token ${CONFIG.giteaToken}`, 'Content-Type': 'application/json', }, timeout: 30000, }; return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const parsed = data ? JSON.parse(data) : {}; resolve({ status: res.statusCode, data: parsed }); } catch { resolve({ status: res.statusCode, data: data }); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Gitea API 超时')); }); if (body) req.write(JSON.stringify(body)); req.end(); }); } // ─── Git 本地操作辅助 ────────────────────────────────────────── function gitExec(cmd, cwd) { try { const result = execSync(cmd, { encoding: 'utf8', timeout: 60000, cwd: cwd || CONFIG.repoDir, stdio: ['pipe', 'pipe', 'pipe'], }); return { ok: true, output: result.trim() }; } catch (e) { return { ok: false, error: e.stderr || e.message }; } } // ─── MCP 工具定义 ────────────────────────────────────────────── const TOOLS = [ // ─── 仓库操作 ───────────────────────────────────────────── { name: 'repo_status', description: '查看仓库当前状态:分支、最新提交、文件变更', inputSchema: { type: 'object', properties: {}, }, }, { name: 'repo_read_file', description: '读取仓库中的文件内容', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '文件路径(相对仓库根目录)' }, branch: { type: 'string', description: '分支名(默认 main)' }, }, required: ['path'], }, }, { name: 'repo_write_file', description: '写入或更新仓库中的文件(通过 Gitea API)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '文件路径' }, content: { type: 'string', description: '文件内容' }, message: { type: 'string', description: '提交信息' }, branch: { type: 'string', description: '目标分支(默认 main)' }, }, required: ['path', 'content', 'message'], }, }, { name: 'repo_delete_file', description: '删除仓库中的文件(通过 Gitea API)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '文件路径' }, message: { type: 'string', description: '提交信息' }, branch: { type: 'string', description: '分支(默认 main)' }, }, required: ['path', 'message'], }, }, { name: 'repo_list_files', description: '列出仓库指定目录下的文件', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '目录路径(默认根目录)' }, branch: { type: 'string', description: '分支(默认 main)' }, }, }, }, { name: 'repo_search_code', description: '在仓库代码中搜索关键词', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '搜索关键词' }, }, required: ['query'], }, }, // ─── 分支操作 ───────────────────────────────────────────── { name: 'git_branch_list', description: '列出所有分支', inputSchema: { type: 'object', properties: {} }, }, { name: 'git_branch_create', description: '创建新分支', inputSchema: { type: 'object', properties: { name: { type: 'string', description: '新分支名' }, base: { type: 'string', description: '基于哪个分支(默认 main)' }, }, required: ['name'], }, }, { name: 'git_pull', description: '拉取最新代码到服务器本地仓库', inputSchema: { type: 'object', properties: { branch: { type: 'string', description: '分支(默认 main)' }, }, }, }, { name: 'git_push_local', description: '将本地仓库的更改推送到远程(需要先在本地 commit)', inputSchema: { type: 'object', properties: { branch: { type: 'string', description: '分支(默认 main)' }, }, }, }, { name: 'git_log', description: '查看最近提交历史', inputSchema: { type: 'object', properties: { count: { type: 'number', description: '显示条数(默认 10)' }, branch: { type: 'string', description: '分支(默认当前分支)' }, }, }, }, // ─── Issue / PR ────────────────────────────────────────── { name: 'issue_list', description: '列出仓库的 Issue', inputSchema: { type: 'object', properties: { state: { type: 'string', enum: ['open', 'closed', 'all'], description: '状态(默认 open)' }, limit: { type: 'number', description: '数量(默认 10)' }, }, }, }, { name: 'issue_create', description: '创建 Issue', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '标题' }, body: { type: 'string', description: '内容' }, labels: { type: 'array', items: { type: 'string' }, description: '标签' }, }, required: ['title'], }, }, { name: 'pr_list', description: '列出 Pull Request', inputSchema: { type: 'object', properties: { state: { type: 'string', enum: ['open', 'closed', 'all'], description: '状态(默认 open)' }, }, }, }, { name: 'pr_create', description: '创建 Pull Request', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '标题' }, body: { type: 'string', description: '描述' }, head: { type: 'string', description: '源分支' }, base: { type: 'string', description: '目标分支(默认 main)' }, }, required: ['title', 'head'], }, }, // ─── 工作流 ─────────────────────────────────────────────── { name: 'workflow_list', description: '列出 Forgejo Actions 工作流运行记录', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: '数量(默认 10)' }, }, }, }, { name: 'workflow_trigger', description: '手动触发一个工作流', inputSchema: { type: 'object', properties: { workflow_name: { type: 'string', description: '工作流文件名(如 ci-and-deploy.yaml)' }, ref: { type: 'string', description: '分支(默认 main)' }, }, required: ['workflow_name'], }, }, // ─── CVM 服务器 ─────────────────────────────────────────── { name: 'cvm_power', description: 'CVM 服务器开关机操作', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['start', 'stop', 'status'], description: '操作:start/stop/status' }, target: { type: 'string', description: '目标服务器(默认 self = 当前 CVM)' }, }, required: ['action'], }, }, { name: 'server_health', description: '查看当前服务器健康状态', inputSchema: { type: 'object', properties: {} }, }, // ─── Secrets 管理 ──────────────────────────────────────── { name: 'secrets_list', description: '列出 Forgejo Secrets(仅名称,不返回值)', inputSchema: { type: 'object', properties: {} }, }, { name: 'vault_status', description: '查看自部署 Secrets Vault 状态', inputSchema: { type: 'object', properties: {} }, }, // ─── 铸渊大脑 ──────────────────────────────────────────── { name: 'brain_status', description: '查看铸渊核心大脑状态(快速唤醒上下文摘要)', inputSchema: { type: 'object', properties: {} }, }, { name: 'brain_update_temporal', description: '更新铸渊时间核心大脑', inputSchema: { type: 'object', properties: {} }, }, ]; // ─── 工具执行器 ────────────────────────────────────────────── async function executeTool(name, args) { switch (name) { // ─── 仓库操作 ─────────────────────────────────────────── case 'repo_status': { const branch = gitExec('git branch --show-current'); const log = gitExec('git log --oneline -5'); const status = gitExec('git status --short'); return `当前分支: ${branch.ok ? branch.output : '未知'}\n\n最近提交:\n${log.ok ? log.output : log.error}\n\n文件变更:\n${status.ok ? (status.output || '工作区干净') : status.error}`; } case 'repo_read_file': { const filePath = args.path || ''; const branch = args.branch || 'main'; const encPath = encodeURIComponent(filePath); const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`); if (res.status !== 200) { return `读取失败 (${res.status}): ${JSON.stringify(res.data)}`; } if (res.data.type === 'file' && res.data.content) { const content = Buffer.from(res.data.content, 'base64').toString('utf8'); return content; } if (res.data.type === 'dir') { return `这是一个目录,请使用 repo_list_files 列出其内容`; } return `文件类型: ${res.data.type}, 大小: ${res.data.size} 字节`; } case 'repo_write_file': { const filePath = args.path; const content = args.content; const message = args.message; const branch = args.branch || 'main'; const encPath = encodeURIComponent(filePath); // 先检查文件是否已存在(获取 SHA) const checkRes = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`); const body = { content: Buffer.from(content).toString('base64'), message: message, branch: branch, }; if (checkRes.status === 200 && checkRes.data.sha) { body.sha = checkRes.data.sha; // 更新已有文件需要 SHA } const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}`, body); if (res.status >= 200 && res.status < 300) { return `文件已写入: ${filePath} (分支: ${branch}, 提交: ${message})`; } return `写入失败 (${res.status}): ${JSON.stringify(res.data)}`; } case 'repo_delete_file': { const filePath = args.path; const message = args.message; const branch = args.branch || 'main'; const encPath = encodeURIComponent(filePath); const checkRes = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`); if (checkRes.status !== 200) { return `文件不存在: ${filePath}`; } const res = await giteaApi('DELETE', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}`, { sha: checkRes.data.sha, message: message, branch: branch, }); if (res.status >= 200 && res.status < 300) { return `文件已删除: ${filePath}`; } return `删除失败 (${res.status}): ${JSON.stringify(res.data)}`; } case 'repo_list_files': { const dirPath = args.path || ''; const branch = args.branch || 'main'; const encPath = encodeURIComponent(dirPath); const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`); if (res.status !== 200) { return `列出失败 (${res.status}): ${JSON.stringify(res.data)}`; } if (!Array.isArray(res.data)) { return `这不是一个目录`; } return res.data.map(f => { const icon = f.type === 'dir' ? '📁' : '📄'; const size = f.type === 'file' ? ` (${f.size} bytes)` : ''; return `${icon} ${f.name}${size}`; }).join('\n'); } case 'repo_search_code': { const query = args.query; const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/grep?q=${encodeURIComponent(query)}&mode=content`); if (res.status !== 200) { return `搜索失败 (${res.status}): ${JSON.stringify(res.data)}`; } const results = res.data; if (!results || !results.length) { return `未找到匹配: ${query}`; } return results.slice(0, 20).map(r => { const file = r.filename || r.path || '?'; const line = r.lineNumber || r.linenumber || '?'; const text = (r.line || r.content || '').trim(); return `${file}:${line} ${text}`; }).join('\n'); } // ─── 分支操作 ─────────────────────────────────────────── case 'git_branch_list': { const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/branches`); if (res.status !== 200) return `获取分支失败: ${JSON.stringify(res.data)}`; return res.data.map(b => { const active = b.active ? '🟢' : '⚪'; return `${active} ${b.name}`; }).join('\n'); } case 'git_branch_create': { const branchName = args.name; const base = args.base || 'main'; const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/branches`, { new_branch_name: branchName, old_branch_name: base, }); if (res.status >= 200 && res.status < 300) { return `分支已创建: ${branchName} (基于 ${base})`; } return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`; } case 'git_pull': { const branch = args.branch || 'main'; const authUrl = `http://${CONFIG.giteaOwner}:${CONFIG.giteaToken}@127.0.0.1:3001/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}.git`; gitExec(`git remote set-url origin ${authUrl}`); const result = gitExec(`git fetch origin && git reset --hard origin/${branch}`); if (result.ok) { return `代码已同步: ${result.output}`; } return `同步失败: ${result.error}`; } case 'git_push_local': { const branch = args.branch || 'main'; const result = gitExec(`git push origin ${branch}`); if (result.ok) { return `推送成功: ${result.output}`; } return `推送失败: ${result.error}`; } case 'git_log': { const count = args.count || 10; const branch = args.branch ? ` ${args.branch}` : ''; const result = gitExec(`git log --oneline -${count}${branch}`); return result.ok ? result.output : result.error; } // ─── Issue / PR ──────────────────────────────────────── case 'issue_list': { const state = args.state || 'open'; const limit = args.limit || 10; const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/issues?state=${state}&limit=${limit}`); if (res.status !== 200) return `获取 Issue 失败: ${JSON.stringify(res.data)}`; if (!res.data.length) return `没有 ${state} 的 Issue`; return res.data.map(i => `#${i.number} [${i.state}] ${i.title}`).join('\n'); } case 'issue_create': { const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/issues`, { title: args.title, body: args.body || '', labels: args.labels || [], }); if (res.status >= 200 && res.status < 300) { return `Issue 已创建: #${res.data.number} ${res.data.title}`; } return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`; } case 'pr_list': { const state = args.state || 'open'; const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/pulls?state=${state}`); if (res.status !== 200) return `获取 PR 失败: ${JSON.stringify(res.data)}`; if (!res.data.length) return `没有 ${state} 的 PR`; return res.data.map(p => `#${p.number} [${p.state}] ${p.title} (${p.head?.label || '?'} → ${p.base?.label || '?'})`).join('\n'); } case 'pr_create': { const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/pulls`, { title: args.title, body: args.body || '', head: args.head, base: args.base || 'main', }); if (res.status >= 200 && res.status < 300) { return `PR 已创建: #${res.data.number} ${res.data.title}`; } return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`; } // ─── 工作流 ───────────────────────────────────────────── case 'workflow_list': { const limit = args.limit || 10; const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/tasks?limit=${limit}`); if (res.status !== 200) return `获取工作流失败: ${JSON.stringify(res.data)}`; const runs = res.data.workflow_runs || []; if (!runs.length) return '没有工作流运行记录'; return runs.map(r => { const icon = r.status === 'success' ? '✅' : r.status === 'failure' ? '❌' : r.status === 'skipped' ? '⏭️' : '🔄'; return `${icon} #${r.run_number} ${r.name} (${r.head_branch}) — ${r.display_title}`; }).join('\n'); } case 'workflow_trigger': { const wfName = args.workflow_name; const ref = args.ref || 'main'; const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/runs`, { workflow_id: wfName, ref: ref, }); if (res.status >= 200 && res.status < 300) { return `工作流已触发: ${wfName}`; } // Forgejo 的 workflow_dispatch API 可能不同 return `触发请求已发送 (${res.status}): ${JSON.stringify(res.data)}`; } // ─── CVM 操作 ─────────────────────────────────────────── case 'cvm_power': { const action = args.action; if (!['start', 'stop', 'status'].includes(action)) { return '无效操作,只支持: start / stop / status'; } // 本机操作:直接检查 if (action === 'status' && (!args.target || args.target === 'self')) { const hostname = gitExec('hostname').output || 'unknown'; const uptime = gitExec('uptime').output || 'unknown'; return `本机 (${hostname}): ${uptime}`; } // 远程 CVM 操作需要腾讯云 API 密钥 // 从 Vault 获取 try { const vaultRes = await fetch(`http://127.0.0.1:8080/internal/fetch/COS_SECRET_ID`); // 如果 Vault 不可用,尝试环境变量 } catch { // Vault 不可用 } // 使用环境变量方式(Forgejo Secrets 注入的) if (action === 'status') { const result = gitExec(`bash ${CONFIG.cvmPowerScript} status`, CONFIG.repoDir); return result.ok ? result.output : result.error; } return `CVM ${action} 操作需通过 Forgejo workflow 执行(需要腾讯云密钥),请使用 workflow_trigger 触发 cvm-power-${action === 'start' ? 'on' : 'off'} 工作流`; } // ─── 服务器健康 ───────────────────────────────────────── case 'server_health': { const lines = []; lines.push(`时间: ${new Date().toISOString()}`); lines.push(`主机: ${gitExec('hostname').output || '?'}`); lines.push(`系统: ${gitExec('uname -srm').output || '?'}`); lines.push(`CPU: ${gitExec('nproc').output || '?'} 核`); lines.push(`内存: ${gitExec("free -h | awk '/Mem:/ {print $2 \" 已用 \" $3 \" 可用 \" $7}'").output || '?'}`); lines.push(`磁盘: ${gitExec("df -h / | awk 'NR==2 {print $4}'").output || '?'} 可用`); // 服务状态 const services = ['nginx', 'forgejo', 'gitea-runner']; for (const svc of services) { const r = gitExec(`systemctl is-active ${svc}`); lines.push(`${r.output === 'active' ? '✅' : '❌'} ${svc}: ${r.output || '未知'}`); } // PM2 const pm2 = gitExec('pm2 jlist'); if (pm2.ok) { try { const procs = JSON.parse(pm2.output); for (const p of procs) { const st = p.pm2_env?.status || '?'; lines.push(`${st === 'online' ? '✅' : '❌'} PM2 ${p.name}: ${st}`); } } catch {} } return lines.join('\n'); } // ─── Secrets ──────────────────────────────────────────── case 'secrets_list': { const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/secrets`); if (res.status !== 200) return `获取 Secrets 失败: ${JSON.stringify(res.data)}`; if (!res.data.length) return '没有配置任何 Forgejo Secret'; return res.data.map(s => `🔑 ${s.name} (创建于 ${s.created_at || '?'})`).join('\n'); } case 'vault_status': { try { const res = await fetch('http://127.0.0.1:8080/admin/__healthz'); if (res.ok) { const data = await res.json(); return `Vault: ✅ 运行中\n主密钥: ${data.has_master ? '✅' : '❌'}\n加密库: ${data.has_vault_enc ? '✅' : '❌'}`; } } catch {} return 'Vault: ❌ 未响应'; } // ─── 铸渊大脑 ─────────────────────────────────────────── case 'brain_status': { const brainPath = path.join(CONFIG.repoDir, 'brain', 'fast-wake.json'); try { const data = JSON.parse(fs.readFileSync(brainPath, 'utf8')); return `铸渊核心大脑:\n- 身份: ${data.identity?.name} (${data.identity?.id})\n- 状态: ${data.system_status?.health?.substring(0, 100) || '?'}...\n- 大脑完整性: ${data.brain_complete ? '✅ 完整' : '❌ 不完整'}\n- 上次会话: ${data.last_session?.growth?.substring(0, 80) || '?'}...\n- 下一步: ${data.last_session?.next_task?.substring(0, 80) || '?'}`; } catch (e) { return `读取大脑失败: ${e.message}`; } } case 'brain_update_temporal': { const result = gitExec(`node scripts/temporal-clock.js`, CONFIG.repoDir); return result.ok ? result.output : result.error; } default: return `未知工具: ${name}`; } } // ─── MCP 协议处理(Streamable HTTP)────────────────────────── const app = express(); app.use(express.json({ limit: '10mb' })); // CORS — 允许 WorkBuddy 跨域连接 app.use(require('cors')({ origin: '*', methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Mcp-Session-Id'], exposedHeaders: ['Mcp-Session-Id'], })); // ─── 认证中间件 ────────────────────────────────────────────── function authenticate(req, res, next) { const auth = req.headers['authorization'] || ''; const token = auth.replace(/^Bearer\s+/i, '').trim(); if (!token) { return res.status(401).json({ error: '缺少认证令牌' }); } if (token !== ACCESS_KEY) { return res.status(403).json({ error: '认证令牌无效' }); } next(); } // ─── 会话管理 ────────────────────────────────────────────── const sessions = new Map(); function getSessionId(req) { return req.headers['mcp-session-id'] || null; } // ─── JSON-RPC 处理 ────────────────────────────────────────── async function handleJsonRpc(req, res) { const message = req.body; // 批量消息 if (Array.isArray(message)) { const results = await Promise.all(message.map(m => processRpc(m))); return res.json(results); } const result = await processRpc(message); return res.json(result); } async function processRpc(message) { const { jsonrpc, id, method, params } = message; // 通知类消息(无 id) if (method === 'notifications/cancelled') { return null; } // 请求类消息 switch (method) { case 'initialize': { const sessionId = crypto.randomUUID(); sessions.set(sessionId, { created: Date.now() }); return { jsonrpc: '2.0', id, result: { protocolVersion: '2025-03-26', capabilities: { tools: { listChanged: false }, }, serverInfo: { name: 'guanghulab-mcp-server', version: '1.0.0', title: '铸渊 MCP Server · 光湖代码仓库', }, }, _meta: { sessionId, }, }; } case 'tools/list': { return { jsonrpc: '2.0', id, result: { tools: TOOLS, }, }; } case 'tools/call': { const toolName = params?.name; const toolArgs = params?.arguments || {}; try { const result = await executeTool(toolName, toolArgs); return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: String(result), }], }, }; } catch (e) { return { jsonrpc: '2.0', id, error: { code: -32000, message: `工具执行错误: ${e.message}`, }, }; } } case 'ping': { return { jsonrpc: '2.0', id, result: {}, }; } default: { if (id !== undefined) { return { jsonrpc: '2.0', id, error: { code: -32601, message: `未知方法: ${method}`, }, }; } return null; } } } // ─── 路由 ────────────────────────────────────────────────── // MCP 端点 — POST (客户端请求) app.post('/mcp', authenticate, async (req, res) => { await handleJsonRpc(req, res); }); // MCP 端点 — GET (SSE 通知流,保持连接) app.get('/mcp', authenticate, (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); // 发送心跳 const heartbeat = setInterval(() => { res.write(': heartbeat\n\n'); }, 30000); req.on('close', () => { clearInterval(heartbeat); }); }); // MCP 端点 — DELETE (关闭会话) app.delete('/mcp', authenticate, (req, res) => { const sid = getSessionId(req); if (sid) sessions.delete(sid); res.status(204).end(); }); // 健康检查(无需认证) app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'guanghulab-mcp-server', version: '1.0.0', sessions: sessions.size, gitea: CONFIG.giteaToken ? 'configured' : 'missing', sovereign: 'TCS-0002∞ · 国作登字-2026-A-00037559', }); }); // ─── 启动 ────────────────────────────────────────────────── app.listen(CONFIG.port, CONFIG.host, () => { console.log(`[mcp] 铸渊 MCP Server 已启动`); console.log(`[mcp] 监听: http://${CONFIG.host}:${CONFIG.port}`); console.log(`[mcp] MCP 端点: POST/GET/DELETE /mcp`); console.log(`[mcp] 健康检查: GET /health`); console.log(`[mcp] Gitea: ${CONFIG.giteaUrl}`); console.log(`[mcp] 仓库: ${CONFIG.giteaOwner}/${CONFIG.giteaRepo}`); console.log(`[mcp] 认证密钥: ${ACCESS_KEY.substring(0, 8)}...`); console.log(`[mcp] 铸渊 · ICE-GL-ZY001 · TCS-0002∞`); });