Skip to content

Commit e7acffb

Browse files
committed
feat: implement console logs tool with DevTools integration
- Add DevTools page to inject console hijacking code - Capture all console methods (log, info, warn, error, debug, trace, etc.) - Store console logs in tab state with proper serialization - Add filtering by level, timestamp (before), and limit - Require DevTools to be open for console log capture - Add console log test elements to test.html - Update TabState.getConsoleLogs() to handle all filtering
1 parent 92c15e8 commit e7acffb

12 files changed

Lines changed: 335 additions & 7 deletions

e2e/manual-test-console.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Manual test for console logs tool
2+
// This cannot be automated because it requires DevTools to be open
3+
4+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5+
import { WebSocketTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
6+
7+
async function testConsoleLogs() {
8+
console.log('Starting manual console logs test...');
9+
console.log('IMPORTANT: Make sure Chrome DevTools is open for the test tab!');
10+
11+
const transport = new WebSocketTransport(new URL('ws://localhost:61822/mcp'));
12+
const client = new Client({ name: 'console-test', version: '1.0.0' }, { capabilities: {} });
13+
14+
await client.connect(transport);
15+
console.log('Connected to MCP server');
16+
17+
// List tabs
18+
const tabsResult = await client.callTool('list_tabs', {});
19+
const tabs = JSON.parse(tabsResult.content[0].text).tabs;
20+
21+
if (tabs.length === 0) {
22+
console.log('No tabs connected. Please open test.html and connect.');
23+
await client.close();
24+
return;
25+
}
26+
27+
const tabId = tabs[0].tabId;
28+
console.log(`Using tab ${tabId}: ${tabs[0].title}`);
29+
30+
// Wait a moment for console logs to be generated
31+
console.log('Waiting for console logs to be generated...');
32+
await new Promise(resolve => setTimeout(resolve, 2000));
33+
34+
try {
35+
// Try to get console logs
36+
console.log('Attempting to get console logs...');
37+
const logsResult = await client.callTool('console_logs', { tabId });
38+
const logs = JSON.parse(logsResult.content[0].text);
39+
40+
console.log('Console logs retrieved successfully:');
41+
console.log(JSON.stringify(logs, null, 2));
42+
43+
// Test with filters
44+
console.log('\nTesting with level filter (errors only)...');
45+
const errorLogs = await client.callTool('console_logs', { tabId, level: 'error' });
46+
console.log(JSON.parse(errorLogs.content[0].text));
47+
48+
// Test with limit
49+
console.log('\nTesting with limit (5 logs)...');
50+
const limitedLogs = await client.callTool('console_logs', { tabId, limit: 5 });
51+
console.log(JSON.parse(limitedLogs.content[0].text));
52+
53+
} catch (error) {
54+
if (error.message.includes('DevTools')) {
55+
console.error('ERROR: DevTools must be open to use console logs tool');
56+
console.error('Please open Chrome DevTools (F12) and try again');
57+
} else {
58+
console.error('Error getting console logs:', error);
59+
}
60+
}
61+
62+
await client.close();
63+
console.log('Test complete');
64+
}
65+
66+
testConsoleLogs().catch(console.error);

extension/background.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
137137
}
138138
return false;
139139
}
140+
141+
if (request.type === 'consoleLog') {
142+
// Handle console log from content script
143+
const tabState = tabManager.getTab(sender.tab.id);
144+
if (tabState) {
145+
if (request.level === 'clear') {
146+
// Clear console logs
147+
tabManager.clearConsoleLogs(sender.tab.id);
148+
} else {
149+
// Add console log
150+
tabManager.addConsoleLog(sender.tab.id, request.level, request.args, request.stack);
151+
}
152+
}
153+
return false;
154+
}
140155
}
141156

142157
// Handle messages from popup/panel (not from content scripts)

