first commit
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
||||
import { BaseBrowserToolExecutor } from '../base-browser';
|
||||
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
||||
|
||||
const DEBUGGER_PROTOCOL_VERSION = '1.3';
|
||||
const DEFAULT_MAX_MESSAGES = 100;
|
||||
|
||||
interface ConsoleToolParams {
|
||||
url?: string;
|
||||
includeExceptions?: boolean;
|
||||
maxMessages?: number;
|
||||
}
|
||||
|
||||
interface ConsoleMessage {
|
||||
timestamp: number;
|
||||
level: string;
|
||||
text: string;
|
||||
args?: any[];
|
||||
source?: string;
|
||||
url?: string;
|
||||
lineNumber?: number;
|
||||
stackTrace?: any;
|
||||
}
|
||||
|
||||
interface ConsoleException {
|
||||
timestamp: number;
|
||||
text: string;
|
||||
url?: string;
|
||||
lineNumber?: number;
|
||||
columnNumber?: number;
|
||||
stackTrace?: any;
|
||||
}
|
||||
|
||||
interface ConsoleResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
tabId: number;
|
||||
tabUrl: string;
|
||||
tabTitle: string;
|
||||
captureStartTime: number;
|
||||
captureEndTime: number;
|
||||
totalDurationMs: number;
|
||||
messages: ConsoleMessage[];
|
||||
exceptions: ConsoleException[];
|
||||
messageCount: number;
|
||||
exceptionCount: number;
|
||||
messageLimitReached: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for capturing console output from browser tabs
|
||||
*/
|
||||
class ConsoleTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.CONSOLE;
|
||||
|
||||
async execute(args: ConsoleToolParams): Promise<ToolResult> {
|
||||
const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
|
||||
|
||||
let targetTab: chrome.tabs.Tab;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
// Navigate to the specified URL
|
||||
targetTab = await this.navigateToUrl(url);
|
||||
} else {
|
||||
// Use current active tab
|
||||
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!activeTab?.id) {
|
||||
return createErrorResponse('No active tab found and no URL provided.');
|
||||
}
|
||||
targetTab = activeTab;
|
||||
}
|
||||
|
||||
if (!targetTab?.id) {
|
||||
return createErrorResponse('Failed to identify target tab.');
|
||||
}
|
||||
|
||||
const tabId = targetTab.id;
|
||||
|
||||
// Capture console messages (one-time capture)
|
||||
const result = await this.captureConsoleMessages(tabId, {
|
||||
includeExceptions,
|
||||
maxMessages,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('ConsoleTool: Critical error during execute:', error);
|
||||
return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
|
||||
// Check if URL is already open
|
||||
const existingTabs = await chrome.tabs.query({ url });
|
||||
|
||||
if (existingTabs.length > 0 && existingTabs[0]?.id) {
|
||||
const tab = existingTabs[0];
|
||||
// Activate the existing tab
|
||||
await chrome.tabs.update(tab.id!, { active: true });
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
return tab;
|
||||
} else {
|
||||
// Create new tab with the URL
|
||||
const newTab = await chrome.tabs.create({ url, active: true });
|
||||
// Wait for tab to be ready
|
||||
await this.waitForTabReady(newTab.id!);
|
||||
return newTab;
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForTabReady(tabId: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const checkTab = async () => {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (tab.status === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkTab, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
// Tab might be closed, resolve anyway
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
checkTab();
|
||||
});
|
||||
}
|
||||
|
||||
private formatConsoleArgs(args: any[]): string {
|
||||
if (!args || args.length === 0) return '';
|
||||
|
||||
return args
|
||||
.map((arg) => {
|
||||
if (arg.type === 'string') {
|
||||
return arg.value || '';
|
||||
} else if (arg.type === 'number') {
|
||||
return String(arg.value || '');
|
||||
} else if (arg.type === 'boolean') {
|
||||
return String(arg.value || '');
|
||||
} else if (arg.type === 'object') {
|
||||
return arg.description || '[Object]';
|
||||
} else if (arg.type === 'undefined') {
|
||||
return 'undefined';
|
||||
} else if (arg.type === 'function') {
|
||||
return arg.description || '[Function]';
|
||||
} else {
|
||||
return arg.description || arg.value || String(arg);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
private async captureConsoleMessages(
|
||||
tabId: number,
|
||||
options: {
|
||||
includeExceptions: boolean;
|
||||
maxMessages: number;
|
||||
},
|
||||
): Promise<ConsoleResult> {
|
||||
const { includeExceptions, maxMessages } = options;
|
||||
const startTime = Date.now();
|
||||
const messages: ConsoleMessage[] = [];
|
||||
const exceptions: ConsoleException[] = [];
|
||||
let limitReached = false;
|
||||
|
||||
try {
|
||||
// Get tab information
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
|
||||
// Check if debugger is already attached
|
||||
const targets = await chrome.debugger.getTargets();
|
||||
const existingTarget = targets.find(
|
||||
(t) => t.tabId === tabId && t.attached && t.type === 'page',
|
||||
);
|
||||
if (existingTarget && !existingTarget.extensionId) {
|
||||
throw new Error(
|
||||
`Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Attach debugger
|
||||
try {
|
||||
await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('Cannot attach to the target with an attached client')) {
|
||||
throw new Error(
|
||||
`Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Set up event listener to collect messages
|
||||
const collectedMessages: any[] = [];
|
||||
const collectedExceptions: any[] = [];
|
||||
|
||||
const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
|
||||
if (source.tabId !== tabId) return;
|
||||
|
||||
if (method === 'Log.entryAdded' && params?.entry) {
|
||||
collectedMessages.push(params.entry);
|
||||
} else if (method === 'Runtime.consoleAPICalled' && params) {
|
||||
// Convert Runtime.consoleAPICalled to Log.entryAdded format
|
||||
const logEntry = {
|
||||
timestamp: params.timestamp,
|
||||
level: params.type || 'log',
|
||||
text: this.formatConsoleArgs(params.args || []),
|
||||
source: 'console-api',
|
||||
url: params.stackTrace?.callFrames?.[0]?.url,
|
||||
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
||||
stackTrace: params.stackTrace,
|
||||
args: params.args,
|
||||
};
|
||||
collectedMessages.push(logEntry);
|
||||
} else if (
|
||||
method === 'Runtime.exceptionThrown' &&
|
||||
includeExceptions &&
|
||||
params?.exceptionDetails
|
||||
) {
|
||||
collectedExceptions.push(params.exceptionDetails);
|
||||
}
|
||||
};
|
||||
|
||||
chrome.debugger.onEvent.addListener(eventListener);
|
||||
|
||||
try {
|
||||
// Enable Runtime domain first to capture console API calls and exceptions
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
|
||||
|
||||
// Also enable Log domain to capture other log entries
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
|
||||
|
||||
// Wait for all messages to be flushed
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Process collected messages
|
||||
for (const entry of collectedMessages) {
|
||||
if (messages.length >= maxMessages) {
|
||||
limitReached = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const message: ConsoleMessage = {
|
||||
timestamp: entry.timestamp,
|
||||
level: entry.level || 'log',
|
||||
text: entry.text || '',
|
||||
source: entry.source,
|
||||
url: entry.url,
|
||||
lineNumber: entry.lineNumber,
|
||||
};
|
||||
|
||||
if (entry.stackTrace) {
|
||||
message.stackTrace = entry.stackTrace;
|
||||
}
|
||||
|
||||
if (entry.args && Array.isArray(entry.args)) {
|
||||
message.args = entry.args;
|
||||
}
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// Process collected exceptions
|
||||
for (const exceptionDetails of collectedExceptions) {
|
||||
const exception: ConsoleException = {
|
||||
timestamp: Date.now(),
|
||||
text:
|
||||
exceptionDetails.text ||
|
||||
exceptionDetails.exception?.description ||
|
||||
'Unknown exception',
|
||||
url: exceptionDetails.url,
|
||||
lineNumber: exceptionDetails.lineNumber,
|
||||
columnNumber: exceptionDetails.columnNumber,
|
||||
};
|
||||
|
||||
if (exceptionDetails.stackTrace) {
|
||||
exception.stackTrace = exceptionDetails.stackTrace;
|
||||
}
|
||||
|
||||
exceptions.push(exception);
|
||||
}
|
||||
} finally {
|
||||
// Clean up
|
||||
chrome.debugger.onEvent.removeListener(eventListener);
|
||||
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.debugger.detach({ tabId });
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
// Sort messages by timestamp
|
||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||
exceptions.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
|
||||
tabId,
|
||||
tabUrl: tab.url || '',
|
||||
tabTitle: tab.title || '',
|
||||
captureStartTime: startTime,
|
||||
captureEndTime: endTime,
|
||||
totalDurationMs: endTime - startTime,
|
||||
messages,
|
||||
exceptions,
|
||||
messageCount: messages.length,
|
||||
exceptionCount: exceptions.length,
|
||||
messageLimitReached: limitReached,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const consoleTool = new ConsoleTool();
|
Reference in New Issue
Block a user