import type { MCPServerConfig, ServerCapabilities, Tool, Resource, Prompt } from '../types'; import { v4 as uuidv4 } from 'uuid'; import { SSETransport } from './SSETransport'; /** * 纯前端 MCP 客户端服务 * 直接在浏览器中连接 MCP 服务器,无需后端中间层 */ export class MCPClientService { private clients = new Map(); private listeners = new Map void>>(); /** * 添加并连接到 MCP 服务器 */ async addServer(config: MCPServerConfig): Promise { try { console.log(`🔗 正在连接到 MCP 服务器: ${config.name} (${config.url})`); let client; if (config.type === 'http') { // HTTP 连接 client = await this.createHttpClient(config); } else if (config.type === 'sse') { // SSE 连接 client = await this.createSSEClient(config); } else { throw new Error(`不支持的连接类型: ${config.type}`); } // 获取服务器能力 const capabilities = await this.getServerCapabilities(client); this.clients.set(config.id, { client, config, capabilities }); console.log(`✅ 成功连接到 MCP 服务器: ${config.name}`); console.log('服务器能力:', capabilities); this.emit(config.id, 'connected', capabilities); return capabilities; } catch (error) { console.error(`❌ 连接 MCP 服务器失败: ${config.name}`); console.error('错误详情:', error); // 检查是否是 CORS 错误 if (error instanceof TypeError && error.message.includes('Failed to fetch')) { const corsError = new Error(`CORS 错误: 无法连接到 ${config.url}。请确保 MCP 服务器启用了 CORS 支持。`); this.emit(config.id, 'error', corsError); throw corsError; } this.emit(config.id, 'error', error); throw error; } } /** * 创建 HTTP 客户端 */ private async createHttpClient(config: MCPServerConfig) { // 将 0.0.0.0 替换为 localhost(浏览器无法访问 0.0.0.0) let baseUrl = config.url.replace(/\/$/, ''); baseUrl = baseUrl.replace('0.0.0.0', 'localhost').replace('127.0.0.1', 'localhost'); // 确保URL包含 /mcp 路径 if (!baseUrl.includes('/mcp')) { baseUrl = baseUrl + '/mcp'; } console.log(`🔄 HTTP原始URL: ${config.url}`); console.log(`🔄 HTTP转换后URL: ${baseUrl}`); // 先测试MCP端点是否可访问 try { console.log(`🔍 测试MCP端点可达性: ${baseUrl}`); const testResponse = await fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-' + Date.now(), method: 'ping' // 随便发一个方法测试连通性 }) }); console.log(`MCP端点响应状态: ${testResponse.status}`); // 如果完全无法连接,fetch会抛出错误 // 如果能连接但返回错误状态码,我们也认为连接有问题 if (!testResponse.ok && testResponse.status >= 500) { throw new Error(`服务器错误: HTTP ${testResponse.status}`); } } catch (error) { console.error(`❌ MCP端点连接失败:`, error); // 检查是否是网络错误 if (error instanceof TypeError) { throw new Error(`网络连接失败: 无法访问 ${baseUrl}。请检查服务器是否运行以及网络连接。`); } throw error; } return { type: 'http', baseUrl, async call(method: string, params: any) { const response = await fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: uuidv4(), method, params }) }); console.log(`MCP 请求 (${method}):`, response.status, response.statusText); if (!response.ok) { throw new Error(`请求失败: ${response.status} ${response.statusText}`); } const result = await response.json(); console.log(`MCP 响应 (${method}):`, result); if (result.error) { throw new Error(result.error.message || '请求错误'); } return result.result; } }; } /** * 创建SSE客户端 */ private async createSSEClient(config: MCPServerConfig): Promise { // 将 0.0.0.0 替换为 localhost(浏览器无法访问 0.0.0.0) let url = config.url; url = url.replace('0.0.0.0', 'localhost').replace('127.0.0.1', 'localhost'); console.log(`🔄 SSE 原始URL: ${config.url}`); console.log(`🔄 SSE 转换后URL: ${url}`); const transport = new SSETransport(url); // 连接SSE await transport.connect(); console.log(`✓ SSE 连接已建立: ${url}`); return { type: 'sse', transport, async call(method: string, params: any) { try { const result = await transport.sendRequest(method, params); return result; } catch (error) { console.error(`SSE 请求失败 (${method}):`, error); throw error; } }, async disconnect() { await transport.disconnect(); }, get connected() { return transport.isConnected; }, // 事件监听 on(event: string, callback: Function) { transport.on(event, callback); }, off(event: string, callback: Function) { transport.off(event, callback); } }; } /** * 获取服务器能力 */ private async getServerCapabilities(client: any): Promise { try { console.log('🔄 正在初始化MCP服务器...'); // 初始化请求 - 这是必须成功的 const initResult = await client.call('initialize', { protocolVersion: '2024-11-05', capabilities: { roots: { listChanged: true }, sampling: {} }, clientInfo: { name: 'MCP-Vue-Client', version: '1.0.0' } }); console.log('✅ MCP服务器初始化成功:', initResult); // 获取工具列表 let tools: Tool[] = []; try { const toolsResult = await client.call('tools/list', {}); tools = toolsResult.tools || []; console.log(`📋 发现 ${tools.length} 个工具`); } catch (error) { console.warn('获取工具列表失败:', error); } // 获取资源列表 let resources: Resource[] = []; try { const resourcesResult = await client.call('resources/list', {}); resources = resourcesResult.resources || []; console.log(`📁 发现 ${resources.length} 个资源`); } catch (error) { console.warn('获取资源列表失败:', error); } // 获取提示列表 let prompts: Prompt[] = []; try { const promptsResult = await client.call('prompts/list', {}); prompts = promptsResult.prompts || []; console.log(`💡 发现 ${prompts.length} 个提示`); } catch (error) { console.warn('获取提示列表失败:', error); } return { tools, resources, prompts }; } catch (error) { console.error('❌ MCP服务器初始化失败:', error); // 初始化失败应该抛出错误,而不是返回空能力 throw new Error(`MCP服务器初始化失败: ${error instanceof Error ? error.message : '未知错误'}`); } } /** * 调用工具 */ async callTool(serverId: string, toolName: string, parameters: Record): Promise { const serverInfo = this.clients.get(serverId); if (!serverInfo) { throw new Error(`服务器 ${serverId} 未连接`); } const { client } = serverInfo; try { console.log(`🔧 调用工具: ${toolName}`, parameters); const result = await client.call('tools/call', { name: toolName, arguments: parameters }); console.log(`✅ 工具调用成功: ${toolName}`, result); return result; } catch (error) { console.error(`❌ 工具调用失败: ${toolName}`, error); throw error; } } /** * 读取资源 */ async readResource(serverId: string, uri: string): Promise { const serverInfo = this.clients.get(serverId); if (!serverInfo) { throw new Error(`服务器 ${serverId} 未连接`); } const { client } = serverInfo; try { console.log(`📖 读取资源: ${uri}`); const result = await client.call('resources/read', { uri }); console.log(`✅ 资源读取成功: ${uri}`, result); return result; } catch (error) { console.error(`❌ 资源读取失败: ${uri}`, error); throw error; } } /** * 获取提示 */ async getPrompt(serverId: string, name: string, args?: Record): Promise { const serverInfo = this.clients.get(serverId); if (!serverInfo) { throw new Error(`服务器 ${serverId} 未连接`); } const { client } = serverInfo; try { console.log(`💭 获取提示: ${name}`, args); const result = await client.call('prompts/get', { name, arguments: args || {} }); console.log(`✅ 提示获取成功: ${name}`, result); return result; } catch (error) { console.error(`❌ 提示获取失败: ${name}`, error); throw error; } } /** * 断开服务器连接 */ async removeServer(serverId: string): Promise { const serverInfo = this.clients.get(serverId); if (serverInfo) { const { client } = serverInfo; try { if (client.type === 'sse' && client.disconnect) { await client.disconnect(); } } catch (error) { console.warn('关闭连接时出错:', error); } this.clients.delete(serverId); } this.listeners.delete(serverId); console.log(`🔌 服务器 ${serverId} 已断开连接`); } /** * 测试服务器连接 */ async testConnection(serverId: string): Promise { const serverInfo = this.clients.get(serverId); if (!serverInfo) { console.log(`❌ 服务器 ${serverId} 未找到客户端实例`); return false; } const { client, config } = serverInfo; try { if (client.type === 'sse') { return client.connected; } else if (client.type === 'http') { // HTTP 连接测试 - 发送真实的MCP初始化请求 console.log(`🔍 测试HTTP MCP连接: ${client.baseUrl}`); const response = await fetch(client.baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-' + Date.now(), method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'MCP-Test-Client', version: '1.0.0' } } }) }); if (!response.ok) { console.log(`❌ HTTP响应失败: ${response.status} ${response.statusText}`); return false; } const data = await response.json(); if (data.error) { console.log(`❌ MCP协议错误:`, data.error); return false; } console.log(`✅ MCP连接测试成功`); return true; } return false; } catch (error) { console.log(`❌ 连接测试异常:`, error); return false; } } /** * 获取所有连接的服务器 */ getConnectedServers(): string[] { return Array.from(this.clients.keys()); } /** * 获取服务器信息 */ getServerInfo(serverId: string) { return this.clients.get(serverId); } /** * 事件监听 */ on(serverId: string, callback: (event: string, data: any) => void): void { if (!this.listeners.has(serverId)) { this.listeners.set(serverId, []); } this.listeners.get(serverId)!.push(callback); } /** * 移除事件监听 */ off(serverId: string, callback?: (event: string, data: any) => void): void { if (!callback) { this.listeners.delete(serverId); } else { const callbacks = this.listeners.get(serverId) || []; const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } } /** * 触发事件 */ private emit(serverId: string, event: string, data: any): void { const callbacks = this.listeners.get(serverId) || []; callbacks.forEach(callback => { try { callback(event, data); } catch (error) { console.error('事件回调执行失败:', error); } }); } /** * 清理所有连接 */ async cleanup(): Promise { const serverIds = Array.from(this.clients.keys()); await Promise.all(serverIds.map(id => this.removeServer(id))); console.log('🧹 所有连接已清理'); } } // 单例导出 export const mcpClientService = new MCPClientService(); // 在页面卸载时清理连接 if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { mcpClientService.cleanup(); }); }