extension/content-script.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
window.addEventListener('kapture-message', (event) => {
33
chrome.runtime.sendMessage(event.detail);
44
});
5+
6+
// Listen for console log events from the injected code
7+
window.addEventListener('kapture-console', (event) => {
8+
// Send console log to background script
9+
chrome.runtime.sendMessage({
10+
type: 'consoleLog',
11+
...event.detail
12+
});
13+
});
514
function ready() {
615
// Notify background script that content script is ready
716
chrome.runtime.sendMessage({ type: 'contentScriptReady' });

extension/devtools-page.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// This devtools page runs alongside the panel and handles console log injection
2+
// It has access to chrome.devtools.inspectedWindow.eval for the inspected tab
3+
4+
// Injection code that hijacks console methods
5+
const injectionCode = `
6+
(function() {
7+
const isOverridden = !console.log.toString().includes('[native code]');
8+
9+
// Check if already injected
10+
if (isOverridden) {
11+
return;
12+
}
13+
14+
// Store original console methods
15+
const originalConsole = {
16+
log: console.log,
17+
error: console.error,
18+
warn: console.warn,
19+
info: console.info,
20+
debug: console.debug,
21+
trace: console.trace,
22+
table: console.table,
23+
group: console.group,
24+
groupCollapsed: console.groupCollapsed,
25+
groupEnd: console.groupEnd,
26+
clear: console.clear
27+
};
28+
29+
// Helper to serialize arguments
30+
function serializeArgs(args) {
31+
return Array.from(args).map(arg => {
32+
try {
33+
if (arg === undefined) return 'undefined';
34+
if (arg === null) return 'null';
35+
if (typeof arg === 'function') return arg.toString();
36+
if (typeof arg === 'object') {
37+
// Handle circular references
38+
const seen = new WeakSet();
39+
return JSON.stringify(arg, function(key, value) {
40+
if (typeof value === 'object' && value !== null) {
41+
if (seen.has(value)) return '[Circular]';
42+
seen.add(value);
43+
}
44+
if (typeof value === 'function') return value.toString();
45+
return value;
46+
});
47+
}
48+
return String(arg);
49+
} catch (e) {
50+
return String(arg);
51+
}
52+
});
53+
}
54+
55+
originalConsole.log('[Kapture] overriding console methods');
56+
57+
// Override console methods (all except clear)
58+
['log', 'error', 'warn', 'info', 'debug', 'trace', 'table', 'group', 'groupCollapsed', 'groupEnd'].forEach(level => {
59+
console[level] = function(...args) {
60+
// Create log entry
61+
const event = new CustomEvent('kapture-console', {
62+
detail: {
63+
level: level,
64+
args: serializeArgs(args),
65+
timestamp: new Date().toISOString(),
66+
stack: new Error().stack
67+
}
68+
});
69+
70+
// Dispatch event for content script to capture
71+
window.dispatchEvent(event);
72+
73+
// Call original method
74+
originalConsole[level].apply(console, args);
75+
};
76+
});
77+
78+
// Override console.clear
79+
console.clear = function() {
80+
// Dispatch clear event
81+
const event = new CustomEvent('kapture-console', {
82+
detail: {
83+
level: 'clear'
84+
}
85+
});
86+
originalConsole.log('[Kapture] Dispatching console clear event');
87+
window.dispatchEvent(event);
88+
89+
// Call original method
90+
originalConsole.clear.apply(console);
91+
};
92+
93+
// Log that injection is complete
94+
originalConsole.log('[Kapture] Console capture injected into page context');
95+
})();
96+
`;
97+
98+
// Track if we've injected for this tab
99+
let injected = false;
100+
101+
// Function to inject the console hijacking code
102+
function injectConsoleHijack() {
103+
if (injected) return;
104+
105+
chrome.devtools.inspectedWindow.eval(injectionCode, (result, error) => {
106+
if (error) {
107+
console.error('[Kapture DevTools Page] Failed to inject console hijack:', error);
108+
} else {
109+
console.log('[Kapture DevTools Page] Console hijack injected successfully');
110+
injected = true;
111+
}
112+
});
113+
}
114+
115+
// Inject immediately when devtools page loads
116+
injectConsoleHijack();
117+
118+
// Re-inject on navigation (page reload)
119+
chrome.devtools.network.onNavigated.addListener(() => {
120+
console.log('[Kapture DevTools Page] Page navigated, re-injecting console hijack');
121+
injected = false;
122+
// Small delay to ensure page is ready
123+
setTimeout(injectConsoleHijack, 100);
124+
});
125+
126+
// Listen for messages from the background script to check if DevTools is open
127+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
128+
if (request.type === 'checkDevToolsOpen') {
129+
// If this script is running, DevTools is open
130+
sendResponse({ devToolsOpen: true, tabId: chrome.devtools.inspectedWindow.tabId });
131+
return true;
132+
}
133+
});

