Skip to content

Commit 24fe14b

Browse files
committed
Add WebDriver BiDi support for real-time browser diagnostics
Automatically enables BiDi when starting a browser session to passively capture console logs, JavaScript errors, and network activity in the background. Falls back silently if the browser/driver doesn't support it. New tools: - get_console_logs: retrieve captured console messages (log, warn, error) - get_page_errors: retrieve JS errors/exceptions with stack traces - get_network_logs: retrieve network responses and failed requests Design decisions: - No opt-in flag — BiDi is an implementation detail, not a user choice - Graceful fallback — older browsers work exactly as before - All BiDi event callbacks wrapped in try/catch to prevent server crashes - Per-session log buffers, cleaned up automatically on close Closes #55
1 parent 74caf79 commit 24fe14b

5 files changed

Lines changed: 584 additions & 4 deletions

File tree

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ A Model Context Protocol (MCP) server implementation for Selenium WebDriver, ena
2323
- Upload files
2424
- Support for headless mode
2525
- Manage browser cookies (add, get, delete)
26+
- **Real-time diagnostics** via WebDriver BiDi:
27+
- Console log capture (info, warn, error)
28+
- JavaScript error detection with stack traces
29+
- Network request monitoring (successes and failures)
2630

2731
## Supported Browsers
2832

