UNPKG

browser-devtools-mcp-server

Version:

MCP服务器,通过WebSocket连接浏览器插件获取DevTools信息,支持Cursor集成

582 lines (512 loc) 16.9 kB
#!/usr/bin/env node // MCP服务器 - 集成WebSocket和MCP协议 // 为Cursor提供浏览器DevTools信息的桥接服务 import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { WebSocketServer } from 'ws'; import { v4 as uuidv4 } from 'uuid'; /** * WebSocket管理器 * 负责管理与浏览器插件的WebSocket连接 */ class WebSocketManager { constructor() { this.wss = null; this.clients = new Map(); // 存储连接的浏览器插件 this.pendingRequests = new Map(); // 存储待处理的请求 this.port = process.env.WS_PORT || 8080; this.init(); } init() { // 创建WebSocket服务器 this.wss = new WebSocketServer({ port: this.port, host: '0.0.0.0' }); console.log(`WebSocket服务器启动在端口 ${this.port}`); this.wss.on('connection', (ws, req) => { const clientId = uuidv4(); console.log(`新的浏览器插件连接: ${clientId}`); // 存储客户端连接 this.clients.set(clientId, { ws: ws, id: clientId, connected: true, lastPing: Date.now(), extensionId: null, config: null }); // 处理消息 ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleClientMessage(clientId, message); } catch (error) { console.error('解析客户端消息失败:', error); } }); // 处理连接关闭 ws.on('close', () => { console.log(`浏览器插件断开连接: ${clientId}`); this.clients.delete(clientId); }); // 处理错误 ws.on('error', (error) => { console.error(`WebSocket错误 (${clientId}):`, error); this.clients.delete(clientId); }); // 发送欢迎消息 this.sendToClient(clientId, { type: 'welcome', data: { serverId: 'mcp-devtools-server', timestamp: Date.now() } }); }); // 定期清理断开的连接 setInterval(() => { this.cleanupConnections(); }, 30000); } handleClientMessage(clientId, message) { const client = this.clients.get(clientId); if (!client) return; console.log(`收到客户端消息 (${clientId}):`, message.type); switch (message.type) { case 'register': // 浏览器插件注册 client.extensionId = message.data.extensionId; client.config = message.data.config; console.log(`插件注册成功: ${client.extensionId}`); break; case 'response': // 处理插件响应 this.handlePluginResponse(message); break; case 'ping': // 心跳响应 client.lastPing = Date.now(); this.sendToClient(clientId, { type: 'pong' }); break; default: console.warn(`未知的客户端消息类型: ${message.type}`); } } handlePluginResponse(message) { const { requestId, data, tabInfo } = message; if (this.pendingRequests.has(requestId)) { const { resolve } = this.pendingRequests.get(requestId); this.pendingRequests.delete(requestId); // 解析响应并返回给MCP客户端 resolve({ success: true, data: data, tabInfo: tabInfo, timestamp: Date.now() }); } } sendToClient(clientId, message) { const client = this.clients.get(clientId); if (client && client.ws.readyState === 1) { // WebSocket.OPEN client.ws.send(JSON.stringify(message)); return true; } return false; } broadcast(message) { let sentCount = 0; this.clients.forEach((client, clientId) => { if (this.sendToClient(clientId, message)) { sentCount++; } }); return sentCount; } async requestFromBrowser(type, data = {}, timeout = 10000) { const requestId = uuidv4(); // 广播请求到所有连接的浏览器插件 const sentCount = this.broadcast({ type: type, requestId: requestId, data: data, timestamp: Date.now() }); if (sentCount === 0) { throw new Error('没有可用的浏览器插件连接'); } // 创建Promise等待响应 return new Promise((resolve, reject) => { this.pendingRequests.set(requestId, { resolve, reject }); // 设置超时 setTimeout(() => { if (this.pendingRequests.has(requestId)) { this.pendingRequests.delete(requestId); reject(new Error('请求超时')); } }, timeout); }); } cleanupConnections() { const now = Date.now(); const timeout = 60000; // 60秒超时 this.clients.forEach((client, clientId) => { if (now - client.lastPing > timeout) { console.log(`清理超时连接: ${clientId}`); client.ws.terminate(); this.clients.delete(clientId); } }); } getStatus() { return { port: this.port, connectedClients: this.clients.size, pendingRequests: this.pendingRequests.size, clients: Array.from(this.clients.values()).map(client => ({ id: client.id, extensionId: client.extensionId, connected: client.connected, lastPing: client.lastPing })) }; } } /** * MCP服务器类 * 实现MCP协议,为Cursor等客户端提供浏览器DevTools信息 */ class DevToolsMCPServer { constructor() { this.server = new Server( { name: 'browser-devtools-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.wsManager = new WebSocketManager(); this.setupToolHandlers(); this.setupErrorHandling(); } setupToolHandlers() { // 注册工具列表 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_console_logs', description: '获取浏览器控制台日志信息,包括错误、警告和普通日志', inputSchema: { type: 'object', properties: { level: { type: 'string', enum: ['all', 'error', 'warn', 'info', 'log'], description: '日志级别过滤器', default: 'all' }, limit: { type: 'number', description: '返回的日志条数限制', default: 100, minimum: 1, maximum: 1000 } } } }, { name: 'get_network_requests', description: '获取浏览器网络请求信息,包括API调用、资源加载等', inputSchema: { type: 'object', properties: { filter: { type: 'string', enum: ['all', 'xhr', 'fetch', 'failed'], description: '网络请求过滤器', default: 'all' }, limit: { type: 'number', description: '返回的请求条数限制', default: 50, minimum: 1, maximum: 500 } } } }, { name: 'get_page_info', description: '获取当前页面的详细信息,包括性能数据、框架检测等', inputSchema: { type: 'object', properties: {} } }, { name: 'execute_script', description: '在浏览器页面中执行JavaScript代码', inputSchema: { type: 'object', properties: { code: { type: 'string', description: '要执行的JavaScript代码' } }, required: ['code'] } }, { name: 'get_server_status', description: '获取MCP服务器和WebSocket连接状态', inputSchema: { type: 'object', properties: {} } } ] }; }); // 处理工具调用 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_console_logs': return await this.getConsoleLogs(args); case 'get_network_requests': return await this.getNetworkRequests(args); case 'get_page_info': return await this.getPageInfo(args); case 'execute_script': return await this.executeScript(args); case 'get_server_status': return await this.getServerStatus(); default: throw new McpError( ErrorCode.MethodNotFound, `未知的工具: ${name}` ); } } catch (error) { console.error(`工具执行错误 (${name}):`, error); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `工具执行失败: ${error.message}` ); } }); } async getConsoleLogs(args = {}) { const { level = 'all', limit = 100 } = args; try { const response = await this.wsManager.requestFromBrowser('getConsoleData', { level, limit }); return { content: [ { type: 'text', text: `控制台日志信息:\n\n` + `总计: ${response.data.summary?.total || 0} 条日志\n` + `错误: ${response.data.summary?.errors || 0} 条\n` + `警告: ${response.data.summary?.warnings || 0} 条\n` + `信息: ${response.data.summary?.info || 0} 条\n\n` + `最近的日志:\n` + (response.data.logs || []) .slice(-limit) .map(log => { const time = new Date(log.timestamp).toLocaleTimeString(); return `[${time}] ${log.level.toUpperCase()}: ${log.message}`; }) .join('\n') } ] }; } catch (error) { throw new McpError( ErrorCode.InternalError, `获取控制台日志失败: ${error.message}` ); } } async getNetworkRequests(args = {}) { const { filter = 'all', limit = 50 } = args; try { const response = await this.wsManager.requestFromBrowser('getNetworkData', { filter, limit }); return { content: [ { type: 'text', text: `网络请求信息:\n\n` + `总计: ${response.data.summary?.total || 0} 个请求\n` + `失败: ${response.data.summary?.failed || 0} 个\n` + `待处理: ${response.data.summary?.pending || 0} 个\n\n` + `最近的请求:\n` + (response.data.requests || []) .slice(-limit) .map(req => { const time = new Date(req.timestamp).toLocaleTimeString(); const status = req.status || 'pending'; const duration = req.duration ? `${req.duration}ms` : '-'; return `[${time}] ${req.method} ${req.url} - ${status} (${duration})`; }) .join('\n') } ] }; } catch (error) { throw new McpError( ErrorCode.InternalError, `获取网络请求失败: ${error.message}` ); } } async getPageInfo(args = {}) { try { const response = await this.wsManager.requestFromBrowser('getPageInfo'); const pageData = response.data; let infoText = `页面信息:\n\n`; if (pageData.url) { infoText += `URL: ${pageData.url}\n`; } if (pageData.title) { infoText += `标题: ${pageData.title}\n`; } if (pageData.performance) { infoText += `\n性能信息:\n`; const perf = pageData.performance; if (perf.navigation) { infoText += `- 首字节时间: ${perf.navigation.firstByte || '-'}ms\n`; infoText += `- DOM交互时间: ${perf.navigation.domInteractive || '-'}ms\n`; infoText += `- 加载完成时间: ${perf.navigation.loadComplete || '-'}ms\n`; } if (perf.memory) { infoText += `- 内存使用: ${Math.round(perf.memory.used / 1024 / 1024)}MB\n`; } } if (pageData.frameworks && pageData.frameworks.length > 0) { infoText += `\n检测到的框架:\n`; pageData.frameworks.forEach(fw => { infoText += `- ${fw.name}: ${fw.version}\n`; }); } if (pageData.resources) { infoText += `\n资源统计:\n`; infoText += `- 总资源数: ${pageData.resources.total}\n`; infoText += `- 失败资源: ${pageData.resources.failed}\n`; if (pageData.resources.byType) { Object.entries(pageData.resources.byType).forEach(([type, count]) => { infoText += `- ${type}: ${count}\n`; }); } } return { content: [ { type: 'text', text: infoText } ] }; } catch (error) { throw new McpError( ErrorCode.InternalError, `获取页面信息失败: ${error.message}` ); } } async executeScript(args) { const { code } = args; if (!code || typeof code !== 'string') { throw new McpError( ErrorCode.InvalidParams, '必须提供有效的JavaScript代码' ); } try { const response = await this.wsManager.requestFromBrowser('executeScript', { code }); return { content: [ { type: 'text', text: response.data.success ? `脚本执行成功:\n${JSON.stringify(response.data.result, null, 2)}` : `脚本执行失败: ${response.data.error}` } ] }; } catch (error) { throw new McpError( ErrorCode.InternalError, `脚本执行失败: ${error.message}` ); } } async getServerStatus() { const status = this.wsManager.getStatus(); return { content: [ { type: 'text', text: `MCP服务器状态:\n\n` + `WebSocket端口: ${status.port}\n` + `连接的浏览器插件: ${status.connectedClients}\n` + `待处理请求: ${status.pendingRequests}\n\n` + `客户端详情:\n` + status.clients.map(client => `- ID: ${client.id}\n` + ` 插件ID: ${client.extensionId || '未知'}\n` + ` 最后心跳: ${new Date(client.lastPing).toLocaleTimeString()}` ).join('\n') } ] }; } setupErrorHandling() { this.server.onerror = (error) => { console.error('MCP服务器错误:', error); }; process.on('SIGINT', async () => { console.log('\n正在关闭服务器...'); await this.server.close(); process.exit(0); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('MCP服务器已启动,等待客户端连接...'); } } // 导出类供CLI使用 export { DevToolsMCPServer }; // 如果直接运行此文件,则启动服务器 if (import.meta.url === `file://${process.argv[1]}`) { const server = new DevToolsMCPServer(); server.run().catch(console.error); }