Broxy backend service built on Cloudflare Worker + Durable Objects. Provides WebSocket bridging, REST API proxy, and MCP (Model Context Protocol) support to expose browser capabilities as callable API services.
- WebSocket Bridging - Establish persistent connections with browser scripts for real-time communication
- REST API Proxy - Proxy requests to browsers via
/api/{userId}/*endpoint with custom response format support - MCP Protocol Support - Implements MCP JSON-RPC 2.0 protocol for AI tool calling
- Durable Objects - Per-user connection state management with long-lived connections and request queuing
- Custom Response Format - Support for custom status codes, headers, and binary responses via base64 encoding
graph TB
subgraph Client["Clients"]
MCPClient["MCP Client<br/>(AI Tool Calling)"]
APIClient["API Consumer<br/>(HTTP Request)"]
end
subgraph Cloudflare["Cloudflare Worker"]
Worker["Worker Entry<br/>src/index.js"]
DO["Durable Object<br/>BrowserConnection"]
MCPHandler["MCP Handler<br/>src/mcpHandler.js"]
end
subgraph Browser["Browser"]
Ext["Browser Script<br/>(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"| APIClient
GET /healthResponse:
{
"status": "ok",
"service": "broxy",
"endpoints": {
"connect": "/connect?id={userId}",
"mcp": "/mcp/{userId}",
"api": "/api/{userId}/{route}"
}
}GET /connect?id={userId}
Upgrade: websocketBrowser scripts establish WebSocket connections through this endpoint.
Connection success message:
{
"type": "connected",
"connectionId": "uuid-xxx",
"message": "Browser bridge connected successfully"
}Request message (Worker → Browser):
{
"type": "request",
"requestId": "uuid-xxx",
"data": {
"method": "GET",
"path": "/api/route",
"query": {},
"headers": {},
"body": null
}
}Response message (Browser → Worker):
{
"type": "response",
"requestId": "uuid-xxx",
"result": { "data": "response data" }
}All HTTP methods are supported:
GET|POST|PUT|DELETE /api/{userId}/{route}Request example:
curl -X POST https://your-worker.workers.dev/api/user123/data/fetch \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/api"}'Success response:
{
"data": {
"result": "browser execution result"
}
}Browser scripts can return custom response format with status code, headers, and body:
{
"status": 201,
"headers": {
"Content-Type": "text/plain",
"X-Custom-Header": "value"
},
"body": "Plain text response"
}Return binary data (image, PDF, etc.) using base64 encoding:
{
"status": 200,
"headers": {
"Content-Type": "image/png"
},
"body": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"isBase64": true
}Example usage - browser script returns an image:
// 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 response (browser not connected):
{
"error": "Browser not connected",
"userId": "user123",
"hint": "Browser script may not be running or userId is invalid"
}Error response (timeout):
{
"error": "timeout",
"userId": "user123",
"details": "Browser did not respond within 30000ms"
}POST /mcp/{userId}
Content-Type: application/jsonInitialize request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "my-client",
"version": "1.0.0"
}
}
}Initialize response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": {
"name": "Broxy MCP Server",
"version": "1.0.0"
}
}
}List tools:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}Tools list response example:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "fetch_page",
"description": "Fetch page content",
"inputSchema": {
"type": "object",
"properties": {
"url": { "type": "string" }
},
"required": ["url"]
}
}
]
}
}Call tool:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "fetch_page",
"arguments": {
"url": "https://example.com"
}
}
}Tool call response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "page content..."
}
]
}
}npx wrangler devDevelopment server starts at http://localhost:8787 by default.
npx wrangler deployAfter successful deployment, the Worker URL will be output, e.g., https://broxy.your-subdomain.workers.dev.
wrangler.toml configuration:
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)| Variable | Default | Description |
|---|---|---|
DEFAULT_TIMEOUT |
30000 |
Browser request timeout (milliseconds) |
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
| Project | Description |
|---|---|
| ext | Browser extension/Tampermonkey script |
| ext-ui | Extension UI (React + TypeScript) |
| www | Static landing page |
MIT
// 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"}'