UNPKG

mcpdog

Version:

MCPDog - Universal MCP Server Manager with Web Interface

365 lines 13.8 kB
import axios from 'axios'; export class ProtocolDetector { timeout = 10000; // 10秒超时 userAgent = 'MCPDog-ProtocolDetector/2.0.1'; constructor(timeout) { if (timeout) { this.timeout = timeout; } } /** * 自动检测端点支持的最佳协议 */ async detectBestProtocol(endpoint, options) { console.error(`🔍 Starting protocol detection for: ${endpoint}`); // 并行测试所有协议 const testResults = await Promise.allSettled([ this.testStreamableHttp(endpoint, options), this.testHttpSse(endpoint, options), this.testBasicHttp(endpoint, options) ]); // 分析测试结果 const results = []; for (let i = 0; i < testResults.length; i++) { const result = testResults[i]; if (result.status === 'fulfilled') { results.push(result.value); } else { console.error(`Protocol test ${i} failed:`, result.reason); } } // 选择最佳协议 return this.selectBestProtocol(results, endpoint); } /** * 测试 Streamable HTTP 协议支持 */ async testStreamableHttp(endpoint, options) { const startTime = Date.now(); try { const response = await axios.post(endpoint, { jsonrpc: '2.0', id: 'protocol-test', method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'mcpdog-detector', version: '2.0.1' } } }, { timeout: options?.timeout || this.timeout, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'User-Agent': this.userAgent, ...(options?.headers || {}) }, validateStatus: (status) => status < 500 // 接受 4xx 错误,可能是认证问题 }); const responseTime = Date.now() - startTime; const features = []; // 检查响应特征 if (response.headers['content-type']?.includes('text/event-stream')) { features.push('SSE_STREAMING'); } if (response.headers['content-type']?.includes('application/json')) { features.push('JSON_RESPONSE'); } // 检查 MCP 协议版本 if (response.data && typeof response.data === 'string') { // SSE 格式响应 if (response.data.includes('event: message')) { features.push('SSE_EVENTS'); } if (response.data.includes('2025-03-26')) { features.push('MCP_2025_03_26'); } } else if (response.data && response.data.result) { // JSON 格式响应 if (response.data.result.protocolVersion) { features.push(`MCP_${response.data.result.protocolVersion.replace(/-/g, '_')}`); } } return { protocol: 'streamable-http', supported: response.status === 200, responseTime, features, error: response.status !== 200 ? `HTTP ${response.status}` : undefined }; } catch (error) { const responseTime = Date.now() - startTime; // 分析错误类型 let errorMessage = error.message; if (error.response) { errorMessage = `HTTP ${error.response.status}: ${error.response.statusText}`; } else if (error.code === 'ECONNREFUSED') { errorMessage = 'Connection refused - server not running'; } else if (error.code === 'ETIMEDOUT') { errorMessage = 'Connection timeout'; } return { protocol: 'streamable-http', supported: false, responseTime, features: [], error: errorMessage }; } } /** * 测试 HTTP+SSE 协议支持 */ async testHttpSse(endpoint, options) { const startTime = Date.now(); try { // 尝试访问 SSE 端点 const sseEndpoint = `${endpoint}/sse`; const response = await axios.get(sseEndpoint, { timeout: Math.min(options?.timeout || this.timeout, 5000), // SSE 连接用较短超时 headers: { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', 'User-Agent': this.userAgent, ...(options?.headers || {}) }, validateStatus: (status) => status < 500 }); const responseTime = Date.now() - startTime; const features = []; if (response.headers['content-type']?.includes('text/event-stream')) { features.push('SSE_ENDPOINT'); } // 检查是否有端点事件 if (response.data && typeof response.data === 'string') { if (response.data.includes('event: endpoint')) { features.push('DYNAMIC_ENDPOINTS'); } if (response.data.includes('sessionId')) { features.push('SESSION_SUPPORT'); } } // 尝试 POST 端点 try { const postResponse = await axios.post(`${endpoint}/mcp`, { jsonrpc: '2.0', id: 'test', method: 'initialize', params: { protocolVersion: '2024-11-05' } }, { timeout: 3000, headers: { 'Content-Type': 'application/json', 'User-Agent': this.userAgent }, validateStatus: () => true }); if (postResponse.status < 500) { features.push('HTTP_POST_ENDPOINT'); } } catch { // POST 端点测试失败,但不影响 SSE 检测 } return { protocol: 'http-sse', supported: response.status === 200 && features.includes('SSE_ENDPOINT'), responseTime, features, error: response.status !== 200 ? `HTTP ${response.status}` : undefined }; } catch (error) { const responseTime = Date.now() - startTime; return { protocol: 'http-sse', supported: false, responseTime, features: [], error: error.response ? `HTTP ${error.response.status}` : error.message }; } } /** * 测试基础 HTTP 支持(作为后备) */ async testBasicHttp(endpoint, options) { const startTime = Date.now(); try { const response = await axios.get(endpoint, { timeout: options?.timeout || this.timeout, headers: { 'User-Agent': this.userAgent, ...(options?.headers || {}) }, validateStatus: () => true }); const responseTime = Date.now() - startTime; const features = []; // 检查 HTTP 特征 if (response.headers['content-type']?.includes('application/json')) { features.push('JSON_SUPPORT'); } if (response.headers['server']) { features.push(`SERVER_${response.headers['server'].replace(/\s+/g, '_')}`); } // 检查是否返回 MCP 相关信息 if (response.data && typeof response.data === 'object') { if (response.data.jsonrpc) { features.push('JSONRPC_SUPPORT'); } if (response.data.result || response.data.error) { features.push('MCP_RESPONSE_FORMAT'); } } return { protocol: 'basic-http', supported: response.status < 400, responseTime, features, error: response.status >= 400 ? `HTTP ${response.status}` : undefined }; } catch (error) { const responseTime = Date.now() - startTime; return { protocol: 'basic-http', supported: false, responseTime, features: [], error: error.response ? `HTTP ${error.response.status}` : error.message }; } } /** * 选择最佳协议 */ selectBestProtocol(results, endpoint) { // 协议优先级权重 const protocolWeights = { 'streamable-http': 100, // 最新协议,最高优先级 'http-sse': 70, // 传统协议,中等优先级 'basic-http': 30 // 基础协议,最低优先级 }; let bestResult = { protocol: 'unknown', confidence: 0, reason: 'No protocols detected' }; for (const result of results) { if (!result.supported) { continue; } // 计算置信度 let confidence = protocolWeights[result.protocol] || 0; // 根据响应时间调整 (响应时间越快,置信度越高) if (result.responseTime < 1000) { confidence += 10; } else if (result.responseTime > 5000) { confidence -= 10; } // 根据功能特征调整 confidence += result.features.length * 5; // 特殊加分 if (result.features.includes('MCP_2025_03_26')) { confidence += 20; // 最新协议版本 } if (result.features.includes('SSE_STREAMING')) { confidence += 15; // 流式支持 } if (result.features.includes('SESSION_SUPPORT')) { confidence += 10; // 会话支持 } // 更新最佳结果 if (confidence > bestResult.confidence) { bestResult = { protocol: result.protocol, confidence: Math.min(confidence, 100), reason: this.generateReasonText(result), endpoint, features: result.features, error: result.error }; } } console.error(`🎯 Best protocol for ${endpoint}: ${bestResult.protocol} (confidence: ${bestResult.confidence}%)`); console.error(`📋 Reason: ${bestResult.reason}`); return bestResult; } /** * 生成检测原因说明 */ generateReasonText(result) { const reasons = []; if (result.protocol === 'streamable-http') { reasons.push('Latest MCP protocol (2025-03-26)'); if (result.features.includes('SSE_STREAMING')) { reasons.push('Supports streaming responses'); } if (result.features.includes('JSON_RESPONSE')) { reasons.push('JSON response format'); } } else if (result.protocol === 'http-sse') { reasons.push('Traditional MCP HTTP+SSE protocol'); if (result.features.includes('DYNAMIC_ENDPOINTS')) { reasons.push('Dynamic endpoint discovery'); } if (result.features.includes('SESSION_SUPPORT')) { reasons.push('Session management'); } } else { reasons.push('Basic HTTP endpoint'); } if (result.responseTime < 1000) { reasons.push(`Fast response (${result.responseTime}ms)`); } return reasons.join(', '); } /** * 为 stdio 协议生成配置建议 */ generateStdioSuggestion(command) { return { protocol: 'stdio', confidence: 95, reason: 'Command-based MCP server', features: ['PROCESS_COMMUNICATION', 'DIRECT_EXECUTION'] }; } /** * 批量检测多个端点 */ async detectMultipleEndpoints(endpoints) { const results = new Map(); console.error(`🔍 Batch protocol detection for ${endpoints.length} endpoints`); const detectionPromises = endpoints.map(async (endpoint) => { try { const result = await this.detectBestProtocol(endpoint); results.set(endpoint, result); } catch (error) { results.set(endpoint, { protocol: 'unknown', confidence: 0, reason: `Detection failed: ${error.message}`, error: error.message }); } }); await Promise.allSettled(detectionPromises); console.error(`✅ Batch detection completed: ${results.size} results`); return results; } } //# sourceMappingURL=protocol-detector.js.map