feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* MCP 客户端
|
||||
* 管理与单个 MCP 服务器的连接
|
||||
*/
|
||||
|
||||
import type {
|
||||
Transport,
|
||||
MCPServerConfig,
|
||||
MCPTool,
|
||||
MCPServerStatus,
|
||||
MCPServerStatusType,
|
||||
MCPToolCallResult,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
ListToolsResult,
|
||||
CallToolParams,
|
||||
CallToolResult,
|
||||
ServerCapabilities,
|
||||
MCPContent,
|
||||
} from './types.js';
|
||||
import { createTransport } from './transports/index.js';
|
||||
|
||||
/** MCP 协议版本 */
|
||||
const PROTOCOL_VERSION = '2024-11-05';
|
||||
|
||||
/** 客户端信息 */
|
||||
const CLIENT_INFO = {
|
||||
name: 'ai-terminal-assistant',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP 客户端类
|
||||
*/
|
||||
export class MCPClient {
|
||||
private transport: Transport;
|
||||
private serverCapabilities?: ServerCapabilities;
|
||||
private serverInfo?: { name: string; version: string };
|
||||
private tools: MCPTool[] = [];
|
||||
private _status: MCPServerStatusType = 'disconnected';
|
||||
private _error?: string;
|
||||
private lastConnected?: Date;
|
||||
|
||||
constructor(
|
||||
private name: string,
|
||||
private config: MCPServerConfig
|
||||
) {
|
||||
this.transport = createTransport(name, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到 MCP 服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this._status === 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = 'connecting';
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
// 启动传输层
|
||||
await this.transport.start();
|
||||
|
||||
// 监听连接关闭
|
||||
this.transport.onClose((error) => {
|
||||
this._status = 'disconnected';
|
||||
if (error) {
|
||||
this._error = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听服务器通知
|
||||
this.transport.onNotification((method, params) => {
|
||||
this.handleNotification(method, params);
|
||||
});
|
||||
|
||||
// 初始化握手
|
||||
await this.initialize();
|
||||
|
||||
// 获取工具列表
|
||||
await this.refreshTools();
|
||||
|
||||
this._status = 'connected';
|
||||
this.lastConnected = new Date();
|
||||
} catch (error) {
|
||||
this._status = 'error';
|
||||
this._error = error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this._status === 'disconnected') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.transport.close();
|
||||
} finally {
|
||||
this._status = 'disconnected';
|
||||
this.tools = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 初始化握手
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
const params: InitializeParams = {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
roots: { listChanged: true },
|
||||
},
|
||||
clientInfo: CLIENT_INFO,
|
||||
};
|
||||
|
||||
const result = (await this.transport.request(
|
||||
'initialize',
|
||||
params
|
||||
)) as InitializeResult;
|
||||
|
||||
this.serverCapabilities = result.capabilities;
|
||||
this.serverInfo = result.serverInfo;
|
||||
|
||||
// 发送 initialized 通知
|
||||
await this.transport.notify('notifications/initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新工具列表
|
||||
*/
|
||||
async refreshTools(): Promise<void> {
|
||||
const result = (await this.transport.request(
|
||||
'tools/list',
|
||||
{}
|
||||
)) as ListToolsResult;
|
||||
|
||||
this.tools = result.tools.map((tool) => ({
|
||||
server: this.name,
|
||||
name: `${this.name}-${tool.name}`,
|
||||
originalName: tool.name,
|
||||
description: tool.description || '',
|
||||
inputSchema: tool.inputSchema,
|
||||
outputSchema: tool.outputSchema,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用工具
|
||||
*/
|
||||
async callTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<MCPToolCallResult> {
|
||||
// 获取原始工具名
|
||||
const tool = this.tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
return {
|
||||
success: false,
|
||||
content: [{ type: 'text', text: `Tool not found: ${toolName}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const params: CallToolParams = {
|
||||
name: tool.originalName,
|
||||
arguments: args,
|
||||
};
|
||||
|
||||
const result = (await this.transport.request(
|
||||
'tools/call',
|
||||
params
|
||||
)) as CallToolResult;
|
||||
|
||||
return {
|
||||
success: !result.isError,
|
||||
content: result.content as MCPContent[],
|
||||
isError: result.isError,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具列表
|
||||
*/
|
||||
getTools(): MCPTool[] {
|
||||
return [...this.tools];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getStatus(): MCPServerStatus {
|
||||
return {
|
||||
name: this.name,
|
||||
type: this.config.type,
|
||||
status: this._status,
|
||||
toolCount: this.tools.length,
|
||||
error: this._error,
|
||||
lastConnected: this.lastConnected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器信息
|
||||
*/
|
||||
getServerInfo(): { name: string; version: string } | undefined {
|
||||
return this.serverInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器能力
|
||||
*/
|
||||
getServerCapabilities(): ServerCapabilities | undefined {
|
||||
return this.serverCapabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务器通知
|
||||
*/
|
||||
private handleNotification(method: string, params: unknown): void {
|
||||
switch (method) {
|
||||
case 'notifications/tools/list_changed':
|
||||
// 工具列表变化,刷新
|
||||
this.refreshTools().catch((error) => {
|
||||
console.error(
|
||||
`[MCP:${this.name}] Failed to refresh tools:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notifications/resources/list_changed':
|
||||
// 资源列表变化(暂不处理)
|
||||
break;
|
||||
|
||||
case 'notifications/prompts/list_changed':
|
||||
// 提示列表变化(暂不处理)
|
||||
break;
|
||||
|
||||
default:
|
||||
console.debug(
|
||||
`[MCP:${this.name}] Unknown notification: ${method}`,
|
||||
params
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* MCP 配置加载和验证
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'yaml';
|
||||
import type {
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
LocalMCPServer,
|
||||
RemoteMCPServer,
|
||||
} from './types.js';
|
||||
|
||||
/** 默认配置值 */
|
||||
const DEFAULTS = {
|
||||
timeout: 30000,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析环境变量引用
|
||||
* 支持 {env:VAR_NAME} 语法
|
||||
*/
|
||||
export function resolveEnvVariables(value: string): string {
|
||||
return value.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
const envValue = process.env[varName];
|
||||
if (envValue === undefined) {
|
||||
console.warn(`警告: 环境变量 ${varName} 未设置`);
|
||||
return '';
|
||||
}
|
||||
return envValue;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归解析对象中的环境变量
|
||||
*/
|
||||
export function resolveEnvInObject<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
return resolveEnvVariables(obj) as T;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => resolveEnvInObject(item)) as T;
|
||||
}
|
||||
if (obj !== null && typeof obj === 'object') {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[key] = resolveEnvInObject(value);
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证本地服务器配置
|
||||
*/
|
||||
function validateLocalServer(
|
||||
name: string,
|
||||
config: LocalMCPServer
|
||||
): string | null {
|
||||
if (!config.command || !Array.isArray(config.command)) {
|
||||
return `服务器 "${name}": command 必须是字符串数组`;
|
||||
}
|
||||
if (config.command.length === 0) {
|
||||
return `服务器 "${name}": command 不能为空`;
|
||||
}
|
||||
if (config.timeout !== undefined && typeof config.timeout !== 'number') {
|
||||
return `服务器 "${name}": timeout 必须是数字`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证远程服务器配置
|
||||
*/
|
||||
function validateRemoteServer(
|
||||
name: string,
|
||||
config: RemoteMCPServer
|
||||
): string | null {
|
||||
if (!config.url || typeof config.url !== 'string') {
|
||||
return `服务器 "${name}": url 是必需的`;
|
||||
}
|
||||
try {
|
||||
new URL(config.url);
|
||||
} catch {
|
||||
return `服务器 "${name}": url 格式无效`;
|
||||
}
|
||||
if (config.timeout !== undefined && typeof config.timeout !== 'number') {
|
||||
return `服务器 "${name}": timeout 必须是数字`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证服务器配置
|
||||
*/
|
||||
function validateServerConfig(
|
||||
name: string,
|
||||
config: MCPServerConfig
|
||||
): string | null {
|
||||
if (!config.type) {
|
||||
return `服务器 "${name}": type 是必需的 (local 或 remote)`;
|
||||
}
|
||||
if (config.type === 'local') {
|
||||
return validateLocalServer(name, config as LocalMCPServer);
|
||||
}
|
||||
if (config.type === 'remote') {
|
||||
return validateRemoteServer(name, config as RemoteMCPServer);
|
||||
}
|
||||
return `服务器 "${name}": type 必须是 "local" 或 "remote"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 MCP 配置
|
||||
*/
|
||||
export function validateMCPConfig(config: MCPConfig): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.mcp) {
|
||||
return errors; // 没有 MCP 配置是合法的
|
||||
}
|
||||
|
||||
if (typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
|
||||
errors.push('mcp 配置必须是对象格式');
|
||||
return errors;
|
||||
}
|
||||
|
||||
for (const [name, serverConfig] of Object.entries(config.mcp)) {
|
||||
// 验证服务器名称
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
||||
errors.push(
|
||||
`服务器名称 "${name}" 无效: 必须以字母开头,只能包含字母、数字、下划线和连字符`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const error = validateServerConfig(name, serverConfig);
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 tools 配置
|
||||
if (config.tools) {
|
||||
if (typeof config.tools !== 'object' || Array.isArray(config.tools)) {
|
||||
errors.push('tools 配置必须是对象格式');
|
||||
} else {
|
||||
for (const [pattern, enabled] of Object.entries(config.tools)) {
|
||||
if (typeof enabled !== 'boolean') {
|
||||
errors.push(`tools["${pattern}"] 的值必须是布尔值`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化服务器配置(添加默认值)
|
||||
*/
|
||||
function normalizeServerConfig(config: MCPServerConfig): MCPServerConfig {
|
||||
return {
|
||||
...config,
|
||||
enabled: config.enabled ?? DEFAULTS.enabled,
|
||||
timeout: config.timeout ?? DEFAULTS.timeout,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 MCP 配置
|
||||
*/
|
||||
export function normalizeMCPConfig(config: MCPConfig): MCPConfig {
|
||||
if (!config.mcp) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const normalizedMcp: Record<string, MCPServerConfig> = {};
|
||||
for (const [name, serverConfig] of Object.entries(config.mcp)) {
|
||||
normalizedMcp[name] = normalizeServerConfig(serverConfig);
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
mcp: normalizedMcp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载配置
|
||||
*/
|
||||
function loadConfigFile(filePath: string): MCPConfig | null {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (ext === '.json' || ext === '.jsonc') {
|
||||
// 移除 JSONC 注释
|
||||
const jsonContent = content.replace(
|
||||
/\/\/.*$|\/\*[\s\S]*?\*\//gm,
|
||||
''
|
||||
);
|
||||
return JSON.parse(jsonContent);
|
||||
}
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
return yaml.parse(content);
|
||||
}
|
||||
|
||||
console.warn(`不支持的配置文件格式: ${ext}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`加载配置文件失败 ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并配置(后者覆盖前者)
|
||||
*/
|
||||
function mergeConfigs(base: MCPConfig, override: MCPConfig): MCPConfig {
|
||||
return {
|
||||
mcp: {
|
||||
...base.mcp,
|
||||
...override.mcp,
|
||||
},
|
||||
tools: {
|
||||
...base.tools,
|
||||
...override.tools,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 MCP 配置
|
||||
* 按优先级从低到高加载:
|
||||
* 1. 用户级: ~/.ai-assist/config.{json,yaml}
|
||||
* 2. 项目级: .ai-assist/config.{json,yaml}
|
||||
*/
|
||||
export function loadMCPConfig(workdir?: string): MCPConfig {
|
||||
const cwd = workdir || process.cwd();
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
|
||||
// 配置文件搜索路径
|
||||
const searchPaths = [
|
||||
// 用户级配置
|
||||
path.join(homeDir, '.ai-assist', 'config.json'),
|
||||
path.join(homeDir, '.ai-assist', 'config.jsonc'),
|
||||
path.join(homeDir, '.ai-assist', 'config.yaml'),
|
||||
path.join(homeDir, '.ai-assist', 'config.yml'),
|
||||
// 项目级配置
|
||||
path.join(cwd, '.ai-assist', 'config.json'),
|
||||
path.join(cwd, '.ai-assist', 'config.jsonc'),
|
||||
path.join(cwd, '.ai-assist', 'config.yaml'),
|
||||
path.join(cwd, '.ai-assist', 'config.yml'),
|
||||
];
|
||||
|
||||
let mergedConfig: MCPConfig = {};
|
||||
|
||||
for (const configPath of searchPaths) {
|
||||
const config = loadConfigFile(configPath);
|
||||
if (config) {
|
||||
mergedConfig = mergeConfigs(mergedConfig, config);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
const errors = validateMCPConfig(mergedConfig);
|
||||
if (errors.length > 0) {
|
||||
console.error('MCP 配置验证失败:');
|
||||
for (const error of errors) {
|
||||
console.error(` - ${error}`);
|
||||
}
|
||||
// 返回空配置而不是无效配置
|
||||
return {};
|
||||
}
|
||||
|
||||
// 规范化配置
|
||||
const normalizedConfig = normalizeMCPConfig(mergedConfig);
|
||||
|
||||
// 解析环境变量
|
||||
return resolveEnvInObject(normalizedConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用的服务器列表
|
||||
*/
|
||||
export function getEnabledServers(
|
||||
config: MCPConfig
|
||||
): Array<{ name: string; config: MCPServerConfig }> {
|
||||
if (!config.mcp) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(config.mcp)
|
||||
.filter(([, serverConfig]) => serverConfig.enabled !== false)
|
||||
.map(([name, serverConfig]) => ({ name, config: serverConfig }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查工具是否被配置启用
|
||||
* 支持通配符匹配,如 "server-*"
|
||||
*/
|
||||
export function isToolEnabled(
|
||||
toolName: string,
|
||||
toolsConfig?: Record<string, boolean>
|
||||
): boolean {
|
||||
if (!toolsConfig) {
|
||||
return true; // 默认启用
|
||||
}
|
||||
|
||||
// 精确匹配优先
|
||||
if (toolName in toolsConfig) {
|
||||
return toolsConfig[toolName];
|
||||
}
|
||||
|
||||
// 通配符匹配(从最具体到最通用)
|
||||
const patterns = Object.keys(toolsConfig)
|
||||
.filter((pattern) => pattern.includes('*'))
|
||||
.sort((a, b) => {
|
||||
// 更具体的模式(* 出现位置更靠后)优先
|
||||
const aPos = a.indexOf('*');
|
||||
const bPos = b.indexOf('*');
|
||||
return bPos - aPos;
|
||||
});
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const regex = new RegExp(
|
||||
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
||||
);
|
||||
if (regex.test(toolName)) {
|
||||
return toolsConfig[pattern];
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 默认启用
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* MCP 模块导出入口
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
LocalMCPServer,
|
||||
RemoteMCPServer,
|
||||
OAuthConfig,
|
||||
MCPTool,
|
||||
MCPServerStatus,
|
||||
MCPServerStatusType,
|
||||
MCPToolCallResult,
|
||||
MCPContent,
|
||||
MCPTextContent,
|
||||
MCPImageContent,
|
||||
MCPResourceContent,
|
||||
Transport,
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
MCPManagerEvents,
|
||||
} from './types.js';
|
||||
|
||||
// 配置相关
|
||||
export {
|
||||
loadMCPConfig,
|
||||
validateMCPConfig,
|
||||
normalizeMCPConfig,
|
||||
getEnabledServers,
|
||||
isToolEnabled,
|
||||
resolveEnvVariables,
|
||||
resolveEnvInObject,
|
||||
} from './config.js';
|
||||
|
||||
// 客户端
|
||||
export { MCPClient } from './client.js';
|
||||
|
||||
// 管理器
|
||||
export { MCPManager, getMCPManager, resetMCPManager } from './manager.js';
|
||||
|
||||
// 工具适配器
|
||||
export { MCPToolAdapter, createMCPToolAdapter } from './tool-adapter.js';
|
||||
|
||||
// 传输层
|
||||
export { createTransport, StdioTransport } from './transports/index.js';
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* MCP Manager
|
||||
* 管理多个 MCP 服务器的生命周期
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import type {
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
MCPTool,
|
||||
MCPServerStatus,
|
||||
MCPToolCallResult,
|
||||
MCPManagerEvents,
|
||||
} from './types.js';
|
||||
import { MCPClient } from './client.js';
|
||||
import { getEnabledServers, isToolEnabled } from './config.js';
|
||||
|
||||
/**
|
||||
* MCP Manager 类
|
||||
* 负责管理所有 MCP 服务器连接
|
||||
*/
|
||||
export class MCPManager extends EventEmitter {
|
||||
private clients: Map<string, MCPClient> = new Map();
|
||||
private config: MCPConfig = {};
|
||||
private initialized = false;
|
||||
|
||||
/**
|
||||
* 初始化所有已启用的服务器
|
||||
*/
|
||||
async initialize(config: MCPConfig): Promise<void> {
|
||||
if (this.initialized) {
|
||||
await this.shutdown();
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
const servers = getEnabledServers(config);
|
||||
|
||||
// 并行连接所有服务器
|
||||
const connectPromises = servers.map(async ({ name, config: serverConfig }) => {
|
||||
try {
|
||||
await this.connectServer(name, serverConfig);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[MCP] Failed to connect to ${name}:`,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(connectPromises);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接单个服务器
|
||||
*/
|
||||
private async connectServer(
|
||||
name: string,
|
||||
config: MCPServerConfig
|
||||
): Promise<void> {
|
||||
const client = new MCPClient(name, config);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
this.clients.set(name, client);
|
||||
this.emit('server:connected', name);
|
||||
} catch (error) {
|
||||
this.emit('server:error', name, error instanceof Error ? error : new Error(String(error)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有连接
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
const disconnectPromises = Array.from(this.clients.entries()).map(
|
||||
async ([name, client]) => {
|
||||
try {
|
||||
await client.disconnect();
|
||||
this.emit('server:disconnected', name);
|
||||
} catch (error) {
|
||||
console.error(`[MCP] Error disconnecting ${name}:`, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(disconnectPromises);
|
||||
this.clients.clear();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连指定服务器
|
||||
*/
|
||||
async reconnect(serverName: string): Promise<void> {
|
||||
const existingClient = this.clients.get(serverName);
|
||||
if (existingClient) {
|
||||
await existingClient.disconnect();
|
||||
this.clients.delete(serverName);
|
||||
this.emit('server:disconnected', serverName);
|
||||
}
|
||||
|
||||
const serverConfig = this.config.mcp?.[serverName];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server not found: ${serverName}`);
|
||||
}
|
||||
|
||||
await this.connectServer(serverName, serverConfig);
|
||||
this.emit('tools:changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用工具
|
||||
*/
|
||||
getTools(): MCPTool[] {
|
||||
const allTools: MCPTool[] = [];
|
||||
|
||||
for (const client of this.clients.values()) {
|
||||
const status = client.getStatus();
|
||||
if (status.status === 'connected') {
|
||||
const tools = client.getTools();
|
||||
// 根据配置过滤工具
|
||||
for (const tool of tools) {
|
||||
if (isToolEnabled(tool.name, this.config.tools)) {
|
||||
allTools.push(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定工具
|
||||
*/
|
||||
getTool(name: string): MCPTool | undefined {
|
||||
for (const client of this.clients.values()) {
|
||||
const tools = client.getTools();
|
||||
const tool = tools.find((t) => t.name === name);
|
||||
if (tool && isToolEnabled(tool.name, this.config.tools)) {
|
||||
return tool;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用工具
|
||||
*/
|
||||
async callTool(
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<MCPToolCallResult> {
|
||||
// 从工具名中解析服务器名
|
||||
const dashIndex = name.indexOf('-');
|
||||
if (dashIndex === -1) {
|
||||
return {
|
||||
success: false,
|
||||
content: [{ type: 'text', text: `Invalid tool name format: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const serverName = name.substring(0, dashIndex);
|
||||
const client = this.clients.get(serverName);
|
||||
|
||||
if (!client) {
|
||||
return {
|
||||
success: false,
|
||||
content: [{ type: 'text', text: `Server not connected: ${serverName}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查工具是否启用
|
||||
if (!isToolEnabled(name, this.config.tools)) {
|
||||
return {
|
||||
success: false,
|
||||
content: [{ type: 'text', text: `Tool is disabled: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return await client.callTool(name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有服务器状态
|
||||
*/
|
||||
getServerStatuses(): MCPServerStatus[] {
|
||||
const statuses: MCPServerStatus[] = [];
|
||||
|
||||
// 已连接的服务器
|
||||
for (const client of this.clients.values()) {
|
||||
statuses.push(client.getStatus());
|
||||
}
|
||||
|
||||
// 未连接但已配置的服务器
|
||||
if (this.config.mcp) {
|
||||
for (const [name, serverConfig] of Object.entries(this.config.mcp)) {
|
||||
if (!this.clients.has(name)) {
|
||||
statuses.push({
|
||||
name,
|
||||
type: serverConfig.type,
|
||||
status: serverConfig.enabled === false ? 'disabled' : 'disconnected',
|
||||
toolCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定服务器状态
|
||||
*/
|
||||
getServerStatus(name: string): MCPServerStatus | undefined {
|
||||
const client = this.clients.get(name);
|
||||
if (client) {
|
||||
return client.getStatus();
|
||||
}
|
||||
|
||||
const serverConfig = this.config.mcp?.[name];
|
||||
if (serverConfig) {
|
||||
return {
|
||||
name,
|
||||
type: serverConfig.type,
|
||||
status: serverConfig.enabled === false ? 'disabled' : 'disconnected',
|
||||
toolCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用服务器
|
||||
*/
|
||||
async setServerEnabled(serverName: string, enabled: boolean): Promise<void> {
|
||||
const serverConfig = this.config.mcp?.[serverName];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server not found: ${serverName}`);
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
// 启用并连接
|
||||
serverConfig.enabled = true;
|
||||
if (!this.clients.has(serverName)) {
|
||||
await this.connectServer(serverName, serverConfig);
|
||||
this.emit('tools:changed');
|
||||
}
|
||||
} else {
|
||||
// 禁用并断开
|
||||
serverConfig.enabled = false;
|
||||
const client = this.clients.get(serverName);
|
||||
if (client) {
|
||||
await client.disconnect();
|
||||
this.clients.delete(serverName);
|
||||
this.emit('server:disconnected', serverName);
|
||||
this.emit('tools:changed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端实例(用于测试)
|
||||
*/
|
||||
getClient(name: string): MCPClient | undefined {
|
||||
return this.clients.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已初始化
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
// 类型安全的事件方法
|
||||
override on<K extends keyof MCPManagerEvents>(
|
||||
event: K,
|
||||
listener: MCPManagerEvents[K]
|
||||
): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
override emit<K extends keyof MCPManagerEvents>(
|
||||
event: K,
|
||||
...args: Parameters<MCPManagerEvents[K]>
|
||||
): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
let mcpManager: MCPManager | null = null;
|
||||
|
||||
/**
|
||||
* 获取 MCP Manager 单例
|
||||
*/
|
||||
export function getMCPManager(): MCPManager {
|
||||
if (!mcpManager) {
|
||||
mcpManager = new MCPManager();
|
||||
}
|
||||
return mcpManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 MCP Manager(用于测试)
|
||||
*/
|
||||
export function resetMCPManager(): void {
|
||||
if (mcpManager) {
|
||||
mcpManager.shutdown().catch(console.error);
|
||||
mcpManager = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* MCP 工具适配器
|
||||
* 将 MCP 工具转换为内部工具格式
|
||||
*/
|
||||
|
||||
import type { ToolParameter, ToolResult } from '../types/index.js';
|
||||
import type { ToolWithMetadata, ToolCategory } from '../tools/types.js';
|
||||
import type { MCPTool, MCPToolCallResult, MCPContent } from './types.js';
|
||||
import type { MCPManager } from './manager.js';
|
||||
|
||||
/**
|
||||
* 将 JSON Schema 类型转换为内部参数类型
|
||||
*/
|
||||
function convertJsonSchemaType(
|
||||
schemaType: string | string[] | undefined
|
||||
): 'string' | 'number' | 'boolean' | 'object' | 'array' {
|
||||
if (Array.isArray(schemaType)) {
|
||||
// 取第一个非 null 类型
|
||||
const type = schemaType.find((t) => t !== 'null');
|
||||
return convertJsonSchemaType(type);
|
||||
}
|
||||
|
||||
switch (schemaType) {
|
||||
case 'string':
|
||||
return 'string';
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'array':
|
||||
return 'array';
|
||||
case 'object':
|
||||
default:
|
||||
return 'object';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON Schema 转换为内部参数定义
|
||||
*/
|
||||
function convertInputSchema(
|
||||
schema: MCPTool['inputSchema']
|
||||
): Record<string, ToolParameter> {
|
||||
const parameters: Record<string, ToolParameter> = {};
|
||||
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
const properties = schema.properties as Record<string, {
|
||||
type?: string | string[];
|
||||
description?: string;
|
||||
}> | undefined;
|
||||
|
||||
const required = (schema.required as string[]) || [];
|
||||
|
||||
if (properties) {
|
||||
for (const [name, prop] of Object.entries(properties)) {
|
||||
parameters[name] = {
|
||||
type: convertJsonSchemaType(prop.type),
|
||||
description: prop.description || '',
|
||||
required: required.includes(name),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 内容转换为字符串输出
|
||||
*/
|
||||
function contentToString(content: MCPContent[]): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const item of content) {
|
||||
switch (item.type) {
|
||||
case 'text':
|
||||
parts.push(item.text);
|
||||
break;
|
||||
case 'image':
|
||||
parts.push(`[Image: ${item.mimeType}]`);
|
||||
break;
|
||||
case 'resource':
|
||||
if (item.resource.text) {
|
||||
parts.push(item.resource.text);
|
||||
} else {
|
||||
parts.push(`[Resource: ${item.resource.uri}]`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 调用结果转换为内部 ToolResult
|
||||
*/
|
||||
function convertToolResult(result: MCPToolCallResult): ToolResult {
|
||||
const output = contentToString(result.content);
|
||||
|
||||
if (result.isError || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: output || 'Tool execution failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 工具适配器类
|
||||
*/
|
||||
export class MCPToolAdapter {
|
||||
constructor(private manager: MCPManager) {}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具适配为内部工具格式
|
||||
*/
|
||||
adaptToInternalTool(mcpTool: MCPTool): ToolWithMetadata {
|
||||
const parameters = convertInputSchema(mcpTool.inputSchema);
|
||||
|
||||
return {
|
||||
name: mcpTool.name,
|
||||
description: mcpTool.description || `MCP tool: ${mcpTool.originalName}`,
|
||||
parameters,
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const result = await this.manager.callTool(mcpTool.name, params);
|
||||
return convertToolResult(result);
|
||||
},
|
||||
metadata: {
|
||||
name: mcpTool.name,
|
||||
category: 'agent' as ToolCategory, // MCP 工具归类为 agent
|
||||
description: mcpTool.description || `MCP tool from ${mcpTool.server}`,
|
||||
keywords: [
|
||||
'mcp',
|
||||
mcpTool.server,
|
||||
mcpTool.originalName,
|
||||
...mcpTool.originalName.split(/[_-]/),
|
||||
],
|
||||
deferLoading: false, // MCP 工具不延迟加载
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量适配工具
|
||||
*/
|
||||
adaptTools(mcpTools: MCPTool[]): ToolWithMetadata[] {
|
||||
return mcpTools.map((tool) => this.adaptToInternalTool(tool));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MCP 工具适配器
|
||||
*/
|
||||
export function createMCPToolAdapter(manager: MCPManager): MCPToolAdapter {
|
||||
return new MCPToolAdapter(manager);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 传输层工厂
|
||||
*/
|
||||
|
||||
import type { Transport, MCPServerConfig, LocalMCPServer } from '../types.js';
|
||||
import { StdioTransport } from './stdio.js';
|
||||
|
||||
/**
|
||||
* 创建传输层实例
|
||||
*/
|
||||
export function createTransport(
|
||||
serverName: string,
|
||||
config: MCPServerConfig
|
||||
): Transport {
|
||||
if (config.type === 'local') {
|
||||
return new StdioTransport(config as LocalMCPServer, serverName);
|
||||
}
|
||||
|
||||
if (config.type === 'remote') {
|
||||
// TODO: 实现 HTTP 传输
|
||||
throw new Error('Remote transport not yet implemented');
|
||||
}
|
||||
|
||||
throw new Error(`Unknown transport type: ${(config as MCPServerConfig).type}`);
|
||||
}
|
||||
|
||||
export { StdioTransport } from './stdio.js';
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* stdio 传输实现
|
||||
* 通过子进程的 stdin/stdout 与 MCP 服务器通信
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import type {
|
||||
Transport,
|
||||
JSONRPCRequest,
|
||||
JSONRPCResponse,
|
||||
JSONRPCNotification,
|
||||
LocalMCPServer,
|
||||
} from '../types.js';
|
||||
|
||||
/** 默认请求超时 */
|
||||
const DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
/** 待处理请求 */
|
||||
interface PendingRequest {
|
||||
resolve: (result: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
timer: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* stdio 传输层实现
|
||||
*/
|
||||
export class StdioTransport extends EventEmitter implements Transport {
|
||||
private process: ChildProcess | null = null;
|
||||
private nextId = 1;
|
||||
private pendingRequests = new Map<string | number, PendingRequest>();
|
||||
private buffer = '';
|
||||
private notificationHandler?: (method: string, params: unknown) => void;
|
||||
private closeHandler?: (error?: Error) => void;
|
||||
private isClosing = false;
|
||||
|
||||
constructor(
|
||||
private config: LocalMCPServer,
|
||||
private serverName: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动传输(创建子进程)
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.process) {
|
||||
throw new Error('Transport already started');
|
||||
}
|
||||
|
||||
const [command, ...args] = this.config.command;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.process = spawn(command, args, {
|
||||
cwd: this.config.cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...this.config.env,
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// 处理 stdout(JSON-RPC 消息)
|
||||
this.process.stdout?.on('data', (data: Buffer) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
// 处理 stderr(日志输出)
|
||||
this.process.stderr?.on('data', (data: Buffer) => {
|
||||
const message = data.toString().trim();
|
||||
if (message) {
|
||||
console.debug(`[MCP:${this.serverName}] ${message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理进程错误
|
||||
this.process.on('error', (error) => {
|
||||
if (!this.isClosing) {
|
||||
this.handleClose(error);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// 处理进程退出
|
||||
this.process.on('exit', (code, signal) => {
|
||||
if (!this.isClosing) {
|
||||
const error =
|
||||
code !== 0
|
||||
? new Error(
|
||||
`Process exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`
|
||||
)
|
||||
: undefined;
|
||||
this.handleClose(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 给进程一点时间启动
|
||||
setTimeout(() => {
|
||||
if (this.process && !this.process.killed) {
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭传输
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
this.isClosing = true;
|
||||
|
||||
// 拒绝所有待处理请求
|
||||
for (const [id, pending] of this.pendingRequests) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error('Transport closed'));
|
||||
this.pendingRequests.delete(id);
|
||||
}
|
||||
|
||||
// 终止子进程
|
||||
if (this.process && !this.process.killed) {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.process?.kill('SIGKILL');
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
this.process!.on('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.process!.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求并等待响应
|
||||
*/
|
||||
async request(method: string, params?: unknown): Promise<unknown> {
|
||||
if (!this.process || this.process.killed) {
|
||||
throw new Error('Transport not connected');
|
||||
}
|
||||
|
||||
const id = this.nextId++;
|
||||
const request: JSONRPCRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`Request timeout after ${timeout}ms: ${method}`));
|
||||
}, timeout);
|
||||
|
||||
this.pendingRequests.set(id, { resolve, reject, timer });
|
||||
this.send(request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知(无响应)
|
||||
*/
|
||||
async notify(method: string, params?: unknown): Promise<void> {
|
||||
if (!this.process || this.process.killed) {
|
||||
throw new Error('Transport not connected');
|
||||
}
|
||||
|
||||
const notification: JSONRPCNotification = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
this.send(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听服务器通知
|
||||
*/
|
||||
onNotification(handler: (method: string, params: unknown) => void): void {
|
||||
this.notificationHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听连接关闭
|
||||
*/
|
||||
onClose(handler: (error?: Error) => void): void {
|
||||
this.closeHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 JSON-RPC 消息
|
||||
*/
|
||||
private send(message: JSONRPCRequest | JSONRPCNotification): void {
|
||||
const data = JSON.stringify(message) + '\n';
|
||||
this.process?.stdin?.write(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的数据
|
||||
*/
|
||||
private handleData(data: Buffer): void {
|
||||
this.buffer += data.toString();
|
||||
|
||||
// 按行分割处理
|
||||
const lines = this.buffer.split('\n');
|
||||
this.buffer = lines.pop() || ''; // 保留不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
this.handleMessage(JSON.parse(line));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[MCP:${this.serverName}] Failed to parse message:`,
|
||||
line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSON-RPC 消息
|
||||
*/
|
||||
private handleMessage(message: JSONRPCResponse | JSONRPCNotification): void {
|
||||
// 响应消息
|
||||
if ('id' in message && message.id !== undefined) {
|
||||
const pending = this.pendingRequests.get(message.id);
|
||||
if (pending) {
|
||||
clearTimeout(pending.timer);
|
||||
this.pendingRequests.delete(message.id);
|
||||
|
||||
if ('error' in message && message.error) {
|
||||
pending.reject(
|
||||
new Error(
|
||||
`${message.error.message} (code: ${message.error.code})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知消息
|
||||
if ('method' in message) {
|
||||
this.notificationHandler?.(message.method, message.params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接关闭
|
||||
*/
|
||||
private handleClose(error?: Error): void {
|
||||
// 拒绝所有待处理请求
|
||||
for (const [id, pending] of this.pendingRequests) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(error || new Error('Connection closed'));
|
||||
this.pendingRequests.delete(id);
|
||||
}
|
||||
|
||||
this.closeHandler?.(error);
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* MCP (Model Context Protocol) 类型定义
|
||||
*/
|
||||
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
|
||||
// ============================================================================
|
||||
// 配置类型
|
||||
// ============================================================================
|
||||
|
||||
/** 本地 MCP 服务器配置 (stdio 传输) */
|
||||
export interface LocalMCPServer {
|
||||
type: 'local';
|
||||
/** 启动命令和参数,如 ["npx", "-y", "@anthropic/mcp-server-filesystem", "/path"] */
|
||||
command: string[];
|
||||
/** 环境变量,支持 {env:VAR} 语法引用系统环境变量 */
|
||||
env?: Record<string, string>;
|
||||
/** 工作目录 */
|
||||
cwd?: string;
|
||||
/** 是否启用,默认 true */
|
||||
enabled?: boolean;
|
||||
/** 超时毫秒数,默认 30000 */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/** 远程 MCP 服务器配置 (HTTP/SSE 传输) */
|
||||
export interface RemoteMCPServer {
|
||||
type: 'remote';
|
||||
/** MCP 端点 URL */
|
||||
url: string;
|
||||
/** 自定义请求头,支持 {env:VAR} 语法 */
|
||||
headers?: Record<string, string>;
|
||||
/** OAuth 配置,空对象 {} 表示自动发现 (RFC 7591) */
|
||||
oauth?: OAuthConfig | Record<string, never>;
|
||||
/** 是否启用,默认 true */
|
||||
enabled?: boolean;
|
||||
/** 超时毫秒数,默认 30000 */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/** OAuth 配置 */
|
||||
export interface OAuthConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope?: string;
|
||||
/** Token 端点,可选,自动发现时不需要 */
|
||||
tokenEndpoint?: string;
|
||||
}
|
||||
|
||||
/** MCP 服务器配置(本地或远程) */
|
||||
export type MCPServerConfig = LocalMCPServer | RemoteMCPServer;
|
||||
|
||||
/** MCP 配置根节点 */
|
||||
export interface MCPConfig {
|
||||
/** MCP 服务器配置,key 为服务器名称 */
|
||||
mcp?: Record<string, MCPServerConfig>;
|
||||
/** 工具启用/禁用配置,支持通配符如 "server-*" */
|
||||
tools?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 运行时类型
|
||||
// ============================================================================
|
||||
|
||||
/** MCP 工具定义 */
|
||||
export interface MCPTool {
|
||||
/** 来源服务器名称 */
|
||||
server: string;
|
||||
/** 完整工具名: {server}-{originalName} */
|
||||
name: string;
|
||||
/** MCP 服务器中的原始名称 */
|
||||
originalName: string;
|
||||
/** 工具描述 */
|
||||
description: string;
|
||||
/** 输入参数 JSON Schema */
|
||||
inputSchema: JSONSchema7;
|
||||
/** 输出结果 JSON Schema(可选) */
|
||||
outputSchema?: JSONSchema7;
|
||||
}
|
||||
|
||||
/** 服务器运行状态 */
|
||||
export type MCPServerStatusType =
|
||||
| 'connected'
|
||||
| 'connecting'
|
||||
| 'disconnected'
|
||||
| 'auth_required'
|
||||
| 'disabled'
|
||||
| 'error';
|
||||
|
||||
/** 服务器状态信息 */
|
||||
export interface MCPServerStatus {
|
||||
/** 服务器名称 */
|
||||
name: string;
|
||||
/** 服务器类型 */
|
||||
type: 'local' | 'remote';
|
||||
/** 当前状态 */
|
||||
status: MCPServerStatusType;
|
||||
/** 工具数量 */
|
||||
toolCount: number;
|
||||
/** 错误信息(当 status 为 error 时) */
|
||||
error?: string;
|
||||
/** 最后连接时间 */
|
||||
lastConnected?: Date;
|
||||
}
|
||||
|
||||
/** 工具调用结果 */
|
||||
export interface MCPToolCallResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 结果内容 */
|
||||
content: MCPContent[];
|
||||
/** 是否为错误结果 */
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
/** MCP 内容类型 */
|
||||
export type MCPContent =
|
||||
| MCPTextContent
|
||||
| MCPImageContent
|
||||
| MCPResourceContent;
|
||||
|
||||
/** 文本内容 */
|
||||
export interface MCPTextContent {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** 图片内容 */
|
||||
export interface MCPImageContent {
|
||||
type: 'image';
|
||||
data: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
/** 资源内容 */
|
||||
export interface MCPResourceContent {
|
||||
type: 'resource';
|
||||
resource: {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
text?: string;
|
||||
blob?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 传输层类型
|
||||
// ============================================================================
|
||||
|
||||
/** JSON-RPC 请求 */
|
||||
export interface JSONRPCRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/** JSON-RPC 响应 */
|
||||
export interface JSONRPCResponse {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
result?: unknown;
|
||||
error?: JSONRPCError;
|
||||
}
|
||||
|
||||
/** JSON-RPC 通知 */
|
||||
export interface JSONRPCNotification {
|
||||
jsonrpc: '2.0';
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/** JSON-RPC 错误 */
|
||||
export interface JSONRPCError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
/** 传输层接口 */
|
||||
export interface Transport {
|
||||
/** 启动传输 */
|
||||
start(): Promise<void>;
|
||||
/** 关闭传输 */
|
||||
close(): Promise<void>;
|
||||
/** 发送请求并等待响应 */
|
||||
request(method: string, params?: unknown): Promise<unknown>;
|
||||
/** 发送通知(无响应) */
|
||||
notify(method: string, params?: unknown): Promise<void>;
|
||||
/** 监听服务器通知 */
|
||||
onNotification(handler: (method: string, params: unknown) => void): void;
|
||||
/** 监听连接关闭 */
|
||||
onClose(handler: (error?: Error) => void): void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP 协议消息类型
|
||||
// ============================================================================
|
||||
|
||||
/** 服务器能力 */
|
||||
export interface ServerCapabilities {
|
||||
tools?: {
|
||||
listChanged?: boolean;
|
||||
};
|
||||
resources?: {
|
||||
subscribe?: boolean;
|
||||
listChanged?: boolean;
|
||||
};
|
||||
prompts?: {
|
||||
listChanged?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/** 客户端能力 */
|
||||
export interface ClientCapabilities {
|
||||
roots?: {
|
||||
listChanged?: boolean;
|
||||
};
|
||||
sampling?: Record<string, never>;
|
||||
}
|
||||
|
||||
/** 初始化请求参数 */
|
||||
export interface InitializeParams {
|
||||
protocolVersion: string;
|
||||
capabilities: ClientCapabilities;
|
||||
clientInfo: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化响应结果 */
|
||||
export interface InitializeResult {
|
||||
protocolVersion: string;
|
||||
capabilities: ServerCapabilities;
|
||||
serverInfo: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
/** 工具列表响应 */
|
||||
export interface ListToolsResult {
|
||||
tools: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema: JSONSchema7;
|
||||
outputSchema?: JSONSchema7;
|
||||
}>;
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
/** 工具调用参数 */
|
||||
export interface CallToolParams {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** 工具调用响应 */
|
||||
export interface CallToolResult {
|
||||
content: MCPContent[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 事件类型
|
||||
// ============================================================================
|
||||
|
||||
/** MCP Manager 事件 */
|
||||
export interface MCPManagerEvents {
|
||||
'server:connected': (serverName: string) => void;
|
||||
'server:disconnected': (serverName: string, error?: Error) => void;
|
||||
'server:error': (serverName: string, error: Error) => void;
|
||||
'tools:changed': () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user