Update code
This commit is contained in:
297
mcp-server-ssh/index.js
Normal file
297
mcp-server-ssh/index.js
Normal file
@@ -0,0 +1,297 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user