import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
interface WebFetcherToolParams {
htmlContent?: boolean; // get the visible HTML content of the current page. default: false
textContent?: boolean; // get the visible text content of the current page. default: true
url?: string; // optional URL to fetch content from (if not provided, uses active tab)
selector?: string; // optional CSS selector to get content from a specific element
}
class WebFetcherTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.WEB_FETCHER;
/**
* Execute web fetcher operation
*/
async execute(args: WebFetcherToolParams): Promise<ToolResult> {
// Handle mutually exclusive parameters: if htmlContent is true, textContent is forced to false
const htmlContent = args.htmlContent === true;
const textContent = htmlContent ? false : args.textContent !== false; // Default is true, unless htmlContent is true or textContent is explicitly set to false
const url = args.url;
const selector = args.selector;
console.log(`Starting web fetcher with options:`, {
htmlContent,
textContent,
url,
selector,
});
try {
// Get tab to fetch content from
let tab;
if (url) {
// If URL is provided, check if it's already open
console.log(`Checking if URL is already open: ${url}`);
const allTabs = await chrome.tabs.query({});
// Find tab with matching URL
const matchingTabs = allTabs.filter((t) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
if (matchingTabs.length > 0) {
// Use existing tab
tab = matchingTabs[0];
console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
} else {
// Create new tab with the URL
console.log(`No existing tab found with URL: ${url}, creating new tab`);
tab = await chrome.tabs.create({ url, active: true });
// Wait for page to load
console.log('Waiting for page to load...');
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} else {
// Use active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tab = tabs[0];
}
if (!tab.id) {
return createErrorResponse('Tab has no ID');
}
// Make sure tab is active
await chrome.tabs.update(tab.id, { active: true });
// Prepare result object
const result: any = {
success: true,
url: tab.url,
title: tab.title,
};
await this.injectContentScript(tab.id, ['inject-scripts/web-fetcher-helper.js']);
// Get HTML content if requested
if (htmlContent) {
const htmlResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_HTML_CONTENT,
selector: selector,
});
if (htmlResponse.success) {
result.htmlContent = htmlResponse.htmlContent;
} else {
console.error('Failed to get HTML content:', htmlResponse.error);
result.htmlContentError = htmlResponse.error;
}
}
// Get text content if requested (and htmlContent is not true)
if (textContent) {
const textResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
selector: selector,
});
if (textResponse.success) {
result.textContent = textResponse.textContent;
// Include article metadata if available
if (textResponse.article) {
result.article = {
title: textResponse.article.title,
byline: textResponse.article.byline,
siteName: textResponse.article.siteName,
excerpt: textResponse.article.excerpt,
lang: textResponse.article.lang,
};
}
// Include page metadata if available
if (textResponse.metadata) {
result.metadata = textResponse.metadata;
}
} else {
console.error('Failed to get text content:', textResponse.error);
result.textContentError = textResponse.error;
}
}
// Interactive elements feature has been removed
// Build readable text response
const contentParts: string[] = [];
// Add metadata header
contentParts.push(`URL: ${result.url}`);
if (result.title) {
contentParts.push(`Title: ${result.title}`);
}
if (result.article) {
if (result.article.byline) contentParts.push(`Author: ${result.article.byline}`);
if (result.article.siteName) contentParts.push(`Site: ${result.article.siteName}`);
}
contentParts.push(''); // Empty line separator
// Add main content
if (result.textContent) {
contentParts.push(result.textContent);
} else if (result.htmlContent) {
contentParts.push(result.htmlContent);
} else if (result.textContentError) {
contentParts.push(`Error: ${result.textContentError}`);
} else if (result.htmlContentError) {
contentParts.push(`Error: ${result.htmlContentError}`);
} else {
contentParts.push('No content found');
}
return {
content: [
{
type: 'text',
text: contentParts.join('\n'),
},
],
isError: false,
};
} catch (error) {
console.error('Error in web fetcher:', error);
return createErrorResponse(
`Error fetching web content: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const webFetcherTool = new WebFetcherTool();
interface GetInteractiveElementsToolParams {
textQuery?: string; // Text to search for within interactive elements (fuzzy search)
selector?: string; // CSS selector to filter interactive elements
includeCoordinates?: boolean; // Include element coordinates in the response (default: true)
types?: string[]; // Types of interactive elements to include (default: all types)
}
class GetInteractiveElementsTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS;
/**
* Execute get interactive elements operation
*/
async execute(args: GetInteractiveElementsToolParams): Promise<ToolResult> {
const { textQuery, selector, includeCoordinates = true, types } = args;
console.log(`Starting get interactive elements with options:`, args);
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse('Active tab has no ID');
}
// Ensure content script is injected
await this.injectContentScript(tab.id, ['inject-scripts/interactive-elements-helper.js']);
// Send message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.GET_INTERACTIVE_ELEMENTS,
textQuery,
selector,
includeCoordinates,
types,
});
if (!result.success) {
return createErrorResponse(result.error || 'Failed to get interactive elements');
}
// Format elements as readable text
const elements = result.elements || [];
const lines: string[] = [];
lines.push(`Found ${elements.length} interactive elements`);
if (textQuery) lines.push(`Query: "${textQuery}"`);
if (selector) lines.push(`Selector: ${selector}`);
lines.push('');
for (const el of elements) {
const parts: string[] = [];
parts.push(`[${el.type || 'element'}]`);
if (el.text) parts.push(`"${el.text}"`);
if (el.selector) parts.push(`(${el.selector})`);
if (el.coordinates) parts.push(`at (${el.coordinates.x}, ${el.coordinates.y})`);
lines.push(parts.join(' '));
}
return {
content: [
{
type: 'text',
text: lines.join('\n'),
},
],
isError: false,
};
} catch (error) {
console.error('Error in get interactive elements operation:', error);
return createErrorResponse(
`Error getting interactive elements: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const getInteractiveElementsTool = new GetInteractiveElementsTool();