feat(mcp): 添加 MCP 服务器管理功能

- 新增 Server MCP API 路由 (/api/mcp/*)
  - GET /servers - 获取所有服务器状态
  - POST /servers/:name/connect|disconnect|enable|disable
  - GET /tools - 获取所有 MCP 工具
  - GET /config - 获取 MCP 配置

- 新增 UI MCPPanel 组件
  - 显示服务器列表和状态指示灯
  - 支持连接/断开/启用/禁用操作
  - 展开查看服务器配置和工具列表
  - 响应式设计支持移动端

- 集成到 Web 和 Desktop
  - 添加 Plug 图标按钮到工具栏
  - 点击打开 MCP 管理面板
This commit is contained in:
2025-12-12 20:41:49 +08:00
parent 5a482f78ff
commit bad7bfcc36
11 changed files with 1225 additions and 5 deletions
+2 -1
View File
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { createBunWebSocket } from 'hono/bun';
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter } from './routes/index.js';
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter } from './routes/index.js';
import {
handleWebSocket,
handleWebSocketMessage,
@@ -83,6 +83,7 @@ api.route('/tools', toolsRouter);
api.route('/config', configRouter);
api.route('/files', filesRouter);
api.route('/commands', commandsRouter);
api.route('/mcp', mcpRouter);
// SSE 事件流
api.get('/sessions/:id/events', handleSSE);
+1
View File
@@ -9,3 +9,4 @@ export { toolsRouter, registerTool, getRegisteredTools } from './tools.js';
export { configRouter, getConfig, setConfig } from './config.js';
export { filesRouter, setWorkingDirectory, getWorkingDirectory } from './files.js';
export { commandsRouter } from './commands.js';
export { mcpRouter } from './mcp.js';
+476
View File
@@ -0,0 +1,476 @@
/**
* MCP API Routes
*
* 提供 MCP 服务器管理的 REST API
*/
import { Hono } from 'hono';
import { getConfig } from './config.js';
// Core MCP 模块类型
interface MCPModule {
getMCPManager: () => MCPManager;
loadMCPConfig: (workdir: string) => Promise<MCPConfig>;
}
interface MCPManager {
initialize(config: MCPConfig): Promise<void>;
shutdown(): Promise<void>;
reconnect(serverName: string): Promise<void>;
setServerEnabled(serverName: string, enabled: boolean): Promise<void>;
getServerStatuses(): MCPServerStatus[];
getServerStatus(name: string): MCPServerStatus | undefined;
getTools(): MCPTool[];
getTool(name: string): MCPTool | undefined;
isInitialized(): boolean;
}
interface MCPConfig {
mcp?: Record<string, MCPServerConfig>;
tools?: Record<string, boolean>;
}
interface MCPServerConfig {
type: 'local' | 'remote';
command?: string[];
url?: string;
env?: Record<string, string>;
cwd?: string;
enabled?: boolean;
timeout?: number;
}
interface MCPServerStatus {
name: string;
type: 'local' | 'remote';
status: 'connected' | 'connecting' | 'disconnected' | 'disabled' | 'error';
toolCount: number;
error?: string;
lastConnected?: Date;
}
interface MCPTool {
server: string;
name: string;
originalName: string;
description: string;
inputSchema: Record<string, unknown>;
}
export const mcpRouter = new Hono();
// Core 模块缓存
let mcpModule: MCPModule | null = null;
let currentConfig: MCPConfig | null = null;
/**
* 初始化 MCP 模块
*/
async function initMCPModule(): Promise<MCPModule | null> {
if (mcpModule) return mcpModule;
try {
const corePath = '@ai-assistant/core';
const core = (await import(corePath)) as Record<string, unknown>;
if (
typeof core.getMCPManager !== 'function' ||
typeof core.loadMCPConfig !== 'function'
) {
console.warn('[MCP] Core module missing MCP exports');
return null;
}
mcpModule = {
getMCPManager: core.getMCPManager as () => MCPManager,
loadMCPConfig: core.loadMCPConfig as (workdir: string) => Promise<MCPConfig>,
};
// 初始化 MCP Manager
const config = getConfig();
currentConfig = await mcpModule.loadMCPConfig(config.workdir);
const manager = mcpModule.getMCPManager();
if (!manager.isInitialized() && currentConfig.mcp) {
await manager.initialize(currentConfig);
}
console.log('[MCP] MCP module initialized');
return mcpModule;
} catch (error) {
console.warn('[MCP] Failed to load MCP module:', error);
return null;
}
}
/**
* GET /mcp/servers - 获取所有服务器状态
*/
mcpRouter.get('/servers', async (c) => {
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
const statuses = manager.getServerStatuses();
// 添加配置信息
const serversWithConfig = statuses.map((status) => {
const serverConfig = currentConfig?.mcp?.[status.name];
return {
...status,
config: serverConfig
? {
type: serverConfig.type,
command: serverConfig.type === 'local' ? serverConfig.command : undefined,
url: serverConfig.type === 'remote' ? serverConfig.url : undefined,
timeout: serverConfig.timeout,
}
: undefined,
};
});
return c.json({
success: true,
data: serversWithConfig,
});
});
/**
* GET /mcp/servers/:name - 获取单个服务器详情
*/
mcpRouter.get('/servers/:name', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
const status = manager.getServerStatus(name);
if (!status) {
return c.json(
{
success: false,
error: `Server not found: ${name}`,
},
404
);
}
const serverConfig = currentConfig?.mcp?.[name];
const tools = manager
.getTools()
.filter((tool) => tool.server === name);
return c.json({
success: true,
data: {
...status,
config: serverConfig
? {
type: serverConfig.type,
command: serverConfig.type === 'local' ? serverConfig.command : undefined,
url: serverConfig.type === 'remote' ? serverConfig.url : undefined,
timeout: serverConfig.timeout,
cwd: serverConfig.type === 'local' ? serverConfig.cwd : undefined,
}
: undefined,
tools: tools.map((t) => ({
name: t.name,
originalName: t.originalName,
description: t.description,
})),
},
});
});
/**
* POST /mcp/servers/:name/connect - 连接服务器
*/
mcpRouter.post('/servers/:name/connect', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
try {
await manager.reconnect(name);
return c.json({
success: true,
data: {
message: `Server ${name} connected`,
status: manager.getServerStatus(name),
},
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Connection failed',
},
500
);
}
});
/**
* POST /mcp/servers/:name/disconnect - 断开服务器
*/
mcpRouter.post('/servers/:name/disconnect', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
try {
// 通过禁用来断开连接
await manager.setServerEnabled(name, false);
return c.json({
success: true,
data: {
message: `Server ${name} disconnected`,
status: manager.getServerStatus(name),
},
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Disconnect failed',
},
500
);
}
});
/**
* POST /mcp/servers/:name/enable - 启用服务器
*/
mcpRouter.post('/servers/:name/enable', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
try {
await manager.setServerEnabled(name, true);
return c.json({
success: true,
data: {
message: `Server ${name} enabled`,
status: manager.getServerStatus(name),
},
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Enable failed',
},
500
);
}
});
/**
* POST /mcp/servers/:name/disable - 禁用服务器
*/
mcpRouter.post('/servers/:name/disable', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
try {
await manager.setServerEnabled(name, false);
return c.json({
success: true,
data: {
message: `Server ${name} disabled`,
status: manager.getServerStatus(name),
},
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Disable failed',
},
500
);
}
});
/**
* GET /mcp/tools - 获取所有 MCP 工具
*/
mcpRouter.get('/tools', async (c) => {
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
const tools = manager.getTools();
return c.json({
success: true,
data: tools.map((tool) => ({
name: tool.name,
server: tool.server,
originalName: tool.originalName,
description: tool.description,
inputSchema: tool.inputSchema,
})),
});
});
/**
* GET /mcp/tools/:name - 获取单个工具详情
*/
mcpRouter.get('/tools/:name', async (c) => {
const name = c.req.param('name');
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
const manager = module.getMCPManager();
const tool = manager.getTool(name);
if (!tool) {
return c.json(
{
success: false,
error: `Tool not found: ${name}`,
},
404
);
}
return c.json({
success: true,
data: {
name: tool.name,
server: tool.server,
originalName: tool.originalName,
description: tool.description,
inputSchema: tool.inputSchema,
},
});
});
/**
* GET /mcp/config - 获取 MCP 配置
*/
mcpRouter.get('/config', async (c) => {
const module = await initMCPModule();
if (!module) {
return c.json(
{
success: false,
error: 'MCP module not available',
},
503
);
}
// 返回配置(隐藏敏感信息)
const safeConfig: MCPConfig = {
mcp: {},
tools: currentConfig?.tools,
};
if (currentConfig?.mcp) {
for (const [name, config] of Object.entries(currentConfig.mcp)) {
safeConfig.mcp![name] = {
type: config.type,
command: config.type === 'local' ? config.command : undefined,
url: config.type === 'remote' ? config.url : undefined,
enabled: config.enabled,
timeout: config.timeout,
};
}
}
return c.json({
success: true,
data: safeConfig,
});
});