first commit

This commit is contained in:
douboer
2025-10-14 14:18:20 +08:00
commit d93bc02772
66 changed files with 21393 additions and 0 deletions

View File

@@ -0,0 +1,483 @@
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<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;
}
}
/**
* 获取提示
*/
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, 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<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();
});
}