WebSocket API¶
The RapidTriageME WebSocket API provides real-time bidirectional communication between the Chrome extension, Browser Connector, MCP server, and remote clients. This enables live streaming of browser data and interactive debugging sessions.
Overview¶
WebSocket connections enable: - Real-time Data Streaming - Live console logs, network requests, and errors - Interactive Debugging - Bidirectional communication with browser - Low Latency - Sub-millisecond response times for local connections - Connection Persistence - Maintains state across page reloads
Connection Architecture¶
graph TB
subgraph Browser
CE[Chrome Extension]
WP[Web Page]
end
subgraph Local Server
BC[Browser Connector<br/>:3025]
end
subgraph Remote Access
CW[Cloudflare Worker<br/>rapidtriage.me]
end
subgraph AI Integration
MCP[MCP Server]
AI[Claude/ChatGPT]
end
CE -.->|ws://localhost:3025/extension-ws| BC
BC -.->|ws://localhost:3025/mcp-ws| MCP
BC -.->|wss://rapidtriage.me/tunnel/{id}| CW
MCP -.->|MCP Protocol| AI
WP -->|DevTools API| CE
Local WebSocket Endpoints¶
Extension Connection¶
ws://localhost:3025/extension-ws
¶
Primary connection for Chrome Extension to Browser Connector.
Connection Headers:
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: rapidtriage-extension-v1
Origin: chrome-extension://extension-id
Authentication: None (localhost only)
MCP Server Connection¶
ws://localhost:3025/mcp-ws
¶
Connection for MCP server integration with AI assistants.
Connection Headers:
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: mcp-server-v1
User-Agent: rapidtriage-mcp/1.0
Remote WebSocket Endpoints¶
Secure Tunnel Connection¶
wss://rapidtriage.me/tunnel/{sessionId}
¶
Encrypted tunnel for remote access through Cloudflare Worker.
Connection Headers:
Authorization: Bearer <jwt_token>
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: rapidtriage-tunnel-v1
X-Session-ID: <session_id>
Authentication: Required JWT token
Message Protocol¶
Message Structure¶
All WebSocket messages follow this JSON structure:
{
"id": "unique-message-id",
"type": "message-type",
"timestamp": 1704067200000,
"data": {
// Message-specific payload
},
"metadata": {
"sessionId": "session-123",
"source": "chrome-extension",
"version": "1.0"
}
}
Message Types¶
Console Messages¶
Console Log
{
"id": "msg_001",
"type": "console-log",
"timestamp": 1704067200000,
"data": {
"level": "info",
"message": "Application started successfully",
"args": ["Application", "started", { "version": "1.0" }],
"source": "app.js:42:8",
"stack": null
},
"metadata": {
"sessionId": "session-123",
"tabId": "tab-456",
"url": "https://example.com"
}
}
Console Error
{
"id": "msg_002",
"type": "console-error",
"timestamp": 1704067200000,
"data": {
"level": "error",
"message": "Uncaught TypeError: Cannot read property 'x' of undefined",
"stack": "TypeError: Cannot read property 'x' of undefined\n at Object.handleClick (app.js:15:5)\n at HTMLButtonElement.onclick (index.html:1:1)",
"source": "app.js:15:5"
}
}
Network Messages¶
Network Request
{
"id": "msg_003",
"type": "network-request",
"timestamp": 1704067200000,
"data": {
"url": "https://api.example.com/users",
"method": "GET",
"status": 200,
"statusText": "OK",
"duration": 145,
"size": {
"request": 512,
"response": 2048
},
"headers": {
"request": {
"Authorization": "Bearer [REDACTED]",
"Content-Type": "application/json"
},
"response": {
"Content-Type": "application/json",
"Cache-Control": "max-age=300"
}
},
"body": {
"request": "{\"page\": 1}",
"response": "{\"users\": [...]}"
}
}
}
Network Error
{
"id": "msg_004",
"type": "network-error",
"timestamp": 1704067200000,
"data": {
"url": "https://api.example.com/broken",
"method": "POST",
"status": 500,
"statusText": "Internal Server Error",
"error": "Server returned 500 status",
"duration": 5000
}
}
Control Messages¶
Heartbeat
{
"id": "hb_001",
"type": "heartbeat",
"timestamp": 1704067200000,
"data": {
"status": "alive",
"uptime": 3600000,
"connections": 3
}
}
Screenshot Request
{
"id": "cmd_001",
"type": "screenshot-request",
"timestamp": 1704067200000,
"data": {
"format": "png",
"quality": 90,
"fullPage": false,
"selector": null
}
}
Screenshot Response
{
"id": "cmd_001",
"type": "screenshot-response",
"timestamp": 1704067200000,
"data": {
"success": true,
"path": "/Users/Downloads/screenshot-2024-01-01.png",
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"size": 150240,
"dimensions": {
"width": 1920,
"height": 1080
}
}
}
Element Selection¶
Element Selected
{
"id": "sel_001",
"type": "element-selected",
"timestamp": 1704067200000,
"data": {
"element": {
"tagName": "DIV",
"id": "main-content",
"className": "container mt-4",
"attributes": {
"data-testid": "main",
"role": "main"
},
"dimensions": {
"width": 1200,
"height": 800,
"top": 100,
"left": 0
},
"styles": {
"backgroundColor": "rgb(255, 255, 255)",
"color": "rgb(33, 37, 41)"
},
"innerHTML": "<h1>Welcome</h1><p>Content...</p>",
"textContent": "Welcome Content...",
"xpath": "/html/body/div[1]/main/div[1]",
"cssSelector": "#main-content"
}
}
}
Connection Management¶
Connection Established¶
Client to Server:
{
"type": "connection-init",
"data": {
"clientType": "chrome-extension",
"version": "1.0.0",
"capabilities": [
"console-capture",
"network-capture",
"screenshot-capture",
"element-selection"
],
"userAgent": "Chrome Extension/1.0",
"tabId": "tab-456"
}
}
Server Response:
{
"type": "connection-ack",
"data": {
"sessionId": "session-123",
"serverId": "rapidtriage-server-1.0",
"supportedProtocols": ["rapidtriage-v1"],
"maxMessageSize": 1048576,
"heartbeatInterval": 30000
}
}
Connection Closed¶
{
"type": "connection-close",
"data": {
"reason": "client-disconnect",
"code": 1000,
"message": "Normal closure"
}
}
Client Implementation Examples¶
JavaScript (Browser)¶
class RapidTriageWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.handlers = new Map();
}
connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url, this.options.protocol);
this.ws.onopen = (event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.sendConnectionInit();
resolve(event);
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse message:', error);
}
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.handleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
} catch (error) {
reject(error);
}
});
}
sendConnectionInit() {
this.send({
type: 'connection-init',
data: {
clientType: 'javascript-client',
version: '1.0.0',
capabilities: ['console-capture', 'network-capture']
}
});
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const fullMessage = {
id: this.generateId(),
timestamp: Date.now(),
...message
};
this.ws.send(JSON.stringify(fullMessage));
} else {
console.error('WebSocket not connected');
}
}
handleMessage(message) {
const handler = this.handlers.get(message.type);
if (handler) {
handler(message);
} else {
console.log('Unhandled message:', message);
}
}
on(messageType, handler) {
this.handlers.set(messageType, handler);
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = Math.pow(2, this.reconnectAttempts) * 1000;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
}
generateId() {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// Usage example
const client = new RapidTriageWebSocket('ws://localhost:3025/extension-ws');
// Handle console logs
client.on('console-log', (message) => {
console.log('Console log received:', message.data);
});
// Handle network requests
client.on('network-request', (message) => {
console.log('Network request:', message.data.url, message.data.status);
});
// Connect
client.connect()
.then(() => console.log('Connected successfully'))
.catch(error => console.error('Connection failed:', error));
Node.js Client¶
const WebSocket = require('ws');
class RapidTriageMCPClient {
constructor(url, token = null) {
this.url = url;
this.token = token;
this.ws = null;
this.sessionId = null;
}
connect() {
const headers = {};
if (this.token) {
headers.Authorization = `Bearer ${this.token}`;
}
this.ws = new WebSocket(this.url, {
protocol: 'mcp-server-v1',
headers: headers
});
this.ws.on('open', () => {
console.log('MCP WebSocket connected');
this.startHeartbeat();
});
this.ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse message:', error);
}
});
this.ws.on('close', (code, reason) => {
console.log('MCP WebSocket closed:', code, reason.toString());
});
this.ws.on('error', (error) => {
console.error('MCP WebSocket error:', error);
});
}
startHeartbeat() {
setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.send({
type: 'heartbeat',
data: { status: 'alive' }
});
}
}, 30000);
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
const fullMessage = {
id: `mcp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
...message
};
this.ws.send(JSON.stringify(fullMessage));
}
}
handleMessage(message) {
switch (message.type) {
case 'connection-ack':
this.sessionId = message.data.sessionId;
console.log('Session established:', this.sessionId);
break;
case 'console-log':
this.handleConsoleLog(message);
break;
case 'network-request':
this.handleNetworkRequest(message);
break;
case 'screenshot-response':
this.handleScreenshot(message);
break;
default:
console.log('Received message:', message.type);
}
}
handleConsoleLog(message) {
console.log(`[${message.data.level.toUpperCase()}] ${message.data.message}`);
}
handleNetworkRequest(message) {
const { method, url, status } = message.data;
console.log(`${method} ${url} -> ${status}`);
}
handleScreenshot(message) {
if (message.data.success) {
console.log('Screenshot captured:', message.data.path);
} else {
console.error('Screenshot failed:', message.data.error);
}
}
requestScreenshot(options = {}) {
this.send({
type: 'screenshot-request',
data: {
format: 'png',
quality: 90,
fullPage: false,
...options
}
});
}
clearLogs() {
this.send({
type: 'clear-logs'
});
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// Usage
const client = new RapidTriageMCPClient('ws://localhost:3025/mcp-ws');
client.connect();
// Request screenshot after 5 seconds
setTimeout(() => {
client.requestScreenshot({ fullPage: true });
}, 5000);
Python Client¶
import asyncio
import json
import websockets
import time
from typing import Dict, Any, Callable, Optional
class RapidTriageWebSocketClient:
def __init__(self, url: str, token: Optional[str] = None):
self.url = url
self.token = token
self.websocket = None
self.handlers = {}
self.session_id = None
async def connect(self):
headers = {}
if self.token:
headers['Authorization'] = f'Bearer {self.token}'
try:
self.websocket = await websockets.connect(
self.url,
extra_headers=headers,
subprotocols=['rapidtriage-client-v1']
)
print("WebSocket connected")
# Start message handler
asyncio.create_task(self._message_handler())
# Send connection init
await self._send_connection_init()
except Exception as e:
print(f"Connection failed: {e}")
raise
async def _message_handler(self):
try:
async for message in self.websocket:
try:
data = json.loads(message)
await self._handle_message(data)
except json.JSONDecodeError as e:
print(f"Failed to parse message: {e}")
except websockets.exceptions.ConnectionClosed:
print("WebSocket connection closed")
except Exception as e:
print(f"Message handler error: {e}")
async def _send_connection_init(self):
await self.send({
'type': 'connection-init',
'data': {
'clientType': 'python-client',
'version': '1.0.0',
'capabilities': [
'console-capture',
'network-capture',
'screenshot-capture'
]
}
})
async def _handle_message(self, message: Dict[str, Any]):
message_type = message.get('type')
if message_type == 'connection-ack':
self.session_id = message['data'].get('sessionId')
print(f"Session established: {self.session_id}")
handler = self.handlers.get(message_type)
if handler:
await handler(message)
else:
print(f"Unhandled message: {message_type}")
def on(self, message_type: str, handler: Callable):
"""Register message handler"""
self.handlers[message_type] = handler
async def send(self, message: Dict[str, Any]):
if self.websocket:
full_message = {
'id': f"py_{int(time.time() * 1000)}_{id(message)}",
'timestamp': int(time.time() * 1000),
**message
}
await self.websocket.send(json.dumps(full_message))
async def request_screenshot(self, **options):
"""Request a screenshot"""
await self.send({
'type': 'screenshot-request',
'data': {
'format': 'png',
'quality': 90,
'fullPage': False,
**options
}
})
async def clear_logs(self):
"""Clear all captured logs"""
await self.send({
'type': 'clear-logs'
})
async def close(self):
if self.websocket:
await self.websocket.close()
# Usage example
async def main():
client = RapidTriageWebSocketClient('ws://localhost:3025/mcp-ws')
# Register handlers
async def handle_console_log(message):
data = message['data']
print(f"[{data['level'].upper()}] {data['message']}")
async def handle_network_request(message):
data = message['data']
print(f"{data['method']} {data['url']} -> {data['status']}")
async def handle_screenshot(message):
data = message['data']
if data['success']:
print(f"Screenshot saved: {data['path']}")
else:
print(f"Screenshot failed: {data.get('error')}")
client.on('console-log', handle_console_log)
client.on('network-request', handle_network_request)
client.on('screenshot-response', handle_screenshot)
# Connect and run
await client.connect()
# Request screenshot after 5 seconds
await asyncio.sleep(5)
await client.request_screenshot(fullPage=True)
# Keep connection alive
await asyncio.sleep(60)
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Authentication & Security¶
JWT Token Authentication¶
For remote connections, include JWT token in connection headers:
const ws = new WebSocket('wss://rapidtriage.me/tunnel/session123', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
});
Origin Validation¶
Server validates connection origins:
// Server-side origin validation
function validateOrigin(request) {
const origin = request.headers.get('Origin');
const allowedOrigins = [
'chrome-extension://extension-id',
'http://localhost:3025',
'https://rapidtriage.me'
];
return allowedOrigins.includes(origin);
}
Error Handling¶
Connection Errors¶
ws.onerror = (error) => {
console.error('WebSocket error:', error);
// Handle specific error types
if (error.code === 1006) {
console.log('Connection closed abnormally - attempting reconnect');
setTimeout(reconnect, 5000);
}
};
Message Validation¶
function validateMessage(message) {
if (!message.id || !message.type || !message.timestamp) {
throw new Error('Invalid message format');
}
if (typeof message.timestamp !== 'number') {
throw new Error('Invalid timestamp');
}
if (Date.now() - message.timestamp > 60000) {
throw new Error('Message too old');
}
return true;
}
Performance Considerations¶
Message Throttling¶
class MessageThrottler {
constructor(maxPerSecond = 100) {
this.maxPerSecond = maxPerSecond;
this.messageCount = 0;
this.windowStart = Date.now();
}
canSend() {
const now = Date.now();
// Reset counter every second
if (now - this.windowStart >= 1000) {
this.messageCount = 0;
this.windowStart = now;
}
return this.messageCount < this.maxPerSecond;
}
recordMessage() {
this.messageCount++;
}
}
Connection Pooling¶
class WebSocketPool {
constructor(url, maxConnections = 5) {
this.url = url;
this.maxConnections = maxConnections;
this.connections = [];
this.currentIndex = 0;
}
async getConnection() {
if (this.connections.length < this.maxConnections) {
const ws = new WebSocket(this.url);
await this.waitForConnection(ws);
this.connections.push(ws);
return ws;
}
// Round-robin existing connections
const connection = this.connections[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.connections.length;
return connection;
}
async waitForConnection(ws) {
return new Promise((resolve, reject) => {
ws.onopen = resolve;
ws.onerror = reject;
});
}
}
Troubleshooting¶
Common Issues¶
Connection refused or timeout
Symptoms: Cannot establish WebSocket connection Solutions: - Verify server is running on correct port - Check firewall settings - Confirm WebSocket endpoint URL
Messages not being received
Symptoms: Connection established but no data flow Solutions: - Check message format validation - Verify handler registration - Enable debug logging - Test with simple message types first
Frequent disconnections
Symptoms: Connection drops repeatedly Solutions: - Implement proper heartbeat/ping-pong - Check network stability - Increase connection timeout - Add exponential backoff for reconnection
Debug Tools¶
# Monitor WebSocket traffic
wscat -c ws://localhost:3025/extension-ws -x '{"type":"heartbeat"}'
# Check connection with curl
curl --include \
--no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
http://localhost:3025/extension-ws
# Network debugging
netstat -an | grep 3025
lsof -i :3025
Next Steps¶
- REST API - HTTP API endpoints
- MCP Protocol - AI integration protocol
- Security Guide - WebSocket security
- Performance Guide - Optimization tips