441 lines
13 KiB
JavaScript
Raw Normal View History

/**
* 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
};