Major refactor: Multi-user Chrome MCP extension with remote server architecture

This commit is contained in:
nasir@endelospay.com
2025-08-21 20:09:57 +05:00
parent d97cad1736
commit 5d869f6a7c
125 changed files with 16249 additions and 11906 deletions

View File

@@ -2,18 +2,66 @@ import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
// Default window dimensions
// Default window dimensions - optimized for automation tools
const DEFAULT_WINDOW_WIDTH = 1280;
const DEFAULT_WINDOW_HEIGHT = 720;
interface NavigateToolParams {
url?: string;
newWindow?: boolean;
backgroundPage?: boolean;
width?: number;
height?: number;
refresh?: boolean;
}
/**
* Helper function to create automation-friendly background windows
* Ensures proper dimensions and timing for web automation tools
*/
async function createAutomationFriendlyBackgroundWindow(
url: string,
width: number,
height: number,
): Promise<chrome.windows.Window | null> {
try {
console.log(`Creating automation-friendly background window: ${width}x${height} for ${url}`);
// Create window with optimal settings for automation
const window = await chrome.windows.create({
url: url,
width: width,
height: height,
focused: false, // Don't steal focus from user
state: chrome.windows.WindowState.NORMAL, // Start in normal state
type: 'normal', // Normal window type for full automation compatibility
// Ensure window is created with proper viewport
left: 0, // Position consistently for automation
top: 0,
});
if (window && window.id !== undefined) {
// Wait for window to be properly established
await new Promise((resolve) => setTimeout(resolve, 1500));
// Verify window still exists and has correct dimensions
const windowInfo = await chrome.windows.get(window.id);
if (windowInfo && windowInfo.width === width && windowInfo.height === height) {
console.log(`Background window ${window.id} established with correct dimensions`);
return window;
} else {
console.warn(`Window ${window.id} dimensions may not be correct`);
return window; // Return anyway, might still work
}
}
return null;
} catch (error) {
console.error('Failed to create automation-friendly background window:', error);
return null;
}
}
/**
* Tool for navigating to URLs in browser tabs or windows
*/
@@ -21,11 +69,26 @@ class NavigateTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NAVIGATE;
async execute(args: NavigateToolParams): Promise<ToolResult> {
// Check if backgroundPage was explicitly provided, if not, check user settings
let backgroundPage = args.backgroundPage;
if (backgroundPage === undefined) {
try {
const result = await chrome.storage.local.get(['openUrlsInBackground']);
// Default to true for background windows (changed from false to true)
backgroundPage =
result.openUrlsInBackground !== undefined ? result.openUrlsInBackground : true;
console.log(`Using stored background page preference: ${backgroundPage}`);
} catch (error) {
console.warn('Failed to load background page preference, using default (true):', error);
backgroundPage = true; // Default to background windows
}
}
const { newWindow = false, width, height, url, refresh = false } = args;
console.log(
`Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
args,
{ ...args, backgroundPage },
);
try {
@@ -121,7 +184,83 @@ class NavigateTool extends BaseBrowserToolExecutor {
}
}
// 2. If URL is not already open, decide how to open it based on options
// 2. Handle background page option
if (backgroundPage) {
console.log(
'Opening URL in background page using full-size window that will be minimized.',
);
const windowWidth = typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH;
const windowHeight = typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT;
// Create automation-friendly background window
const backgroundWindow = await createAutomationFriendlyBackgroundWindow(
url!,
windowWidth,
windowHeight,
);
if (backgroundWindow && backgroundWindow.id !== undefined) {
console.log(
`Background window created with ID: ${backgroundWindow.id}, dimensions: ${windowWidth}x${windowHeight}`,
);
try {
// Verify window still exists before minimizing
const windowInfo = await chrome.windows.get(backgroundWindow.id);
if (windowInfo) {
console.log(
`Minimizing window ${backgroundWindow.id} while preserving automation accessibility`,
);
// Now minimize the window to keep it in background while maintaining automation accessibility
await chrome.windows.update(backgroundWindow.id, {
state: chrome.windows.WindowState.MINIMIZED,
});
console.log(
`URL opened in background Window ID: ${backgroundWindow.id} (${windowWidth}x${windowHeight} then minimized)`,
);
}
} catch (error) {
console.warn(`Failed to minimize window ${backgroundWindow.id}:`, error);
// Continue anyway as the window was created successfully
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message:
'Opened URL in background page (full-size window then minimized for automation compatibility)',
windowId: backgroundWindow.id,
width: windowWidth,
height: windowHeight,
tabs: backgroundWindow.tabs
? backgroundWindow.tabs.map((tab) => ({
tabId: tab.id,
url: tab.url,
}))
: [],
automationReady: true,
minimized: true,
dimensions: `${windowWidth}x${windowHeight}`,
}),
},
],
isError: false,
};
} else {
console.error('Failed to create automation-friendly background window');
return createErrorResponse(
'Failed to create background window with proper automation compatibility',
);
}
}
// 3. If URL is not already open, decide how to open it based on options
const openInNewWindow = newWindow || typeof width === 'number' || typeof height === 'number';
if (openInNewWindow) {

View File

@@ -0,0 +1,184 @@
import { BaseBrowserToolExecutor } from '../base-browser';
import { createErrorResponse, createSuccessResponse } from '../../../../common/tool-handler';
import { ERROR_MESSAGES } from '../../../../common/constants';
export class EnhancedSearchTool extends BaseBrowserToolExecutor {
async chromeSearchGoogle(args: {
query: string;
openGoogle?: boolean;
extractResults?: boolean;
maxResults?: number;
}) {
const { query, openGoogle = true, extractResults = true, maxResults = 10 } = args;
try {
// Step 1: Navigate to Google if requested
if (openGoogle) {
await this.navigateToGoogle();
await this.sleep(3000); // Wait for page to load
}
// Step 2: Find and fill search box
const searchSuccess = await this.performGoogleSearch(query);
if (!searchSuccess) {
return createErrorResponse(
'Failed to perform Google search - could not find or interact with search box',
);
}
// Step 3: Wait for results to load
await this.sleep(3000);
// Step 4: Extract results if requested
if (extractResults) {
const results = await this.extractSearchResults(maxResults);
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: true,
results,
});
}
return createSuccessResponse({
query,
searchCompleted: true,
resultsExtracted: false,
message: 'Google search completed successfully',
});
} catch (error) {
return createErrorResponse(
`Error performing Google search: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
async chromeSubmitForm(args: {
formSelector?: string;
inputSelector?: string;
submitMethod?: 'enter' | 'button' | 'auto';
}) {
const { formSelector = 'form', inputSelector, submitMethod = 'auto' } = args;
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tabId = tabs[0].id;
// Inject form submission script
await this.injectContentScript(tabId, ['inject-scripts/form-submit-helper.js']);
const result = await this.sendMessageToTab(tabId, {
action: 'submitForm',
formSelector,
inputSelector,
submitMethod,
});
if (result.error) {
return createErrorResponse(result.error);
}
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
`Error submitting form: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
private async navigateToGoogle(): Promise<void> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
await chrome.tabs.update(tabs[0].id, { url: 'https://www.google.com' });
}
private async performGoogleSearch(query: string): Promise<boolean> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
// Enhanced search box selectors
const searchSelectors = [
'#APjFqb', // Main Google search box ID
'textarea[name="q"]', // Google search textarea
'input[name="q"]', // Google search input (fallback)
'[role="combobox"]', // Role-based selector
'.gLFyf', // Google search box class
'textarea[aria-label*="Search"]', // Aria-label based
'[title*="Search"]', // Title attribute
'.gsfi', // Google search field input class
'#lst-ib', // Alternative Google search ID
'input[type="search"]', // Generic search input
'textarea[role="combobox"]', // Textarea with combobox role
];
// Inject search helper script
await this.injectContentScript(tabId, ['inject-scripts/enhanced-search-helper.js']);
for (const selector of searchSelectors) {
try {
const result = await this.sendMessageToTab(tabId, {
action: 'performGoogleSearch',
selector,
query,
});
if (result.success) {
return true;
}
} catch (error) {
console.debug(`Search selector ${selector} failed:`, error);
continue;
}
}
return false;
}
private async extractSearchResults(maxResults: number): Promise<any[]> {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
throw new Error('No active tab found');
}
const tabId = tabs[0].id;
const result = await this.sendMessageToTab(tabId, {
action: 'extractSearchResults',
maxResults,
});
return result.results || [];
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export tool instances
export const searchGoogleTool = new (class extends EnhancedSearchTool {
name = 'chrome_search_google';
async execute(args: any) {
return await this.chromeSearchGoogle(args);
}
})();
export const submitFormTool = new (class extends EnhancedSearchTool {
name = 'chrome_submit_form';
async execute(args: any) {
return await this.chromeSubmitForm(args);
}
})();

View File

@@ -12,3 +12,4 @@ export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { consoleTool } from './console';
export { searchGoogleTool, submitFormTool } from './enhanced-search';

View File

@@ -134,10 +134,13 @@ class NetworkDebuggerStartTool extends BaseBrowserToolExecutor {
}
NetworkDebuggerStartTool.instance = this;
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
// Only add listeners if chrome APIs are available (not during build)
if (typeof chrome !== 'undefined' && chrome.debugger?.onEvent) {
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
}
}
private handleTabRemoved(tabId: number) {

View File

@@ -3,7 +3,7 @@ import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 30000; // For sending a single request via content script
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 60000; // For sending a single request via content script - increased from 30000
interface NetworkRequestToolParams {
url: string; // URL is always required

View File

@@ -17,15 +17,31 @@ export interface ToolCallParam {
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
console.log('🛠️ [Tool Handler] Executing tool:', {
toolName: param.name,
hasArgs: !!param.args,
availableTools: Array.from(toolsMap.keys()),
args: param.args,
});
const tool = toolsMap.get(param.name);
if (!tool) {
console.error('🛠️ [Tool Handler] Tool not found:', param.name);
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Starting tool execution for:', param.name);
const result = await tool.execute(param.args);
console.log('🛠️ [Tool Handler] Tool execution completed:', {
toolName: param.name,
hasResult: !!result,
isError: result?.isError,
result,
});
return result;
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
console.error(`🛠️ [Tool Handler] Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);