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-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-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" ,
description : "Two-phase intelligent page analysis with token efficiency optimization" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
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-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" ,
description : "Extract and summarize structured content with token efficiency optimization" ,
2025-06-13 23:21:32 +02:00
inputSchema : {
type : "object" ,
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-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" ,
description : "Click on a specific page element" ,
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-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" ,
description : "Fill input field with enhanced focus and event simulation for modern web apps" ,
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-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" ,
description : "Navigate to specified URL and wait for page load" ,
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" ,
description : "Create a new tab with optional URL and activation" ,
inputSchema : {
type : "object" ,
properties : {
url : {
type : "string" ,
description : "URL to open in the new tab (optional)"
} ,
active : {
type : "boolean" ,
default : true ,
description : "Whether to activate the new tab"
} ,
wait _for : {
type : "string" ,
description : "CSS selector to wait for after tab creation (if URL provided)"
} ,
timeout : {
type : "number" ,
default : 10000 ,
description : "Maximum wait time 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 : "Get list of all open tabs with their details" ,
inputSchema : {
type : "object" ,
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-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" ,
description : "Get the currently selected text on the page" ,
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"
}
}
}
} ,
{
name : "page_scroll" ,
description : "Scroll the page in various directions and amounts - critical for long pages" ,
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"
}
}
}
} ,
{
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 ) ) {
throw new Error ( ` 🛡️ Safety Mode is enabled. This tool ( ${ method } ) is blocked to prevent page modifications. To disable Safety Mode, open the OpenDia extension popup and toggle off "Safety Mode". ` ) ;
}
2025-06-13 23:21:32 +02:00
let result ;
switch ( method ) {
2025-06-25 19:07:09 +02:00
// New automation tools
case "page_analyze" :
result = await sendToContentScript ( 'analyze' , params ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "page_extract_content" :
result = await sendToContentScript ( 'extract_content' , params ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "element_click" :
result = await sendToContentScript ( 'element_click' , params ) ;
2025-06-13 23:21:32 +02:00
break ;
2025-06-25 19:07:09 +02:00
case "element_fill" :
result = await sendToContentScript ( 'element_fill' , params ) ;
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" :
result = await sendToContentScript ( 'wait_for' , params ) ;
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" :
result = await sendToContentScript ( 'get_element_state' , params ) ;
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" :
result = await sendToContentScript ( 'page_scroll' , params ) ;
break ;
case "get_page_links" :
result = await sendToContentScript ( 'get_page_links' , params ) ;
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-06-25 19:07:09 +02:00
// Enhanced content script communication
async function sendToContentScript ( action , data ) {
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
if ( ! activeTab ) {
throw new Error ( 'No active tab found' ) ;
}
return new Promise ( ( resolve , reject ) => {
chrome . tabs . sendMessage ( activeTab . id , { action , data } , ( response ) => {
if ( chrome . runtime . lastError ) {
reject ( new Error ( 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-06-27 15:01:01 +02:00
reject ( new Error ( 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-06-27 15:01:01 +02:00
// Tab Management Functions
async function createTab ( params ) {
const { url , active = true , wait _for , timeout = 10000 } = params ;
const createProperties = { active } ;
if ( url ) {
createProperties . url = url ;
}
const newTab = await chrome . tabs . create ( createProperties ) ;
// If URL was provided and wait_for is specified, wait for the element
if ( url && wait _for ) {
try {
await waitForElement ( newTab . id , wait _for , timeout ) ;
} catch ( error ) {
return {
success : true ,
tab _id : newTab . id ,
url : newTab . url ,
warning : ` Tab created but wait condition failed: ${ error . message } `
} ;
}
}
return {
success : true ,
tab _id : newTab . id ,
url : newTab . url || 'about:blank' ,
active : newTab . active ,
title : newTab . title || 'New Tab'
} ;
}
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 ) {
const { current _window _only = true , include _details = true } = params ;
const queryOptions = { } ;
if ( current _window _only ) {
queryOptions . currentWindow = true ;
}
const tabs = await chrome . tabs . query ( queryOptions ) ;
const tabList = tabs . map ( tab => {
const basicInfo = {
id : tab . id ,
url : tab . url ,
active : tab . active ,
title : tab . title
} ;
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 ;
} ) ;
return {
success : true ,
tabs : tabList ,
count : tabList . length ,
active _tab : tabs . find ( tab => tab . active ) ? . id || null
} ;
}
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 ,
max _length = 10000
} = params ;
try {
// 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 ( )
}
} ;
}
// Execute script to get selected text
2025-06-25 19:07:09 +02:00
const results = await chrome . scripting . executeScript ( {
target : { tabId : activeTab . 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 : {
id : activeTab . id ,
url : activeTab . url ,
title : activeTab . 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 : {
id : activeTab . id ,
url : activeTab . url ,
title : activeTab . title
}
}
} ;
// 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
} ) ;