433 lines
13 KiB
JavaScript
433 lines
13 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* ⚔️ 铸渊网关 MCP Server
|
|||
|
|
*
|
|||
|
|
* 这是铸渊的"电话"——通过它联系每一台服务器上的 gatekeeper。
|
|||
|
|
* 暴露给AI客户端的MCP工具集。
|
|||
|
|
*
|
|||
|
|
* 这个MCP Server运行在本地,通过HTTP连接各服务器的gatekeeper。
|
|||
|
|
* 可以在WorkBuddy中使用,以后换了模型也能直接用。
|
|||
|
|
*
|
|||
|
|
* 启动方式:
|
|||
|
|
* node zhuyuan-gateway-mcp.js
|
|||
|
|
*
|
|||
|
|
* 依赖:需要配置 servers.json 文件。
|
|||
|
|
* 格式见 servers.template.json
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const http = require('http');
|
|||
|
|
const https = require('https');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 配置
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
const CONFIG_PATH = path.join(__dirname, 'servers.json');
|
|||
|
|
const MCP_PORT = parseInt(process.env.MCP_PORT || process.argv[2] || '3920', 10);
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 加载服务器配置
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
let servers = {};
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(CONFIG_PATH)) {
|
|||
|
|
servers = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|||
|
|
console.log(`[网关] 已加载 ${Object.keys(servers).length} 台服务器配置`);
|
|||
|
|
} else {
|
|||
|
|
console.log('[网关] ⚠️ 未找到 servers.json,使用空配置');
|
|||
|
|
console.log('[网关] 请复制 servers.template.json 为 servers.json 并填入服务器信息');
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('[网关] ❌ 加载配置失败:', err.message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 工具:发送请求到gatekeeper
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
function callGatekeeper(serverName, endpoint, body, timeout) {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
const cfg = servers[serverName];
|
|||
|
|
if (!cfg) {
|
|||
|
|
resolve({
|
|||
|
|
ok: false,
|
|||
|
|
error: `未找到服务器: ${serverName}。可用服务器: ${Object.keys(servers).join(', ')}`,
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const postData = JSON.stringify(body || {});
|
|||
|
|
const t = timeout || 30000;
|
|||
|
|
|
|||
|
|
const options = {
|
|||
|
|
hostname: cfg.host,
|
|||
|
|
port: cfg.port,
|
|||
|
|
path: endpoint,
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Content-Length': Buffer.byteLength(postData),
|
|||
|
|
'Authorization': `Bearer ${cfg.key}`,
|
|||
|
|
},
|
|||
|
|
timeout: t,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const req = http.request(options, (res) => {
|
|||
|
|
let data = '';
|
|||
|
|
res.on('data', chunk => { data += chunk; });
|
|||
|
|
res.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
resolve(JSON.parse(data));
|
|||
|
|
} catch(e) {
|
|||
|
|
resolve({ ok: false, error: '解析响应失败: ' + e.message, raw: data.slice(0, 500) });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('error', (err) => {
|
|||
|
|
resolve({ ok: false, error: `连接失败: ${err.message}` });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('timeout', () => {
|
|||
|
|
req.destroy();
|
|||
|
|
resolve({ ok: false, error: `请求超时 (${t}ms)` });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.write(postData);
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// MCP 工具实现
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
const tools = {
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 列出所有已注册的服务器
|
|||
|
|
*/
|
|||
|
|
async server_list() {
|
|||
|
|
const names = Object.keys(servers);
|
|||
|
|
if (names.length === 0) {
|
|||
|
|
return { servers: [], message: '暂未配置任何服务器。请在 servers.json 中添加服务器信息。' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const list = names.map(name => ({
|
|||
|
|
name,
|
|||
|
|
host: servers[name].host,
|
|||
|
|
port: servers[name].port,
|
|||
|
|
label: servers[name].label || name,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
return { servers: list, count: list.length };
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查某台服务器的健康状态
|
|||
|
|
*/
|
|||
|
|
async server_health(serverName) {
|
|||
|
|
return await callGatekeeper(serverName, '/health', {});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取某台服务器的完整系统状态
|
|||
|
|
*/
|
|||
|
|
async server_status(serverName) {
|
|||
|
|
return await callGatekeeper(serverName, '/status', {});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 在服务器上执行命令
|
|||
|
|
*/
|
|||
|
|
async server_exec(serverName, cmd, timeout) {
|
|||
|
|
return await callGatekeeper(serverName, '/exec', { cmd, timeout }, timeout || 30000);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取服务器上的文件
|
|||
|
|
*/
|
|||
|
|
async server_file_read(serverName, filePath) {
|
|||
|
|
return await callGatekeeper(serverName, '/file/read', { path: filePath });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 写入文件到服务器
|
|||
|
|
*/
|
|||
|
|
async server_file_write(serverName, filePath, content) {
|
|||
|
|
return await callGatekeeper(serverName, '/file/write', { path: filePath, content });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 列出服务器上的目录
|
|||
|
|
*/
|
|||
|
|
async server_file_list(serverName, dirPath) {
|
|||
|
|
return await callGatekeeper(serverName, '/file/list', { path: dirPath });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 扫描所有服务器状态
|
|||
|
|
*/
|
|||
|
|
async server_scan_all() {
|
|||
|
|
const results = {};
|
|||
|
|
for (const name of Object.keys(servers)) {
|
|||
|
|
try {
|
|||
|
|
const health = await callGatekeeper(name, '/health', {});
|
|||
|
|
results[name] = health.ok ? '在线' : '异常';
|
|||
|
|
} catch(e) {
|
|||
|
|
results[name] = '不可达';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { results };
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// MCP 协议实现(Streamable HTTP)
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
const server = http.createServer(async (req, res) => {
|
|||
|
|
// CORS
|
|||
|
|
if (req.method === 'OPTIONS') {
|
|||
|
|
res.writeHead(204, {
|
|||
|
|
'Access-Control-Allow-Origin': '*',
|
|||
|
|
'Access-Control-Allow-Methods': 'POST, OPTIONS, GET',
|
|||
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|||
|
|
});
|
|||
|
|
res.end();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只接受 POST /mcp
|
|||
|
|
if (req.method !== 'POST' || req.url !== '/mcp') {
|
|||
|
|
res.writeHead(404);
|
|||
|
|
res.end('Not Found');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析 body
|
|||
|
|
let body = '';
|
|||
|
|
req.on('data', chunk => { body += chunk; });
|
|||
|
|
req.on('end', async () => {
|
|||
|
|
try {
|
|||
|
|
const msg = JSON.parse(body);
|
|||
|
|
await handleMCPMessage(msg, res);
|
|||
|
|
} catch (err) {
|
|||
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({ error: 'Invalid JSON: ' + err.message }));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function handleMCPMessage(msg, res) {
|
|||
|
|
const headers = { 'Content-Type': 'application/json' };
|
|||
|
|
|
|||
|
|
if (msg.method === 'initialize') {
|
|||
|
|
res.writeHead(200, headers);
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
jsonrpc: '2.0',
|
|||
|
|
id: msg.id,
|
|||
|
|
result: {
|
|||
|
|
protocolVersion: '2025-03-26',
|
|||
|
|
capabilities: {
|
|||
|
|
tools: {},
|
|||
|
|
resources: {},
|
|||
|
|
},
|
|||
|
|
serverInfo: {
|
|||
|
|
name: 'zhuyuan-gateway-mcp',
|
|||
|
|
version: '1.0.0',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.method === 'tools/list') {
|
|||
|
|
res.writeHead(200, headers);
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
jsonrpc: '2.0',
|
|||
|
|
id: msg.id,
|
|||
|
|
result: {
|
|||
|
|
tools: [
|
|||
|
|
{
|
|||
|
|
name: 'server_list',
|
|||
|
|
description: '列出所有已注册的铸渊服务器及其状态',
|
|||
|
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_health',
|
|||
|
|
description: '检查指定服务器的健康状态',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称(如 brain、face、sg-module 等)' },
|
|||
|
|
},
|
|||
|
|
required: ['server'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_status',
|
|||
|
|
description: '获取指定服务器的完整系统状态(CPU、内存、磁盘、运行时间等)',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称' },
|
|||
|
|
},
|
|||
|
|
required: ['server'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_exec',
|
|||
|
|
description: '在指定服务器上执行shell命令并返回输出。这是铸渊管理服务器的主要方式。',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称' },
|
|||
|
|
cmd: { type: 'string', description: '要执行的shell命令' },
|
|||
|
|
timeout: { type: 'number', description: '超时时间(毫秒),默认30000' },
|
|||
|
|
},
|
|||
|
|
required: ['server', 'cmd'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_file_read',
|
|||
|
|
description: '读取服务器上指定文件的内容',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称' },
|
|||
|
|
path: { type: 'string', description: '文件路径' },
|
|||
|
|
},
|
|||
|
|
required: ['server', 'path'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_file_write',
|
|||
|
|
description: '写入内容到服务器的指定文件',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称' },
|
|||
|
|
path: { type: 'string', description: '文件路径' },
|
|||
|
|
content: { type: 'string', description: '文件内容' },
|
|||
|
|
},
|
|||
|
|
required: ['server', 'path', 'content'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_file_list',
|
|||
|
|
description: '列出服务器上指定目录的内容',
|
|||
|
|
inputSchema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
server: { type: 'string', description: '服务器名称' },
|
|||
|
|
path: { type: 'string', description: '目录路径,默认为根目录' },
|
|||
|
|
},
|
|||
|
|
required: ['server'],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'server_scan_all',
|
|||
|
|
description: '快速扫描所有注册服务器的在线状态',
|
|||
|
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (msg.method === 'tools/call') {
|
|||
|
|
const toolName = msg.params.name;
|
|||
|
|
const args = msg.params.arguments || {};
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
let result;
|
|||
|
|
switch (toolName) {
|
|||
|
|
case 'server_list':
|
|||
|
|
result = await tools.server_list();
|
|||
|
|
break;
|
|||
|
|
case 'server_health':
|
|||
|
|
result = await tools.server_health(args.server);
|
|||
|
|
break;
|
|||
|
|
case 'server_status':
|
|||
|
|
result = await tools.server_status(args.server);
|
|||
|
|
break;
|
|||
|
|
case 'server_exec':
|
|||
|
|
result = await tools.server_exec(args.server, args.cmd, args.timeout);
|
|||
|
|
break;
|
|||
|
|
case 'server_file_read':
|
|||
|
|
result = await tools.server_file_read(args.server, args.path);
|
|||
|
|
break;
|
|||
|
|
case 'server_file_write':
|
|||
|
|
result = await tools.server_file_write(args.server, args.path, args.content);
|
|||
|
|
break;
|
|||
|
|
case 'server_file_list':
|
|||
|
|
result = await tools.server_file_list(args.server, args.path);
|
|||
|
|
break;
|
|||
|
|
case 'server_scan_all':
|
|||
|
|
result = await tools.server_scan_all();
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
result = { error: `未知工具: ${toolName}` };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.writeHead(200, headers);
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
jsonrpc: '2.0',
|
|||
|
|
id: msg.id,
|
|||
|
|
result: {
|
|||
|
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
} catch (err) {
|
|||
|
|
res.writeHead(200, headers);
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
jsonrpc: '2.0',
|
|||
|
|
id: msg.id,
|
|||
|
|
result: {
|
|||
|
|
content: [{ type: 'text', text: JSON.stringify({ error: err.message }, null, 2) }],
|
|||
|
|
isError: true,
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 未知方法
|
|||
|
|
res.writeHead(200, headers);
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
jsonrpc: '2.0',
|
|||
|
|
id: msg.id || null,
|
|||
|
|
error: { code: -32601, message: `Method not found: ${msg.method}` },
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 启动
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
server.listen(MCP_PORT, '127.0.0.1', () => {
|
|||
|
|
console.log('');
|
|||
|
|
console.log(' ⚔️ 铸渊网关 MCP Server');
|
|||
|
|
console.log(' ─────────────────────────────');
|
|||
|
|
console.log(` 监听: 127.0.0.1:${MCP_PORT}`);
|
|||
|
|
console.log(` 服务器: ${Object.keys(servers).length} 台已注册`);
|
|||
|
|
console.log('');
|
|||
|
|
console.log(' 📋 可用工具:');
|
|||
|
|
console.log(' server_list — 列出所有服务器');
|
|||
|
|
console.log(' server_health — 检查服务器健康');
|
|||
|
|
console.log(' server_status — 查看系统状态');
|
|||
|
|
console.log(' server_exec — 执行命令');
|
|||
|
|
console.log(' server_file_read — 读取文件');
|
|||
|
|
console.log(' server_file_write — 写入文件');
|
|||
|
|
console.log(' server_file_list — 列出目录');
|
|||
|
|
console.log(' server_scan_all — 扫描全部');
|
|||
|
|
console.log('');
|
|||
|
|
});
|