在实际开发中,我们常常需要将浏览器环境的能力(如页面信息获取、DOM操作、网络请求拦截等)集成到自动化脚本或AI辅助编程工具中。一个典型的场景是,希望像“kimi-code”这类AI编程助手能够直接调用浏览器的功能,例如自动填写表单、截图、提取数据或模拟用户交互,从而扩展其代码生成和任务执行的能力。要实现这一点,核心在于建立一个桥梁,让外部工具能够安全、可控地与浏览器进行通信。
本文的目标是构建一个Chrome扩展程序,它不提供用户界面,而是作为一个后台服务运行。这个扩展的核心功能是暴露一个基于MCP(Model Context Protocol)协议的服务器,使得“kimi-code”或其他兼容MCP的客户端能够通过此协议,将浏览器本身作为一个“工具表面”来调用。我们将从理解MCP协议与Chrome扩展的通信基础开始,逐步完成扩展的Manifest V3配置、后台服务脚本编写、MCP服务器实现,并最终实现一个从“kimi-code”客户端触发浏览器标签页截图的实际案例。整个过程将涵盖环境准备、代码实现、调试排错以及生产环境部署的注意事项。
1. 理解核心概念:MCP协议与Chrome扩展的通信机制
在开始编码之前,必须厘清几个关键概念,这决定了我们整个项目的架构设计。
1.1 什么是MCP(Model Context Protocol)?
MCP是一种通信协议,旨在为大型语言模型(LLM)或AI助手提供一个标准化的方式来发现、调用外部工具和访问上下文数据。你可以把它想象成AI世界的“API网关”或“RPC框架”。一个MCP服务器(Server)对外暴露一系列工具(Tools)和资源(Resources),而MCP客户端(Client,如kimi-code)则可以连接到服务器,列出可用的工具并调用它们。
对于本项目而言,我们的Chrome扩展将扮演MCP服务器的角色。它提供的“工具”就是各种浏览器操作,例如capture_visible_tab(捕获可见标签页)、get_page_content(获取页面内容)等。kimi-code作为客户端,通过MCP协议与我们的扩展通信,从而间接操作浏览器。
1.2 Chrome扩展作为MCP服务器的可行性
Chrome扩展通常由几个部分组成:manifest.json(清单文件)、背景脚本(Background Script)、内容脚本(Content Script)和弹出页面(Popup)。要让扩展成为一个常驻的服务器,我们需要使用后台服务(Service Worker),这是Manifest V3中替代传统后台页面的技术。Service Worker在扩展安装后即可独立运行,监听事件并保持活动状态,非常适合作为常驻的MCP服务器。
然而,Service Worker运行在一个独立的、无DOM的环境中,并且有生命周期限制(可能在不活动时被终止)。同时,MCP协议通常基于标准输入输出(stdio)或WebSocket进行通信,这与扩展的典型通信方式(如chrome.runtimeAPI)不同。因此,我们需要在扩展中创建一个“适配层”,可能通过chrome.runtime.onConnect或chrome.runtime.onMessage来模拟MCP服务器与外部Native Host(一个本地应用)的通信,再由Native Host与kimi-code客户端进行stdio通信。这是本项目最大的架构挑战。
1.3 技术栈与依赖关系
基于以上分析,我们确定以下技术栈和组件:
- Chrome扩展 (Manifest V3): 提供浏览器API的访问权限,运行后台Service Worker。
- MCP Server SDK (JavaScript/TypeScript): 用于实现MCP协议的服务端逻辑。我们可以使用官方或社区提供的JavaScript SDK。
- Native Host (可选,但推荐): 一个本地应用程序,作为扩展与kimi-code客户端之间的桥梁。它通过
nativeMessaging与扩展通信,并通过stdio与MCP客户端通信。这简化了扩展端的复杂度,使其只需处理浏览器API和简单的消息转发。 - kimi-code 或 其他MCP客户端: 作为工具的调用方。
本文将采用一种相对简洁的实现路径:在Chrome扩展的Service Worker中直接实现一个简化版的MCP服务器,并通过chrome.runtime.onConnectExternal监听来自外部连接。为了演示,我们假设kimi-code客户端能够通过一个自定义的通信通道(如WebSocket或特定的Chrome扩展消息端口)连接到我们的扩展。在实际生产环境中,可能需要配合Native Host。
2. 环境准备与项目结构搭建
在开始编写代码前,需要准备好开发环境并创建清晰的项目目录。
2.1 开发环境要求
确保你的开发环境满足以下要求:
| 组件 | 要求 | 说明 |
|---|---|---|
| Node.js | 版本 16 或更高 | 用于包管理和运行可能的构建脚本。 |
| npm 或 yarn | 最新稳定版 | 包管理工具。 |
| Chrome / Chromium | 版本 88 或更高 | 必须支持Manifest V3。 |
| 代码编辑器 | VS Code 等 | 推荐安装 Chrome 扩展开发相关插件。 |
| kimi-code 或 MCP 客户端 | 支持 MCP 协议 | 用于测试工具调用。本文将以一个模拟的客户端脚本进行演示。 |
2.2 创建项目目录与初始化
首先,创建一个新的项目文件夹并初始化package.json。
mkdir chrome-extension-mcp-server cd chrome-extension-mcp-server npm init -y接下来,安装我们可能需要的依赖。由于我们将直接实现MCP协议逻辑,这里选择安装一个基础的WebSocket库,以便于扩展与外部测试客户端通信(模拟MCP over WebSocket)。同时,我们也会安装TypeScript及相关类型定义,以获得更好的开发体验。
npm install ws @types/ws --save-dev npm install typescript @types/chrome --save-dev初始化TypeScript配置:
npx tsc --init编辑生成的tsconfig.json,确保包含Chrome类型定义并输出到合适的目录(例如dist)。
2.3 项目目录结构
创建以下目录和文件,形成清晰的项目结构:
chrome-extension-mcp-server/ ├── dist/ # TypeScript编译输出目录(可选) ├── src/ # 源代码目录 │ ├── background/ # 后台Service Worker │ │ └── service-worker.ts │ └── utils/ # 工具函数 │ └── mcp-protocol.ts ├── public/ # 静态资源(本例中可能为空) ├── manifest.json # 扩展清单文件 ├── package.json ├── tsconfig.json └── README.md3. 编写Chrome扩展核心:Manifest V3配置
manifest.json是扩展的“身份证”和“说明书”,它定义了扩展的权限、资源和行为。对于我们的MCP服务器扩展,关键配置如下:
{ "manifest_version": 3, "name": "Browser MCP Server", "version": "1.0.0", "description": "Exposes browser capabilities as tools via MCP protocol.", "permissions": [ "activeTab", "scripting", "tabs", "webNavigation", "storage" ], "host_permissions": [ "<all_urls>" ], "background": { "service_worker": "src/background/service-worker.js", "type": "module" }, "externally_connectable": { "matches": [ "https://kimi-code.example.com/*" // 允许连接的特定来源,生产环境需严格限制 ], "ids": ["*"] // 允许所有扩展连接,仅用于开发测试 }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" } }关键配置解释:
manifest_version: 3:必须使用Manifest V3。permissions:activeTab:允许在当前活动标签页执行操作(如截图)。scripting:允许注入和执行脚本(未来可能用于DOM操作)。tabs:允许查询和管理浏览器标签页。webNavigation:监听页面导航事件(可选,用于上下文感知)。storage:用于存储配置或会话数据(可选)。
host_permissions: ["<all_urls>"]:允许扩展在所有网站上运行。这是高权限配置,在生产扩展中应根据最小权限原则,精确指定所需域名。background.service_worker:指定后台Service Worker的入口文件。注意,Manifest V3中Service Worker文件不能是TypeScript,必须是JavaScript。因此我们需要将service-worker.ts编译为service-worker.js,或者直接编写JS文件。本文示例将直接使用.js以简化。externally_connectable:这是至关重要的配置。它定义了哪些外部网页或扩展可以连接到我们的扩展。为了让kimi-code(或其本地代理)能够连接,我们必须在这里声明允许的来源。示例中使用了https://kimi-code.example.com作为占位符。在开发测试时,可以暂时使用"*",但上线前必须修改为确切的、受信任的源,否则会带来严重的安全风险。content_security_policy:定义了扩展页面的安全策略,防止XSS等攻击。我们采用了默认的严格策略。
4. 实现后台Service Worker与MCP服务器逻辑
这是扩展的核心。我们的Service Worker需要做两件事:1) 实现MCP协议的消息处理;2) 暴露浏览器工具。
4.1 建立外部连接监听
在src/background/service-worker.js中,我们首先监听来自外部的连接。这里我们使用chrome.runtime.onConnectExternal,它允许被externally_connectable中定义的源连接。
// src/background/service-worker.js // 存储所有活动连接的端口 const connections = new Map(); // 监听来自外部的连接(例如,从kimi-code的本地代理) chrome.runtime.onConnectExternal.addListener((port) => { console.log(`[MCP Server] External connection established from: ${port.name}`); // 为新连接分配一个唯一ID const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; connections.set(connectionId, port); // 初始化MCP会话:发送“initialized”通知(根据MCP协议) port.postMessage({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }); // 监听来自客户端的消息 port.onMessage.addListener((message) => { handleClientMessage(message, port, connectionId); }); // 处理连接断开 port.onDisconnect.addListener(() => { console.log(`[MCP Server] Connection ${connectionId} disconnected.`); connections.delete(connectionId); }); }); // 处理客户端消息的核心函数 async function handleClientMessage(message, port, connectionId) { console.log(`[MCP Server] Received message from ${connectionId}:`, message); // 基本JSON-RPC 2.0校验 if (message.jsonrpc !== "2.0") { sendError(port, null, -32600, "Invalid JSON-RPC version."); return; } const { id, method, params } = message; try { switch (method) { case "tools/list": await handleListTools(id, port); break; case "tools/call": await handleCallTool(id, params, port); break; // 可以添加其他MCP方法,如 `resources/list`, `resources/read` 等 default: sendError(port, id, -32601, `Method not found: ${method}`); } } catch (error) { console.error(`[MCP Server] Error handling method ${method}:`, error); sendError(port, id, -32603, `Internal error: ${error.message}`); } } // 发送错误响应 function sendError(port, id, code, message) { port.postMessage({ jsonrpc: "2.0", id, error: { code, message } }); }4.2 实现MCP工具列表与调用
接下来,实现handleListTools和handleCallTool函数。这些函数定义了我们的扩展向客户端暴露了哪些浏览器工具。
// src/background/service-worker.js (续) // 定义可用的工具列表 const availableTools = [ { name: "capture_visible_tab", description: "Captures a screenshot of the currently active tab.", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["png", "jpeg"], description: "The image format.", default: "png" }, quality: { type: "integer", minimum: 0, maximum: 100, description: "The quality of the capture (for jpeg).", default: 80 } } } }, { name: "get_active_tab_info", description: "Gets the URL and title of the currently active tab.", inputSchema: { type: "object", properties: {} // 此工具不需要参数 } } // 未来可以添加更多工具,如 `execute_script`, `navigate_to_url` 等 ]; // 处理 tools/list 请求 async function handleListTools(requestId, port) { port.postMessage({ jsonrpc: "2.0", id: requestId, result: { tools: availableTools } }); } // 处理 tools/call 请求 async function handleCallTool(requestId, params, port) { const { name, arguments: toolArgs } = params; let result; switch (name) { case "capture_visible_tab": result = await captureVisibleTab(toolArgs); break; case "get_active_tab_info": result = await getActiveTabInfo(); break; default: throw new Error(`Tool not found: ${name}`); } port.postMessage({ jsonrpc: "2.0", id: requestId, result: { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] } }); }4.3 实现具体的浏览器工具函数
现在,实现具体的工具函数,它们将调用Chrome扩展API。
// src/background/service-worker.js (续) // 工具函数:捕获当前可见标签页 async function captureVisibleTab(args = {}) { const { format = 'png', quality = 80 } = args; // 1. 获取当前活动标签页 const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!activeTab) { throw new Error('No active tab found.'); } if (activeTab.url.startsWith('chrome://') || activeTab.url.startsWith('chrome-extension://')) { throw new Error('Cannot capture internal Chrome pages.'); } // 2. 调用 captureVisibleTab API // 注意:此API捕获的是调用者标签页的内容。在Service Worker中,我们需要指定一个tabId。 // 这里我们使用 chrome.tabs.captureVisibleTab,它捕获当前窗口的可见标签页。 const dataUrl = await chrome.tabs.captureVisibleTab(null, { format, quality }); // 3. 返回结果。在实际MCP协议中,可能需要返回资源引用或base64数据。 // 这里我们返回一个包含base64图像数据的对象。 return { success: true, tabId: activeTab.id, tabTitle: activeTab.title, tabUrl: activeTab.url, imageData: dataUrl, // data:image/png;base64,... format, timestamp: new Date().toISOString() }; } // 工具函数:获取活动标签页信息 async function getActiveTabInfo() { const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!activeTab) { throw new Error('No active tab found.'); } return { id: activeTab.id, title: activeTab.title, url: activeTab.url, status: activeTab.status, windowId: activeTab.windowId }; }5. 构建、加载扩展与运行验证
代码编写完成后,需要将其构建为扩展并加载到Chrome中进行测试。
5.1 构建项目(如果使用TypeScript)
如果你使用了TypeScript,需要先编译。在package.json中添加脚本:
{ "scripts": { "build": "tsc", "watch": "tsc --watch" } }运行npm run build将TypeScript编译到dist目录。然后需要更新manifest.json中的service_worker路径指向编译后的JS文件(例如"dist/background/service-worker.js")。
5.2 加载未打包的扩展
- 打开Chrome浏览器,进入扩展管理页面 (
chrome://extensions)。 - 开启右上角的“开发者模式”。
- 点击“加载已解压的扩展程序”。
- 选择包含
manifest.json的项目根目录(chrome-extension-mcp-server)。 - 加载成功后,你应该能在扩展列表中找到“Browser MCP Server”,并看到其ID(如
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)。
5.3 创建测试客户端脚本
为了验证扩展的MCP服务器是否工作,我们需要一个模拟的MCP客户端。由于扩展通过chrome.runtime.connect接收连接,我们可以写一个简单的Node.js脚本,使用chrome-remote-interface或直接通过chrome.debugger协议来模拟连接。但更简单的方式是在浏览器内部创建一个测试页面,利用externally_connectable配置进行连接。
创建一个测试HTML文件test-client.html,放在项目根目录下:
<!DOCTYPE html> <html> <head> <title>MCP Client Test</title> </head> <body> <h1>MCP Client Test Page</h1> <button id="listTools">List Tools</button> <button id="captureTab">Capture Tab</button> <button id="getTabInfo">Get Tab Info</button> <pre id="output"></pre> <script> const extensionId = '你的扩展ID'; // 替换为实际扩展ID let port = null; function log(message) { document.getElementById('output').textContent += message + '\n'; } function connectToExtension() { if (port) return port; // 连接到扩展 port = chrome.runtime.connect(extensionId, {name: "test-client"}); port.onMessage.addListener((msg) => { log('<< Received: ' + JSON.stringify(msg, null, 2)); }); port.onDisconnect.addListener(() => { log('Disconnected from extension.'); port = null; }); log('Connected to extension.'); return port; } function sendRequest(method, params) { const port = connectToExtension(); const requestId = Date.now(); const message = { jsonrpc: "2.0", id: requestId, method, params }; log('>> Sending: ' + JSON.stringify(message, null, 2)); port.postMessage(message); } document.getElementById('listTools').onclick = () => { sendRequest('tools/list', {}); }; document.getElementById('captureTab').onclick = () => { sendRequest('tools/call', { name: 'capture_visible_tab', arguments: { format: 'png' } }); }; document.getElementById('getTabInfo').onclick = () => { sendRequest('tools/call', { name: 'get_active_tab_info', arguments: {} }); }; // 注意:此页面必须通过 http/https 服务打开,且域名需在 externall_connectable 的 matches 中。 // 为了方便,你可以暂时修改 manifest.json 的 matches 为 ["<all_urls>"] 进行测试,但完成后务必改回。 </script> </body> </html>重要:你需要通过一个本地HTTP服务器(如python -m http.server 8000或npx serve)来运行这个test-client.html页面,并且其域名(如http://localhost:8000)必须添加到manifest.json的externally_connectable.matches中。同时,将脚本中的extensionId替换为你扩展的实际ID。
5.4 运行测试与验证
- 修改
manifest.json中的externally_connectable.matches,临时加入"http://localhost:8000/*"。 - 在Chrome中重新加载扩展(在
chrome://extensions页面点击扩展卡片上的刷新图标)。 - 启动本地HTTP服务器,并在Chrome中打开
http://localhost:8000/test-client.html。 - 点击页面上的“List Tools”按钮。如果一切正常,你应该在页面下方的输出区域看到来自扩展的响应,其中包含
capture_visible_tab和get_active_tab_info两个工具的定义。 - 打开一个普通网页(如
https://www.example.com),然后点击“Capture Tab”按钮。稍等片刻,你应该会收到一个包含imageData(一个很长的base64字符串)的响应。你可以将这个base64字符串复制到浏览器的地址栏(格式为data:image/png;base64,...)来验证图片是否正确。 - 点击“Get Tab Info”按钮,应返回当前活动标签页的URL和标题。
6. 常见问题排查与调试技巧
在开发和测试过程中,你可能会遇到以下问题。这里提供排查思路。
6.1 连接失败或无法建立连接
| 问题现象 | 可能原因 | 检查与解决 |
|---|---|---|
| 点击按钮无任何响应,控制台无错误。 | 1. 扩展未正确加载或已禁用。 2. externally_connectable配置不匹配。3. 测试页面的源(协议、域名、端口)未在 matches中列出。 | 1. 检查chrome://extensions,确保扩展已启用且无错误。2. 仔细核对 manifest.json中matches的每一个字符,确保包含测试页面的完整源(如http://localhost:8000)。3. 在测试页面按F12打开开发者工具,查看Console是否有“Cannot connect to extension”等错误。 |
控制台报错:Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist. | Service Worker可能已休眠或终止。Chrome为了节省资源,会停止不活动的Service Worker。 | 1. 确保在测试前有用户交互(如点击按钮)来“唤醒”Service Worker。 2. 在Service Worker开头添加 console.log,观察其是否被重新启动。这是Manifest V3 Service Worker的正常行为,你的代码需要能处理冷启动。 |
6.2 MCP协议消息处理错误
| 问题现象 | 可能原因 | 检查与解决 |
|---|---|---|
收到响应,但格式不符合JSON-RPC 2.0,或返回了-32601 Method not found。 | 1. 客户端发送的消息格式错误(缺少jsonrpc: "2.0")。2. method字段的值与服务器端switch语句中的case不匹配。 | 1. 在handleClientMessage函数开始处打印收到的原始message,检查其结构。2. 确保 method字符串完全匹配,包括大小写。MCP协议方法通常是tools/list和tools/call。 |
调用capture_visible_tab失败,返回权限错误或空白图片。 | 1. 活动标签页是Chrome内部页面(如chrome://extensions),这些页面不允许截图。2. 标签页尚未完全加载完成。 3. 扩展没有 activeTab或<all_urls>权限。 | 1. 在captureVisibleTab函数中添加对chrome://和chrome-extension://URL的检查并抛出友好错误。2. 确保目标网页是普通的HTTP/HTTPS页面,并且已加载完毕。 3. 确认 manifest.json中的permissions和host_permissions已正确声明。 |
6.3 Service Worker生命周期与状态管理
| 问题现象 | 可能原因 | 检查与解决 |
|---|---|---|
| 第一次调用成功,但几分钟后调用失败,需要刷新页面才能恢复。 | Service Worker因不活动被浏览器终止。所有变量状态(如connectionsMap)丢失。 | 这是Manifest V3的预期行为。解决方案: 1.不要依赖Service Worker的内存状态。将需要持久化的数据(如连接信息、会话)使用 chrome.storageAPI存储。2. 实现重连逻辑。客户端在发送消息前应检查端口状态,如果断开则重新连接。 3. 考虑使用 chrome.alarmsAPI定期执行轻量任务,以保持Service Worker活跃(但需谨慎,避免滥用)。 |
6.4 安全与权限警告
| 问题现象 | 风险与建议 |
|---|---|
在扩展审核或用户安装时,被提示权限过高(特别是<all_urls>)。 | host_permissions: ["<all_urls>"]是一个强大的权限,可能会降低用户安装意愿或导致商店审核更严格。最佳实践:1.按需申请:如果工具只在用户主动点击时运行,考虑使用 activeTab权限,它仅在用户与扩展交互后授予临时权限。2.可选权限:对于某些高级功能,可以使用 chrome.permissions.request在运行时动态请求权限,并清晰告知用户为何需要。3.限定域名:如果只为特定网站服务,将 <all_urls>替换为具体的域名模式,如["https://*.example.com/*"]。 |
7. 生产环境最佳实践与扩展方向
一个可用于实际项目的MCP服务器扩展,还需要考虑更多因素。
7.1 安全加固
- 严格限制连接源:永远不要在生产环境的
externally_connectable.matches中使用"*"。只允许受信任的、特定的源(如kimi-code官方客户端的本地服务器地址http://127.0.0.1:某个特定端口)。 - 消息验证与鉴权:在
handleClientMessage中,除了JSON-RPC格式校验,还应验证消息来源。可以为每个连接设置一个简单的令牌(Token)鉴权机制。 - 输入清理:对从客户端接收的所有参数进行严格的类型和范围检查,防止注入攻击。
- 最小权限原则:如前所述,仔细审查
permissions和host_permissions,只申请必要的权限。
7.2 健壮性提升
- 错误处理与日志:实现更精细的错误处理,并将关键操作和错误记录到
chrome.storage.local或远程日志服务,便于排查。 - 心跳与重连:实现客户端与扩展之间的心跳机制,及时发现连接断开并尝试重连。
- 状态恢复:将关键状态(如注册的工具列表、活动连接信息)持久化到
chrome.storage,以便Service Worker被终止后重启时能够恢复。 - 工具调用超时:为每个工具调用设置超时限制,防止长时间运行的任务阻塞服务。
7.3 功能扩展
你现在已经拥有了一个基础的框架,可以轻松添加更多强大的浏览器工具:
execute_script: 在指定标签页中执行JavaScript代码,并返回结果。navigate_to_url: 控制浏览器导航到指定URL。extract_page_content: 获取页面的文本内容、链接或结构化数据。monitor_network_requests: 监听和拦截特定网络请求。manage_cookies: 读取或设置特定网站的Cookie。simulate_user_input: 模拟鼠标点击、键盘输入等用户交互。
添加新工具只需三步:
- 在
availableTools数组中定义新工具的名称、描述和输入模式。 - 在
handleCallTool函数的switch语句中添加新的case。 - 实现对应的工具函数,调用相应的Chrome API。
7.4 与kimi-code等客户端的深度集成
本文演示的是通过一个网页测试客户端进行连接。要与真正的kimi-code集成,通常需要:
- Native Host应用:开发一个小的本地应用程序。该应用通过stdio与kimi-code(作为MCP客户端)通信,同时通过
nativeMessagingAPI与Chrome扩展通信。这样,kimi-code就不需要直接处理浏览器扩展的连接细节。 - 定义清晰的工具契约:与kimi-code的开发者协作,明确每个工具的名称、参数、返回值格式和语义,确保双方理解一致。
- 处理复杂的上下文:浏览器操作往往依赖于当前页面状态。需要考虑如何将页面上下文(如选中的元素、当前的登录状态)安全地传递给AI模型,并处理多标签页环境。
通过以上步骤,你构建的不仅仅是一个简单的Chrome扩展,而是一个将浏览器强大能力开放给AI编程助手的标准化桥梁。这种模式可以极大地扩展AI助手在Web自动化、数据抓取、界面测试等场景的应用边界。在开始添加更复杂的功能之前,请务必确保基础通信链路的安全与稳定。