Aaron Elijah Mars daa4b55ee5 Update to version 1.1.0 with tab_list timeout fixes and enhanced documentation
- Fixed tab_list timeout issue on Chrome by increasing content script timeout from 1s to 3s
- Updated all package.json and manifest files to version 1.1.0
- Set check_content_script default to false to avoid timeouts
- Enhanced DXT build script with comprehensive installation guide and GitHub repo links
- Fixed OpenDia logo path in README.md
- Updated tool examples and documentation to reflect timeout improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 23:43:30 +02:00

2047 lines
63 KiB
JavaScript

// Import WebExtension polyfill at the top
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
globalThis.browser = chrome;
}
// Browser detection
const browserInfo = {
isFirefox: typeof browser !== 'undefined' && browser.runtime.getManifest().applications?.gecko,
isChrome: typeof chrome !== 'undefined' && !browser.runtime.getManifest().applications?.gecko,
isServiceWorker: typeof importScripts === 'function',
manifestVersion: browser.runtime.getManifest().manifest_version
};
console.log('🌐 Browser detected:', browserInfo);
// MCP Server connection configuration
let MCP_SERVER_URL = 'ws://localhost:5555'; // Default, will be auto-discovered
let lastKnownPorts = { websocket: 5555, http: 5556 }; // Cache for port discovery
// Safety Mode configuration
let safetyModeEnabled = false;
const WRITE_EDIT_TOOLS = [
'element_click',
'element_fill'
];
// Load safety mode state on startup
browser.storage.local.get(['safetyMode'], (result) => {
safetyModeEnabled = result.safetyMode || false;
});
// Cross-browser WebSocket connection manager
class ConnectionManager {
constructor() {
this.mcpSocket = null;
this.reconnectInterval = null;
this.reconnectAttempts = 0;
this.heartbeatInterval = null;
this.isServiceWorker = browserInfo.isServiceWorker;
this.isFirefox = browserInfo.isFirefox;
}
async connect() {
if (this.isServiceWorker) {
// Chrome MV3: Create fresh connection for each operation
console.log('🔧 Chrome MV3: Creating temporary connection');
await this.createConnection();
} else {
// Firefox MV2: Maintain persistent connection
if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) {
console.log('🦊 Firefox MV2: Creating persistent connection');
await this.createConnection();
} else {
console.log('🦊 Firefox MV2: Using existing connection');
}
}
}
async createConnection() {
try {
// Try port discovery if using default URL or if connection failed
if (MCP_SERVER_URL === 'ws://localhost:5555' || this.reconnectAttempts > 2) {
await this.discoverServerPorts();
this.reconnectAttempts = 0; // Reset attempts after discovery
}
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
this.mcpSocket = new WebSocket(MCP_SERVER_URL);
this.mcpSocket.onopen = () => {
console.log('✅ Connected to MCP server');
this.clearReconnectInterval();
this.reconnectAttempts = 0; // Reset attempts on successful connection
const tools = getAvailableTools();
console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name));
// Register available browser functions
this.mcpSocket.send(JSON.stringify({
type: 'register',
tools: tools
}));
// Setup heartbeat for persistent connections
if (!this.isServiceWorker) {
this.setupHeartbeat();
}
};
this.mcpSocket.onmessage = async (event) => {
const message = JSON.parse(event.data);
await handleMCPRequest(message);
};
this.mcpSocket.onclose = (event) => {
console.log(`❌ Disconnected from MCP server (code: ${event.code}, reason: ${event.reason})`);
this.clearHeartbeat(); // Clear heartbeat on disconnect
this.reconnectAttempts++;
// Check if this was a normal closure or abnormal
if (event.code !== 1000 && event.code !== 1001) {
console.log('🔄 Abnormal WebSocket closure, will attempt reconnection');
if (!this.isServiceWorker) {
// Firefox: Attempt to reconnect
this.scheduleReconnect();
}
// Chrome: Will reconnect on next message
} else {
console.log('🔄 Normal WebSocket closure');
}
};
this.mcpSocket.onerror = (error) => {
console.log('⚠️ MCP WebSocket error:', error);
this.reconnectAttempts++;
};
} catch (error) {
console.error('Connection failed:', error);
if (!this.isServiceWorker) {
this.scheduleReconnect();
}
}
}
async discoverServerPorts() {
// Try common HTTP ports to find the server
const commonPorts = [5556, 5557, 5558, 3001, 6001, 6002, 6003];
for (const httpPort of commonPorts) {
try {
const response = await fetch(`http://localhost:${httpPort}/ports`);
if (response.ok) {
const portInfo = await response.json();
console.log('🔍 Discovered server ports:', portInfo);
lastKnownPorts = { websocket: portInfo.websocket, http: portInfo.http };
MCP_SERVER_URL = portInfo.websocketUrl;
return portInfo;
}
} catch (error) {
// Port not available or not OpenDia server, continue searching
}
}
console.log('⚠️ Port discovery failed, using defaults');
return null;
}
setupHeartbeat() {
// Only maintain heartbeat in persistent background pages
this.clearHeartbeat();
this.heartbeatInterval = setInterval(() => {
if (this.mcpSocket?.readyState === WebSocket.OPEN) {
this.mcpSocket.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
} else if (this.mcpSocket?.readyState === WebSocket.CLOSED) {
console.log('🔄 WebSocket closed, attempting reconnection...');
this.connect();
}
}, 15000); // More frequent heartbeat for better reliability
}
clearHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
scheduleReconnect() {
this.clearReconnectInterval();
// Exponential backoff for reconnection attempts
const backoffTime = Math.min(5000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`🔄 Scheduling reconnection in ${backoffTime}ms (attempt ${this.reconnectAttempts})`);
this.reconnectInterval = setInterval(() => {
if (this.reconnectAttempts < 10) {
this.connect();
} else {
console.log('❌ Maximum reconnection attempts reached');
this.clearReconnectInterval();
}
}, backoffTime);
}
clearReconnectInterval() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
}
}
async ensureConnection() {
if (this.isServiceWorker) {
// Chrome: Always create fresh connection
await this.connect();
} else {
// Firefox: Use existing or create new
if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) {
await this.connect();
}
}
return this.mcpSocket;
}
send(message) {
if (this.mcpSocket && this.mcpSocket.readyState === WebSocket.OPEN) {
this.mcpSocket.send(JSON.stringify(message));
} else {
console.error('WebSocket not connected');
}
}
getStatus() {
return {
connected: this.mcpSocket && this.mcpSocket.readyState === WebSocket.OPEN,
browserInfo: browserInfo,
connectionType: this.isServiceWorker ? 'temporary' : 'persistent'
};
}
}
// Create global connection manager
const connectionManager = new ConnectionManager();
// Content script management for background tabs
async function ensureContentScriptReady(tabId, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
// Test if content script is responsive
const response = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Content script ping timeout'));
}, 2000);
browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout);
if (browser.runtime.lastError) {
reject(new Error(browser.runtime.lastError.message));
} else {
resolve(response);
}
});
});
if (response && response.success) {
console.log(`✅ Content script ready in tab ${tabId}`);
return true;
}
} catch (error) {
console.log(`⚠️ Content script not responsive in tab ${tabId}, attempt ${attempt}/${retries}`);
if (attempt === retries) {
// Last attempt - try to inject content script
try {
const tab = await browser.tabs.get(tabId);
// Check if tab URL is injectable (not chrome://, chrome-extension://, etc.)
if (!isInjectableUrl(tab.url)) {
throw new Error(`Cannot inject content script into ${tab.url} - restricted URL`);
}
console.log(`🔄 Injecting content script into tab ${tabId}`);
// Use appropriate API based on browser
if (browser.scripting) {
// Chrome MV3
await browser.scripting.executeScript({
target: { tabId: tabId },
files: ['src/content/content.js']
});
} else {
// Firefox MV2 - check if already injected first
try {
const result = await browser.tabs.executeScript(tabId, {
code: 'typeof window.OpenDiaContentScriptLoaded !== "undefined"'
});
if (result && result[0]) {
console.log(`🔄 Content script already present in tab ${tabId}`);
return true;
}
} catch (e) {
// Continue with injection if check fails
}
await browser.tabs.executeScript(tabId, {
file: 'src/content/content.js'
});
}
// Wait a moment for script to initialize
await new Promise(resolve => setTimeout(resolve, 1000));
// Test again
const testResponse = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout after injection')), 3000);
browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout);
if (browser.runtime.lastError) {
reject(new Error(browser.runtime.lastError.message));
} else {
resolve(response);
}
});
});
if (testResponse && testResponse.success) {
console.log(`✅ Content script successfully injected into tab ${tabId}`);
return true;
}
} catch (injectionError) {
throw new Error(`Failed to inject content script into tab ${tabId}: ${injectionError.message}`);
}
}
// Wait before retry
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
throw new Error(`Content script not available in tab ${tabId} after ${retries} attempts`);
}
// Check if URL allows content script injection
function isInjectableUrl(url) {
if (!url) return false;
const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:', 'about:'];
const restrictedDomains = ['chrome.google.com', 'addons.mozilla.org'];
// Check protocol
if (restrictedProtocols.some(protocol => url.startsWith(protocol))) {
return false;
}
// Check special browser pages
if (url.startsWith('https://chrome.google.com/webstore') ||
url.includes('chrome://') ||
restrictedDomains.some(domain => url.includes(domain))) {
return false;
}
return true;
}
// Get content script readiness status for a tab
async function getTabContentScriptStatus(tabId) {
try {
const tab = await browser.tabs.get(tabId);
if (!isInjectableUrl(tab.url)) {
return { ready: false, reason: 'restricted_url', url: tab.url };
}
const response = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve(null), 3000); // Increase to 3 seconds
browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout);
resolve(response);
});
});
if (response && response.success) {
return { ready: true, reason: 'active', url: tab.url };
} else {
return { ready: false, reason: 'not_loaded', url: tab.url };
}
} catch (error) {
return { ready: false, reason: 'tab_error', error: error.message };
}
}
// Define available browser automation tools for MCP
function getAvailableTools() {
return [
/*
🎯 BACKGROUND TAB WORKFLOW GUIDE:
1. DISCOVER TABS: Use tab_list with check_content_script=false to see all tabs and their IDs
2. TARGET SPECIFIC TABS: Add tab_id parameter to any tool to work on background tabs
3. MULTI-TAB OPERATIONS: Process multiple tabs without switching between them
Example Multi-Tab Workflow:
- tab_list({check_content_script: false}) → Get tab IDs quickly
- page_analyze({intent_hint: "article", tab_id: 12345}) → Analyze background research tab
- page_extract_content({content_type: "article", tab_id: 12345}) → Extract content without switching
- get_selected_text({tab_id: 67890}) → Get quotes from another background tab
Perfect for: Research workflows, content analysis, form processing, social media management
*/
// Page Analysis Tools
{
name: "page_analyze",
description: "🔍 BACKGROUND TAB READY: Analyze any tab without switching to it! Two-phase intelligent page analysis with token efficiency optimization. Use tab_id parameter to analyze background tabs while staying on current page.",
inputSchema: {
type: "object",
examples: [
{ intent_hint: "analyze", phase: "discover" }, // Current tab quick analysis
{ intent_hint: "login", tab_id: 12345 }, // Background tab login form analysis
{ intent_hint: "post_create", tab_id: 67890, phase: "detailed" } // Background tab detailed analysis
],
properties: {
intent_hint: {
type: "string",
description: "User intent: login, signup, search, post_create, comment, menu, submit, etc."
},
phase: {
type: "string",
enum: ["discover", "detailed"],
default: "discover",
description: "Analysis phase: 'discover' for quick scan (<100 tokens), 'detailed' for full analysis"
},
focus_areas: {
type: "array",
items: { type: "string" },
description: "Areas to analyze in detail: buttons, forms, navigation, search_elements"
},
max_results: {
type: "number",
default: 5,
maximum: 15,
description: "Maximum number of elements to return"
},
element_ids: {
type: "array",
items: { type: "string" },
description: "Expand specific quick match IDs from discover phase (e.g. ['q1', 'q2'])"
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Specify tab ID to analyze background tabs without switching! Get tab IDs from tab_list. If omitted, analyzes current active tab."
}
},
required: ["intent_hint"]
}
},
{
name: "page_extract_content",
description: "📄 BACKGROUND TAB READY: Extract content from any tab without switching! Perfect for analyzing multiple research tabs, articles, or pages simultaneously. Use tab_id to target specific background tabs.",
inputSchema: {
type: "object",
examples: [
{ content_type: "article" }, // Extract from current tab
{ content_type: "article", tab_id: 12345 }, // Extract from background research tab
{ content_type: "posts", tab_id: 67890, max_items: 10 } // Extract social media posts from background tab
],
properties: {
content_type: {
type: "string",
enum: ["article", "search_results", "posts"],
description: "Type of content to extract"
},
max_items: {
type: "number",
description: "Maximum number of items to extract (for lists/collections)",
default: 20
},
summarize: {
type: "boolean",
default: true,
description: "Return summary instead of full content to save tokens"
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Extract content from specific background tab without switching! Use tab_list to get tab IDs. Perfect for processing multiple research tabs."
}
},
required: ["content_type"]
}
},
// Element Interaction Tools
{
name: "element_click",
description: "🖱️ BACKGROUND TAB READY: Click elements in any tab without switching! Perform actions on background tabs while staying on current page. Use tab_id to target specific tabs.",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Unique element identifier from page_analyze"
},
click_type: {
type: "string",
enum: ["left", "right", "double"],
default: "left"
},
wait_after: {
type: "number",
description: "Milliseconds to wait after click",
default: 500
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Click elements in background tabs without switching! Get tab IDs from tab_list to interact with multiple tabs efficiently."
}
},
required: ["element_id"]
}
},
{
name: "element_fill",
description: "✏️ BACKGROUND TAB READY: Fill forms in any tab without switching! Enhanced focus and event simulation for modern web apps. Use tab_id to fill forms in background tabs.",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Unique element identifier from page_analyze"
},
value: {
type: "string",
description: "Text value to input"
},
clear_first: {
type: "boolean",
description: "Clear existing content before filling",
default: true
},
force_focus: {
type: "boolean",
description: "Use enhanced focus sequence with click simulation for modern apps",
default: true
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Fill forms in background tabs without switching! Perfect for batch form filling across multiple tabs. Get tab IDs from tab_list."
}
},
required: ["element_id", "value"]
}
},
// Navigation Tools
{
name: "page_navigate",
description: "Navigate CURRENT tab to a new URL. Use tab_create instead if you want to open a NEW tab with a URL.",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "URL to navigate to"
},
wait_for: {
type: "string",
description: "CSS selector to wait for after navigation (ensures page is ready)"
},
timeout: {
type: "number",
description: "Maximum wait time in milliseconds",
default: 10000
}
},
required: ["url"]
}
},
{
name: "page_wait_for",
description: "Wait for specific element or condition on current page",
inputSchema: {
type: "object",
properties: {
condition_type: {
type: "string",
enum: ["element_visible", "text_present"],
description: "Type of condition to wait for"
},
selector: {
type: "string",
description: "CSS selector for element-based conditions"
},
text: {
type: "string",
description: "Text to wait for (when condition_type is 'text_present')"
},
timeout: {
type: "number",
description: "Maximum wait time in milliseconds",
default: 5000
}
},
required: ["condition_type"]
}
},
// Tab Management Tools
{
name: "tab_create",
description: "Creates tabs. CRITICAL: For multiple identical tabs, ALWAYS use 'count' parameter! Examples: {url: 'https://x.com', count: 5} creates 5 Twitter tabs. {url: 'https://github.com', count: 10} creates 10 GitHub tabs. Single tab: {url: 'https://example.com'}. Multiple different URLs: {urls: ['url1', 'url2']}.",
inputSchema: {
type: "object",
examples: [
{ url: "https://x.com", count: 5 }, // CORRECT: Creates 5 identical Twitter tabs in one batch
{ url: "https://github.com", count: 10 }, // CORRECT: Creates 10 GitHub tabs
{ urls: ["https://x.com/post1", "https://x.com/post2", "https://google.com"] }, // CORRECT: Different URLs in batch
{ url: "https://example.com" } // Single tab only
],
properties: {
url: {
type: "string",
description: "Single URL to open. Can be used with 'count' to create multiple identical tabs"
},
urls: {
type: "array",
items: { type: "string" },
description: "PREFERRED FOR MULTIPLE URLS: Array of URLs to open ALL AT ONCE in a single batch operation. Pass ALL URLs here instead of making multiple calls! Example: ['https://x.com/post1', 'https://x.com/post2', 'https://google.com']",
maxItems: 100
},
count: {
type: "number",
default: 1,
minimum: 1,
maximum: 50,
description: "REQUIRED FOR MULTIPLE IDENTICAL TABS: Set this to N to create N copies of the same URL. For '5 Twitter tabs' use count=5 with url='https://x.com'. DO NOT make 5 separate calls!"
},
active: {
type: "boolean",
default: true,
description: "Whether to activate the last created tab (single tab only)"
},
wait_for: {
type: "string",
description: "CSS selector to wait for after tab creation (single tab only)"
},
timeout: {
type: "number",
default: 10000,
description: "Maximum wait time per tab in milliseconds"
},
batch_settings: {
type: "object",
description: "Performance control settings for batch operations",
properties: {
chunk_size: {
type: "number",
default: 5,
minimum: 1,
maximum: 10,
description: "Number of tabs to create per batch"
},
delay_between_chunks: {
type: "number",
default: 1000,
minimum: 100,
maximum: 5000,
description: "Delay between batches in milliseconds"
},
delay_between_tabs: {
type: "number",
default: 200,
minimum: 50,
maximum: 1000,
description: "Delay between individual tabs in milliseconds"
}
}
}
}
}
},
{
name: "tab_close",
description: "Close specific tab(s) by ID or close current tab",
inputSchema: {
type: "object",
properties: {
tab_id: {
type: "number",
description: "Specific tab ID to close (optional, closes current tab if not provided)"
},
tab_ids: {
type: "array",
items: { type: "number" },
description: "Array of tab IDs to close multiple tabs"
}
}
}
},
{
name: "tab_list",
description: "📋 TAB DISCOVERY: Get list of all open tabs with IDs for background tab targeting! Shows content script readiness status and tab details. Essential for multi-tab workflows - use tab IDs with other tools to work on background tabs.",
inputSchema: {
type: "object",
examples: [
{ check_content_script: false }, // RECOMMENDED: Default false to avoid timeouts
{ current_window_only: false, check_content_script: false } // Get all tabs across windows
],
properties: {
current_window_only: {
type: "boolean",
default: true,
description: "Only return tabs from the current window"
},
include_details: {
type: "boolean",
default: true,
description: "Include additional tab details (title, favicon, etc.)"
},
check_content_script: {
type: "boolean",
default: false,
description: "🔍 ESSENTIAL FOR BACKGROUND TABS: Check which tabs are ready for background operations! Set to true when planning multi-tab workflows to see which tabs can be targeted."
}
}
}
},
{
name: "tab_switch",
description: "Switch to a specific tab by ID",
inputSchema: {
type: "object",
properties: {
tab_id: {
type: "number",
description: "Tab ID to switch to"
}
},
required: ["tab_id"]
}
},
// Element State Tools
{
name: "element_get_state",
description: "Get detailed state information for a specific element (disabled, clickable, etc.)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze"
}
},
required: ["element_id"]
}
},
// Workspace and Reference Management Tools
{
name: "get_bookmarks",
description: "Get all bookmarks or search for specific bookmarks",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for bookmarks (optional)"
}
}
}
},
{
name: "add_bookmark",
description: "Add a new bookmark",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Title of the bookmark"
},
url: {
type: "string",
description: "URL of the bookmark"
},
parentId: {
type: "string",
description: "ID of the parent folder (optional)"
}
},
required: ["title", "url"]
}
},
{
name: "get_history",
description: "Search browser history with comprehensive filters for finding previous work by date/keywords",
inputSchema: {
type: "object",
properties: {
keywords: {
type: "string",
description: "Search keywords to match in page titles and URLs"
},
start_date: {
type: "string",
format: "date-time",
description: "Start date for history search (ISO 8601 format)"
},
end_date: {
type: "string",
format: "date-time",
description: "End date for history search (ISO 8601 format)"
},
domains: {
type: "array",
items: { type: "string" },
description: "Filter by specific domains (e.g., ['github.com', 'stackoverflow.com'])"
},
min_visit_count: {
type: "number",
default: 1,
description: "Minimum visit count threshold"
},
max_results: {
type: "number",
default: 50,
maximum: 500,
description: "Maximum number of results to return"
},
sort_by: {
type: "string",
enum: ["visit_time", "visit_count", "title"],
default: "visit_time",
description: "Sort results by visit time, visit count, or title"
},
sort_order: {
type: "string",
enum: ["desc", "asc"],
default: "desc",
description: "Sort order (descending or ascending)"
}
}
}
},
{
name: "get_selected_text",
description: "📝 BACKGROUND TAB READY: Get selected text from any tab without switching! Perfect for collecting quotes, citations, or highlighted content from multiple research tabs simultaneously.",
inputSchema: {
type: "object",
properties: {
include_metadata: {
type: "boolean",
default: true,
description: "Include metadata about the selection (element info, position, etc.)"
},
max_length: {
type: "number",
default: 10000,
description: "Maximum length of text to return"
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Get selected text from background tabs without switching! Perfect for collecting quotes or snippets from multiple research tabs."
}
}
}
},
{
name: "page_scroll",
description: "📜 BACKGROUND TAB READY: Scroll any tab without switching! Critical for long pages. Navigate through content in background tabs while staying on current page. Use tab_id to target specific tabs.",
inputSchema: {
type: "object",
properties: {
direction: {
type: "string",
enum: ["up", "down", "left", "right", "top", "bottom"],
default: "down",
description: "Direction to scroll"
},
amount: {
type: "string",
enum: ["small", "medium", "large", "page", "custom"],
default: "medium",
description: "Amount to scroll"
},
pixels: {
type: "number",
description: "Custom pixel amount (when amount is 'custom')"
},
smooth: {
type: "boolean",
default: true,
description: "Use smooth scrolling animation"
},
element_id: {
type: "string",
description: "Scroll to specific element (overrides direction/amount)"
},
wait_after: {
type: "number",
default: 500,
description: "Milliseconds to wait after scrolling"
},
tab_id: {
type: "number",
description: "🎯 TARGET ANY TAB: Scroll content in background tabs without switching! Perfect for navigating long documents or pages in multiple tabs simultaneously."
}
}
}
},
{
name: "get_page_links",
description: "Get all hyperlinks on the current page with filtering options",
inputSchema: {
type: "object",
properties: {
link_type: {
type: "string",
enum: ["all", "internal", "external"],
default: "all",
description: "Filter by internal/external links"
},
domains: {
type: "array",
items: { type: "string" },
description: "Filter by specific domains (optional)"
},
max_results: {
type: "number",
default: 50,
maximum: 200,
description: "Maximum links to return"
}
}
}
},
{
name: "page_style",
description: "🎨 Transform page appearance with themes, colors, fonts, and fun effects! Apply preset themes like 'dark_hacker', 'retro_80s', or create custom styles. Perfect for making boring pages fun or improving readability.",
inputSchema: {
type: "object",
examples: [
{ mode: "preset", theme: "dark_hacker" },
{ mode: "custom", background: "#000", text_color: "#00ff00", font: "monospace" },
{ mode: "ai_mood", mood: "cozy coffee shop vibes", intensity: "strong" },
{ mode: "effect", effect: "matrix_rain", duration: 30 }
],
properties: {
mode: {
type: "string",
enum: ["preset", "custom", "ai_mood", "effect", "reset"],
description: "Styling mode to use"
},
theme: {
type: "string",
enum: ["dark_hacker", "retro_80s", "rainbow_party", "minimalist_zen", "high_contrast", "cyberpunk", "pastel_dream", "newspaper"],
description: "Preset theme name (when mode=preset)"
},
background: {
type: "string",
description: "Background color/gradient"
},
text_color: {
type: "string",
description: "Text color"
},
font: {
type: "string",
description: "Font family"
},
font_size: {
type: "string",
description: "Font size (e.g., '1.2em', '16px')"
},
mood: {
type: "string",
description: "Describe desired mood/feeling (when mode=ai_mood)"
},
intensity: {
type: "string",
enum: ["subtle", "medium", "strong"],
default: "medium"
},
effect: {
type: "string",
enum: ["matrix_rain", "floating_particles", "cursor_trail", "neon_glow", "typing_effect"]
},
duration: {
type: "number",
description: "Effect duration in seconds",
default: 10
},
remember: {
type: "boolean",
description: "Remember this style for this website",
default: false
}
},
required: ["mode"]
}
},
];
}
// Handle MCP requests with enhanced automation tools
async function handleMCPRequest(message) {
const { id, method, params } = message;
try {
// Ensure connection for Chrome service workers
await connectionManager.ensureConnection();
// Safety Mode check: Block write/edit tools if safety mode is enabled
if (safetyModeEnabled && WRITE_EDIT_TOOLS.includes(method)) {
const targetInfo = params.tab_id ? `tab ${params.tab_id}` : 'the current page';
throw new Error(`🛡️ Safety Mode is enabled. This tool (${method}) is blocked to prevent modifications to ${targetInfo}. To disable Safety Mode, open the OpenDia extension popup and toggle off "Safety Mode".`);
}
let result;
switch (method) {
// New automation tools with background tab support
case "page_analyze":
result = await sendToContentScript('analyze', params, params.tab_id);
break;
case "page_extract_content":
result = await sendToContentScript('extract_content', params, params.tab_id);
break;
case "element_click":
result = await sendToContentScript('element_click', params, params.tab_id);
break;
case "element_fill":
result = await sendToContentScript('element_fill', params, params.tab_id);
break;
case "page_navigate":
result = await navigateToUrl(params.url, params.wait_for, params.timeout);
break;
case "page_wait_for":
result = await sendToContentScript('wait_for', params, params.tab_id);
break;
// Tab management tools
case "tab_create":
result = await createTab(params);
break;
case "tab_close":
result = await closeTabs(params);
break;
case "tab_list":
result = await listTabs(params);
break;
case "tab_switch":
result = await switchToTab(params.tab_id);
break;
// Element state tools
case "element_get_state":
result = await sendToContentScript('get_element_state', params, params.tab_id);
break;
// Workspace and Reference Management Tools
case "get_bookmarks":
result = await getBookmarks(params);
break;
case "add_bookmark":
result = await addBookmark(params);
break;
case "get_history":
result = await getHistory(params);
break;
case "get_selected_text":
result = await getSelectedText(params);
break;
case "page_scroll":
result = await sendToContentScript('page_scroll', params, params.tab_id);
break;
case "get_page_links":
result = await sendToContentScript('get_page_links', params, params.tab_id);
break;
case "page_style":
result = await sendToContentScript('page_style', params, params.tab_id);
break;
default:
throw new Error(`Unknown method: ${method}`);
}
// Send success response
connectionManager.send({
id,
result,
});
} catch (error) {
// Send error response
connectionManager.send({
id,
error: {
message: error.message,
code: -32603,
},
});
}
}
// Enhanced content script communication with background tab support
async function sendToContentScript(action, data, targetTabId = null) {
let targetTab;
if (targetTabId) {
// Use specific tab
try {
targetTab = await browser.tabs.get(targetTabId);
} catch (error) {
throw new Error(`Tab ${targetTabId} not found or inaccessible`);
}
} else {
// Fallback to active tab (maintains compatibility)
const [activeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!activeTab) {
throw new Error('No active tab found');
}
targetTab = activeTab;
}
// Ensure content script is available in the target tab
await ensureContentScriptReady(targetTab.id);
return new Promise((resolve, reject) => {
browser.tabs.sendMessage(targetTab.id, { action, data }, (response) => {
if (browser.runtime.lastError) {
reject(new Error(`Tab ${targetTab.id}: ${browser.runtime.lastError.message}`));
} else if (response && response.success) {
resolve(response.data);
} else {
reject(new Error(`Tab ${targetTab.id}: ${response?.error || 'Unknown error'}`));
}
});
});
}
async function navigateToUrl(url, waitFor, timeout = 10000) {
const [activeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
await browser.tabs.update(activeTab.id, { url });
// If waitFor is specified, wait for the element to appear
if (waitFor) {
try {
await waitForElement(activeTab.id, waitFor, timeout);
} catch (error) {
return { success: true, tabId: activeTab.id, warning: `Navigation completed but wait condition failed: ${error.message}` };
}
}
return { success: true, tabId: activeTab.id, url: url };
}
async function waitForElement(tabId, selector, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const result = await browser.tabs.sendMessage(tabId, {
action: 'wait_for',
data: {
condition_type: 'element_visible',
selector: selector,
timeout: 1000
}
});
if (result.success) {
return true;
}
} catch (error) {
// Content script might not be ready yet, continue waiting
}
// Wait 500ms before next check
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new Error(`Timeout waiting for element: ${selector}`);
}
// Enhanced Tab Management Functions with Batch Support
async function createTab(params) {
const {
url,
urls,
count = 1,
active = true,
wait_for,
timeout = 10000,
batch_settings = {}
} = params;
// Smart hint: If creating single tab but description suggests multiple, provide guidance
if (count === 1 && !urls) {
console.log(`💡 Single tab creation. For multiple identical tabs, use count parameter: {"url": "${url}", "count": N}`);
}
// Validate parameters
const validation = validateTabCreateParams(params);
if (!validation.valid) {
throw new Error(validation.error);
}
console.log(`🎯 Tab creation request:`, { url, urls, count, batch_settings, hasBatchSettings: !!batch_settings });
// Determine operation type
if (urls && urls.length > 0) {
// Batch creation with multiple URLs
console.log(`🚀 Using batch mode with ${urls.length} URLs`);
return await createTabsBatch(urls, active, wait_for, timeout, batch_settings);
} else if (url && count > 1) {
// Batch creation with same URL repeated
console.log(`🔄 Using repeat mode: ${count} copies of ${url}`);
const urlArray = Array(count).fill(url);
return await createTabsBatch(urlArray, active, wait_for, timeout, batch_settings);
} else {
// Single tab creation (legacy behavior)
console.log(`📱 Using single tab mode for: ${url || 'about:blank'}`);
return await createSingleTab(url, active, wait_for, timeout);
}
}
// Parameter validation
function validateTabCreateParams(params) {
const { url, urls, count = 1 } = params;
// Check for conflicting parameters
if (url && urls) {
return { valid: false, error: "Cannot specify both 'url' and 'urls' parameters" };
}
if (urls && count > 1) {
return { valid: false, error: "Cannot use 'count' with 'urls' array" };
}
// Allow empty URL for about:blank tabs
if (!url && !urls && count > 1) {
return { valid: false, error: "Must specify 'url' when using 'count' parameter" };
}
// Validate URLs array
if (urls) {
if (!Array.isArray(urls) || urls.length === 0) {
return { valid: false, error: "'urls' must be a non-empty array" };
}
if (urls.length > 100) {
return { valid: false, error: "Maximum 100 URLs allowed in batch operation" };
}
// Validate each URL
for (let i = 0; i < urls.length; i++) {
if (typeof urls[i] !== 'string' || !urls[i].trim()) {
return { valid: false, error: `Invalid URL at index ${i}: must be a non-empty string` };
}
}
}
// Validate count
if (count < 1 || count > 50) {
return { valid: false, error: "Count must be between 1 and 50" };
}
return { valid: true };
}
// Single tab creation (original behavior)
async function createSingleTab(url, active, wait_for, timeout) {
const createProperties = { active };
if (url) {
createProperties.url = url;
}
console.log(`🔍 Creating single tab with properties:`, createProperties);
const newTab = await browser.tabs.create(createProperties);
console.log(`📝 Tab created:`, { id: newTab.id, url: newTab.url, pendingUrl: newTab.pendingUrl });
// Wait a moment for the URL to load
if (url) {
await new Promise(resolve => setTimeout(resolve, 500));
// Check if tab loaded correctly
try {
const updatedTab = await browser.tabs.get(newTab.id);
console.log(`🔄 Tab after load check:`, { id: updatedTab.id, url: updatedTab.url, status: updatedTab.status });
// If URL was provided and wait_for is specified, wait for the element
if (wait_for) {
try {
await waitForElement(newTab.id, wait_for, timeout);
} catch (error) {
return {
success: true,
tab_id: newTab.id,
url: updatedTab.url,
actual_url: updatedTab.url,
requested_url: url,
warning: `Tab created but wait condition failed: ${error.message}`
};
}
}
return {
success: true,
tab_id: newTab.id,
url: updatedTab.url || updatedTab.pendingUrl || url,
actual_url: updatedTab.url || updatedTab.pendingUrl,
requested_url: url,
active: updatedTab.active,
status: updatedTab.status,
title: updatedTab.title || 'New Tab',
note: updatedTab.url === 'about:blank' && updatedTab.pendingUrl ? 'Tab is still loading' : undefined
};
} catch (error) {
console.error(`❌ Error checking tab status:`, error);
return {
success: true,
tab_id: newTab.id,
url: newTab.url || 'about:blank',
actual_url: newTab.url,
requested_url: url,
active: newTab.active,
title: newTab.title || 'New Tab',
warning: `Tab created but status check failed: ${error.message}`
};
}
}
return {
success: true,
tab_id: newTab.id,
url: newTab.url || 'about:blank',
active: newTab.active,
title: newTab.title || 'New Tab'
};
}
// Batch tab creation with performance throttling
async function createTabsBatch(urls, active, wait_for, timeout, batch_settings = {}) {
console.log('🔍 createTabsBatch called with:', { urls: urls.length, batch_settings });
const {
chunk_size = 5,
delay_between_chunks = 1000,
delay_between_tabs = 200
} = batch_settings || {};
const startTime = Date.now();
const totalTabs = urls.length;
const createdTabs = [];
const errors = [];
// Performance warnings
const warnings = [];
if (totalTabs > 20) {
warnings.push(`Creating ${totalTabs} tabs may impact browser performance`);
}
if (totalTabs > 45) {
warnings.push(`Large batch (${totalTabs} tabs) may hit Chrome's tab limits or cause memory issues`);
}
console.log(`🚀 Starting batch tab creation: ${totalTabs} tabs in chunks of ${chunk_size}`);
// Process in chunks
for (let chunkStart = 0; chunkStart < urls.length; chunkStart += chunk_size) {
const chunkEnd = Math.min(chunkStart + chunk_size, urls.length);
const chunk = urls.slice(chunkStart, chunkEnd);
const chunkIndex = Math.floor(chunkStart / chunk_size) + 1;
const totalChunks = Math.ceil(urls.length / chunk_size);
// Reduced logging for better performance
if (totalTabs > 10) {
console.log(`📦 Chunk ${chunkIndex}/${totalChunks}`);
}
// Create tabs in current chunk with delays
for (let i = 0; i < chunk.length; i++) {
const url = chunk[i];
const globalIndex = chunkStart + i;
const isLastTab = globalIndex === totalTabs - 1;
try {
// Only activate the very last tab if active=true
const shouldActivate = active && isLastTab;
const tab = await browser.tabs.create({
url: url,
active: shouldActivate
});
// Wait a moment and check actual URL
await new Promise(resolve => setTimeout(resolve, 300));
const updatedTab = await browser.tabs.get(tab.id);
createdTabs.push({
tab_id: tab.id,
url: updatedTab.url || url,
requested_url: url,
index: globalIndex,
active: updatedTab.active,
title: updatedTab.title || `Tab ${globalIndex + 1}`
});
// Only log for small batches to avoid context overflow
if (totalTabs <= 5) {
console.log(`✅ Created tab ${globalIndex + 1}/${totalTabs}: ${url} (ID: ${tab.id})`);
}
// Wait between individual tabs (except last in chunk)
if (i < chunk.length - 1) {
await new Promise(resolve => setTimeout(resolve, delay_between_tabs));
}
} catch (error) {
console.error(`❌ Failed to create tab ${globalIndex + 1}: ${error.message}`);
errors.push({
index: globalIndex,
url: url,
error: error.message
});
}
}
// Wait between chunks (except after last chunk)
if (chunkEnd < urls.length) {
if (totalTabs <= 10) {
console.log(`⏳ Waiting ${delay_between_chunks}ms before next chunk...`);
}
await new Promise(resolve => setTimeout(resolve, delay_between_chunks));
}
}
const executionTime = Date.now() - startTime;
const successCount = createdTabs.length;
const errorCount = errors.length;
console.log(`🏁 Batch creation complete: ${successCount}/${totalTabs} successful in ${executionTime}ms`);
// Prepare result - simplified to avoid context overflow
const result = {
success: errorCount === 0,
batch_operation: true,
summary: {
total_requested: totalTabs,
successful: successCount,
failed: errorCount,
execution_time_ms: executionTime
},
// Only include full tab details for small batches
created_tabs: totalTabs <= 10 ? createdTabs : createdTabs.map(tab => ({
tab_id: tab.tab_id,
url: tab.url
}))
};
// Add warnings if any
if (warnings.length > 0) {
result.warnings = warnings;
}
// Add errors if any
if (errors.length > 0) {
result.errors = errors;
result.partial_success = successCount > 0;
}
// Add active tab info
const activeTabs = createdTabs.filter(tab => tab.active);
if (activeTabs.length > 0) {
result.active_tab = activeTabs[0];
}
return result;
}
// Utility function to generate URLs for testing/demo purposes
function generateTestUrls(baseUrl, count) {
const urls = [];
for (let i = 1; i <= count; i++) {
urls.push(`${baseUrl}?tab=${i}`);
}
return urls;
}
// Batch operation helper functions
function estimateBatchTime(urlCount, batchSettings = {}) {
const {
chunk_size = 5,
delay_between_chunks = 1000,
delay_between_tabs = 200
} = batchSettings || {};
const totalChunks = Math.ceil(urlCount / chunk_size);
const timePerChunk = (chunk_size - 1) * delay_between_tabs; // delays within chunk
const timeForChunks = totalChunks * timePerChunk;
const timeBetweenChunks = (totalChunks - 1) * delay_between_chunks;
return timeForChunks + timeBetweenChunks; // in milliseconds
}
async function closeTabs(params) {
const { tab_id, tab_ids } = params;
let tabsToClose = [];
if (tab_ids && Array.isArray(tab_ids)) {
// Close multiple tabs
tabsToClose = tab_ids;
} else if (tab_id) {
// Close specific tab
tabsToClose = [tab_id];
} else {
// Close current tab
const [activeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (activeTab) {
tabsToClose = [activeTab.id];
}
}
if (tabsToClose.length === 0) {
throw new Error('No tabs specified to close');
}
// Close tabs
await browser.tabs.remove(tabsToClose);
return {
success: true,
closed_tabs: tabsToClose,
count: tabsToClose.length
};
}
async function listTabs(params) {
const {
current_window_only = true,
include_details = true,
check_content_script = false
} = params;
const queryOptions = {};
if (current_window_only) {
queryOptions.currentWindow = true;
}
const tabs = await browser.tabs.query(queryOptions);
// Check content script status if requested
const contentScriptStatuses = new Map();
if (check_content_script) {
const statusPromises = tabs.map(async (tab) => {
try {
const status = await getTabContentScriptStatus(tab.id);
return [tab.id, status];
} catch (error) {
return [tab.id, { ready: false, reason: 'error', error: error.message }];
}
});
const results = await Promise.all(statusPromises);
results.forEach(([tabId, status]) => {
contentScriptStatuses.set(tabId, status);
});
}
const tabList = tabs.map(tab => {
const basicInfo = {
id: tab.id,
url: tab.url,
active: tab.active,
title: tab.title
};
// Add content script status if checked
if (check_content_script) {
const scriptStatus = contentScriptStatuses.get(tab.id);
basicInfo.content_script = {
ready: scriptStatus?.ready || false,
reason: scriptStatus?.reason || 'unknown',
injectable: isInjectableUrl(tab.url)
};
}
if (include_details) {
return {
...basicInfo,
index: tab.index,
pinned: tab.pinned,
status: tab.status,
favIconUrl: tab.favIconUrl,
windowId: tab.windowId,
incognito: tab.incognito
};
}
return basicInfo;
});
// Calculate summary statistics
const summary = {
total_tabs: tabList.length,
active_tab: tabs.find(tab => tab.active)?.id || null
};
if (check_content_script) {
const readyTabs = tabList.filter(tab => tab.content_script?.ready).length;
const injectableTabs = tabList.filter(tab => tab.content_script?.injectable).length;
summary.content_script_stats = {
ready_count: readyTabs,
injectable_count: injectableTabs,
restricted_count: tabList.length - injectableTabs
};
}
return {
success: true,
tabs: tabList,
count: tabList.length,
summary
};
}
async function switchToTab(tabId) {
// First, get tab info to ensure it exists
const tab = await browser.tabs.get(tabId);
if (!tab) {
throw new Error(`Tab with ID ${tabId} not found`);
}
// Switch to the tab
await browser.tabs.update(tabId, { active: true });
// Also focus the window containing the tab
await browser.windows.update(tab.windowId, { focused: true });
return {
success: true,
tab_id: tabId,
url: tab.url,
title: tab.title,
window_id: tab.windowId
};
}
// Workspace and Reference Management Functions
async function getBookmarks(params) {
const { query } = params;
let bookmarks;
if (query) {
bookmarks = await browser.bookmarks.search(query);
} else {
bookmarks = await browser.bookmarks.getTree();
}
return {
success: true,
bookmarks,
count: bookmarks.length
};
}
async function addBookmark(params) {
const { title, url, parentId } = params;
const bookmark = await browser.bookmarks.create({
title,
url,
parentId
});
return {
success: true,
bookmark
};
}
// History Management Function
async function getHistory(params) {
const {
keywords = "",
start_date,
end_date,
domains = [],
min_visit_count = 1,
max_results = 50,
sort_by = "visit_time",
sort_order = "desc"
} = params;
try {
// Browser History API search configuration
const searchQuery = {
text: keywords,
maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering
};
// Add date range if specified
if (start_date) {
searchQuery.startTime = new Date(start_date).getTime();
}
if (end_date) {
searchQuery.endTime = new Date(end_date).getTime();
}
// Execute history search
const historyItems = await browser.history.search(searchQuery);
// Apply advanced filters
let filteredItems = historyItems.filter(item => {
// Domain filter
if (domains.length > 0) {
try {
const itemDomain = new URL(item.url).hostname;
if (!domains.some(domain => itemDomain.includes(domain))) {
return false;
}
} catch (e) {
// Skip items with invalid URLs
return false;
}
}
// Visit count filter
if (item.visitCount < min_visit_count) {
return false;
}
return true;
});
// Sort results
filteredItems.sort((a, b) => {
let aVal, bVal;
switch (sort_by) {
case "visit_count":
aVal = a.visitCount;
bVal = b.visitCount;
break;
case "title":
aVal = (a.title || "").toLowerCase();
bVal = (b.title || "").toLowerCase();
break;
default: // visit_time
aVal = a.lastVisitTime;
bVal = b.lastVisitTime;
}
if (sort_order === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
// Limit results
const results = filteredItems.slice(0, max_results);
// Format response with comprehensive metadata
return {
success: true,
history_items: results.map(item => {
let domain;
try {
domain = new URL(item.url).hostname;
} catch (e) {
domain = "invalid-url";
}
return {
id: item.id,
url: item.url,
title: item.title || "Untitled",
last_visit_time: new Date(item.lastVisitTime).toISOString(),
visit_count: item.visitCount,
domain: domain,
typed_count: item.typedCount || 0
};
}),
metadata: {
total_found: filteredItems.length,
returned_count: results.length,
search_params: {
keywords: keywords || null,
date_range: start_date && end_date ?
`${start_date} to ${end_date}` :
start_date ? `from ${start_date}` :
end_date ? `until ${end_date}` : null,
domains: domains.length > 0 ? domains : null,
min_visit_count,
sort_by,
sort_order
},
execution_time: new Date().toISOString(),
over_fetched: historyItems.length,
filters_applied: {
domain_filter: domains.length > 0,
visit_count_filter: min_visit_count > 1,
date_filter: !!(start_date || end_date),
keyword_filter: !!keywords
}
}
};
} catch (error) {
return {
success: false,
error: `History search failed: ${error.message}`,
history_items: [],
metadata: {
total_found: 0,
returned_count: 0,
search_params: params,
execution_time: new Date().toISOString()
}
};
}
}
// Selected Text Management Function
async function getSelectedText(params) {
const {
include_metadata = true,
max_length = 10000,
tab_id
} = params;
try {
let targetTab;
if (tab_id) {
// Use specific tab
try {
targetTab = await browser.tabs.get(tab_id);
} catch (error) {
return {
success: false,
error: `Tab ${tab_id} not found or inaccessible`,
selected_text: "",
metadata: {
execution_time: new Date().toISOString()
}
};
}
} else {
// Get the active tab
const [activeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!activeTab) {
return {
success: false,
error: "No active tab found",
selected_text: "",
metadata: {
execution_time: new Date().toISOString()
}
};
}
targetTab = activeTab;
}
// Execute script to get selected text - handle browser differences
let results;
if (browser.scripting) {
// Chrome MV3
results = await browser.scripting.executeScript({
target: { tabId: targetTab.id },
func: getSelectionFunction
});
} else {
// Firefox MV2
results = await browser.tabs.executeScript(targetTab.id, {
code: `(${getSelectionFunction.toString()})()`
});
}
const result = results[0]?.result || results[0];
if (!result) {
return {
success: false,
error: "Failed to execute selection script",
selected_text: "",
metadata: {
execution_time: new Date().toISOString()
}
};
}
if (!result.hasSelection) {
return {
success: true,
selected_text: "",
has_selection: false,
message: "No text is currently selected on the page",
metadata: {
execution_time: new Date().toISOString(),
tab_info: {
id: targetTab.id,
url: targetTab.url,
title: targetTab.title
}
}
};
}
// Truncate text if it exceeds max_length
let selectedText = result.text;
let truncated = false;
if (selectedText.length > max_length) {
selectedText = selectedText.substring(0, max_length);
truncated = true;
}
const response = {
success: true,
selected_text: selectedText,
has_selection: true,
character_count: result.text.length,
truncated: truncated,
metadata: {
execution_time: new Date().toISOString(),
tab_info: {
id: targetTab.id,
url: targetTab.url,
title: targetTab.title
}
}
};
// Include detailed metadata if requested
if (include_metadata && result.metadata) {
response.selection_metadata = result.metadata;
}
return response;
} catch (error) {
return {
success: false,
error: `Failed to get selected text: ${error.message}`,
selected_text: "",
has_selection: false,
metadata: {
execution_time: new Date().toISOString(),
error_details: error.stack
}
};
}
}
// Function to execute in page context
function getSelectionFunction() {
const selection = window.getSelection();
const selectedText = selection.toString();
if (!selectedText) {
return {
text: "",
hasSelection: false,
metadata: null
};
}
// Get metadata about the selection
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// Get parent element info
const commonAncestor = range.commonAncestorContainer;
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
? commonAncestor.parentElement
: commonAncestor;
const metadata = {
length: selectedText.length,
word_count: selectedText.trim().split(/\s+/).filter(word => word.length > 0).length,
line_count: selectedText.split('\n').length,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
},
parent_element: {
tag_name: parentElement.tagName?.toLowerCase(),
class_name: parentElement.className,
id: parentElement.id,
text_content_length: parentElement.textContent?.length || 0
},
page_info: {
url: window.location.href,
title: document.title,
domain: window.location.hostname
},
selection_info: {
anchor_offset: selection.anchorOffset,
focus_offset: selection.focusOffset,
range_count: selection.rangeCount,
is_collapsed: selection.isCollapsed
}
};
return {
text: selectedText,
hasSelection: true,
metadata: metadata
};
}
// Initialize connection when extension loads (with delay for server startup)
setTimeout(() => {
connectionManager.connect();
}, 1000);
// Handle messages from popup
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "getStatus") {
sendResponse(connectionManager.getStatus());
} else if (request.action === "getToolCount") {
const tools = getAvailableTools();
sendResponse({
toolCount: tools.length,
tools: tools.map(t => t.name)
});
} else if (request.action === "reconnect") {
connectionManager.connect();
sendResponse({ success: true });
} else if (request.action === "getPorts") {
sendResponse({
current: lastKnownPorts,
websocketUrl: MCP_SERVER_URL
});
} else if (request.action === "setSafetyMode") {
safetyModeEnabled = request.enabled;
console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`);
sendResponse({ success: true });
} else if (request.action === "test") {
connectionManager.send({ type: "test", timestamp: Date.now() });
sendResponse({ success: true });
}
return true; // Keep the message channel open
});