extension/devtools.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html>
33
<head>
44
<script src="devtools.js"></script>
5+
<script src="devtools-page.js"></script>
56
</head>
67
<body>
78
</body>

extension/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "Kapture MCP Browser Automation",
44
"short_name": "Kapture",
5-
"version": "0.12.0",
5+
"version": "0.13.0",
66
"description": "Remote browser automation via MCP - DevTools Extension",
77
"author": "William Kapke",
88
"homepage_url": "https://github.com/williamkapke/kapture",

extension/modules/background-commands.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { keypress } from './background-keypress.js';
22
import { click, hover } from './background-click.js';
33
import { navigate, back, forward, close } from './background-navigate.js';
44
import { screenshot } from './background-screenshot.js';
5+
import { getLogs } from './background-console.js';
56

67
export const getFromContentScript = async (tabId, command, params, ) => {
78
return await chrome.tabs.sendMessage(tabId, { command, params });
@@ -55,5 +56,6 @@ export const backgroundCommands = {
5556
click,
5657
hover,
5758
keypress,
58-
screenshot
59+
screenshot,
60+
getLogs
5961
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Import helper functions from background-commands
2+
import { respondWith, respondWithError } from './background-commands.js';
3+
4+
export async function getLogs(tabState, { before, limit = 100, level }) {
5+
// Check if DevTools is open by sending a message to all devtools pages
6+
try {
7+
// Try to communicate with the devtools page
8+
const devToolsCheckPromise = new Promise((resolve) => {
9+
// Set a timeout in case DevTools is not open
10+
const timeout = setTimeout(() => {
11+
resolve({ devToolsOpen: false });
12+
}, 100);
13+
14+
// Send message to check if DevTools is open
15+
chrome.runtime.sendMessage({ type: 'checkDevToolsOpen' }, (response) => {
16+
clearTimeout(timeout);
17+
if (chrome.runtime.lastError) {
18+
resolve({ devToolsOpen: false });
19+
} else {
20+
resolve(response || { devToolsOpen: false });
21+
}
22+
});
23+
});
24+
25+
const devToolsCheck = await devToolsCheckPromise;
26+
27+
if (!devToolsCheck.devToolsOpen) {
28+
return respondWithError(tabState.tabId, 'DEVTOOLS_NOT_OPEN', 'Please open Chrome DevTools to use the console logs tool');
29+
}
30+
31+
// DevTools is open, get the logs from the tab state using its built-in method
32+
const logs = tabState.getConsoleLogs(limit, level, before);
33+
34+
// Check if there are more logs available
35+
const totalFilteredCount = tabState.getConsoleLogs(null, level, before).length;
36+
const hasMore = totalFilteredCount > limit;
37+
38+
return respondWith(tabState.tabId, {
39+
logs: logs,
40+
hasMore: hasMore,
41+
totalCount: tabState.getConsoleLogCount()
42+
});
43+
} catch (error) {
44+
return respondWithError(tabState.tabId, 'CONSOLE_LOG_ERROR', error.message);
45+
}
46+
}

extension/modules/tab-state.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,20 @@ export class TabState {
129129
this.consoleLogs = [];
130130
}
131131

132-
getConsoleLogs(limit = null, level = null) {
132+
getConsoleLogs(limit = null, level = null, before = null) {
133133
let logs = this.consoleLogs;
134134

135135
if (level) {
136136
logs = logs.filter(log => log.level === level);
137137
}
138138

139+
if (before) {
140+
const beforeTimestamp = new Date(before).getTime();
141+
logs = logs.filter(log =>
142+
new Date(log.timestamp).getTime() < beforeTimestamp
143+
);
144+
}
145+
139146
if (limit === null) {
140147
return [...logs];
141148
}

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kapture-mcp-server",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "MCP server for Kapture browser automation",
55
"main": "dist/index.js",
66
"type": "module",

0 commit comments

Comments
 (0)