Broxy工人
基于Cloudflare Worker+Durable Objects构建的Broxy后端服务。提供WebSocket桥接、REST API代理和MCP(模型上下文协议)支持,以将浏览器功能公开为可调用的API服务。
特性
- WebSocket桥接 -与浏览器脚本建立持久连接以进行实时通信
- REST API代理 -通过以下方式向浏览器发送代理请求
/api/{userId}/*支持自定义响应格式的端点 - MCP协议支持 -实现MCP JSON-RPC 2.0协议用于AI工具调用
- 耐用物品 -具有长期连接和请求队列的每用户连接状态管理
- 自定义响应格式 -通过base64编码支持自定义状态代码、标头和二进制响应
建筑
graph TB
subgraph Client["Clients"]
MCPClient["MCP Client
(AI Tool Calling)"]
APIClient["API Consumer
(HTTP Request)"]
end
subgraph Cloudflare["Cloudflare Worker"]
Worker["Worker Entry
src/index.js"]
DO["Durable Object
BrowserConnection"]
MCPHandler["MCP Handler
src/mcpHandler.js"]
end
subgraph Browser["Browser"]
Ext["Browser Script
(ext/)"]
Handler["Tool Handlers"]
end
MCPClient -->|"POST /mcp/{userId}"| Worker
APIClient -->|"GET/POST /api/{userId}/*"| Worker
Worker -->|"Route"| DO
DO -->|"JSON-RPC"| MCPHandler
DO |"WebSocket"| Ext
Ext -->|"Execute"| Handler
Handler -->|"Result"| Ext
Ext -->|"Response"| DO
DO -->|"Return"| Worker
Worker -->|"Response"| MCPClient
Worker -->|"Response"| APIClientAPI终点
健康检查
GET /health答复:
{
"status": "ok",
"service": "broxy",
"endpoints": {
"connect": "/connect?id={userId}",
"mcp": "/mcp/{userId}",
"api": "/api/{userId}/{route}"
}
}WebSocket连接
GET /connect?id={userId}
Upgrade: websocket浏览器脚本通过此端点建立WebSocket连接。
连接成功消息:
{
"type": "connected",
"connectionId": "uuid-xxx",
"message": "Browser bridge connected successfully"
}请求消息(Worker→ 浏览器):
{
"type": "request",
"requestId": "uuid-xxx",
"data": {
"method": "GET",
"path": "/api/route",
"query": {},
"headers": {},
"body": null
}
}响应消息(浏览器→ 工人):
{
"type": "response",
"requestId": "uuid-xxx",
"result": { "data": "response data" }
}REST API代理
支持所有HTTP方法:
GET|POST|PUT|DELETE /api/{userId}/{route}请求示例:
curl -X POST https://your-worker.workers.dev/api/user123/data/fetch \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/api"}'成功响应:
{
"data": {
"result": "browser execution result"
}
}自定义响应格式
浏览器脚本可以返回带有状态代码、标题和正文的自定义响应格式:
{
"status": 201,
"headers": {
"Content-Type": "text/plain",
"X-Custom-Header": "value"
},
"body": "Plain text response"
}返回二进制数据(图像、PDF等) 使用base64编码:
{
"status": 200,
"headers": {
"Content-Type": "image/png"
},
"body": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"isBase64": true
}示例用法-浏览器脚本返回图像:
// Browser script handler
async function handleRequest(request) {
if (request.path === '/screenshot') {
const canvas = document.createElement('canvas');
// ... capture screenshot
const dataUrl = canvas.toDataURL('image/png');
const base64 = dataUrl.split(',')[1];
return {
status: 200,
headers: { 'Content-Type': 'image/png' },
body: base64,
isBase64: true
};
}
}错误响应(浏览器未连接):
{
"error": "Browser not connected",
"userId": "user123",
"hint": "Browser script may not be running or userId is invalid"
}错误响应(超时):
{
"error": "timeout",
"userId": "user123",
"details": "Browser did not respond within 30000ms"
}MCP JSON-RPC端点
POST /mcp/{userId}
Content-Type: application/json初始化请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "my-client",
"version": "1.0.0"
}
}
}初始化响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": {
"name": "Broxy MCP Server",
"version": "1.0.0"
}
}
}列出工具:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}工具列表响应示例:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "fetch_page",
"description": "Fetch page content",
"inputSchema": {
"type": "object",
"properties": {
"url": { "type": "string" }
},
"required": ["url"]
}
}
]
}
}呼叫工具:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "fetch_page",
"arguments": {
"url": "https://example.com"
}
}
}工具调用响应:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "page content..."
}
]
}
}本地开发
npx wrangler dev开发服务器启动于 http://localhost:8787 默认情况下。
部署
npx wrangler deploy成功部署后,将输出Worker URL,例如。, https://broxy.your-subdomain.workers.dev.
配置
wrangler.toml 配置:
name = "broxy" # Worker name
main = "src/index.js" # Entry file
compatibility_date = "2024-01-01" # Compatibility date
# Durable Objects binding
[[durable_objects.bindings]]
name = "BROWSER_CONNECTIONS" # Binding name (referenced in code)
class_name = "BrowserConnection" # Durable Object class name
# Migration config (required for first deployment)
[[migrations]]
tag = "v1"
new_sqlite_classes = ["BrowserConnection"]
# Environment variables
[vars]
DEFAULT_TIMEOUT = "30000" # Request timeout (milliseconds)环境变量
| 变量 | 默认值 | 描述 |
|---|---|---|
DEFAULT_TIMEOUT | 30000 | 浏览器请求超时(毫秒) |
目录结构
worker/
├── src/
│ ├── index.js # Main entry, route dispatching
│ ├── durableObject.js # Durable Object, browser connection management
│ └── mcpHandler.js # MCP JSON-RPC protocol handler
└── wrangler.toml # Cloudflare Worker configuration相关项目
许可证
麻省理工学院
用法示例
完整浏览器脚本示例
// Tampermonkey/extension script
const WS_URL = 'wss://your-worker.workers.dev/connect?id=your-user-id';
let ws;
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => console.log('Connected to Broxy');
ws.onmessage = async (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'request') {
const result = await handleRequest(msg.data);
ws.send(JSON.stringify({
type: 'response',
requestId: msg.requestId,
result
}));
}
};
ws.onclose = () => setTimeout(connect, 3000);
}
async function handleRequest(req) {
// Handle different routes
if (req.path === '/screenshot') {
return await captureScreenshot();
}
if (req.path === '/fetch') {
return await fetchData(req.body.url);
}
// Default: return data as-is
return { path: req.path, method: req.method };
}
async function captureScreenshot() {
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(document.body, 0, 0);
const dataUrl = canvas.toDataURL('image/png');
const base64 = dataUrl.split(',')[1];
return {
status: 200,
headers: { 'Content-Type': 'image/png' },
body: base64,
isBase64: true
};
}
connect();客户来电
# Get screenshot
curl https://your-worker.workers.dev/api/your-user-id/screenshot --output screenshot.png
# Fetch data
curl -X POST https://your-worker.workers.dev/api/your-user-id/fetch \
-H "Content-Type: application/json" \
-d '{"url": "https://api.example.com/data"}'