mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 10:14:34 +00:00
fix(browser): harden browser MCP websocket startup
- fall back across globalThis.WebSocket, undici, and ws for installed runtimes - normalize socket event handling for both WHATWG and ws implementations - add an installed-copy regression test for missing ws in Bun global layouts
This commit is contained in:
1 parent
329e7a11f7
commit
d30f5803a7
2 files changed
+176
-33
No files matched your search
+119
-27
@@ -1,6 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { WebSocket } = require('ws');
|
||||
function loadWebSocketImplementation() {
|
||||
if (typeof globalThis.WebSocket === 'function') {
|
||||
return globalThis.WebSocket;
|
||||
}
|
||||
|
||||
try {
|
||||
const { WebSocket } = require('undici');
|
||||
if (typeof WebSocket === 'function') {
|
||||
return WebSocket;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the legacy ws dependency when available.
|
||||
}
|
||||
|
||||
try {
|
||||
const { WebSocket } = require('ws');
|
||||
if (typeof WebSocket === 'function') {
|
||||
return WebSocket;
|
||||
}
|
||||
} catch {
|
||||
// Surface a dedicated error below if no implementation is available.
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Browser MCP could not find a WebSocket implementation. Tried globalThis.WebSocket, undici, and ws.'
|
||||
);
|
||||
}
|
||||
|
||||
const WebSocket = loadWebSocketImplementation();
|
||||
|
||||
const PROTOCOL_VERSION = '2024-11-05';
|
||||
const SERVER_NAME = 'ccs-browser';
|
||||
@@ -29,6 +57,66 @@ const NAVIGATION_POLL_INTERVAL_MS = 100;
|
||||
let inputBuffer = Buffer.alloc(0);
|
||||
let requestCounter = 0;
|
||||
|
||||
function addSocketListener(socket, eventName, handler) {
|
||||
if (typeof socket.addEventListener === 'function') {
|
||||
socket.addEventListener(eventName, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof socket.on === 'function') {
|
||||
socket.on(eventName, handler);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSocketMessageText(message) {
|
||||
const data = message && typeof message === 'object' && 'data' in message ? message.data : message;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return data.toString('utf8');
|
||||
}
|
||||
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return Buffer.from(data).toString('utf8');
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
|
||||
}
|
||||
|
||||
if (data && typeof data.text === 'function') {
|
||||
return await data.text();
|
||||
}
|
||||
|
||||
return String(data);
|
||||
}
|
||||
|
||||
function closeSocket(socket) {
|
||||
if (typeof socket.close === 'function') {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
function abortSocket(socket) {
|
||||
if (typeof socket.terminate === 'function') {
|
||||
socket.terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
closeSocket(socket);
|
||||
}
|
||||
|
||||
function toSocketError(error) {
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return new Error('Browser MCP lost the DevTools websocket connection.');
|
||||
}
|
||||
|
||||
function shouldExposeTools() {
|
||||
return Boolean(process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL);
|
||||
}
|
||||
@@ -298,7 +386,7 @@ async function sendCdpCommand(page, method, params = {}) {
|
||||
const timer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
ws.terminate();
|
||||
abortSocket(ws);
|
||||
reject(new Error('Browser MCP timed out waiting for a DevTools response.'));
|
||||
}
|
||||
}, CDP_TIMEOUT_MS);
|
||||
@@ -309,10 +397,10 @@ async function sendCdpCommand(page, method, params = {}) {
|
||||
}
|
||||
clearTimeout(timer);
|
||||
settled = true;
|
||||
reject(error);
|
||||
reject(toSocketError(error));
|
||||
}
|
||||
|
||||
ws.on('open', () => {
|
||||
addSocketListener(ws, 'open', () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
id: requestId,
|
||||
@@ -322,39 +410,43 @@ async function sendCdpCommand(page, method, params = {}) {
|
||||
);
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
addSocketListener(ws, 'message', (data) => {
|
||||
void (async () => {
|
||||
const raw = await getSocketMessageText(data);
|
||||
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(data.toString());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.id !== requestId) {
|
||||
return;
|
||||
}
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timer);
|
||||
settled = true;
|
||||
ws.close();
|
||||
if (message.id !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.error) {
|
||||
reject(new Error(message.error.message || 'DevTools request failed.'));
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
settled = true;
|
||||
closeSocket(ws);
|
||||
|
||||
resolve(message.result || null);
|
||||
if (message.error) {
|
||||
reject(new Error(message.error.message || 'DevTools request failed.'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(message.result || null);
|
||||
})().catch(settleError);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
addSocketListener(ws, 'error', (error) => {
|
||||
settleError(error);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
addSocketListener(ws, 'close', () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user