Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

441 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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