Major refactor: Multi-user Chrome MCP extension with remote server architecture
This commit is contained in:
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
})();
|
@@ -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';
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
);
|
||||
|
Reference in New Issue
Block a user