Files
map-client-vue/web/src/services/MCPClientService.ts
2025-10-14 21:52:11 +08:00

496 lines
14 KiB
TypeScript
Raw 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.

import type { MCPServerConfig } from '../types';
import type { ServerCapabilities, Tool, Resource, Prompt } from '../types/index';
import { v4 as uuidv4 } from 'uuid';
import { SSETransport } from './SSETransport';
/**
* 纯前端 MCP 客户端服务
* 直接在浏览器中连接 MCP 服务器,无需后端中间层
*/
export class MCPClientService {
private clients = new Map<string, any>();
private listeners = new Map<string, Array<(event: string, data: any) => void>>();
/**
* 添加并连接到 MCP 服务器
*/
async addServer(config: MCPServerConfig): Promise<ServerCapabilities> {
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<any> {
// 将 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<ServerCapabilities> {
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<string, any>): Promise<any> {
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<any> {
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;
}
}
/**
* 获取服务器的工具列表
*/
getTools(serverId: string): Tool[] {
const serverInfo = this.clients.get(serverId);
if (!serverInfo) {
console.warn(`服务器 ${serverId} 未连接`);
return [];
}
return serverInfo.capabilities?.tools || [];
}
/**
* 获取提示
*/
async getPrompt(serverId: string, name: string, args?: Record<string, any>): Promise<any> {
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<void> {
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<boolean> {
const serverInfo = this.clients.get(serverId);
if (!serverInfo) {
console.log(`❌ 服务器 ${serverId} 未找到客户端实例`);
return false;
}
const { client } = 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<void> {
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();
});
}