618 lines
20 KiB
TypeScript
618 lines
20 KiB
TypeScript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
|
import { BaseBrowserToolExecutor } from '../base-browser';
|
|
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
|
|
|
// 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
|
|
*/
|
|
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, backgroundPage },
|
|
);
|
|
|
|
try {
|
|
// Handle refresh option first
|
|
if (refresh) {
|
|
console.log('Refreshing current active tab');
|
|
|
|
// Get current active tab
|
|
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
|
|
if (!activeTab || !activeTab.id) {
|
|
return createErrorResponse('No active tab found to refresh');
|
|
}
|
|
|
|
// Reload the tab
|
|
await chrome.tabs.reload(activeTab.id);
|
|
|
|
console.log(`Refreshed tab ID: ${activeTab.id}`);
|
|
|
|
// Get updated tab information
|
|
const updatedTab = await chrome.tabs.get(activeTab.id);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Successfully refreshed current tab',
|
|
tabId: updatedTab.id,
|
|
windowId: updatedTab.windowId,
|
|
url: updatedTab.url,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
|
|
// Validate that url is provided when not refreshing
|
|
if (!url) {
|
|
return createErrorResponse('URL parameter is required when refresh is not true');
|
|
}
|
|
|
|
// 1. Check if URL is already open
|
|
// Get all tabs and manually compare URLs
|
|
console.log(`Checking if URL is already open: ${url}`);
|
|
// Get all tabs
|
|
const allTabs = await chrome.tabs.query({});
|
|
// Manually filter matching tabs
|
|
const tabs = allTabs.filter((tab) => {
|
|
// Normalize URLs for comparison (remove trailing slashes)
|
|
const tabUrl = tab.url?.endsWith('/') ? tab.url.slice(0, -1) : tab.url;
|
|
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
|
|
return tabUrl === targetUrl;
|
|
});
|
|
console.log(`Found ${tabs.length} matching tabs`);
|
|
|
|
if (tabs && tabs.length > 0) {
|
|
const existingTab = tabs[0];
|
|
console.log(
|
|
`URL already open in Tab ID: ${existingTab.id}, Window ID: ${existingTab.windowId}`,
|
|
);
|
|
|
|
if (existingTab.id !== undefined) {
|
|
// Activate the tab
|
|
await chrome.tabs.update(existingTab.id, { active: true });
|
|
|
|
if (existingTab.windowId !== undefined) {
|
|
// Bring the window containing this tab to the foreground and focus it
|
|
await chrome.windows.update(existingTab.windowId, { focused: true });
|
|
}
|
|
|
|
console.log(`Activated existing Tab ID: ${existingTab.id}`);
|
|
// Get updated tab information and return it
|
|
const updatedTab = await chrome.tabs.get(existingTab.id);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Activated existing tab',
|
|
tabId: updatedTab.id,
|
|
windowId: updatedTab.windowId,
|
|
url: updatedTab.url,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
console.log('Opening URL in a new window.');
|
|
|
|
// Create new window
|
|
const newWindow = await chrome.windows.create({
|
|
url: url,
|
|
width: typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH,
|
|
height: typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT,
|
|
focused: true,
|
|
});
|
|
|
|
if (newWindow && newWindow.id !== undefined) {
|
|
console.log(`URL opened in new Window ID: ${newWindow.id}`);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Opened URL in new window',
|
|
windowId: newWindow.id,
|
|
tabs: newWindow.tabs
|
|
? newWindow.tabs.map((tab) => ({
|
|
tabId: tab.id,
|
|
url: tab.url,
|
|
}))
|
|
: [],
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
} else {
|
|
console.log('Opening URL in the last active window.');
|
|
// Try to open a new tab in the most recently active window
|
|
const lastFocusedWindow = await chrome.windows.getLastFocused({ populate: false });
|
|
|
|
if (lastFocusedWindow && lastFocusedWindow.id !== undefined) {
|
|
console.log(`Found last focused Window ID: ${lastFocusedWindow.id}`);
|
|
|
|
const newTab = await chrome.tabs.create({
|
|
url: url,
|
|
windowId: lastFocusedWindow.id,
|
|
active: true,
|
|
});
|
|
|
|
// Ensure the window also gets focus
|
|
await chrome.windows.update(lastFocusedWindow.id, { focused: true });
|
|
|
|
console.log(
|
|
`URL opened in new Tab ID: ${newTab.id} in existing Window ID: ${lastFocusedWindow.id}`,
|
|
);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Opened URL in new tab in existing window',
|
|
tabId: newTab.id,
|
|
windowId: lastFocusedWindow.id,
|
|
url: newTab.url,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
} else {
|
|
// In rare cases, if there's no recently active window (e.g., browser just started with no windows)
|
|
// Fall back to opening in a new window
|
|
console.warn('No last focused window found, falling back to creating a new window.');
|
|
|
|
const fallbackWindow = await chrome.windows.create({
|
|
url: url,
|
|
width: DEFAULT_WINDOW_WIDTH,
|
|
height: DEFAULT_WINDOW_HEIGHT,
|
|
focused: true,
|
|
});
|
|
|
|
if (fallbackWindow && fallbackWindow.id !== undefined) {
|
|
console.log(`URL opened in fallback new Window ID: ${fallbackWindow.id}`);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Opened URL in new window',
|
|
windowId: fallbackWindow.id,
|
|
tabs: fallbackWindow.tabs
|
|
? fallbackWindow.tabs.map((tab) => ({
|
|
tabId: tab.id,
|
|
url: tab.url,
|
|
}))
|
|
: [],
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// If all attempts fail, return a generic error
|
|
return createErrorResponse('Failed to open URL: Unknown error occurred');
|
|
} catch (error) {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
|
|
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
|
|
} else {
|
|
console.error('Error in navigate:', error);
|
|
return createErrorResponse(
|
|
`Error navigating to URL: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export const navigateTool = new NavigateTool();
|
|
|
|
interface CloseTabsToolParams {
|
|
tabIds?: number[];
|
|
url?: string;
|
|
}
|
|
|
|
/**
|
|
* Tool for closing browser tabs
|
|
*/
|
|
class CloseTabsTool extends BaseBrowserToolExecutor {
|
|
name = TOOL_NAMES.BROWSER.CLOSE_TABS;
|
|
|
|
async execute(args: CloseTabsToolParams): Promise<ToolResult> {
|
|
const { tabIds, url } = args;
|
|
let urlPattern = url;
|
|
console.log(`Attempting to close tabs with options:`, args);
|
|
|
|
try {
|
|
// If URL is provided, close all tabs matching that URL
|
|
if (urlPattern) {
|
|
console.log(`Searching for tabs with URL: ${url}`);
|
|
if (!urlPattern.endsWith('/')) {
|
|
urlPattern += '/*';
|
|
}
|
|
const tabs = await chrome.tabs.query({ url });
|
|
|
|
if (!tabs || tabs.length === 0) {
|
|
console.log(`No tabs found with URL: ${url}`);
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: false,
|
|
message: `No tabs found with URL: ${url}`,
|
|
closedCount: 0,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
|
|
console.log(`Found ${tabs.length} tabs with URL: ${url}`);
|
|
const tabIdsToClose = tabs
|
|
.map((tab) => tab.id)
|
|
.filter((id): id is number => id !== undefined);
|
|
|
|
if (tabIdsToClose.length === 0) {
|
|
return createErrorResponse('Found tabs but could not get their IDs');
|
|
}
|
|
|
|
await chrome.tabs.remove(tabIdsToClose);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: `Closed ${tabIdsToClose.length} tabs with URL: ${url}`,
|
|
closedCount: tabIdsToClose.length,
|
|
closedTabIds: tabIdsToClose,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
|
|
// If tabIds are provided, close those tabs
|
|
if (tabIds && tabIds.length > 0) {
|
|
console.log(`Closing tabs with IDs: ${tabIds.join(', ')}`);
|
|
|
|
// Verify that all tabIds exist
|
|
const existingTabs = await Promise.all(
|
|
tabIds.map(async (tabId) => {
|
|
try {
|
|
return await chrome.tabs.get(tabId);
|
|
} catch (error) {
|
|
console.warn(`Tab with ID ${tabId} not found`);
|
|
return null;
|
|
}
|
|
}),
|
|
);
|
|
|
|
const validTabIds = existingTabs
|
|
.filter((tab): tab is chrome.tabs.Tab => tab !== null)
|
|
.map((tab) => tab.id)
|
|
.filter((id): id is number => id !== undefined);
|
|
|
|
if (validTabIds.length === 0) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: false,
|
|
message: 'None of the provided tab IDs exist',
|
|
closedCount: 0,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
|
|
await chrome.tabs.remove(validTabIds);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: `Closed ${validTabIds.length} tabs`,
|
|
closedCount: validTabIds.length,
|
|
closedTabIds: validTabIds,
|
|
invalidTabIds: tabIds.filter((id) => !validTabIds.includes(id)),
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
}
|
|
|
|
// If no tabIds or URL provided, close the current active tab
|
|
console.log('No tabIds or URL provided, closing active tab');
|
|
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
|
|
if (!activeTab || !activeTab.id) {
|
|
return createErrorResponse('No active tab found');
|
|
}
|
|
|
|
await chrome.tabs.remove(activeTab.id);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: 'Closed active tab',
|
|
closedCount: 1,
|
|
closedTabIds: [activeTab.id],
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in CloseTabsTool.execute:', error);
|
|
return createErrorResponse(
|
|
`Error closing tabs: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const closeTabsTool = new CloseTabsTool();
|
|
|
|
interface GoBackOrForwardToolParams {
|
|
isForward?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Tool for navigating back or forward in browser history
|
|
*/
|
|
class GoBackOrForwardTool extends BaseBrowserToolExecutor {
|
|
name = TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD;
|
|
|
|
async execute(args: GoBackOrForwardToolParams): Promise<ToolResult> {
|
|
const { isForward = false } = args;
|
|
|
|
console.log(`Attempting to navigate ${isForward ? 'forward' : 'back'} in browser history`);
|
|
|
|
try {
|
|
// Get current active tab
|
|
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
|
|
if (!activeTab || !activeTab.id) {
|
|
return createErrorResponse('No active tab found');
|
|
}
|
|
|
|
// Navigate back or forward based on the isForward parameter
|
|
if (isForward) {
|
|
await chrome.tabs.goForward(activeTab.id);
|
|
console.log(`Navigated forward in tab ID: ${activeTab.id}`);
|
|
} else {
|
|
await chrome.tabs.goBack(activeTab.id);
|
|
console.log(`Navigated back in tab ID: ${activeTab.id}`);
|
|
}
|
|
|
|
// Get updated tab information
|
|
const updatedTab = await chrome.tabs.get(activeTab.id);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: `Successfully navigated ${isForward ? 'forward' : 'back'} in browser history`,
|
|
tabId: updatedTab.id,
|
|
windowId: updatedTab.windowId,
|
|
url: updatedTab.url,
|
|
}),
|
|
},
|
|
],
|
|
isError: false,
|
|
};
|
|
} catch (error) {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
|
|
return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
|
|
} else {
|
|
console.error('Error in GoBackOrForwardTool.execute:', error);
|
|
return createErrorResponse(
|
|
`Error navigating ${isForward ? 'forward' : 'back'}: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const goBackOrForwardTool = new GoBackOrForwardTool();
|