Major refactor: Multi-user Chrome MCP extension with remote server architecture
This commit is contained in:
@@ -1,38 +1,369 @@
|
||||
import { initNativeHostListener } from './native-host';
|
||||
import {
|
||||
initSemanticSimilarityListener,
|
||||
initializeSemanticEngineIfCached,
|
||||
} from './semantic-similarity';
|
||||
// Native messaging removed - using remote server only
|
||||
// import { initNativeHostListener } from './native-host';
|
||||
// Temporarily disable semantic similarity to focus on connection issues
|
||||
// import {
|
||||
// initSemanticSimilarityListener,
|
||||
// initializeSemanticEngineIfCached,
|
||||
// } from './semantic-similarity';
|
||||
import { initStorageManagerListener } from './storage-manager';
|
||||
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
|
||||
import { RemoteServerClient } from '@/utils/remote-server-client';
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '@/common/env-config';
|
||||
import { handleCallTool } from './tools';
|
||||
|
||||
// Global remote server client instance
|
||||
let remoteServerClient: RemoteServerClient | null = null;
|
||||
|
||||
/**
|
||||
* Background script entry point
|
||||
* Initializes all background services and listeners
|
||||
*/
|
||||
export default defineBackground(() => {
|
||||
// Initialize core services
|
||||
initNativeHostListener();
|
||||
initSemanticSimilarityListener();
|
||||
// Initialize remote server client first (prioritize over native messaging)
|
||||
initRemoteServerClient();
|
||||
|
||||
// Initialize core services (native messaging removed)
|
||||
// initNativeHostListener();
|
||||
// initSemanticSimilarityListener();
|
||||
initStorageManagerListener();
|
||||
|
||||
// Initialize browser event listeners for connection persistence
|
||||
initBrowserEventListeners();
|
||||
|
||||
// Conditionally initialize semantic similarity engine if model cache exists
|
||||
initializeSemanticEngineIfCached()
|
||||
.then((initialized) => {
|
||||
if (initialized) {
|
||||
console.log('Background: Semantic similarity engine initialized from cache');
|
||||
} else {
|
||||
console.log(
|
||||
'Background: Semantic similarity engine initialization skipped (no cache found)',
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Background: Failed to conditionally initialize semantic engine:', error);
|
||||
});
|
||||
// initializeSemanticEngineIfCached()
|
||||
// .then((initialized) => {
|
||||
// if (initialized) {
|
||||
// console.log('Background: Semantic similarity engine initialized from cache');
|
||||
// } else {
|
||||
// console.log(
|
||||
// 'Background: Semantic similarity engine initialization skipped (no cache found)',
|
||||
// );
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.warn('Background: Failed to conditionally initialize semantic engine:', error);
|
||||
// });
|
||||
|
||||
// Initial cleanup on startup
|
||||
cleanupModelCache().catch((error) => {
|
||||
console.warn('Background: Initial cache cleanup failed:', error);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize remote server client (without auto-connecting)
|
||||
*/
|
||||
function initRemoteServerClient() {
|
||||
try {
|
||||
remoteServerClient = new RemoteServerClient({
|
||||
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
|
||||
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
|
||||
maxReconnectAttempts: 50, // Increased for better reliability
|
||||
});
|
||||
|
||||
console.log('Background: Remote server client initialized (not connected)');
|
||||
console.log('Background: Use popup to manually connect to remote server');
|
||||
} catch (error) {
|
||||
console.error('Background: Failed to initialize remote server client:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the remote server client instance
|
||||
*/
|
||||
export function getRemoteServerClient(): RemoteServerClient | null {
|
||||
return remoteServerClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize browser event listeners for connection persistence
|
||||
*/
|
||||
function initBrowserEventListeners() {
|
||||
// Listen for browser startup events
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
console.log('Background: Browser startup detected. Manual connection required via popup.');
|
||||
if (remoteServerClient) {
|
||||
console.log('Background: Remote server client ready for manual connection');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for extension installation/update events
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
console.log('Background: Extension installed/updated:', details.reason);
|
||||
if (details.reason === 'update') {
|
||||
console.log('Background: Extension updated, manual connection required');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for browser suspension/resume events (Chrome specific)
|
||||
if (chrome.runtime.onSuspend) {
|
||||
chrome.runtime.onSuspend.addListener(() => {
|
||||
console.log('Background: Browser suspending, connection state saved');
|
||||
// Connection state is automatically saved when connected
|
||||
});
|
||||
}
|
||||
|
||||
if (chrome.runtime.onSuspendCanceled) {
|
||||
chrome.runtime.onSuspendCanceled.addListener(() => {
|
||||
console.log('Background: Browser suspend canceled, maintaining connection');
|
||||
});
|
||||
}
|
||||
|
||||
// Monitor tab events to ensure connection persists across tab operations
|
||||
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||
// Connection should persist regardless of tab switches
|
||||
if (remoteServerClient && remoteServerClient.isConnected()) {
|
||||
console.log(`Background: Tab switched to ${activeInfo.tabId}, connection maintained`);
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor window events
|
||||
chrome.windows.onFocusChanged.addListener((windowId) => {
|
||||
// Connection should persist regardless of window focus changes
|
||||
if (
|
||||
remoteServerClient &&
|
||||
remoteServerClient.isConnected() &&
|
||||
windowId !== chrome.windows.WINDOW_ID_NONE
|
||||
) {
|
||||
console.log(`Background: Window focus changed to ${windowId}, connection maintained`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Background: Browser event listeners initialized for connection persistence');
|
||||
|
||||
// Start periodic connection health check
|
||||
startConnectionHealthCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic connection health check to maintain persistent connections
|
||||
*/
|
||||
function startConnectionHealthCheck() {
|
||||
// Check connection health every 5 minutes (for monitoring only, no auto-reconnection)
|
||||
setInterval(
|
||||
() => {
|
||||
if (remoteServerClient) {
|
||||
const isConnected = remoteServerClient.isConnected();
|
||||
console.log(`Background: Connection health check - Connected: ${isConnected}`);
|
||||
|
||||
if (!isConnected) {
|
||||
console.log('Background: Connection lost. Use popup to manually reconnect.');
|
||||
// No automatic reconnection - user must manually reconnect via popup
|
||||
}
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
); // 5 minutes
|
||||
|
||||
console.log(
|
||||
'Background: Connection health check started (monitoring only, no auto-reconnection)',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from popup for remote server control
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'getRemoteServerStatus') {
|
||||
const status = remoteServerClient?.getStatus() || {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
reconnectAttempts: 0,
|
||||
connectionTime: undefined,
|
||||
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
|
||||
};
|
||||
sendResponse(status);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'connectRemoteServer') {
|
||||
if (!remoteServerClient) {
|
||||
sendResponse({ success: false, error: 'Remote server client not initialized' });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (remoteServerClient.isConnected()) {
|
||||
sendResponse({ success: true, message: 'Already connected' });
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Background: Attempting to connect to remote server...');
|
||||
remoteServerClient
|
||||
.connect()
|
||||
.then(() => {
|
||||
console.log('Background: Successfully connected to remote server');
|
||||
sendResponse({ success: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Background: Failed to connect to remote server:', error);
|
||||
sendResponse({ success: false, error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'disconnectRemoteServer') {
|
||||
if (!remoteServerClient) {
|
||||
sendResponse({ success: false, error: 'Remote server client not initialized' });
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Background: Disconnecting from remote server...');
|
||||
try {
|
||||
remoteServerClient.disconnect();
|
||||
console.log('Background: Successfully disconnected from remote server');
|
||||
sendResponse({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Background: Error during disconnect:', error);
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Disconnect failed',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'restoreRemoteConnection') {
|
||||
if (!remoteServerClient) {
|
||||
sendResponse({ success: false, error: 'Remote server client not initialized' });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (remoteServerClient.isConnected()) {
|
||||
sendResponse({ success: true, message: 'Already connected' });
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Background: Attempting to restore previous connection...');
|
||||
remoteServerClient
|
||||
.restoreConnectionFromState()
|
||||
.then((restored) => {
|
||||
if (restored) {
|
||||
console.log('Background: Successfully restored previous connection');
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
console.log('Background: No previous connection to restore');
|
||||
sendResponse({ success: false, error: 'No previous connection found' });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Background: Failed to restore previous connection:', error);
|
||||
sendResponse({ success: false, error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'getCurrentUserId') {
|
||||
if (!remoteServerClient) {
|
||||
sendResponse({ success: false, error: 'Remote server client not initialized' });
|
||||
return true;
|
||||
}
|
||||
|
||||
remoteServerClient
|
||||
.getCurrentUserId()
|
||||
.then((userId) => {
|
||||
sendResponse({ success: true, userId });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Background: Failed to get current user ID:', error);
|
||||
sendResponse({ success: false, error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'callTool') {
|
||||
handleCallTool({ name: message.toolName, args: message.params })
|
||||
.then((result) => {
|
||||
sendResponse(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'injectUserIdHelper') {
|
||||
injectUserIdHelper(message.tabId)
|
||||
.then((result) => {
|
||||
sendResponse(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ success: false, error: error.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Inject user ID helper script into a specific tab
|
||||
*/
|
||||
async function injectUserIdHelper(tabId?: number): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
let targetTabId = tabId;
|
||||
|
||||
// If no tab ID provided, use the active tab
|
||||
if (!targetTabId) {
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tabs[0]?.id) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
targetTabId = tabs[0].id;
|
||||
}
|
||||
|
||||
// Inject the user ID helper script
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: targetTabId },
|
||||
files: ['inject-scripts/user-id-helper.js'],
|
||||
});
|
||||
|
||||
// Get current user ID and inject it
|
||||
if (remoteServerClient) {
|
||||
const userId = await remoteServerClient.getCurrentUserId();
|
||||
if (userId) {
|
||||
// Inject the user ID into the page
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: targetTabId },
|
||||
func: (userId) => {
|
||||
// Make user ID available globally
|
||||
(window as any).chromeExtensionUserId = userId;
|
||||
|
||||
// Store in sessionStorage
|
||||
try {
|
||||
sessionStorage.setItem('chromeExtensionUserId', userId);
|
||||
} catch (e) {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
// Dispatch event for pages waiting for user ID
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('chromeExtensionUserIdReady', {
|
||||
detail: { userId: userId },
|
||||
}),
|
||||
);
|
||||
|
||||
console.log('Chrome Extension User ID injected:', userId);
|
||||
},
|
||||
args: [userId],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `User ID helper injected into tab ${targetTabId} with user ID: ${userId}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
message: `User ID helper injected into tab ${targetTabId} but no user ID available (not connected)`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
message: `User ID helper injected into tab ${targetTabId} but remote server client not initialized`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to inject user ID helper:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@@ -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