2025-06-13 23:21:32 +02:00
// MCP Server connection configuration
2025-07-12 21:04:26 +02:00
let MCP _SERVER _URL = 'ws://localhost:5555' ; // Default, will be auto-discovered
2025-06-13 23:21:32 +02:00
let mcpSocket = null ;
let reconnectInterval = null ;
let reconnectAttempts = 0 ;
2025-07-12 21:04:26 +02:00
let lastKnownPorts = { websocket : 5555 , http : 5556 } ; // Cache for port discovery
2025-07-15 17:43:08 +02:00
// Safety Mode configuration
let safetyModeEnabled = false ;
const WRITE _EDIT _TOOLS = [
'element_click' ,
'element_fill'
] ;
// Load safety mode state on startup
chrome . storage . local . get ( [ 'safetyMode' ] , ( result ) => {
safetyModeEnabled = result . safetyMode || false ;
} ) ;
2025-07-15 19:52:39 +02:00
// 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 ) ;
chrome . tabs . sendMessage ( tabId , { action : 'ping' } , ( response ) => {
clearTimeout ( timeout ) ;
if ( chrome . runtime . lastError ) {
reject ( new Error ( chrome . 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 chrome . 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 } ` ) ;
await chrome . scripting . executeScript ( {
target : { tabId : tabId } ,
files : [ '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 ) ;
chrome . tabs . sendMessage ( tabId , { action : 'ping' } , ( response ) => {
clearTimeout ( timeout ) ;
if ( chrome . runtime . lastError ) {
reject ( new Error ( chrome . 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:' ] ;
const restrictedDomains = [ 'chrome.google.com' ] ;
// Check protocol
if ( restrictedProtocols . some ( protocol => url . startsWith ( protocol ) ) ) {
return false ;
}
// Check special Chrome 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 chrome . 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 ) , 1000 ) ;
chrome . 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 } ;
}
}
2025-07-12 21:04:26 +02:00
// Port discovery function
async function 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
}
}
// Fallback to default if discovery fails
console . log ( '⚠️ Port discovery failed, using defaults' ) ;
return null ;
}
2025-06-13 23:21:32 +02:00
// Initialize WebSocket connection to MCP server
2025-07-12 21:04:26 +02:00
async function connectToMCPServer ( ) {
2025-06-13 23:21:32 +02:00
if ( mcpSocket && mcpSocket . readyState === WebSocket . OPEN ) return ;
2025-07-12 21:04:26 +02:00
// Try port discovery if using default URL or if connection failed
if ( MCP _SERVER _URL === 'ws://localhost:5555' || reconnectAttempts > 2 ) {
await discoverServerPorts ( ) ;
reconnectAttempts = 0 ; // Reset attempts after discovery
}
2025-06-25 19:07:09 +02:00
console . log ( '🔗 Connecting to MCP server at' , MCP _SERVER _URL ) ;
2025-06-13 23:21:32 +02:00
mcpSocket = new WebSocket ( MCP _SERVER _URL ) ;
mcpSocket . onopen = ( ) => {
2025-06-25 19:07:09 +02:00
console . log ( '✅ Connected to MCP server' ) ;
2025-06-13 23:21:32 +02:00
clearInterval ( reconnectInterval ) ;
2025-06-25 19:07:09 +02:00
const tools = getAvailableTools ( ) ;
console . log ( ` 🔧 Registering ${ tools . length } tools: ` , tools . map ( t => t . name ) ) ;
2025-06-13 23:21:32 +02:00
// Register available browser functions
mcpSocket . send ( JSON . stringify ( {
type : 'register' ,
2025-06-25 19:07:09 +02:00
tools : tools
2025-06-13 23:21:32 +02:00
} ) ) ;
} ;
mcpSocket . onmessage = async ( event ) => {
const message = JSON . parse ( event . data ) ;
await handleMCPRequest ( message ) ;
} ;
mcpSocket . onclose = ( ) => {
2025-06-25 19:07:09 +02:00
console . log ( '❌ Disconnected from MCP server, will reconnect...' ) ;
2025-07-12 21:04:26 +02:00
reconnectAttempts ++ ;
// Clear any existing reconnect interval
if ( reconnectInterval ) {
clearInterval ( reconnectInterval ) ;
}
2025-06-13 23:21:32 +02:00
// Attempt to reconnect every 5 seconds
reconnectInterval = setInterval ( connectToMCPServer , 5000 ) ;
} ;
mcpSocket . onerror = ( error ) => {
2025-06-25 19:07:09 +02:00
console . log ( '⚠️ MCP WebSocket error:' , error ) ;
2025-07-12 21:04:26 +02:00
reconnectAttempts ++ ;
2025-06-13 23:21:32 +02:00
} ;
}
2025-06-25 19:07:09 +02:00
// Define available browser automation tools for MCP
2025-06-13 23:21:32 +02:00
function getAvailableTools ( ) {
return [
2025-07-15 19:52:39 +02:00
/ *
🎯 BACKGROUND TAB WORKFLOW GUIDE :
1. DISCOVER TABS : Use tab _list with check _content _script = true 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 : true } ) → Get tab IDs and readiness status
- 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
* /
2025-06-25 19:07:09 +02:00
// Page Analysis Tools
2025-06-13 23:21:32 +02:00
{
2025-06-25 19:07:09 +02:00
name : "page_analyze" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
2025-07-15 19:52:39 +02:00
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
] ,
2025-06-13 23:21:32 +02:00
properties : {
2025-06-25 19:07:09 +02:00
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"
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
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'])"
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "intent_hint" ]
}
2025-06-13 23:21:32 +02:00
} ,
{
2025-06-25 19:07:09 +02:00
name : "page_extract_content" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
2025-07-15 19:52:39 +02:00
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
] ,
2025-06-13 23:21:32 +02:00
properties : {
2025-06-25 19:07:09 +02:00
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"
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "content_type" ]
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
// Element Interaction Tools
2025-06-13 23:21:32 +02:00
{
2025-06-25 19:07:09 +02:00
name : "element_click" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-25 19:07:09 +02:00
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
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "element_id" ]
}
2025-06-13 23:21:32 +02:00
} ,
{
2025-06-25 19:07:09 +02:00
name : "element_fill" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-25 19:07:09 +02:00
element _id : {
2025-06-13 23:21:32 +02:00
type : "string" ,
2025-06-25 19:07:09 +02:00
description : "Unique element identifier from page_analyze"
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
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
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "element_id" , "value" ]
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
// Navigation Tools
2025-06-13 23:21:32 +02:00
{
2025-06-25 19:07:09 +02:00
name : "page_navigate" ,
2025-07-15 19:52:39 +02:00
description : "Navigate CURRENT tab to a new URL. Use tab_create instead if you want to open a NEW tab with a URL." ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-25 19:07:09 +02:00
url : {
2025-06-13 23:21:32 +02:00
type : "string" ,
2025-06-25 19:07:09 +02:00
description : "URL to navigate to"
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
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
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "url" ]
}
2025-06-13 23:21:32 +02:00
} ,
{
2025-06-25 19:07:09 +02:00
name : "page_wait_for" ,
description : "Wait for specific element or condition on current page" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-25 19:07:09 +02:00
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
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "condition_type" ]
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-27 15:01:01 +02:00
// Tab Management Tools
{
name : "tab_create" ,
2025-07-15 19:52:39 +02:00
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']}." ,
2025-06-27 15:01:01 +02:00
inputSchema : {
type : "object" ,
2025-07-15 19:52:39 +02:00
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
] ,
2025-06-27 15:01:01 +02:00
properties : {
url : {
type : "string" ,
2025-07-15 19:52:39 +02:00
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!"
2025-06-27 15:01:01 +02:00
} ,
active : {
type : "boolean" ,
default : true ,
2025-07-15 19:52:39 +02:00
description : "Whether to activate the last created tab (single tab only)"
2025-06-27 15:01:01 +02:00
} ,
wait _for : {
type : "string" ,
2025-07-15 19:52:39 +02:00
description : "CSS selector to wait for after tab creation (single tab only)"
2025-06-27 15:01:01 +02:00
} ,
timeout : {
type : "number" ,
default : 10000 ,
2025-07-15 19:52:39 +02:00
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"
}
}
2025-06-27 15:01:01 +02:00
}
}
}
} ,
{
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" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-27 15:01:01 +02:00
inputSchema : {
type : "object" ,
2025-07-15 19:52:39 +02:00
examples : [
{ check _content _script : true } , // RECOMMENDED: Check which tabs are ready for background operations
{ current _window _only : false , check _content _script : true } // Check all tabs across windows
] ,
2025-06-27 15:01:01 +02:00
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.)"
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-27 15:01:01 +02:00
}
}
}
} ,
2025-06-13 23:21:32 +02:00
{
2025-06-27 15:01:01 +02:00
name : "tab_switch" ,
description : "Switch to a specific tab by ID" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-27 15:01:01 +02:00
tab _id : {
type : "number" ,
description : "Tab ID to switch to"
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-27 15:01:01 +02:00
required : [ "tab_id" ]
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
// Element State Tools
2025-06-13 23:21:32 +02:00
{
2025-06-25 19:07:09 +02:00
name : "element_get_state" ,
description : "Get detailed state information for a specific element (disabled, clickable, etc.)" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
properties : {
2025-06-25 19:07:09 +02:00
element _id : {
type : "string" ,
description : "Element ID from page_analyze"
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-25 19:07:09 +02:00
required : [ "element_id" ]
}
2025-06-13 23:21:32 +02:00
} ,
2025-06-27 15:01:01 +02:00
// Workspace and Reference Management Tools
2025-06-13 23:21:32 +02:00
{
2025-06-27 15:01:01 +02:00
name : "get_bookmarks" ,
description : "Get all bookmarks or search for specific bookmarks" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
2025-06-27 15:01:01 +02:00
properties : {
query : {
type : "string" ,
description : "Search query for bookmarks (optional)"
}
}
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
{
2025-06-27 15:01:01 +02:00
name : "add_bookmark" ,
description : "Add a new bookmark" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
2025-06-27 15:01:01 +02:00
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" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-27 15:01:01 +02:00
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"
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-27 15:01:01 +02:00
}
}
}
} ,
{
name : "page_scroll" ,
2025-07-15 19:52:39 +02:00
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." ,
2025-06-27 15:01:01 +02:00
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"
2025-07-15 19:52:39 +02:00
} ,
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."
2025-06-27 15:01:01 +02:00
}
}
}
} ,
{
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"
}
}
2025-06-25 19:07:09 +02:00
}
2025-06-13 23:21:32 +02:00
} ,
] ;
}
2025-06-25 19:07:09 +02:00
// Handle MCP requests with enhanced automation tools
2025-06-13 23:21:32 +02:00
async function handleMCPRequest ( message ) {
const { id , method , params } = message ;
try {
2025-07-15 17:43:08 +02:00
// Safety Mode check: Block write/edit tools if safety mode is enabled
if ( safetyModeEnabled && WRITE _EDIT _TOOLS . includes ( method ) ) {
2025-07-15 19:52:39 +02:00
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". ` ) ;
2025-07-15 17:43:08 +02:00
}
2025-06-13 23:21:32 +02:00
let result ;
switch ( method ) {
2025-07-15 19:52:39 +02:00
// New automation tools with background tab support
2025-06-25 19:07:09 +02:00
case "page_analyze" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'analyze' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "page_extract_content" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'extract_content' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "element_click" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'element_click' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "element_fill" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'element_fill' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "page_navigate" :
result = await navigateToUrl ( params . url , params . wait _for , params . timeout ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "page_wait_for" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'wait_for' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-27 15:01:01 +02:00
// 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 ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
// Element state tools
case "element_get_state" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'get_element_state' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-27 15:01:01 +02:00
// Workspace and Reference Management Tools
case "get_bookmarks" :
result = await getBookmarks ( params ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-27 15:01:01 +02:00
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" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'page_scroll' , params , params . tab _id ) ;
2025-06-27 15:01:01 +02:00
break ;
case "get_page_links" :
2025-07-15 19:52:39 +02:00
result = await sendToContentScript ( 'get_page_links' , params , params . tab _id ) ;
2025-06-13 23:21:32 +02:00
break ;
default :
throw new Error ( ` Unknown method: ${ method } ` ) ;
}
// Send success response
mcpSocket . send (
JSON . stringify ( {
id ,
result ,
} )
) ;
} catch ( error ) {
// Send error response
mcpSocket . send (
JSON . stringify ( {
id ,
error : {
message : error . message ,
code : - 32603 ,
} ,
} )
) ;
}
}
2025-07-15 19:52:39 +02:00
// Enhanced content script communication with background tab support
async function sendToContentScript ( action , data , targetTabId = null ) {
let targetTab ;
2025-06-25 19:07:09 +02:00
2025-07-15 19:52:39 +02:00
if ( targetTabId ) {
// Use specific tab
try {
targetTab = await chrome . tabs . get ( targetTabId ) ;
} catch ( error ) {
throw new Error ( ` Tab ${ targetTabId } not found or inaccessible ` ) ;
}
} else {
// Fallback to active tab (maintains compatibility)
const [ activeTab ] = await chrome . tabs . query ( {
active : true ,
currentWindow : true ,
} ) ;
if ( ! activeTab ) {
throw new Error ( 'No active tab found' ) ;
}
targetTab = activeTab ;
2025-06-25 19:07:09 +02:00
}
2025-07-15 19:52:39 +02:00
// Ensure content script is available in the target tab
await ensureContentScriptReady ( targetTab . id ) ;
2025-06-25 19:07:09 +02:00
return new Promise ( ( resolve , reject ) => {
2025-07-15 19:52:39 +02:00
chrome . tabs . sendMessage ( targetTab . id , { action , data } , ( response ) => {
2025-06-25 19:07:09 +02:00
if ( chrome . runtime . lastError ) {
2025-07-15 19:52:39 +02:00
reject ( new Error ( ` Tab ${ targetTab . id } : ${ chrome . runtime . lastError . message } ` ) ) ;
2025-06-27 15:01:01 +02:00
} else if ( response && response . success ) {
2025-06-25 19:07:09 +02:00
resolve ( response . data ) ;
} else {
2025-07-15 19:52:39 +02:00
reject ( new Error ( ` Tab ${ targetTab . id } : ${ response ? . error || 'Unknown error' } ` ) ) ;
2025-06-25 19:07:09 +02:00
}
} ) ;
2025-06-13 23:21:32 +02:00
} ) ;
}
2025-06-25 19:07:09 +02:00
async function navigateToUrl ( url , waitFor , timeout = 10000 ) {
2025-06-13 23:21:32 +02:00
const [ activeTab ] = await chrome . tabs . query ( {
active : true ,
currentWindow : true ,
} ) ;
2025-06-25 19:07:09 +02:00
await chrome . 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 } ` } ;
}
2025-06-13 23:21:32 +02:00
}
2025-06-25 19:07:09 +02:00
return { success : true , tabId : activeTab . id , url : url } ;
2025-06-13 23:21:32 +02:00
}
2025-06-25 19:07:09 +02:00
async function waitForElement ( tabId , selector , timeout = 5000 ) {
const startTime = Date . now ( ) ;
while ( Date . now ( ) - startTime < timeout ) {
try {
const result = await chrome . tabs . sendMessage ( tabId , {
action : 'wait_for' ,
data : {
condition _type : 'element_visible' ,
selector : selector ,
timeout : 1000
2025-06-13 23:21:32 +02:00
}
2025-06-25 19:07:09 +02:00
} ) ;
if ( result . success ) {
return true ;
2025-06-13 23:21:32 +02:00
}
2025-06-25 19:07:09 +02:00
} 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 } ` ) ;
2025-06-13 23:21:32 +02:00
}
2025-07-15 19:52:39 +02:00
// Enhanced Tab Management Functions with Batch Support
2025-06-27 15:01:01 +02:00
async function createTab ( params ) {
2025-07-15 19:52:39 +02:00
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 } ) ;
2025-06-27 15:01:01 +02:00
2025-07-15 19:52:39 +02:00
// 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 ) {
2025-06-27 15:01:01 +02:00
const createProperties = { active } ;
if ( url ) {
createProperties . url = url ;
}
2025-07-15 19:52:39 +02:00
console . log ( ` 🔍 Creating single tab with properties: ` , createProperties ) ;
2025-06-27 15:01:01 +02:00
const newTab = await chrome . tabs . create ( createProperties ) ;
2025-07-15 19:52:39 +02:00
console . log ( ` 📝 Tab created: ` , { id : newTab . id , url : newTab . url , pendingUrl : newTab . pendingUrl } ) ;
2025-06-27 15:01:01 +02:00
2025-07-15 19:52:39 +02:00
// Wait a moment for the URL to load
if ( url ) {
await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
// Check if tab loaded correctly
2025-06-27 15:01:01 +02:00
try {
2025-07-15 19:52:39 +02:00
const updatedTab = await chrome . 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
} ;
2025-06-27 15:01:01 +02:00
} catch ( error ) {
2025-07-15 19:52:39 +02:00
console . error ( ` ❌ Error checking tab status: ` , error ) ;
2025-06-27 15:01:01 +02:00
return {
success : true ,
tab _id : newTab . id ,
2025-07-15 19:52:39 +02:00
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 } `
2025-06-27 15:01:01 +02:00
} ;
}
}
return {
success : true ,
tab _id : newTab . id ,
url : newTab . url || 'about:blank' ,
active : newTab . active ,
title : newTab . title || 'New Tab'
} ;
}
2025-07-15 19:52:39 +02:00
// 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 chrome . tabs . create ( {
url : url ,
active : shouldActivate
} ) ;
// Wait a moment and check actual URL
await new Promise ( resolve => setTimeout ( resolve , 300 ) ) ;
const updatedTab = await chrome . 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
}
2025-06-27 15:01:01 +02:00
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 chrome . 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 chrome . tabs . remove ( tabsToClose ) ;
return {
success : true ,
closed _tabs : tabsToClose ,
count : tabsToClose . length
} ;
}
async function listTabs ( params ) {
2025-07-15 19:52:39 +02:00
const {
current _window _only = true ,
include _details = true ,
check _content _script = false
} = params ;
2025-06-27 15:01:01 +02:00
const queryOptions = { } ;
if ( current _window _only ) {
queryOptions . currentWindow = true ;
}
const tabs = await chrome . tabs . query ( queryOptions ) ;
2025-07-15 19:52:39 +02:00
// 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 ) ;
} ) ;
}
2025-06-27 15:01:01 +02:00
const tabList = tabs . map ( tab => {
const basicInfo = {
id : tab . id ,
url : tab . url ,
active : tab . active ,
title : tab . title
} ;
2025-07-15 19:52:39 +02:00
// 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 )
} ;
}
2025-06-27 15:01:01 +02:00
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 ;
} ) ;
2025-07-15 19:52:39 +02:00
// 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
} ;
}
2025-06-27 15:01:01 +02:00
return {
success : true ,
tabs : tabList ,
count : tabList . length ,
2025-07-15 19:52:39 +02:00
summary
2025-06-27 15:01:01 +02:00
} ;
}
async function switchToTab ( tabId ) {
// First, get tab info to ensure it exists
const tab = await chrome . tabs . get ( tabId ) ;
if ( ! tab ) {
throw new Error ( ` Tab with ID ${ tabId } not found ` ) ;
}
// Switch to the tab
await chrome . tabs . update ( tabId , { active : true } ) ;
// Also focus the window containing the tab
await chrome . 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 chrome . bookmarks . search ( query ) ;
} else {
bookmarks = await chrome . bookmarks . getTree ( ) ;
}
return {
success : true ,
bookmarks ,
count : bookmarks . length
} ;
}
async function addBookmark ( params ) {
const { title , url , parentId } = params ;
const bookmark = await chrome . bookmarks . create ( {
title ,
url ,
parentId
2025-06-13 23:21:32 +02:00
} ) ;
2025-06-25 19:07:09 +02:00
2025-06-27 15:01:01 +02:00
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 ;
2025-06-25 19:07:09 +02:00
try {
2025-06-27 15:01:01 +02:00
// Chrome 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 chrome . 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 ,
2025-07-15 19:52:39 +02:00
max _length = 10000 ,
tab _id
2025-06-27 15:01:01 +02:00
} = params ;
try {
2025-07-15 19:52:39 +02:00
let targetTab ;
2025-06-27 15:01:01 +02:00
2025-07-15 19:52:39 +02:00
if ( tab _id ) {
// Use specific tab
try {
targetTab = await chrome . 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 chrome . 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 ;
2025-06-27 15:01:01 +02:00
}
// Execute script to get selected text
2025-06-25 19:07:09 +02:00
const results = await chrome . scripting . executeScript ( {
2025-07-15 19:52:39 +02:00
target : { tabId : targetTab . id } ,
2025-06-27 15:01:01 +02:00
func : ( ) => {
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
2025-06-25 19:07:09 +02:00
}
2025-06-27 15:01:01 +02:00
} ;
return {
text : selectedText ,
hasSelection : true ,
metadata : metadata
} ;
}
} ) ;
const result = results [ 0 ] ? . result ;
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 : {
2025-07-15 19:52:39 +02:00
id : targetTab . id ,
url : targetTab . url ,
title : targetTab . title
2025-06-25 19:07:09 +02:00
}
}
2025-06-27 15:01:01 +02:00
} ;
}
// 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 : {
2025-07-15 19:52:39 +02:00
id : targetTab . id ,
url : targetTab . url ,
title : targetTab . title
2025-06-27 15:01:01 +02:00
}
}
} ;
// Include detailed metadata if requested
if ( include _metadata && result . metadata ) {
response . selection _metadata = result . metadata ;
}
return response ;
2025-06-25 19:07:09 +02:00
} catch ( error ) {
return {
success : false ,
2025-06-27 15:01:01 +02:00
error : ` Failed to get selected text: ${ error . message } ` ,
selected _text : "" ,
has _selection : false ,
metadata : {
execution _time : new Date ( ) . toISOString ( ) ,
error _details : error . stack
}
2025-06-25 19:07:09 +02:00
} ;
}
2025-06-13 23:21:32 +02:00
}
2025-07-12 21:04:26 +02:00
// Initialize connection when extension loads (with delay for server startup)
setTimeout ( ( ) => {
connectToMCPServer ( ) ;
} , 1000 ) ;
2025-06-13 23:21:32 +02:00
// Heartbeat to keep connection alive
setInterval ( ( ) => {
if ( mcpSocket && mcpSocket . readyState === WebSocket . OPEN ) {
mcpSocket . send ( JSON . stringify ( { type : "ping" , timestamp : Date . now ( ) } ) ) ;
}
} , 30000 ) ; // Every 30 seconds
// Handle messages from popup
chrome . runtime . onMessage . addListener ( ( request , sender , sendResponse ) => {
if ( request . action === "getStatus" ) {
sendResponse ( {
connected : mcpSocket && mcpSocket . readyState === WebSocket . OPEN ,
} ) ;
2025-06-27 15:01:01 +02:00
} else if ( request . action === "getToolCount" ) {
const tools = getAvailableTools ( ) ;
sendResponse ( {
toolCount : tools . length ,
tools : tools . map ( t => t . name )
} ) ;
2025-06-13 23:21:32 +02:00
} else if ( request . action === "reconnect" ) {
connectToMCPServer ( ) ;
sendResponse ( { success : true } ) ;
2025-07-12 21:04:26 +02:00
} else if ( request . action === "getPorts" ) {
sendResponse ( {
current : lastKnownPorts ,
websocketUrl : MCP _SERVER _URL
} ) ;
2025-07-15 17:43:08 +02:00
} else if ( request . action === "setSafetyMode" ) {
safetyModeEnabled = request . enabled ;
console . log ( ` 🛡️ Safety Mode ${ safetyModeEnabled ? 'ENABLED' : 'DISABLED' } ` ) ;
sendResponse ( { success : true } ) ;
2025-06-13 23:21:32 +02:00
} else if ( request . action === "test" ) {
if ( mcpSocket && mcpSocket . readyState === WebSocket . OPEN ) {
mcpSocket . send ( JSON . stringify ( { type : "test" , timestamp : Date . now ( ) } ) ) ;
}
sendResponse ( { success : true } ) ;
}
return true ; // Keep the message channel open
2025-06-27 15:01:01 +02:00
} ) ;