browser-devtools-mcp-server
Version:
MCP服务器,通过WebSocket连接浏览器插件获取DevTools信息,支持Cursor集成
582 lines (512 loc) • 16.9 kB
JavaScript
// 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);
}