441 lines
13 KiB
JavaScript
441 lines
13 KiB
JavaScript
/**
|
||
* GMP-Agent MCP 工具集定义 + 实现
|
||
* 工单编号: GH-GMP-004
|
||
* 开发者: 译典A05 (5TH-LE-HK-A05)
|
||
* 职责: 将 GMP-Agent 的核心功能暴露为 MCP 协议工具
|
||
*
|
||
* MCP 工具映射 (对齐 GMP-AGENT-SPEC v1.0 第5章):
|
||
* gmp.install → 授权安装模块
|
||
* gmp.uninstall → 卸载模块
|
||
* gmp.status → 查看已安装模块清单
|
||
* gmp.health → 全模块健康检查
|
||
* gmp.list_available → 列出仓库可用模块
|
||
*
|
||
* 接口约定:
|
||
* - 每个工具遵循 JSON-RPC 2.0 风格 (id, method, params → result/error)
|
||
* - inputSchema 兼容 MCP Tool 规范 (name, description, inputSchema)
|
||
* - 内部调用 GMPAgent 实例方法, 不直接操作文件系统
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const { createLogger } = require('./lib/logger');
|
||
const logger = createLogger('mcp-tools');
|
||
|
||
// ─── 工具定义 (MCP Tool Schema) ─────────────────────────
|
||
|
||
const TOOL_DEFINITIONS = [
|
||
{
|
||
name: 'gmp.install',
|
||
description: '授权安装一个 GMP 模块到服务器。需要模块名称,可选指定仓库URL和分支。安装过程包括:克隆代码→验证manifest→安装依赖→自检→注册。',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
moduleName: {
|
||
type: 'string',
|
||
description: '要安装的模块名称 (对应仓库中 guanghu-self-hosted/ 下的目录名)'
|
||
},
|
||
repoUrl: {
|
||
type: 'string',
|
||
description: '仓库克隆URL (可选, 默认使用配置中的 defaultRepoUrl)'
|
||
},
|
||
branch: {
|
||
type: 'string',
|
||
description: '分支名 (可选, 默认 main)'
|
||
}
|
||
},
|
||
required: ['moduleName']
|
||
}
|
||
},
|
||
{
|
||
name: 'gmp.uninstall',
|
||
description: '卸载一个已安装的 GMP 模块。停止进程→清理文件→从注册表移除。',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
moduleName: {
|
||
type: 'string',
|
||
description: '要卸载的模块名称'
|
||
}
|
||
},
|
||
required: ['moduleName']
|
||
}
|
||
},
|
||
{
|
||
name: 'gmp.status',
|
||
description: '查看所有已安装 GMP 模块的清单和状态。返回模块名称、安装时间、运行状态等信息。',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {},
|
||
required: []
|
||
}
|
||
},
|
||
{
|
||
name: 'gmp.health',
|
||
description: '执行全模块健康检查。检查每个已安装模块的进程状态和端口响应,返回详细健康报告。',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
moduleName: {
|
||
type: 'string',
|
||
description: '指定单个模块名称进行检查 (可选, 不传则检查全部)'
|
||
}
|
||
},
|
||
required: []
|
||
}
|
||
},
|
||
{
|
||
name: 'gmp.list_available',
|
||
description: '列出仓库中所有可用的 GMP 模块 (尚未安装的)。扫描仓库 guanghu-self-hosted/ 目录下包含 manifest.yaml 的子目录。',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
branch: {
|
||
type: 'string',
|
||
description: '要扫描的分支 (可选, 默认 main)'
|
||
}
|
||
},
|
||
required: []
|
||
}
|
||
}
|
||
];
|
||
|
||
// ─── MCP 工具路由器 ─────────────────────────────────────
|
||
|
||
class MCPToolRouter {
|
||
/**
|
||
* @param {object} agent - GMPAgent 实例 (app.js 中的 GMPAgent)
|
||
*/
|
||
constructor(agent) {
|
||
this.agent = agent;
|
||
this.handlers = new Map();
|
||
this._registerHandlers();
|
||
}
|
||
|
||
/**
|
||
* 注册所有工具处理器
|
||
*/
|
||
_registerHandlers() {
|
||
this.handlers.set('gmp.install', this._handleInstall.bind(this));
|
||
this.handlers.set('gmp.uninstall', this._handleUninstall.bind(this));
|
||
this.handlers.set('gmp.status', this._handleStatus.bind(this));
|
||
this.handlers.set('gmp.health', this._handleHealth.bind(this));
|
||
this.handlers.set('gmp.list_available', this._handleListAvailable.bind(this));
|
||
}
|
||
|
||
/**
|
||
* 获取所有工具定义 (供 MCP listTools 使用)
|
||
* @returns {Array<object>}
|
||
*/
|
||
listTools() {
|
||
return TOOL_DEFINITIONS;
|
||
}
|
||
|
||
/**
|
||
* 执行工具调用 (供 MCP callTool 使用)
|
||
* @param {string} toolName - 工具名称
|
||
* @param {object} params - 工具参数
|
||
* @returns {Promise<object>} 执行结果
|
||
*/
|
||
async callTool(toolName, params = {}) {
|
||
const handler = this.handlers.get(toolName);
|
||
if (!handler) {
|
||
return {
|
||
error: {
|
||
code: -32601,
|
||
message: 'Tool not found: ' + toolName
|
||
}
|
||
};
|
||
}
|
||
|
||
const startTime = Date.now();
|
||
logger.info('MCP tool call: ' + toolName, { params });
|
||
|
||
try {
|
||
const result = await handler(params);
|
||
const duration = Date.now() - startTime;
|
||
logger.info('MCP tool completed: ' + toolName + ' (' + duration + 'ms)');
|
||
return { result, duration };
|
||
} catch (err) {
|
||
const duration = Date.now() - startTime;
|
||
logger.error('MCP tool failed: ' + toolName + ' -> ' + err.message + ' (' + duration + 'ms)');
|
||
return {
|
||
error: {
|
||
code: -32000,
|
||
message: err.message
|
||
},
|
||
duration
|
||
};
|
||
}
|
||
}
|
||
|
||
// ─── 工具处理器 ───────────────────────────────────────
|
||
|
||
/**
|
||
* gmp.install — 安装模块
|
||
*/
|
||
async _handleInstall(params) {
|
||
const { moduleName, repoUrl, branch } = params;
|
||
if (!moduleName) {
|
||
throw new Error('缺少必填参数: moduleName');
|
||
}
|
||
|
||
// 检查是否已安装
|
||
if (this.agent.installedModules.has(moduleName)) {
|
||
throw new Error('模块已安装: ' + moduleName + '。如需更新请先卸载。');
|
||
}
|
||
|
||
const result = await this.agent.installer.install({
|
||
repoUrl,
|
||
moduleName,
|
||
branch,
|
||
autoTriggered: false
|
||
});
|
||
|
||
return {
|
||
status: 'installed',
|
||
module: moduleName,
|
||
branch: result.branch,
|
||
duration: result.duration,
|
||
selfCheck: result.selfCheck,
|
||
message: '模块 ' + moduleName + ' 安装成功'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* gmp.uninstall — 卸载模块
|
||
*/
|
||
async _handleUninstall(params) {
|
||
const { moduleName } = params;
|
||
if (!moduleName) {
|
||
throw new Error('缺少必填参数: moduleName');
|
||
}
|
||
|
||
if (!this.agent.installedModules.has(moduleName)) {
|
||
throw new Error('模块未安装: ' + moduleName);
|
||
}
|
||
|
||
const result = await this.agent.uninstaller.uninstall({ moduleName });
|
||
|
||
return {
|
||
status: 'uninstalled',
|
||
module: moduleName,
|
||
message: '模块 ' + moduleName + ' 已卸载'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* gmp.status — 查看已安装模块清单
|
||
*/
|
||
async _handleStatus() {
|
||
const modules = [];
|
||
for (const [name, info] of this.agent.installedModules) {
|
||
modules.push({
|
||
name: name,
|
||
status: info.status,
|
||
path: info.path,
|
||
installedAt: info.installedAt,
|
||
branch: info.branch || 'unknown',
|
||
health: info.health || 'unknown',
|
||
lastHealthCheck: info.lastHealthCheck || null
|
||
});
|
||
}
|
||
|
||
return {
|
||
agentStatus: this.agent.status,
|
||
agentUptime: Math.floor((Date.now() - this.agent.startTime) / 1000),
|
||
modulesCount: modules.length,
|
||
modules: modules
|
||
};
|
||
}
|
||
|
||
/**
|
||
* gmp.health — 全模块健康检查
|
||
*/
|
||
async _handleHealth(params) {
|
||
const { moduleName } = params;
|
||
|
||
// 如果指定了单个模块
|
||
if (moduleName) {
|
||
const info = this.agent.installedModules.get(moduleName);
|
||
if (!info) {
|
||
throw new Error('模块未安装: ' + moduleName);
|
||
}
|
||
const health = await this._checkSingleModuleHealth(moduleName, info);
|
||
return { modules: [health] };
|
||
}
|
||
|
||
// 全模块检查
|
||
const results = [];
|
||
for (const [name, info] of this.agent.installedModules) {
|
||
const health = await this._checkSingleModuleHealth(name, info);
|
||
results.push(health);
|
||
}
|
||
|
||
const allOk = results.every(r => r.health === 'ok');
|
||
|
||
return {
|
||
overall: allOk ? 'healthy' : 'degraded',
|
||
checkedAt: new Date().toISOString(),
|
||
modulesChecked: results.length,
|
||
modules: results
|
||
};
|
||
}
|
||
|
||
/**
|
||
* gmp.list_available — 列出仓库可用模块
|
||
*/
|
||
async _handleListAvailable(params) {
|
||
// 注意: 在生产环境中,这需要通过 GitHub API 或本地缓存扫描仓库
|
||
// 当前实现: 扫描本地 modules 目录的 _available 缓存
|
||
// 如果缓存不存在,返回提示信息
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const availableCachePath = path.join(this.agent.config.baseDir, 'available-modules.json');
|
||
|
||
if (fs.existsSync(availableCachePath)) {
|
||
try {
|
||
const cache = JSON.parse(fs.readFileSync(availableCachePath, 'utf-8'));
|
||
// 过滤掉已安装的
|
||
const available = (cache.modules || []).filter(
|
||
m => !this.agent.installedModules.has(m.name)
|
||
);
|
||
return {
|
||
available: available,
|
||
total: available.length,
|
||
cachedAt: cache.updatedAt || 'unknown',
|
||
note: '数据来自本地缓存,可能不是最新'
|
||
};
|
||
} catch (err) {
|
||
logger.warn('读取可用模块缓存失败: ' + err.message);
|
||
}
|
||
}
|
||
|
||
return {
|
||
available: [],
|
||
total: 0,
|
||
note: '可用模块缓存不存在。请通过 GitHub API 扫描仓库或手动创建 available-modules.json'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 检查单个模块健康状态
|
||
* @param {string} name - 模块名
|
||
* @param {object} info - 模块信息
|
||
* @returns {Promise<object>} 健康检查结果
|
||
*/
|
||
async _checkSingleModuleHealth(name, info) {
|
||
const result = {
|
||
name: name,
|
||
health: 'unknown',
|
||
checkedAt: new Date().toISOString(),
|
||
details: {}
|
||
};
|
||
|
||
try {
|
||
// 检查1: 模块目录是否存在
|
||
const fs = require('fs');
|
||
if (!fs.existsSync(info.path)) {
|
||
result.health = 'fail';
|
||
result.details.error = '模块目录不存在: ' + info.path;
|
||
return result;
|
||
}
|
||
|
||
// 检查2: manifest.yaml 是否存在
|
||
const path = require('path');
|
||
const manifestPath = path.join(info.path, 'manifest.yaml');
|
||
result.details.manifestExists = fs.existsSync(manifestPath);
|
||
|
||
// 检查3: 如果模块有端口配置,尝试 HTTP 健康检查
|
||
if (info.port) {
|
||
try {
|
||
const http = require('http');
|
||
const healthOk = await new Promise((resolve) => {
|
||
const req = http.get(
|
||
'http://127.0.0.1:' + info.port + '/health',
|
||
{ timeout: 5000 },
|
||
(res) => resolve(res.statusCode >= 200 && res.statusCode < 400)
|
||
);
|
||
req.on('error', () => resolve(false));
|
||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||
});
|
||
result.details.httpHealth = healthOk;
|
||
} catch {
|
||
result.details.httpHealth = false;
|
||
}
|
||
}
|
||
|
||
// 综合判定
|
||
result.health = 'ok';
|
||
if (info.port && result.details.httpHealth === false) {
|
||
result.health = 'degraded';
|
||
}
|
||
|
||
} catch (err) {
|
||
result.health = 'fail';
|
||
result.details.error = err.message;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// ─── Express 路由注册 ───────────────────────────────────
|
||
|
||
/**
|
||
* 注册 MCP 工具相关的 Express 路由
|
||
* @param {object} app - Express app
|
||
* @param {MCPToolRouter} router - MCP 工具路由器
|
||
*/
|
||
function registerMCPRoutes(app, router) {
|
||
// MCP listTools — 列出所有可用工具
|
||
app.get('/mcp/tools', (req, res) => {
|
||
res.json({
|
||
tools: router.listTools(),
|
||
total: router.listTools().length,
|
||
protocol: 'MCP/1.0'
|
||
});
|
||
});
|
||
|
||
// MCP callTool — 执行工具调用 (JSON-RPC 2.0 风格)
|
||
app.post('/mcp/call', async (req, res) => {
|
||
const { id, method, params } = req.body;
|
||
|
||
if (!method) {
|
||
return res.status(400).json({
|
||
jsonrpc: '2.0',
|
||
id: id || null,
|
||
error: { code: -32600, message: '缺少 method 参数' }
|
||
});
|
||
}
|
||
|
||
const result = await router.callTool(method, params || {});
|
||
|
||
if (result.error) {
|
||
return res.status(result.error.code === -32601 ? 404 : 500).json({
|
||
jsonrpc: '2.0',
|
||
id: id || null,
|
||
error: result.error
|
||
});
|
||
}
|
||
|
||
res.json({
|
||
jsonrpc: '2.0',
|
||
id: id || null,
|
||
result: result.result,
|
||
meta: { duration: result.duration }
|
||
});
|
||
});
|
||
|
||
logger.info('MCP 工具路由已注册: /mcp/tools, /mcp/call');
|
||
}
|
||
|
||
// ─── 导出 ──────────────────────────────────────────────
|
||
|
||
module.exports = {
|
||
MCPToolRouter,
|
||
TOOL_DEFINITIONS,
|
||
registerMCPRoutes
|
||
};
|