import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "ssh2"; // ========== SSH 配置(通过环境变量传入) ========== const SSH_CONFIG = { host: process.env.SSH_HOST || "", port: parseInt(process.env.SSH_PORT || "22"), username: process.env.SSH_USER || "root", password: process.env.SSH_PASSWORD || "", // 如果用密钥认证,设置 SSH_PRIVATE_KEY_PATH privateKey: process.env.SSH_PRIVATE_KEY_PATH ? (await import("fs")).readFileSync(process.env.SSH_PRIVATE_KEY_PATH) : undefined, readyTimeout: 10000, keepaliveInterval: 5000, }; // ========== SSH 执行命令 ========== function sshExec(command, timeout = 30000) { return new Promise((resolve, reject) => { const conn = new Client(); let output = ""; let errorOutput = ""; let timer = null; conn .on("ready", () => { timer = setTimeout(() => { conn.end(); resolve({ stdout: output, stderr: errorOutput + "\n[TIMEOUT] Command timed out after " + timeout + "ms", code: -1, }); }, timeout); conn.exec(command, (err, stream) => { if (err) { clearTimeout(timer); conn.end(); return reject(err); } stream .on("close", (code) => { clearTimeout(timer); conn.end(); resolve({ stdout: output, stderr: errorOutput, code }); }) .on("data", (data) => { output += data.toString(); }) .stderr.on("data", (data) => { errorOutput += data.toString(); }); }); }) .on("error", (err) => { if (timer) clearTimeout(timer); reject(new Error(`SSH connection failed: ${err.message}`)); }) .connect(SSH_CONFIG); }); } // ========== MCP Server ========== const server = new Server( { name: "ssh-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); // 工具列表 server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "ssh_run", description: "在远程服务器上执行 Shell 命令。用于检查服务状态、查看日志、读取配置文件等。", inputSchema: { type: "object", properties: { command: { type: "string", description: "要执行的 Shell 命令", }, timeout: { type: "number", description: "超时时间(毫秒),默认 30000", }, }, required: ["command"], }, }, { name: "ssh_read_file", description: "读取远程服务器上的文件内容", inputSchema: { type: "object", properties: { path: { type: "string", description: "远程文件的绝对路径", }, maxLines: { type: "number", description: "最大读取行数,默认 200", }, }, required: ["path"], }, }, { name: "ssh_write_file", description: "写入内容到远程服务器上的文件(覆盖写入)", inputSchema: { type: "object", properties: { path: { type: "string", description: "远程文件的绝对路径", }, content: { type: "string", description: "要写入的文件内容", }, }, required: ["path", "content"], }, }, { name: "ssh_check_bigwo", description: "一键检查 BigWo 项目部署状态:端口、PM2、Nginx、.env 配置、SSL、FC 回调可达性等", inputSchema: { type: "object", properties: {}, }, }, ], })); // 工具执行 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "ssh_run": { const { command, timeout } = args; const result = await sshExec(command, timeout || 30000); const text = [ `$ ${command}`, `Exit code: ${result.code}`, result.stdout ? `--- stdout ---\n${result.stdout}` : "(no stdout)", result.stderr ? `--- stderr ---\n${result.stderr}` : "", ] .filter(Boolean) .join("\n"); return { content: [{ type: "text", text }] }; } case "ssh_read_file": { const { path, maxLines } = args; const limit = maxLines || 200; const result = await sshExec( `head -n ${limit} ${JSON.stringify(path)} 2>&1 && echo "---" && wc -l < ${JSON.stringify(path)} 2>/dev/null` ); return { content: [ { type: "text", text: `File: ${path}\n${result.stdout}${result.stderr ? "\n" + result.stderr : ""}`, }, ], }; } case "ssh_write_file": { const { path, content } = args; // 使用 heredoc 写入,避免转义问题 const escapedContent = content.replace(/'/g, "'\\''"); const result = await sshExec( `cat > ${JSON.stringify(path)} << 'MCPEOF'\n${content}\nMCPEOF` ); if (result.code === 0) { return { content: [{ type: "text", text: `✅ 文件已写入: ${path}` }], }; } return { content: [ { type: "text", text: `❌ 写入失败: ${result.stderr || result.stdout}`, }, ], }; } case "ssh_check_bigwo": { const checks = []; const PROJECT = "/www/wwwroot/demo.tensorgrove.com.cn"; // 1. PM2 状态 const pm2 = await sshExec("pm2 jlist 2>/dev/null || echo 'PM2_NOT_FOUND'"); checks.push("=== 1. PM2 进程状态 ===\n" + pm2.stdout); // 2. 后端端口监听 const ports = await sshExec("ss -tlnp | grep -E '3001|3012|80|443'"); checks.push("=== 2. 端口监听 ===\n" + ports.stdout); // 3. .env 关键配置(脱敏) const env = await sshExec( `grep -E '^(PORT|FC_SERVER_URL|VOLC_ARK_KNOWLEDGE|VOLC_RTC_APP_ID|VOLC_ARK_ENDPOINT_ID|COZE_)' ${PROJECT}/server/.env 2>/dev/null | sed 's/=.\\{8\\}/=***REDACTED***/g'` ); checks.push("=== 3. .env 关键配置 ===\n" + (env.stdout || "(文件不存在或为空)")); // 4. .env 完整键列表 const envKeys = await sshExec( `grep -v '^#' ${PROJECT}/server/.env 2>/dev/null | grep '=' | cut -d= -f1` ); checks.push("=== 4. .env 所有配置键 ===\n" + (envKeys.stdout || "(无)")); // 5. Nginx 配置 const nginx = await sshExec( `grep -A5 'location /api' /www/server/panel/vhost/nginx/demo.tensorgrove.com.cn.conf 2>/dev/null || grep -rA5 'location /api' /www/server/panel/vhost/nginx/ 2>/dev/null | head -20` ); checks.push("=== 5. Nginx API 代理配置 ===\n" + (nginx.stdout || "(未找到)")); // 6. 本地测试后端 API const health = await sshExec( `curl -s http://127.0.0.1:3012/api/health 2>&1 || curl -s http://127.0.0.1:3001/api/health 2>&1 || echo 'BACKEND_UNREACHABLE'` ); checks.push("=== 6. 后端 Health Check ===\n" + health.stdout); // 7. 测试 FC 回调可达性(外部 HTTPS) const fcTest = await sshExec( `curl -s -o /dev/null -w '%{http_code}' -X POST https://demo.tensorgrove.com.cn/api/voice/fc_callback -H 'Content-Type: application/json' -d '{"Type":"test"}' 2>&1` ); checks.push( "=== 7. FC 回调外部可达性 ===\n" + `HTTPS POST → HTTP Status: ${fcTest.stdout}` ); // 8. SSL 证书状态 const ssl = await sshExec( `echo | openssl s_client -connect demo.tensorgrove.com.cn:443 -servername demo.tensorgrove.com.cn 2>/dev/null | openssl x509 -noout -dates 2>/dev/null || echo 'SSL_CHECK_FAILED'` ); checks.push("=== 8. SSL 证书 ===\n" + ssl.stdout); // 9. PM2 最近日志 const logs = await sshExec( "pm2 logs bigwo-server --nostream --lines 30 2>/dev/null || echo 'NO_LOGS'" ); checks.push("=== 9. PM2 最近30行日志 ===\n" + logs.stdout + (logs.stderr || "")); // 10. 前端 dist 是否存在 const dist = await sshExec( `ls -la ${PROJECT}/client/dist/index.html 2>&1 && echo "DIST_OK" || echo "DIST_MISSING"` ); checks.push("=== 10. 前端构建产物 ===\n" + dist.stdout); return { content: [{ type: "text", text: checks.join("\n\n") }], }; } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, }; } }); // 启动 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("SSH MCP Server running on stdio"); } main().catch((err) => { console.error("Fatal:", err); process.exit(1); });