browser-console-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,718 @@
1
+ /**
2
+ * Browser MCP Server
3
+ *
4
+ * 在浏览器中运行的MCP服务器,提供页面内容访问和JavaScript执行能力给Cursor
5
+ */
6
+
7
+ (() => {
8
+ // MCP服务器版本
9
+ const VERSION = "1.0.0";
10
+
11
+ // MCP协议常量
12
+ const MCP_VERSION = "2025-03-26";
13
+ const RPC_VERSION = "2.0";
14
+
15
+ // 工具定义
16
+ const TOOLS = {
17
+ // 执行JavaScript代码
18
+ executeJS: {
19
+ name: "executeJS",
20
+ description: "在当前页面上下文执行JavaScript代码",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ code: {
25
+ type: "string",
26
+ description: "要执行的JavaScript代码",
27
+ },
28
+ },
29
+ required: ["code"],
30
+ },
31
+ annotations: {
32
+ title: "执行JavaScript",
33
+ readOnlyHint: false,
34
+ destructiveHint: true,
35
+ idempotentHint: false,
36
+ openWorldHint: true,
37
+ },
38
+ handler: async (params) => {
39
+ try {
40
+ // 使用Function构造函数创建函数,然后执行
41
+ // 这允许代码访问window对象和DOM
42
+ const result = new Function(params.code || "return null;")();
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: `操作成功: ${JSON.stringify(result)}`,
48
+ },
49
+ ],
50
+ };
51
+ } catch (error) {
52
+ return {
53
+ isError: true,
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: `错误: ${error.message}`,
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ },
63
+ },
64
+
65
+ // 获取页面HTML
66
+ getPageHTML: {
67
+ name: "getPageHTML",
68
+ description: "获取当前页面的HTML内容",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {},
72
+ required: [],
73
+ },
74
+ annotations: {
75
+ title: "获取页面HTML",
76
+ readOnlyHint: true,
77
+ openWorldHint: false,
78
+ },
79
+ handler: async (params) => {
80
+ try {
81
+ return {
82
+ html: document.documentElement.outerHTML,
83
+ };
84
+ } catch (error) {
85
+ throw new Error(`获取HTML出错: ${error.message}`);
86
+ }
87
+ },
88
+ },
89
+
90
+ // 获取页面标题
91
+ getPageTitle: {
92
+ name: "getPageTitle",
93
+ description: "获取当前页面的标题",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {},
97
+ required: [],
98
+ },
99
+ annotations: {
100
+ title: "获取页面标题",
101
+ readOnlyHint: true,
102
+ openWorldHint: false,
103
+ },
104
+ handler: async () => {
105
+ try {
106
+ return {
107
+ title: document.title,
108
+ };
109
+ } catch (error) {
110
+ throw new Error(`获取标题出错: ${error.message}`);
111
+ }
112
+ },
113
+ },
114
+
115
+ // 获取元素
116
+ getElements: {
117
+ name: "getElements",
118
+ description: "使用CSS选择器获取页面上的元素",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ selector: {
123
+ type: "string",
124
+ description: "CSS选择器",
125
+ },
126
+ },
127
+ required: ["selector"],
128
+ },
129
+ annotations: {
130
+ title: "获取元素",
131
+ readOnlyHint: true,
132
+ openWorldHint: false,
133
+ },
134
+ handler: async (params) => {
135
+ try {
136
+ const elements = [...document.querySelectorAll(params.selector)];
137
+ return {
138
+ elements: elements.map((el) => ({
139
+ tagName: el.tagName,
140
+ id: el.id,
141
+ className: el.className,
142
+ textContent: el.textContent.trim().substring(0, 500),
143
+ attributes: [...el.attributes].reduce((attrs, attr) => {
144
+ attrs[attr.name] = attr.value;
145
+ return attrs;
146
+ }, {}),
147
+ })),
148
+ };
149
+ } catch (error) {
150
+ throw new Error(`获取元素出错: ${error.message}`);
151
+ }
152
+ },
153
+ },
154
+
155
+ // 截取页面截图
156
+ captureScreenshot: {
157
+ name: "captureScreenshot",
158
+ description: "捕获当前页面的截图(使用html2canvas)",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ selector: {
163
+ type: "string",
164
+ description: "可选的CSS选择器,用于捕获特定元素",
165
+ default: "body",
166
+ },
167
+ },
168
+ required: [],
169
+ },
170
+ annotations: {
171
+ title: "截取页面截图",
172
+ readOnlyHint: true,
173
+ openWorldHint: false,
174
+ },
175
+ handler: async (params) => {
176
+ try {
177
+ // 检查html2canvas是否可用
178
+ if (typeof html2canvas === "undefined") {
179
+ throw new Error("html2canvas未加载,无法截图");
180
+ }
181
+
182
+ const selector = params.selector || "body";
183
+ const element = document.querySelector(selector);
184
+
185
+ if (!element) {
186
+ throw new Error(`找不到元素: ${selector}`);
187
+ }
188
+
189
+ const canvas = await html2canvas(element);
190
+ const dataUrl = canvas.toDataURL("image/png");
191
+
192
+ return {
193
+ imageDataUrl: dataUrl,
194
+ };
195
+ } catch (error) {
196
+ throw new Error(`截图出错: ${error.message}`);
197
+ }
198
+ },
199
+ },
200
+
201
+ // 获取页面URL
202
+ getPageURL: {
203
+ name: "getPageURL",
204
+ description: "获取当前页面的URL",
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {},
208
+ required: [],
209
+ },
210
+ annotations: {
211
+ title: "获取页面URL",
212
+ readOnlyHint: true,
213
+ openWorldHint: false,
214
+ },
215
+ handler: async () => {
216
+ try {
217
+ return {
218
+ url: window.location.href,
219
+ };
220
+ } catch (error) {
221
+ throw new Error(`获取URL出错: ${error.message}`);
222
+ }
223
+ },
224
+ },
225
+
226
+ // 点击元素
227
+ clickElement: {
228
+ name: "clickElement",
229
+ description: "点击页面上的元素",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ selector: {
234
+ type: "string",
235
+ description: "要点击元素的CSS选择器",
236
+ },
237
+ },
238
+ required: ["selector"],
239
+ },
240
+ annotations: {
241
+ title: "点击元素",
242
+ readOnlyHint: false,
243
+ destructiveHint: true,
244
+ idempotentHint: false,
245
+ openWorldHint: false,
246
+ },
247
+ handler: async (params) => {
248
+ try {
249
+ const element = document.querySelector(params.selector);
250
+
251
+ if (!element) {
252
+ throw new Error(`找不到元素: ${params.selector}`);
253
+ }
254
+
255
+ element.click();
256
+
257
+ return {
258
+ success: true,
259
+ message: `成功点击元素: ${params.selector}`,
260
+ };
261
+ } catch (error) {
262
+ throw new Error(`点击元素出错: ${error.message}`);
263
+ }
264
+ },
265
+ },
266
+
267
+ // 输入文本
268
+ inputText: {
269
+ name: "inputText",
270
+ description: "向输入框填入文本",
271
+ inputSchema: {
272
+ type: "object",
273
+ properties: {
274
+ selector: {
275
+ type: "string",
276
+ description: "输入框的CSS选择器",
277
+ },
278
+ text: {
279
+ type: "string",
280
+ description: "要输入的文本",
281
+ },
282
+ },
283
+ required: ["selector", "text"],
284
+ },
285
+ annotations: {
286
+ title: "输入文本",
287
+ readOnlyHint: false,
288
+ destructiveHint: true,
289
+ idempotentHint: false,
290
+ openWorldHint: false,
291
+ },
292
+ handler: async (params) => {
293
+ try {
294
+ const input = document.querySelector(params.selector);
295
+
296
+ if (!input) {
297
+ throw new Error(`找不到输入框: ${params.selector}`);
298
+ }
299
+
300
+ if (input.tagName !== "INPUT" && input.tagName !== "TEXTAREA") {
301
+ throw new Error(`选择的元素不是输入框: ${input.tagName}`);
302
+ }
303
+
304
+ input.value = params.text;
305
+
306
+ // 触发input事件以通知表单变化
307
+ input.dispatchEvent(new Event("input", { bubbles: true }));
308
+
309
+ return {
310
+ success: true,
311
+ message: `成功输入文本到: ${params.selector}`,
312
+ };
313
+ } catch (error) {
314
+ throw new Error(`输入文本出错: ${error.message}`);
315
+ }
316
+ },
317
+ },
318
+ };
319
+
320
+ // MCP服务器类
321
+ class BrowserMCPServer {
322
+ constructor() {
323
+ this.socket = null;
324
+ this.connected = false;
325
+ this.nextId = 1;
326
+ this.pendingRequests = new Map();
327
+
328
+ // 初始化并绑定方法
329
+ this.initSocket = this.initSocket.bind(this);
330
+ this.handleMessage = this.handleMessage.bind(this);
331
+ this.sendResponse = this.sendResponse.bind(this);
332
+ this.handleRequest = this.handleRequest.bind(this);
333
+ }
334
+
335
+ /**
336
+ * 初始化WebSocket连接
337
+ * @param {string} url WebSocket服务器URL
338
+ */
339
+ initSocket(url) {
340
+ try {
341
+ // 关闭现有连接
342
+ if (this.socket) {
343
+ this.socket.close();
344
+ }
345
+
346
+ this.socket = new WebSocket(url);
347
+
348
+ this.socket.onopen = () => {
349
+ console.log("[Browser MCP] 已连接到Cursor");
350
+ this.connected = true;
351
+
352
+ // 发送初始化和capability协商消息
353
+ this.sendInitialization();
354
+ };
355
+
356
+ this.socket.onmessage = (event) => {
357
+ try {
358
+ const message = JSON.parse(event.data);
359
+ this.handleMessage(message);
360
+ } catch (error) {
361
+ console.error("[Browser MCP] 消息解析错误:", error);
362
+ }
363
+ };
364
+
365
+ this.socket.onclose = () => {
366
+ console.log("[Browser MCP] 连接已关闭");
367
+ this.connected = false;
368
+ };
369
+
370
+ this.socket.onerror = (error) => {
371
+ console.error("[Browser MCP] WebSocket错误:", error);
372
+ };
373
+
374
+ return true;
375
+ } catch (error) {
376
+ console.error("[Browser MCP] 初始化WebSocket出错:", error);
377
+ return false;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * 发送MCP初始化和能力协商消息
383
+ */
384
+ sendInitialization() {
385
+ // 发送初始化消息
386
+ const message = {
387
+ jsonrpc: RPC_VERSION,
388
+ method: "initialize",
389
+ id: this.nextId++,
390
+ params: {
391
+ protocol_version: MCP_VERSION,
392
+ name: "Browser MCP Server",
393
+ version: VERSION,
394
+ vendor: "Browser Console MCP",
395
+ offerings: {
396
+ tools: Object.keys(TOOLS).map((key) => {
397
+ const tool = TOOLS[key];
398
+ return {
399
+ name: tool.name,
400
+ description: tool.description,
401
+ inputSchema: tool.inputSchema,
402
+ annotations: tool.annotations,
403
+ };
404
+ }),
405
+ },
406
+ },
407
+ };
408
+
409
+ // 发送消息
410
+ this.socket.send(JSON.stringify(message));
411
+
412
+ // 记录日志(避免直接发送中文日志)
413
+ console.log("Initialization message sent");
414
+ }
415
+
416
+ /**
417
+ * 处理传入的MCP消息
418
+ * @param {Object} message 消息对象
419
+ */
420
+ handleMessage(message) {
421
+ try {
422
+ // 如果消息是字符串,则解析为JSON
423
+ const jsonMessage =
424
+ typeof message === "string" ? JSON.parse(message) : message;
425
+
426
+ // 处理特定类型的请求
427
+ if (jsonMessage.type === "execute_js" && jsonMessage.code) {
428
+ // 执行JavaScript代码
429
+ try {
430
+ const result = new Function(jsonMessage.code || "return null;")();
431
+ this.sendResponse(jsonMessage.requestId, { result });
432
+ } catch (error) {
433
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
434
+ }
435
+ return;
436
+ }
437
+
438
+ if (jsonMessage.type === "get_page_html") {
439
+ // 获取页面HTML
440
+ try {
441
+ const html = document.documentElement.outerHTML;
442
+ this.sendResponse(jsonMessage.requestId, { html });
443
+ } catch (error) {
444
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
445
+ }
446
+ return;
447
+ }
448
+
449
+ if (jsonMessage.type === "get_page_title") {
450
+ // 获取页面标题
451
+ try {
452
+ const title = document.title;
453
+ this.sendResponse(jsonMessage.requestId, { title });
454
+ } catch (error) {
455
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
456
+ }
457
+ return;
458
+ }
459
+
460
+ if (jsonMessage.type === "get_elements" && jsonMessage.selector) {
461
+ // 获取元素
462
+ try {
463
+ const elements = [
464
+ ...document.querySelectorAll(jsonMessage.selector),
465
+ ];
466
+ const result = elements.map((el) => ({
467
+ tagName: el.tagName,
468
+ id: el.id,
469
+ className: el.className,
470
+ textContent: el.textContent.trim().substring(0, 500),
471
+ attributes: [...el.attributes].reduce((attrs, attr) => {
472
+ attrs[attr.name] = attr.value;
473
+ return attrs;
474
+ }, {}),
475
+ }));
476
+ this.sendResponse(jsonMessage.requestId, { elements: result });
477
+ } catch (error) {
478
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
479
+ }
480
+ return;
481
+ }
482
+
483
+ if (jsonMessage.type === "capture_screenshot") {
484
+ // 截取页面截图
485
+ try {
486
+ if (typeof html2canvas === "undefined") {
487
+ throw new Error("html2canvas未加载,无法截图");
488
+ }
489
+
490
+ const selector = jsonMessage.selector || "body";
491
+ const element = document.querySelector(selector);
492
+
493
+ if (!element) {
494
+ throw new Error(`找不到元素: ${selector}`);
495
+ }
496
+
497
+ html2canvas(element)
498
+ .then((canvas) => {
499
+ const dataUrl = canvas.toDataURL("image/png");
500
+ this.sendResponse(jsonMessage.requestId, {
501
+ imageDataUrl: dataUrl,
502
+ });
503
+ })
504
+ .catch((error) => {
505
+ this.sendResponse(jsonMessage.requestId, {
506
+ error: error.message,
507
+ });
508
+ });
509
+ } catch (error) {
510
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
511
+ }
512
+ return;
513
+ }
514
+
515
+ if (jsonMessage.type === "get_page_url") {
516
+ // 获取页面URL
517
+ try {
518
+ const url = window.location.href;
519
+ this.sendResponse(jsonMessage.requestId, { url });
520
+ } catch (error) {
521
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
522
+ }
523
+ return;
524
+ }
525
+
526
+ if (jsonMessage.type === "click_element" && jsonMessage.selector) {
527
+ // 点击元素
528
+ try {
529
+ const element = document.querySelector(jsonMessage.selector);
530
+
531
+ if (!element) {
532
+ throw new Error(`找不到元素: ${jsonMessage.selector}`);
533
+ }
534
+
535
+ element.click();
536
+ this.sendResponse(jsonMessage.requestId, {
537
+ success: true,
538
+ message: `成功点击元素: ${jsonMessage.selector}`,
539
+ });
540
+ } catch (error) {
541
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
542
+ }
543
+ return;
544
+ }
545
+
546
+ if (
547
+ jsonMessage.type === "input_text" &&
548
+ jsonMessage.selector &&
549
+ jsonMessage.text !== undefined
550
+ ) {
551
+ // 输入文本
552
+ try {
553
+ const input = document.querySelector(jsonMessage.selector);
554
+
555
+ if (!input) {
556
+ throw new Error(`找不到输入框: ${jsonMessage.selector}`);
557
+ }
558
+
559
+ if (input.tagName !== "INPUT" && input.tagName !== "TEXTAREA") {
560
+ throw new Error(`选择的元素不是输入框: ${input.tagName}`);
561
+ }
562
+
563
+ input.value = jsonMessage.text;
564
+ input.dispatchEvent(new Event("input", { bubbles: true }));
565
+ this.sendResponse(jsonMessage.requestId, {
566
+ success: true,
567
+ message: `成功输入文本到: ${jsonMessage.selector}`,
568
+ });
569
+ } catch (error) {
570
+ this.sendResponse(jsonMessage.requestId, { error: error.message });
571
+ }
572
+ return;
573
+ }
574
+
575
+ // 处理RPC请求
576
+ if (jsonMessage.jsonrpc === RPC_VERSION) {
577
+ if (jsonMessage.method) {
578
+ // 这是一个请求
579
+ this.handleRequest(jsonMessage);
580
+ } else if (
581
+ jsonMessage.result !== undefined ||
582
+ jsonMessage.error !== undefined
583
+ ) {
584
+ // 这是一个响应,目前不处理
585
+ console.log("收到响应:", jsonMessage);
586
+ }
587
+ }
588
+ } catch (error) {
589
+ console.error("消息解析错误:", error.message);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * 处理RPC请求
595
+ * @param {Object} request 请求对象
596
+ */
597
+ async handleRequest(request) {
598
+ const { id, method, params } = request;
599
+
600
+ // 处理初始化响应
601
+ if (method === "initialize/result") {
602
+ console.log("[Browser MCP] 初始化成功");
603
+ this.sendResponse(id, { success: true });
604
+ return;
605
+ }
606
+
607
+ // 处理工具调用
608
+ if (method === "executeToolCall") {
609
+ const { toolCall } = params;
610
+
611
+ if (!toolCall || !toolCall.name) {
612
+ this.sendErrorResponse(id, -32602, "无效的工具调用参数");
613
+ return;
614
+ }
615
+
616
+ const tool = Object.values(TOOLS).find((t) => t.name === toolCall.name);
617
+
618
+ if (!tool) {
619
+ this.sendErrorResponse(id, -32601, `找不到工具: ${toolCall.name}`);
620
+ return;
621
+ }
622
+
623
+ try {
624
+ const result = await tool.handler(toolCall.parameters || {});
625
+ this.sendResponse(id, { result });
626
+ } catch (error) {
627
+ this.sendErrorResponse(id, -32603, error.message);
628
+ }
629
+
630
+ return;
631
+ }
632
+
633
+ // 处理未知方法
634
+ this.sendErrorResponse(id, -32601, `方法不存在: ${method}`);
635
+ }
636
+
637
+ /**
638
+ * 发送响应
639
+ * @param {number} id 请求ID
640
+ * @param {Object} result 响应结果
641
+ */
642
+ sendResponse(id, result) {
643
+ if (!this.connected || !this.socket) {
644
+ console.error("[Browser MCP] 未连接,无法发送响应");
645
+ return;
646
+ }
647
+
648
+ const response = {
649
+ jsonrpc: RPC_VERSION,
650
+ id,
651
+ result,
652
+ };
653
+
654
+ this.socket.send(JSON.stringify(response));
655
+ }
656
+
657
+ /**
658
+ * 发送错误响应
659
+ * @param {number} id 请求ID
660
+ * @param {number} code 错误代码
661
+ * @param {string} message 错误消息
662
+ */
663
+ sendErrorResponse(id, code, message) {
664
+ if (!this.connected || !this.socket) {
665
+ console.error("[Browser MCP] 未连接,无法发送错误响应");
666
+ return;
667
+ }
668
+
669
+ const response = {
670
+ jsonrpc: RPC_VERSION,
671
+ id,
672
+ error: {
673
+ code,
674
+ message,
675
+ },
676
+ };
677
+
678
+ this.socket.send(JSON.stringify(response));
679
+ }
680
+
681
+ /**
682
+ * 发送请求
683
+ * @param {string} method 方法名
684
+ * @param {Object} params 参数
685
+ * @returns {Promise} 响应承诺
686
+ */
687
+ sendRequest(method, params) {
688
+ return new Promise((resolve, reject) => {
689
+ if (!this.connected || !this.socket) {
690
+ reject(new Error("未连接"));
691
+ return;
692
+ }
693
+
694
+ const id = this.nextId++;
695
+ const request = {
696
+ jsonrpc: RPC_VERSION,
697
+ id,
698
+ method,
699
+ params,
700
+ };
701
+
702
+ this.pendingRequests.set(id, { resolve, reject });
703
+ });
704
+ }
705
+ }
706
+
707
+ // 创建全局实例
708
+ window.browserMCP = new BrowserMCPServer();
709
+
710
+ // 暴露全局辅助函数,用于从控制台连接
711
+ window.connectMCP = (url = "ws://localhost:9000") => {
712
+ return window.browserMCP.initSocket(url);
713
+ };
714
+
715
+ console.log(
716
+ "[Browser MCP] MCP服务器已初始化,使用window.connectMCP(url)连接到Cursor",
717
+ );
718
+ })();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Browser MCP Relay Server
3
+ *
4
+ * This server is responsible for:
5
+ * 1. Providing static file service, including browser-mcp-server.js and browser-inject.js
6
+ * 2. Establishing WebSocket server to receive connections from browsers
7
+ * 3. Establishing communication with the MCP server in the browser
8
+ * 4. Communicating with Cursor via stdio (MCP protocol)
9
+ */
10
+ export {};