230 lines
7.5 KiB
TypeScript
230 lines
7.5 KiB
TypeScript
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
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(result),
|
|
},
|
|
],
|
|
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');
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: true,
|
|
elements: result.elements,
|
|
count: result.elements.length,
|
|
query: {
|
|
textQuery,
|
|
selector,
|
|
types: types || 'all',
|
|
},
|
|
}),
|
|
},
|
|
],
|
|
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();
|