@@ -791,6 +795,67 @@ Deletes cookies from the current browser session. Deletes a specific cookie by n
791795
}
792796
```
793797

798+
### get_console_logs
799+
Retrieves captured browser console messages (log, warn, error, etc.). Console logs are automatically captured in the background via WebDriver BiDi when the browser supports it — no configuration needed.
800+
801+
**Parameters:**
802+
| Parameter | Type | Required | Description |
803+
|-----------|------|----------|-------------|
804+
| clear | boolean | No | Clear the captured logs after retrieving them (default: false) |
805+
806+
**Example:**
807+
```json
808+
{
809+
"tool": "get_console_logs",
810+
"parameters": {}
811+
}
812+
```
813+
814+
### get_page_errors
815+
Retrieves captured JavaScript errors and uncaught exceptions with full stack traces. Errors are automatically captured in the background via WebDriver BiDi.
816+
817+
**Parameters:**
818+
| Parameter | Type | Required | Description |
819+
|-----------|------|----------|-------------|
820+
| clear | boolean | No | Clear the captured errors after retrieving them (default: false) |
821+
822+
**Example:**
823+
```json
824+
{
825+
"tool": "get_page_errors",
826+
"parameters": {}
827+
}
828+
```
829+
830+
### get_network_logs
831+
Retrieves captured network activity including successful responses and failed requests. Network logs are automatically captured in the background via WebDriver BiDi.
832+
833+
**Parameters:**
834+
| Parameter | Type | Required | Description |
835+
|-----------|------|----------|-------------|
836+
| clear | boolean | No | Clear the captured logs after retrieving them (default: false) |
837+
838+
**Example:**
839+
```json
840+
{
841+
"tool": "get_network_logs",
842+
"parameters": {}
843+
}
844+
```
845+
846+
---
847+
848+
## Diagnostics (WebDriver BiDi)
849+
850+
The server automatically enables [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) when starting a browser session. BiDi provides real-time, passive capture of browser diagnostics — console messages, JavaScript errors, and network activity are collected in the background without any extra configuration.
851+
852+
This is especially useful for AI agents: when something goes wrong on a page, the agent can check `get_console_logs` and `get_page_errors` to understand *why*, rather than relying solely on screenshots.
853+
854+
- **Automatic**: BiDi is enabled by default when the browser supports it
855+
- **Graceful fallback**: If the browser or driver doesn't support BiDi, the session starts normally and the diagnostic tools return a helpful message
856+
- **No performance impact**: Logs are passively captured via event listeners — no polling or extra requests
857+
- **Per-session**: Each browser session has its own log buffers, cleaned up automatically on session close
858+
794859
## License
795860

796861
MIT

src/lib/server.js

Lines changed: 225 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import { Options as FirefoxOptions } from 'selenium-webdriver/firefox.js';
1010
import { Options as EdgeOptions } from 'selenium-webdriver/edge.js';
1111
import { Options as SafariOptions } from 'selenium-webdriver/safari.js';
1212

13+
// BiDi imports — loaded dynamically to avoid hard failures if not available
14+
let LogInspector, Network;
15+
try {
16+
LogInspector = (await import('selenium-webdriver/bidi/logInspector.js')).default;
17+
const networkModule = await import('selenium-webdriver/bidi/network.js');
18+
Network = networkModule.Network;
19+
} catch (_) {
20+
// BiDi modules not available in this selenium-webdriver version
21+
LogInspector = null;
22+
Network = null;
23+
}
24+
1325

1426
// Create an MCP server
1527
const server = new McpServer({
@@ -20,7 +32,13 @@ const server = new McpServer({
2032
// Server state
2133
const state = {
2234
drivers: new Map(),
23-
currentSession: null
35+
currentSession: null,
36+
bidiAvailable: new Map(), // sessionId → boolean
37+
consoleLogs: new Map(), // sessionId → Array
38+
pageErrors: new Map(), // sessionId → Array
39+
networkLogs: new Map(), // sessionId → Array
40+
logInspectors: new Map(), // sessionId → LogInspector (for cleanup)
41+
networkInspectors: new Map() // sessionId → Network (for cleanup)
2442
};
2543

2644
// Helper functions
@@ -69,6 +87,12 @@ server.tool(
6987
let builder = new Builder();
7088
let driver;
7189
let warnings = [];
90+
91+
// Enable BiDi websocket if the modules are available
92+
if (LogInspector && Network) {
93+
builder = builder.withCapabilities({ 'webSocketUrl': true });
94+
}
95+
7296
switch (browser) {
7397
case 'chrome': {
7498
const chromeOptions = new ChromeOptions();
@@ -134,7 +158,89 @@ server.tool(
134158
state.drivers.set(sessionId, driver);
135159
state.currentSession = sessionId;
136160

161+
// Attempt to enable BiDi for real-time log capture
162+
let bidiEnabled = false;
163+
if (LogInspector && Network) {
164+
try {
165+
state.consoleLogs.set(sessionId, []);
166+
state.pageErrors.set(sessionId, []);
167+
state.networkLogs.set(sessionId, []);
168+
169+
const logInspector = await LogInspector(driver);
170+
await logInspector.onConsoleEntry((entry) => {
171+
try {
172+
const logs = state.consoleLogs.get(sessionId);
173+
if (logs) {
174+
logs.push({
175+
level: entry.level,
176+
text: entry.text,
177+
timestamp: entry.timestamp,
178+
type: entry.type,
179+
method: entry.method,
180+
args: entry.args
181+
});
182+
}
183+
} catch (_) { /* ignore malformed entry */ }
184+
});
185+
await logInspector.onJavascriptLog((entry) => {
186+
try {
187+
const errors = state.pageErrors.get(sessionId);
188+
if (errors) {
189+
errors.push({
190+
level: entry.level,
191+
text: entry.text,
192+
timestamp: entry.timestamp,
193+
type: entry.type,
194+
stackTrace: entry.stackTrace
195+
});
196+
}
197+
} catch (_) { /* ignore malformed entry */ }
198+
});
199+
state.logInspectors.set(sessionId, logInspector);
200+
201+
const network = await Network(driver);
202+
await network.responseCompleted((event) => {
203+
try {
204+
const logs = state.networkLogs.get(sessionId);
205+
if (logs) {
206+
logs.push({
207+
type: 'response',
208+
url: event.request?.url,
209+
status: event.response?.status,
210+
method: event.request?.method,
211+
mimeType: event.response?.mimeType,
212+
timestamp: Date.now()
213+
});
214+
}
215+
} catch (_) { /* ignore malformed event */ }
216+
});
217+
await network.fetchError((event) => {
218+
try {
219+
const logs = state.networkLogs.get(sessionId);
220+
if (logs) {
221+
logs.push({
222+
type: 'error',
223+
url: event.request?.url,
224+
method: event.request?.method,
225+
errorText: event.errorText,
226+
timestamp: Date.now()
227+
});
228+
}
229+
} catch (_) { /* ignore malformed event */ }
230+
});
231+
state.networkInspectors.set(sessionId, network);
232+
233+
bidiEnabled = true;
234+
} catch (_) {
235+
// BiDi not supported by this browser/driver — continue without it
236+
}
237+
}
238+
state.bidiAvailable.set(sessionId, bidiEnabled);
239+
137240
let message = `Browser started with session_id: ${sessionId}`;
241+
if (bidiEnabled) {
242+
message += ' (BiDi enabled: console logs, JS errors, and network activity are being captured)';
243+
}
138244
if (warnings.length > 0) {
139245
message += `\nWarnings: ${warnings.join(' ')}`;
140246
}
@@ -473,9 +579,16 @@ server.tool(
473579
async () => {
474580
try {
475581
const driver = getDriver();
476-
await driver.quit();
477-
state.drivers.delete(state.currentSession);
478582
const sessionId = state.currentSession;
583+
await driver.quit();
584+
state.drivers.delete(sessionId);
585+
// Clean up BiDi state
586+
state.bidiAvailable.delete(sessionId);
587+
state.consoleLogs.delete(sessionId);
588+
state.pageErrors.delete(sessionId);
589+
state.networkLogs.delete(sessionId);
590+
state.logInspectors.delete(sessionId);
591+
state.networkInspectors.delete(sessionId);
479592
state.currentSession = null;
480593
return {
481594
content: [{ type: 'text', text: `Browser session ${sessionId} closed` }]
@@ -957,6 +1070,109 @@ server.tool(
9571070
}
9581071
);
9591072

1073+
// BiDi Diagnostic Tools
1074+
server.tool(
1075+
"get_console_logs",
1076+
"returns browser console messages (log, warn, info, debug) captured via WebDriver BiDi. Useful for debugging page behavior, seeing application output, and catching warnings.",
1077+
{
1078+
clear: z.boolean().optional().describe("Clear the logs after returning them (default: false)")
1079+
},
1080+
async ({ clear = false }) => {
1081+
try {
1082+
getDriver(); // ensure active session
1083+
const sessionId = state.currentSession;
1084+
if (!state.bidiAvailable.get(sessionId)) {
1085+
return {
1086+
content: [{ type: 'text', text: 'Console log capture is not available (BiDi not supported by this browser/driver)' }]
1087+
};
1088+
}
1089+
const logs = state.consoleLogs.get(sessionId) || [];
1090+
const result = logs.length === 0
1091+
? 'No console logs captured'
1092+
: JSON.stringify(logs, null, 2);
1093+
if (clear) {
1094+
state.consoleLogs.set(sessionId, []);
1095+
}
1096+
return {
1097+
content: [{ type: 'text', text: result }]
1098+
};
1099+
} catch (e) {
1100+
return {
1101+
content: [{ type: 'text', text: `Error getting console logs: ${e.message}` }],
1102+
isError: true
1103+
};
1104+
}
1105+
}
1106+
);
1107+
1108+
server.tool(
1109+
"get_page_errors",
1110+
"returns JavaScript errors and exceptions captured via WebDriver BiDi. Includes stack traces when available. Essential for diagnosing why a page is broken or a feature isn't working.",
1111+
{
1112+
clear: z.boolean().optional().describe("Clear the errors after returning them (default: false)")
1113+
},
1114+
async ({ clear = false }) => {
1115+
try {
1116+
getDriver(); // ensure active session
1117+
const sessionId = state.currentSession;
1118+
if (!state.bidiAvailable.get(sessionId)) {
1119+
return {
1120+
content: [{ type: 'text', text: 'Page error capture is not available (BiDi not supported by this browser/driver)' }]
1121+
};
1122+
}
1123+
const errors = state.pageErrors.get(sessionId) || [];
1124+
const result = errors.length === 0
1125+
? 'No page errors captured'
1126+
: JSON.stringify(errors, null, 2);
1127+
if (clear) {
1128+
state.pageErrors.set(sessionId, []);
1129+
}
1130+
return {
1131+
content: [{ type: 'text', text: result }]
1132+
};
1133+
} catch (e) {
1134+
return {
1135+
content: [{ type: 'text', text: `Error getting page errors: ${e.message}` }],
1136+
isError: true
1137+
};
1138+
}
1139+
}
1140+
);
1141+
1142+
server.tool(
1143+
"get_network_logs",
1144+
"returns network activity (completed responses and failed requests) captured via WebDriver BiDi. Shows HTTP status codes, URLs, methods, and error details. Useful for diagnosing failed API calls and broken resources.",
1145+
{
1146+
clear: z.boolean().optional().describe("Clear the logs after returning them (default: false)")
1147+
},
1148+
async ({ clear = false }) => {
1149+
try {
1150+
getDriver(); // ensure active session
1151+
const sessionId = state.currentSession;
1152+
if (!state.bidiAvailable.get(sessionId)) {
1153+
return {
1154+
content: [{ type: 'text', text: 'Network log capture is not available (BiDi not supported by this browser/driver)' }]
1155+
};
1156+
}
1157+
const logs = state.networkLogs.get(sessionId) || [];
1158+
const result = logs.length === 0
1159+
? 'No network activity captured'
1160+
: JSON.stringify(logs, null, 2);
1161+
if (clear) {
1162+
state.networkLogs.set(sessionId, []);
1163+
}
1164+
return {
1165+
content: [{ type: 'text', text: result }]
1166+
};
1167+
} catch (e) {
1168+
return {
1169+
content: [{ type: 'text', text: `Error getting network logs: ${e.message}` }],
1170+
isError: true
1171+
};
1172+
}
1173+
}
1174+
);
1175+
9601176
// Resources
9611177
server.resource(
9621178
"browser-status",
@@ -986,6 +1202,12 @@ async function cleanup() {
9861202
}
9871203
}
9881204
state.drivers.clear();
1205+
state.bidiAvailable.clear();
1206+
state.consoleLogs.clear();
1207+
state.pageErrors.clear();
1208+
state.networkLogs.clear();
1209+
state.logInspectors.clear();
1210+
state.networkInspectors.clear();
9891211
state.currentSession = null;
9901212
process.exit(0);
9911213
}

0 commit comments

Comments
 (0)