opendia/opendia-mcp/server.js
Aaron Elijah Mars 0b09201160 Update to version 1.0.6 with Firefox support and improved extension management
- Add Firefox extension support with Manifest V2
- Update all version numbers to 1.0.6
- Build Chrome and Firefox extension packages
- Clean up extension directory structure
- Update README with Firefox installation instructions
- Update server messages to mention Chrome/Firefox support
- Add .gitignore for extension directory
- Create release packages for both browsers

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 22:57:39 +02:00

2510 lines
84 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const WebSocket = require("ws");
const express = require("express");
const net = require('net');
const { exec } = require('child_process');
// ADD: New imports for SSE transport
const cors = require('cors');
const { createServer } = require('http');
const { spawn } = require('child_process');
// ADD: Enhanced command line argument parsing
const args = process.argv.slice(2);
const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel');
const sseOnly = args.includes('--sse-only');
const killExisting = args.includes('--kill-existing');
// Parse port arguments
const wsPortArg = args.find(arg => arg.startsWith('--ws-port='));
const httpPortArg = args.find(arg => arg.startsWith('--http-port='));
const portArg = args.find(arg => arg.startsWith('--port='));
// Default ports (changed from 3000/3001 to 5555/5556)
let WS_PORT = wsPortArg ? parseInt(wsPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) : 5555);
let HTTP_PORT = httpPortArg ? parseInt(httpPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) + 1 : 5556);
// Port conflict detection utilities
async function checkPortInUse(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.listen(port, () => {
server.once('close', () => resolve(false));
server.close();
});
server.on('error', () => resolve(true));
});
}
async function checkIfOpenDiaProcess(port) {
return new Promise((resolve) => {
exec(`lsof -ti:${port}`, (error, stdout) => {
if (error || !stdout.trim()) {
resolve(false);
return;
}
const pid = stdout.trim().split('\n')[0];
exec(`ps -p ${pid} -o command=`, (psError, psOutput) => {
resolve(!psError && (
psOutput.includes('opendia') ||
psOutput.includes('server.js') ||
psOutput.includes('node') && psOutput.includes('opendia')
));
});
});
});
}
async function findAvailablePort(startPort) {
let port = startPort;
while (await checkPortInUse(port)) {
port++;
if (port > startPort + 100) { // Safety limit
throw new Error(`Could not find available port after checking ${port - startPort} ports`);
}
}
return port;
}
async function killExistingOpenDia(port) {
return new Promise((resolve) => {
exec(`lsof -ti:${port}`, async (error, stdout) => {
if (error || !stdout.trim()) {
resolve(false);
return;
}
const pids = stdout.trim().split('\n');
let killedAny = false;
for (const pid of pids) {
const isOpenDia = await checkIfOpenDiaProcess(port);
if (isOpenDia) {
exec(`kill ${pid}`, (killError) => {
if (!killError) {
console.error(`🔧 Killed existing OpenDia process (PID: ${pid})`);
killedAny = true;
}
});
}
}
// Wait a moment for processes to fully exit
setTimeout(() => resolve(killedAny), 1000);
});
});
}
async function handlePortConflict(port, portName) {
const isInUse = await checkPortInUse(port);
if (!isInUse) {
return port; // Port is free, use it
}
// Port is busy - give user options
console.error(`⚠️ ${portName} port ${port} is already in use`);
// Check if it's likely another OpenDia instance
const isOpenDia = await checkIfOpenDiaProcess(port);
if (isOpenDia) {
console.error(`🔍 Detected existing OpenDia instance on port ${port} (this should not happen after cleanup)`);
console.error(`⚠️ Attempting to kill remaining process...`);
await killExistingOpenDia(port);
await new Promise(resolve => setTimeout(resolve, 1000));
// Check if port is now free
const stillInUse = await checkPortInUse(port);
if (!stillInUse) {
console.error(`✅ Port ${port} is now available`);
return port;
}
// If still in use, find alternative port
const altPort = await findAvailablePort(port + 1);
console.error(`🔄 Port ${port} still busy, using port ${altPort}`);
if (portName === 'WebSocket') {
console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
}
return altPort;
} else {
// Something else is using the port - auto-increment
const altPort = await findAvailablePort(port + 1);
console.error(`🔄 ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`);
if (portName === 'WebSocket') {
console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
}
return altPort;
}
}
// ADD: Express app setup
const app = express();
app.use(cors());
app.use(express.json());
// WebSocket server for Chrome/Firefox Extension (will be initialized after port conflict resolution)
let wss = null;
let chromeExtensionSocket = null;
let availableTools = [];
// Tool call tracking
const pendingCalls = new Map();
// Simple MCP protocol implementation over stdio
async function handleMCPRequest(request) {
const { method, params, id } = request;
// Handle notifications (no id means it's a notification)
if (!id && method && method.startsWith("notifications/")) {
console.error(`Received notification: ${method}`);
return null; // No response needed for notifications
}
// Handle requests that don't need implementation
if (id === undefined || id === null) {
return null; // No response for notifications
}
try {
let result;
switch (method) {
case "initialize":
// RESPOND IMMEDIATELY - don't wait for extension
console.error(
`MCP client initializing: ${params?.clientInfo?.name || "unknown"}`
);
result = {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
},
serverInfo: {
name: "browser-mcp-server",
version: "2.0.0",
},
instructions:
"🎯 Enhanced browser automation with anti-detection bypass for Twitter/X, LinkedIn, Facebook. Extension may take a moment to connect.",
};
break;
case "tools/list":
// Debug logging
console.error(
`Tools/list called. Extension connected: ${
chromeExtensionSocket &&
chromeExtensionSocket.readyState === WebSocket.OPEN
}, Available tools: ${availableTools.length}`
);
// Return tools from extension if available, otherwise fallback tools
if (
chromeExtensionSocket &&
chromeExtensionSocket.readyState === WebSocket.OPEN &&
availableTools.length > 0
) {
console.error(
`Returning ${availableTools.length} tools from extension`
);
result = {
tools: availableTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
} else {
// Return basic fallback tools
console.error("Extension not connected, returning fallback tools");
result = {
tools: getFallbackTools(),
};
}
break;
case "tools/call":
if (
!chromeExtensionSocket ||
chromeExtensionSocket.readyState !== WebSocket.OPEN
) {
// Extension not connected - return helpful error
result = {
content: [
{
type: "text",
text: "❌ Browser Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n\nFor Chrome: \n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the Chrome extension folder\n\nFor Firefox:\n1. Go to about:debugging#/runtime/this-firefox\n2. Click 'Load Temporary Add-on...'\n3. Select the manifest-firefox.json file\n\n🎯 Features: Anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation",
},
],
isError: true,
};
} else {
// Extension connected - try the tool call
try {
const toolResult = await callBrowserTool(
params.name,
params.arguments || {}
);
// Format response based on tool type
const formattedResult = formatToolResult(params.name, toolResult);
result = {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
} catch (error) {
result = {
content: [
{
type: "text",
text: `❌ Tool execution failed: ${error.message}`,
},
],
isError: true,
};
}
}
break;
case "resources/list":
// Return empty resources list
result = { resources: [] };
break;
case "prompts/list":
// Return available workflow prompts
result = {
prompts: [
{
name: "post_to_social",
description: "Post content to social media platforms with anti-detection bypass",
arguments: [
{
name: "content",
description: "The content to post",
required: true
},
{
name: "platform",
description: "Target platform (twitter, linkedin, facebook)",
required: false
}
]
},
{
name: "post_selected_quote",
description: "Post currently selected text as a quote with commentary",
arguments: [
{
name: "commentary",
description: "Your commentary on the selected text",
required: false
}
]
},
{
name: "research_workflow",
description: "Research a topic using current page and bookmarking findings",
arguments: [
{
name: "topic",
description: "Research topic or query",
required: true
},
{
name: "depth",
description: "Research depth: quick, thorough, comprehensive",
required: false
}
]
},
{
name: "analyze_browsing_session",
description: "Analyze current browsing session and provide insights",
arguments: [
{
name: "focus",
description: "Analysis focus: productivity, research, trends",
required: false
}
]
},
{
name: "organize_tabs",
description: "Organize and clean up browser tabs intelligently",
arguments: [
{
name: "strategy",
description: "Organization strategy: close_duplicates, group_by_domain, archive_old",
required: false
}
]
},
{
name: "fill_form_assistant",
description: "Analyze and help fill out forms on the current page",
arguments: [
{
name: "form_type",
description: "Type of form: contact, registration, survey, application",
required: false
}
]
}
]
};
break;
case "prompts/get":
// Execute specific workflow based on prompt name
const promptName = params.name;
const promptArgs = params.arguments || {};
try {
let workflowResult;
switch (promptName) {
case "post_to_social":
workflowResult = await executePostToSocialWorkflow(promptArgs);
break;
case "post_selected_quote":
workflowResult = await executePostSelectedQuoteWorkflow(promptArgs);
break;
case "research_workflow":
workflowResult = await executeResearchWorkflow(promptArgs);
break;
case "analyze_browsing_session":
workflowResult = await executeSessionAnalysisWorkflow(promptArgs);
break;
case "organize_tabs":
workflowResult = await executeOrganizeTabsWorkflow(promptArgs);
break;
case "fill_form_assistant":
workflowResult = await executeFillFormWorkflow(promptArgs);
break;
default:
throw new Error(`Unknown prompt: ${promptName}`);
}
result = {
content: [
{
type: "text",
text: workflowResult
}
]
};
} catch (error) {
result = {
content: [
{
type: "text",
text: `❌ Workflow execution failed: ${error.message}`
}
],
isError: true
};
}
break;
default:
throw new Error(`Unknown method: ${method}`);
}
return { jsonrpc: "2.0", id, result };
} catch (error) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message: error.message,
},
};
}
}
// Enhanced tool result formatting with anti-detection support
function formatToolResult(toolName, result) {
const metadata = {
tool: toolName,
execution_time: result.execution_time || 0,
timestamp: new Date().toISOString(),
};
switch (toolName) {
case "page_analyze":
return formatPageAnalyzeResult(result, metadata);
case "page_extract_content":
return formatContentExtractionResult(result, metadata);
case "element_click":
return formatElementClickResult(result, metadata);
case "element_fill":
return formatElementFillResult(result, metadata);
case "page_navigate":
return `✅ Successfully navigated to: ${
result.url || "unknown URL"
}\n\n${JSON.stringify(metadata, null, 2)}`;
case "page_wait_for":
return (
`✅ Condition met: ${result.condition_type || "unknown"}\n` +
`Wait time: ${result.wait_time || 0}ms\n\n${JSON.stringify(
metadata,
null,
2
)}`
);
case "get_history":
return formatHistoryResult(result, metadata);
case "get_selected_text":
return formatSelectedTextResult(result, metadata);
case "page_scroll":
return formatScrollResult(result, metadata);
case "get_page_links":
return formatLinksResult(result, metadata);
case "tab_create":
return formatTabCreateResult(result, metadata);
case "tab_close":
return formatTabCloseResult(result, metadata);
case "tab_list":
return formatTabListResult(result, metadata);
case "tab_switch":
return formatTabSwitchResult(result, metadata);
case "element_get_state":
return formatElementStateResult(result, metadata);
case "page_style":
return formatPageStyleResult(result, metadata);
default:
// Legacy tools or unknown tools
return JSON.stringify(result, null, 2);
}
}
function formatPageAnalyzeResult(result, metadata) {
if (result.elements && result.elements.length > 0) {
const platformInfo = result.summary?.anti_detection_platform
? `\n🎯 Anti-detection platform detected: ${result.summary.anti_detection_platform}`
: "";
const summary =
`Found ${result.elements.length} relevant elements using ${result.method}:${platformInfo}\n\n` +
result.elements
.map((el) => {
const readyStatus = el.ready ? "✅ Ready" : "⚠️ Not ready";
const stateInfo = el.state === "disabled" ? " (disabled)" : "";
return `${el.name} (${el.type}) - Confidence: ${el.conf}% ${readyStatus}${stateInfo}\n Element ID: ${el.id}`;
})
.join("\n\n");
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
} else {
const intentHint = result.intent_hint || "unknown";
const platformInfo = result.summary?.anti_detection_platform
? `\nPlatform: ${result.summary.anti_detection_platform}`
: "";
return `No relevant elements found for intent: "${intentHint}"${platformInfo}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
}
}
function formatContentExtractionResult(result, metadata) {
const contentSummary = `Extracted ${result.content_type} content using ${result.method}:\n\n`;
if (result.content) {
// Check if this is full content extraction (summarize=false) or summary
// If it's a content object with properties, show full content
// If it's a string or small content, it's probably summarized
let preview;
if (typeof result.content === "string") {
// String content - likely summarized, keep truncation
preview = result.content.substring(0, 500) + (result.content.length > 500 ? "..." : "");
} else if (result.content && typeof result.content === "object") {
// Object content - check if it's full content extraction
if (result.content.content && result.content.content.length > 1000) {
// This looks like full content extraction - don't truncate
preview = JSON.stringify(result.content, null, 2);
} else {
// Smaller content, apply truncation
preview = JSON.stringify(result.content, null, 2).substring(0, 500);
}
} else {
// Fallback
preview = JSON.stringify(result.content, null, 2).substring(0, 500);
}
return `${contentSummary}${preview}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
} else if (result.summary) {
// Enhanced summarized content response
const summaryText = formatContentSummary(
result.summary,
result.content_type
);
return `${contentSummary}${summaryText}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
} else {
return `${contentSummary}No content found\n\n${JSON.stringify(
metadata,
null,
2
)}`;
}
}
function formatContentSummary(summary, contentType) {
switch (contentType) {
case "article":
return (
`📰 Article: "${summary.title}"\n` +
`📝 Word count: ${summary.word_count}\n` +
`⏱️ Reading time: ${summary.reading_time} minutes\n` +
`🖼️ Has media: ${summary.has_images || summary.has_videos}\n` +
`Preview: ${summary.preview}`
);
case "search_results":
return (
`🔍 Search Results Summary:\n` +
`📊 Total results: ${summary.total_results}\n` +
`🏆 Quality score: ${summary.quality_score}/100\n` +
`📈 Average relevance: ${Math.round(summary.avg_score * 100)}%\n` +
`🌐 Top domains: ${summary.top_domains
?.map((d) => d.domain)
.join(", ")}\n` +
`📝 Result types: ${summary.result_types?.join(", ")}`
);
case "posts":
return (
`📱 Social Posts Summary:\n` +
`📊 Post count: ${summary.post_count}\n` +
`📝 Average length: ${summary.avg_length} characters\n` +
`❤️ Total engagement: ${summary.engagement_total}\n` +
`🖼️ Posts with media: ${summary.has_media_count}\n` +
`👥 Unique authors: ${summary.authors}\n` +
`📋 Post types: ${summary.post_types?.join(", ")}`
);
default:
return JSON.stringify(summary, null, 2);
}
}
function formatElementClickResult(result, metadata) {
return (
`✅ Successfully clicked element: ${
result.element_name || result.element_id
}\n` +
`Click type: ${result.click_type || "left"}\n\n${JSON.stringify(
metadata,
null,
2
)}`
);
}
function formatElementFillResult(result, metadata) {
// Enhanced formatting for anti-detection bypass methods
const methodEmojis = {
twitter_direct_bypass: "🐦 Twitter Direct Bypass",
linkedin_direct_bypass: "💼 LinkedIn Direct Bypass",
facebook_direct_bypass: "📘 Facebook Direct Bypass",
generic_direct_bypass: "🎯 Generic Direct Bypass",
standard_fill: "🔧 Standard Fill",
anti_detection_bypass: "🛡️ Anti-Detection Bypass",
};
const methodDisplay = methodEmojis[result.method] || result.method;
const successIcon = result.success ? "✅" : "❌";
let fillResult = `${successIcon} Element fill ${
result.success ? "completed" : "failed"
} using ${methodDisplay}\n`;
fillResult += `📝 Target: ${result.element_name || result.element_id}\n`;
fillResult += `💬 Input: "${result.value}"\n`;
if (result.actual_value) {
fillResult += `📄 Result: "${result.actual_value}"\n`;
}
// Add bypass-specific information
if (
result.method?.includes("bypass") &&
result.execCommand_result !== undefined
) {
fillResult += `🔧 execCommand success: ${result.execCommand_result}\n`;
}
if (!result.success && result.method?.includes("bypass")) {
fillResult += `\n⚠️ Direct bypass failed - page may have enhanced detection. Try refreshing the page.\n`;
}
return `${fillResult}\n${JSON.stringify(metadata, null, 2)}`;
}
function formatHistoryResult(result, metadata) {
if (!result.history_items || result.history_items.length === 0) {
return `🕒 No history items found matching the criteria\n\n${JSON.stringify(metadata, null, 2)}`;
}
const summary = `🕒 Found ${result.history_items.length} history items (${result.metadata.total_found} total matches):\n\n`;
const items = result.history_items.map((item, index) => {
const visitInfo = `Visits: ${item.visit_count}`;
const timeInfo = new Date(item.last_visit_time).toLocaleDateString();
const domainInfo = `[${item.domain}]`;
return `${index + 1}. **${item.title}**\n ${domainInfo} ${visitInfo} | Last: ${timeInfo}\n URL: ${item.url}`;
}).join('\n\n');
const searchSummary = result.metadata.search_params.keywords ?
`\n🔍 Search: "${result.metadata.search_params.keywords}"` : '';
const dateSummary = result.metadata.search_params.date_range ?
`\n📅 Date range: ${result.metadata.search_params.date_range}` : '';
const domainSummary = result.metadata.search_params.domains ?
`\n🌐 Domains: ${result.metadata.search_params.domains.join(', ')}` : '';
const visitSummary = result.metadata.search_params.min_visit_count > 1 ?
`\n📊 Min visits: ${result.metadata.search_params.min_visit_count}` : '';
return `${summary}${items}${searchSummary}${dateSummary}${domainSummary}${visitSummary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatSelectedTextResult(result, metadata) {
if (!result.has_selection) {
return `📝 No text selected\n\n${result.message || "No text is currently selected on the page"}\n\n${JSON.stringify(metadata, null, 2)}`;
}
const textPreview = result.selected_text.length > 200
? result.selected_text.substring(0, 200) + "..."
: result.selected_text;
let summary = `📝 Selected Text (${result.character_count} characters):\n\n"${textPreview}"`;
if (result.truncated) {
summary += `\n\n⚠️ Text was truncated to fit length limit`;
}
if (result.selection_metadata) {
const meta = result.selection_metadata;
summary += `\n\n📊 Selection Details:`;
summary += `\n• Word count: ${meta.word_count}`;
summary += `\n• Line count: ${meta.line_count}`;
summary += `\n• Position: ${Math.round(meta.position.x)}, ${Math.round(meta.position.y)}`;
if (meta.parent_element.tag_name) {
summary += `\n• Parent element: <${meta.parent_element.tag_name}>`;
if (meta.parent_element.class_name) {
summary += ` class="${meta.parent_element.class_name}"`;
}
}
if (meta.page_info) {
summary += `\n• Page: ${meta.page_info.title}`;
summary += `\n• Domain: ${meta.page_info.domain}`;
}
}
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatScrollResult(result, metadata) {
if (!result.success) {
return `📜 Scroll failed: ${result.error || "Unknown error"}\n\n${JSON.stringify(metadata, null, 2)}`;
}
let summary = `📜 Page scrolled successfully`;
if (result.direction) {
summary += ` ${result.direction}`;
}
if (result.amount && result.amount !== "custom") {
summary += ` (${result.amount})`;
} else if (result.pixels) {
summary += ` (${result.pixels}px)`;
}
if (result.element_scrolled) {
summary += `\n🎯 Scrolled to element: ${result.element_scrolled}`;
}
if (result.scroll_position) {
summary += `\n📍 New position: x=${result.scroll_position.x}, y=${result.scroll_position.y}`;
}
if (result.page_dimensions) {
const { width, height, scrollWidth, scrollHeight } = result.page_dimensions;
summary += `\n📐 Page size: ${width}x${height} (scrollable: ${scrollWidth}x${scrollHeight})`;
}
if (result.wait_time) {
summary += `\n⏱️ Waited ${result.wait_time}ms after scroll`;
}
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatLinksResult(result, metadata) {
if (!result.links || result.links.length === 0) {
return `🔗 No links found on the page\n\n${JSON.stringify(metadata, null, 2)}`;
}
const summary = `🔗 Found ${result.returned} links (${result.total_found} total on page):\n`;
const currentDomain = result.current_domain ? `\n🌐 Current domain: ${result.current_domain}` : '';
const linksList = result.links.map((link, index) => {
const typeIcon = link.type === 'internal' ? '🏠' : '🌐';
const linkText = link.text.length > 50 ? link.text.substring(0, 50) + '...' : link.text;
const displayText = linkText || '[No text]';
const title = link.title ? `\n Title: ${link.title}` : '';
const domain = link.domain ? ` [${link.domain}]` : '';
return `${index + 1}. ${typeIcon} **${displayText}**${domain}${title}\n URL: ${link.url}`;
}).join('\n\n');
const filterInfo = [];
if (result.links.some(l => l.type === 'internal') && result.links.some(l => l.type === 'external')) {
const internal = result.links.filter(l => l.type === 'internal').length;
const external = result.links.filter(l => l.type === 'external').length;
filterInfo.push(`📊 Internal: ${internal}, External: ${external}`);
}
const filterSummary = filterInfo.length > 0 ? `\n${filterInfo.join('\n')}` : '';
return `${summary}${currentDomain}${filterSummary}\n\n${linksList}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatTabCreateResult(result, metadata) {
// Handle batch operations
if (result.batch_operation) {
const { summary, created_tabs, settings_used, warnings, errors } = result;
let output = `🚀 Batch tab creation completed
📊 Summary: ${summary.successful}/${summary.total_requested} tabs created successfully
⏱️ Execution time: ${summary.execution_time_ms}ms
📦 Chunks processed: ${summary.chunks_processed}
`;
// Add warnings if any
if (warnings && warnings.length > 0) {
output += `⚠️ Warnings:\n${warnings.map(w => `${w}`).join('\n')}\n\n`;
}
// Add created tabs info
if (created_tabs && created_tabs.length > 0) {
output += `✅ Created tabs:\n`;
created_tabs.forEach((tab, index) => {
output += ` ${index + 1}. ${tab.title || 'New Tab'} (ID: ${tab.tab_id})\n`;
output += ` 🌐 ${tab.actual_url || tab.url}\n`;
if (tab.active) output += ` 🎯 Active tab\n`;
});
output += '\n';
}
// Add errors if any
if (errors && errors.length > 0) {
output += `❌ Errors:\n`;
errors.forEach((error, index) => {
output += ` ${index + 1}. ${error.url}: ${error.error}\n`;
});
output += '\n';
}
// Add settings used (if available)
if (settings_used) {
output += `⚙️ Settings used:\n`;
output += ` • Chunk size: ${settings_used.chunk_size}\n`;
output += ` • Delay between chunks: ${settings_used.delay_between_chunks}ms\n`;
output += ` • Delay between tabs: ${settings_used.delay_between_tabs}ms\n`;
}
return output + `\n${JSON.stringify(metadata, null, 2)}`;
}
// Handle single tab operations (existing logic)
if (result.success) {
return `✅ New tab created successfully
🆔 Tab ID: ${result.tab_id}
🌐 URL: ${result.url || 'about:blank'}
🎯 Active: ${result.active ? 'Yes' : 'No'}
📝 Title: ${result.title || 'New Tab'}
${result.warning ? `⚠️ Warning: ${result.warning}` : ''}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `❌ Failed to create tab: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatTabCloseResult(result, metadata) {
if (result.success) {
const tabText = result.count === 1 ? 'tab' : 'tabs';
return `✅ Successfully closed ${result.count} ${tabText}
🆔 Closed tab IDs: ${result.closed_tabs.join(', ')}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `❌ Failed to close tabs: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatTabListResult(result, metadata) {
if (!result.success || !result.tabs || result.tabs.length === 0) {
return `📋 No tabs found
${JSON.stringify(metadata, null, 2)}`;
}
const summary = `📋 Found ${result.count} open tabs:
🎯 Active tab: ${result.active_tab || 'None'}
`;
const tabsList = result.tabs.map((tab, index) => {
const activeIcon = tab.active ? '🟢' : '⚪';
const statusInfo = tab.status ? ` [${tab.status}]` : '';
const pinnedInfo = tab.pinned ? ' 📌' : '';
return `${index + 1}. ${activeIcon} **${tab.title}**${pinnedInfo}${statusInfo}
🆔 ID: ${tab.id} | 🌐 ${tab.url}`;
}).join('\n\n');
return `${summary}${tabsList}
${JSON.stringify(metadata, null, 2)}`;
}
function formatTabSwitchResult(result, metadata) {
if (result.success) {
return `✅ Successfully switched to tab
🆔 Tab ID: ${result.tab_id}
📝 Title: ${result.title}
🌐 URL: ${result.url}
🏠 Window ID: ${result.window_id}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `❌ Failed to switch tabs: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatElementStateResult(result, metadata) {
const element = result.element_name || result.element_id || 'Unknown element';
const state = result.state || {};
let summary = `🔍 Element State: ${element}
📊 **Interaction Readiness**: ${state.interaction_ready ? '✅ Ready' : '❌ Not Ready'}
**Detailed State:**
• Disabled: ${state.disabled ? '❌ Yes' : '✅ No'}
• Visible: ${state.visible ? '✅ Yes' : '❌ No'}
• Clickable: ${state.clickable ? '✅ Yes' : '❌ No'}
• Focusable: ${state.focusable ? '✅ Yes' : '❌ No'}
• Has Text: ${state.hasText ? '✅ Yes' : '❌ No'}
• Is Empty: ${state.isEmpty ? '❌ Yes' : '✅ No'}`;
if (result.current_value) {
summary += `
📝 **Current Value**: "${result.current_value}"`;
}
return `${summary}
${JSON.stringify(metadata, null, 2)}`;
}
function formatPageStyleResult(result, metadata) {
const successIcon = result.success ? '✅' : '❌';
const statusText = result.success ? 'successfully applied' : 'failed to apply';
let summary = `🎨 Page styling ${statusText}\n\n`;
summary += `📄 **Operation Details:**\n`;
summary += `• **Mode:** ${result.mode || 'Unknown'}\n`;
if (result.theme) {
summary += `• **Theme:** ${result.theme}\n`;
}
if (result.applied_css !== undefined) {
summary += `• **CSS Applied:** ${result.applied_css} characters\n`;
}
if (result.description) {
summary += `• **Result:** ${result.description}\n`;
}
if (result.remember_enabled) {
summary += `• **Saved:** Style preferences saved for this domain\n`;
}
if (result.effect_duration) {
summary += `• **Effect Duration:** ${result.effect_duration} seconds\n`;
}
if (result.mood) {
summary += `• **Mood Applied:** "${result.mood}"\n`;
}
if (result.intensity) {
summary += `• **Intensity:** ${result.intensity}\n`;
}
if (!result.success && result.error) {
summary += `\n❌ **Error:** ${result.error}\n`;
}
if (result.warning) {
summary += `\n⚠️ **Warning:** ${result.warning}\n`;
}
summary += `\n💡 **Tip:** Use mode="reset" to restore original page styling`;
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
// Enhanced fallback tools when extension is not connected
function getFallbackTools() {
return [
{
name: "page_analyze",
description:
"🔍 BACKGROUND TAB READY: Analyze any tab without switching! Two-phase intelligent page analysis with token efficiency optimization. Use tab_id parameter to analyze background tabs while staying on current page. (Extension required)",
inputSchema: {
type: "object",
properties: {
intent_hint: {
type: "string",
description:
"What user wants to do: post_tweet, search, login, etc.",
},
phase: {
type: "string",
enum: ["discover", "detailed"],
default: "discover",
description:
"Analysis phase: 'discover' for quick scan, 'detailed' for full analysis",
},
},
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. (Extension required)",
inputSchema: {
type: "object",
properties: {
content_type: {
type: "string",
enum: ["article", "search_results", "posts"],
description: "Type of content to extract",
},
summarize: {
type: "boolean",
default: true,
description:
"Return summary instead of full content (saves tokens)",
},
},
required: ["content_type"],
},
},
{
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. (Extension required)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze",
},
click_type: {
type: "string",
enum: ["left", "right", "double"],
default: "left",
},
},
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 with anti-detection bypass for Twitter/X, LinkedIn, Facebook. Use tab_id to fill forms in background tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze",
},
value: {
type: "string",
description: "Text to input",
},
clear_first: {
type: "boolean",
default: true,
description: "Clear existing content before filling",
},
},
required: ["element_id", "value"],
},
},
{
name: "page_navigate",
description:
"🧭 Navigate to URLs with wait conditions (Extension required)",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to navigate to" },
wait_for: {
type: "string",
description: "CSS selector to wait for after navigation",
},
},
required: ["url"],
},
},
{
name: "page_wait_for",
description: "⏳ Wait for elements or conditions (Extension required)",
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_visible condition)",
},
text: {
type: "string",
description: "Text to wait for (for text_present condition)",
},
},
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 (Extension required)",
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. (Extension required)",
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.)"
}
}
}
},
{
name: "tab_switch",
description: "🔄 Switch to a specific tab by ID (Extension required)",
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.) (Extension required)",
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 (Extension required)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for bookmarks (optional)"
}
}
}
},
{
name: "add_bookmark",
description: "Add a new bookmark (Extension required)",
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 (Extension required)",
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"
},
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"
}
}
}
},
{
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. (Extension required)",
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: "📜 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. (Extension required)",
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 smart filtering (Extension required)",
inputSchema: {
type: "object",
properties: {
include_internal: {
type: "boolean",
default: true,
description: "Include internal links (same domain)"
},
include_external: {
type: "boolean",
default: true,
description: "Include external links (different domains)"
},
domain_filter: {
type: "string",
description: "Filter links to include only specific domain(s)"
},
max_results: {
type: "number",
default: 100,
maximum: 500,
description: "Maximum number of 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"]
}
},
];
}
// Call browser tool through Chrome/Firefox Extension
async function callBrowserTool(toolName, args) {
if (
!chromeExtensionSocket ||
chromeExtensionSocket.readyState !== WebSocket.OPEN
) {
throw new Error(
"Browser Extension not connected. Make sure the extension is installed and active."
);
}
const callId = Date.now().toString();
return new Promise((resolve, reject) => {
pendingCalls.set(callId, { resolve, reject });
chromeExtensionSocket.send(
JSON.stringify({
id: callId,
method: toolName,
params: args,
})
);
// Timeout after 30 seconds
setTimeout(() => {
if (pendingCalls.has(callId)) {
pendingCalls.delete(callId);
reject(new Error("Tool call timeout"));
}
}, 30000);
});
}
// Handle tool responses from Chrome/Firefox Extension
function handleToolResponse(message) {
const pending = pendingCalls.get(message.id);
if (pending) {
pendingCalls.delete(message.id);
if (message.error) {
pending.reject(new Error(message.error.message));
} else {
pending.resolve(message.result);
}
}
}
// Setup WebSocket connection handlers
function setupWebSocketHandlers() {
wss.on("connection", (ws) => {
console.error("Browser Extension connected");
chromeExtensionSocket = ws;
// Set up ping/pong for keepalive
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on("message", (data) => {
try {
const message = JSON.parse(data);
if (message.type === "register") {
availableTools = message.tools;
console.error(
`✅ Registered ${availableTools.length} browser tools from extension`
);
console.error(
`🎯 Enhanced tools with anti-detection bypass: ${availableTools
.map((t) => t.name)
.join(", ")}`
);
} else if (message.type === "ping") {
// Respond to ping with pong
ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
} else if (message.id) {
// Handle tool response
handleToolResponse(message);
}
} catch (error) {
console.error("Error processing message:", error);
}
});
ws.on("close", () => {
console.error("Browser Extension disconnected");
chromeExtensionSocket = null;
availableTools = []; // Clear tools when extension disconnects
clearInterval(pingInterval);
});
ws.on("error", (error) => {
console.error("WebSocket error:", error);
});
ws.on("pong", () => {
// Extension is alive
});
});
}
// ADD: SSE/HTTP endpoints for online AI
app.route('/sse')
.get((req, res) => {
// SSE stream for connection
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
});
res.write(`data: ${JSON.stringify({
type: 'connection',
status: 'connected',
server: 'OpenDia MCP Server',
version: '1.0.0'
})}\n\n`);
// Heartbeat to keep connection alive
const heartbeat = setInterval(() => {
res.write(`data: ${JSON.stringify({
type: 'heartbeat',
timestamp: Date.now()
})}\n\n`);
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
console.error('SSE client disconnected');
});
console.error('SSE client connected');
})
.post(async (req, res) => {
// MCP requests from online AI
console.error('MCP request received via SSE:', req.body);
try {
const result = await handleMCPRequest(req.body);
res.json({
jsonrpc: "2.0",
id: req.body.id,
result: result
});
} catch (error) {
res.status(500).json({
jsonrpc: "2.0",
id: req.body.id,
error: { code: -32603, message: error.message }
});
}
});
// ADD: CORS preflight handler
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control');
res.sendStatus(200);
});
// Read from stdin
let inputBuffer = "";
if (!sseOnly) {
process.stdin.on("data", async (chunk) => {
inputBuffer += chunk.toString();
// Process complete lines
const lines = inputBuffer.split("\n");
inputBuffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
const request = JSON.parse(line);
const response = await handleMCPRequest(request);
// Only send response if one was generated (not for notifications)
if (response) {
process.stdout.write(JSON.stringify(response) + "\n");
}
} catch (error) {
console.error("Error processing request:", error);
}
}
}
});
}
// ADD: Health check endpoint (update existing one)
app.get('/health', (req, res) => {
res.json({
status: 'ok',
chromeExtensionConnected: chromeExtensionSocket !== null,
availableTools: availableTools.length,
transport: sseOnly ? 'sse-only' : 'hybrid',
tunnelEnabled: enableTunnel,
ports: {
websocket: WS_PORT,
http: HTTP_PORT
},
features: [
'Anti-detection bypass for Twitter/X, LinkedIn, Facebook',
'Two-phase intelligent page analysis',
'Smart content extraction with summarization',
'Element state detection and interaction readiness',
'Performance analytics and token optimization',
'SSE transport for online AI services'
]
});
});
// ADD: Port discovery endpoint for Chrome/Firefox extension
app.get('/ports', (req, res) => {
res.json({
websocket: WS_PORT,
http: HTTP_PORT,
websocketUrl: `ws://localhost:${WS_PORT}`,
httpUrl: `http://localhost:${HTTP_PORT}`,
sseUrl: `http://localhost:${HTTP_PORT}/sse`
});
});
// START: Enhanced server startup with port conflict resolution
async function startServer() {
console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features");
console.error(`📊 Default ports: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`);
// Always kill existing OpenDia processes on startup
console.error('🔧 Checking for existing OpenDia processes...');
const wsKilled = await killExistingOpenDia(WS_PORT);
const httpKilled = await killExistingOpenDia(HTTP_PORT);
if (wsKilled || httpKilled) {
console.error('✅ Existing processes terminated');
// Wait for ports to be fully released
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
console.error(' No existing OpenDia processes found');
}
// Resolve port conflicts
WS_PORT = await handlePortConflict(WS_PORT, 'WebSocket');
HTTP_PORT = await handlePortConflict(HTTP_PORT, 'HTTP');
// Ensure HTTP port doesn't conflict with resolved WebSocket port
if (HTTP_PORT === WS_PORT) {
HTTP_PORT = await findAvailablePort(WS_PORT + 1);
console.error(`🔄 HTTP port adjusted to ${HTTP_PORT} to avoid WebSocket conflict`);
}
// Initialize WebSocket server after port resolution
wss = new WebSocket.Server({ port: WS_PORT });
// Set up WebSocket connection handling
setupWebSocketHandlers();
console.error(`✅ Ports resolved: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`);
// Start HTTP server
const httpServer = app.listen(HTTP_PORT, () => {
console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`);
console.error(`🔌 Browser Extension connected on ws://localhost:${WS_PORT}`);
console.error("🎯 Features: Anti-detection bypass + intelligent automation");
});
// Auto-tunnel if requested
if (enableTunnel) {
try {
console.error('🔄 Starting automatic tunnel...');
// Use the system ngrok binary directly
const ngrokProcess = spawn('ngrok', ['http', HTTP_PORT, '--log', 'stdout'], {
stdio: ['ignore', 'pipe', 'pipe']
});
let tunnelUrl = null;
// Wait for tunnel URL
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ngrokProcess.kill();
reject(new Error('Tunnel startup timeout'));
}, 10000);
ngrokProcess.stdout.on('data', (data) => {
const output = data.toString();
const match = output.match(/url=https:\/\/[^\s]+/);
if (match) {
tunnelUrl = match[0].replace('url=', '');
clearTimeout(timeout);
resolve();
}
});
ngrokProcess.stderr.on('data', (data) => {
const error = data.toString();
if (error.includes('error') || error.includes('failed')) {
clearTimeout(timeout);
ngrokProcess.kill();
reject(new Error(error.trim()));
}
});
ngrokProcess.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
if (tunnelUrl) {
console.error('');
console.error('🎉 OPENDIA READY!');
console.error('📋 Copy this URL for online AI services:');
console.error(`🔗 ${tunnelUrl}/sse`);
console.error('');
console.error('💡 ChatGPT: Settings → Connectors → Custom Connector');
console.error('💡 Claude Web: Add as external MCP server (if supported)');
console.error('');
console.error('🏠 Local access still available:');
console.error('🔗 http://localhost:3001/sse');
console.error('');
// Store ngrok process for cleanup
global.ngrokProcess = ngrokProcess;
} else {
throw new Error('Could not extract tunnel URL');
}
} catch (error) {
console.error('❌ Tunnel failed:', error.message);
console.error('');
console.error('💡 MANUAL NGROK OPTION:');
console.error(` 1. Run: ngrok http ${HTTP_PORT}`);
console.error(' 2. Use the ngrok URL + /sse');
console.error('');
console.error('💡 Or use local URL:');
console.error(` 🔗 http://localhost:${HTTP_PORT}/sse`);
console.error('');
}
} else {
console.error('');
console.error('🏠 LOCAL MODE:');
console.error(`🔗 SSE endpoint: http://localhost:${HTTP_PORT}/sse`);
console.error('💡 For online AI access, restart with --tunnel flag');
console.error('');
}
// Display transport info
if (sseOnly) {
console.error('📡 Transport: SSE-only (stdio disabled)');
console.error(`💡 Configure Claude Desktop with: http://localhost:${HTTP_PORT}/sse`);
} else {
console.error('📡 Transport: Hybrid (stdio + SSE)');
console.error('💡 Claude Desktop: Works with existing config');
console.error('💡 Online AI: Use SSE endpoint above');
}
// Display port configuration help
console.error('');
console.error('🔧 Port Configuration:');
console.error(` Current: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`);
console.error(' Custom: npx opendia --ws-port=6000 --http-port=6001');
console.error(' Or: npx opendia --port=6000 (uses 6000 and 6001)');
console.error(' Note: Existing processes are automatically terminated');
console.error('');
}
// Cleanup on exit
process.on('SIGINT', async () => {
console.error('🔄 Shutting down...');
if (enableTunnel && global.ngrokProcess) {
console.error('🔄 Closing tunnel...');
try {
global.ngrokProcess.kill('SIGTERM');
} catch (error) {
// Ignore cleanup errors
}
}
process.exit();
});
// Workflow execution functions
async function executePostToSocialWorkflow(args) {
const { content, platform = "auto" } = args;
if (!content) {
throw new Error("Content is required for social media posting");
}
try {
// Analyze the current page to determine platform and find posting elements
const pageAnalysis = await callBrowserTool('page_analyze', {
intent_hint: 'post_create',
phase: 'discover',
max_results: 3
});
if (!pageAnalysis.elements || pageAnalysis.elements.length === 0) {
throw new Error("No posting elements found on current page. Please navigate to a social media platform.");
}
// Find the best textarea element for posting
const textareaElement = pageAnalysis.elements.find(el =>
el.type === 'textarea' || el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet')
);
if (!textareaElement) {
throw new Error("No suitable posting textarea found on current page");
}
// Fill the content using anti-detection bypass
const fillResult = await callBrowserTool('element_fill', {
element_id: textareaElement.id,
value: content,
clear_first: true
});
if (!fillResult.success) {
throw new Error(`Failed to fill content: ${fillResult.actual_value}`);
}
// Look for submit button
const submitElement = pageAnalysis.elements.find(el =>
el.type === 'button' && (el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet') || el.name.toLowerCase().includes('share'))
);
let result = `✅ Successfully posted content to social media!\n\n`;
result += `📝 **Content posted:** "${content}"\n`;
result += `🎯 **Platform detected:** ${pageAnalysis.summary?.anti_detection_platform || 'Generic'}\n`;
result += `🔧 **Method used:** ${fillResult.method}\n`;
result += `📊 **Fill success:** ${fillResult.success ? 'Yes' : 'No'}\n`;
if (submitElement) {
result += `\n💡 **Next step:** Click the "${submitElement.name}" button to publish your post.`;
} else {
result += `\n💡 **Next step:** Look for a "Post" or "Tweet" button to publish your content.`;
}
return result;
} catch (error) {
throw new Error(`Social media posting failed: ${error.message}`);
}
}
async function executePostSelectedQuoteWorkflow(args) {
const { commentary = "" } = args;
try {
// Get selected text from current page
const selectedText = await callBrowserTool('get_selected_text', {
include_metadata: true,
max_length: 1000
});
if (!selectedText.has_selection) {
throw new Error("No text is currently selected. Please select some text first.");
}
// Format the quote with commentary
let quoteContent = `"${selectedText.selected_text}"`;
if (selectedText.selection_metadata?.page_info?.title) {
quoteContent += `\n\n${selectedText.selection_metadata.page_info.title}`;
}
if (selectedText.selection_metadata?.page_info?.url) {
quoteContent += `\n${selectedText.selection_metadata.page_info.url}`;
}
if (commentary) {
quoteContent += `\n\n${commentary}`;
}
// Execute the post workflow with the formatted quote
const postResult = await executePostToSocialWorkflow({ content: quoteContent });
let result = `🎯 **Selected Quote Posting Workflow**\n\n`;
result += `📝 **Selected text:** "${selectedText.selected_text.substring(0, 100)}${selectedText.selected_text.length > 100 ? '...' : ''}"\n`;
result += `📄 **Source:** ${selectedText.selection_metadata?.page_info?.title || 'Current page'}\n`;
result += `💬 **Commentary:** ${commentary || 'None'}\n`;
result += `📊 **Character count:** ${quoteContent.length}\n\n`;
result += postResult;
return result;
} catch (error) {
throw new Error(`Quote posting workflow failed: ${error.message}`);
}
}
async function executeResearchWorkflow(args) {
const { topic, depth = "thorough" } = args;
if (!topic) {
throw new Error("Research topic is required");
}
try {
// Analyze current page content
const pageContent = await callBrowserTool('page_extract_content', {
content_type: 'article',
summarize: true
});
// Get current page links for related research
const pageLinks = await callBrowserTool('get_page_links', {
include_internal: true,
include_external: true,
max_results: 20
});
// Search browsing history for related content
const historyResults = await callBrowserTool('get_history', {
keywords: topic,
max_results: 10,
sort_by: 'visit_time'
});
// Bookmark current page if it has relevant content - URL will be obtained from browser extension
const currentUrl = pageContent.content?.url;
const currentTitle = pageContent.summary?.title;
if (currentUrl && currentTitle) {
try {
await callBrowserTool('add_bookmark', {
title: `[Research: ${topic}] ${currentTitle}`,
url: currentUrl
});
} catch (bookmarkError) {
console.warn('Bookmark creation failed:', bookmarkError.message);
}
}
// Compile research summary
let result = `🔍 **Research Workflow: ${topic}**\n\n`;
// Current page analysis
result += `📄 **Current Page Analysis:**\n`;
if (pageContent.summary) {
result += `• **Title:** ${pageContent.summary.title || 'N/A'}\n`;
result += `• **Word count:** ${pageContent.summary.word_count || 0}\n`;
result += `• **Reading time:** ${pageContent.summary.reading_time || 0} minutes\n`;
result += `• **Has media:** ${pageContent.summary.has_images || pageContent.summary.has_videos ? 'Yes' : 'No'}\n`;
if (pageContent.summary.preview) {
result += `• **Preview:** ${pageContent.summary.preview}\n`;
}
}
// Related links
result += `\n🔗 **Related Links Found:** ${pageLinks.returned}\n`;
const relevantLinks = pageLinks.links.filter(link =>
link.text.toLowerCase().includes(topic.toLowerCase()) ||
link.url.toLowerCase().includes(topic.toLowerCase())
).slice(0, 5);
if (relevantLinks.length > 0) {
result += `**Top relevant links:**\n`;
relevantLinks.forEach((link, index) => {
result += `${index + 1}. [${link.text}](${link.url})\n`;
});
}
// History analysis
result += `\n📚 **Previous Research:**\n`;
if (historyResults.history_items && historyResults.history_items.length > 0) {
result += `Found ${historyResults.history_items.length} related pages in your history:\n`;
historyResults.history_items.slice(0, 5).forEach((item, index) => {
result += `${index + 1}. **${item.title}** (visited ${item.visit_count} times)\n`;
result += ` ${item.url}\n`;
});
} else {
result += `No previous research found in browsing history.\n`;
}
// Research recommendations
result += `\n💡 **Next Steps:**\n`;
if (depth === "comprehensive") {
result += `• Explore the ${pageLinks.returned} links found on current page\n`;
result += `• Cross-reference with ${historyResults.metadata?.total_found || 0} historical visits\n`;
result += `• Consider bookmarking additional relevant pages\n`;
} else if (depth === "thorough") {
result += `• Review top ${Math.min(5, pageLinks.returned)} most relevant links\n`;
result += `• Check recent history for related content\n`;
} else {
result += `• Focus on current page content and top 3 related links\n`;
}
result += `\n✅ **Current page bookmarked for reference**`;
return result;
} catch (error) {
throw new Error(`Research workflow failed: ${error.message}`);
}
}
async function executeSessionAnalysisWorkflow(args) {
const { focus = "productivity" } = args;
try {
// Get all open tabs
const tabList = await callBrowserTool('tab_list', {
current_window_only: false,
include_details: true
});
// Get recent browsing history
const recentHistory = await callBrowserTool('get_history', {
max_results: 50,
sort_by: 'visit_time',
sort_order: 'desc'
});
// Analyze current page
const currentPageContent = await callBrowserTool('page_extract_content', {
content_type: 'article',
summarize: true
});
// Process tabs data
const tabs = tabList.tabs || [];
const domains = [...new Set(tabs.map(tab => {
try {
return new URL(tab.url).hostname;
} catch {
return 'unknown';
}
}))];
// Categorize tabs by domain type
const socialMediaDomains = ['twitter.com', 'x.com', 'linkedin.com', 'facebook.com', 'instagram.com'];
const productivityDomains = ['docs.google.com', 'notion.so', 'obsidian.md', 'github.com'];
const newsDomains = ['news.google.com', 'bbc.com', 'cnn.com', 'reuters.com'];
const categorizedTabs = {
social: tabs.filter(tab => socialMediaDomains.some(domain => tab.url.includes(domain))),
productivity: tabs.filter(tab => productivityDomains.some(domain => tab.url.includes(domain))),
news: tabs.filter(tab => newsDomains.some(domain => tab.url.includes(domain))),
other: tabs.filter(tab =>
!socialMediaDomains.some(domain => tab.url.includes(domain)) &&
!productivityDomains.some(domain => tab.url.includes(domain)) &&
!newsDomains.some(domain => tab.url.includes(domain))
)
};
// Compile analysis
let result = `📊 **Browsing Session Analysis**\n\n`;
// Session overview
result += `🎯 **Session Overview:**\n`;
result += `• **Total open tabs:** ${tabs.length}\n`;
result += `• **Unique domains:** ${domains.length}\n`;
result += `• **Active tab:** ${tabList.active_tab ? 'Yes' : 'No'}\n`;
result += `• **Recent history items:** ${recentHistory.metadata?.total_found || 0}\n`;
// Tab categorization
result += `\n📂 **Tab Categories:**\n`;
result += `• **Social Media:** ${categorizedTabs.social.length} tabs\n`;
result += `• **Productivity:** ${categorizedTabs.productivity.length} tabs\n`;
result += `• **News/Information:** ${categorizedTabs.news.length} tabs\n`;
result += `• **Other:** ${categorizedTabs.other.length} tabs\n`;
// Domain analysis
result += `\n🌐 **Top Domains:**\n`;
const domainCounts = {};
tabs.forEach(tab => {
try {
const domain = new URL(tab.url).hostname;
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
} catch {}
});
Object.entries(domainCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.forEach(([domain, count]) => {
result += `• **${domain}:** ${count} tab${count > 1 ? 's' : ''}\n`;
});
// Focus-specific analysis
if (focus === "productivity") {
result += `\n💼 **Productivity Analysis:**\n`;
const duplicateTabs = tabs.filter((tab, index) =>
tabs.findIndex(t => t.url === tab.url) !== index
);
result += `• **Duplicate tabs:** ${duplicateTabs.length}\n`;
result += `• **Productivity tools:** ${categorizedTabs.productivity.length}\n`;
result += `• **Social media distractions:** ${categorizedTabs.social.length}\n`;
if (categorizedTabs.productivity.length > 0) {
result += `\n**Active productivity tools:**\n`;
categorizedTabs.productivity.slice(0, 3).forEach(tab => {
result += `${tab.title}\n`;
});
}
} else if (focus === "research") {
result += `\n🔍 **Research Analysis:**\n`;
result += `• **Information sources:** ${categorizedTabs.news.length + categorizedTabs.other.length}\n`;
result += `• **Research depth:** ${recentHistory.metadata?.total_found > 20 ? 'Deep' : 'Surface'}\n`;
if (currentPageContent.summary) {
result += `• **Current page type:** ${currentPageContent.content_type || 'Unknown'}\n`;
result += `• **Reading time:** ${currentPageContent.summary.reading_time || 0} minutes\n`;
}
}
// Recommendations
result += `\n💡 **Recommendations:**\n`;
if (tabs.length > 20) {
result += `• Consider closing some tabs to improve performance\n`;
}
if (domainCounts['twitter.com'] > 3 || domainCounts['x.com'] > 3) {
result += `• Multiple social media tabs detected - consider consolidating\n`;
}
if (categorizedTabs.productivity.length > 0 && categorizedTabs.social.length > 0) {
result += `• Mix of productivity and social tabs - consider separate browsing sessions\n`;
}
result += `\n📈 **Session Score:** ${Math.round(((categorizedTabs.productivity.length + categorizedTabs.news.length) / tabs.length) * 100)}% productive`;
return result;
} catch (error) {
throw new Error(`Session analysis workflow failed: ${error.message}`);
}
}
async function executeOrganizeTabsWorkflow(args) {
const { strategy = "close_duplicates" } = args;
try {
// Get all open tabs
const tabList = await callBrowserTool('tab_list', {
current_window_only: false,
include_details: true
});
const tabs = tabList.tabs || [];
let result = `🗂️ **Tab Organization Workflow**\n\n`;
result += `📊 **Starting with ${tabs.length} tabs**\n\n`;
let closedTabs = [];
let organizedTabs = [];
if (strategy === "close_duplicates") {
// Find and close duplicate tabs
const seenUrls = new Set();
const duplicates = [];
tabs.forEach(tab => {
if (seenUrls.has(tab.url)) {
duplicates.push(tab);
} else {
seenUrls.add(tab.url);
}
});
// Close duplicate tabs (keep the first occurrence)
if (duplicates.length > 0) {
const tabIds = duplicates.map(tab => tab.id);
const closeResult = await callBrowserTool('tab_close', {
tab_ids: tabIds
});
if (closeResult.success) {
closedTabs = duplicates;
result += `✅ **Closed ${duplicates.length} duplicate tabs:**\n`;
duplicates.forEach(tab => {
result += `${tab.title}\n`;
});
}
} else {
result += `✅ **No duplicate tabs found**\n`;
}
} else if (strategy === "group_by_domain") {
// Group tabs by domain
const domainGroups = {};
tabs.forEach(tab => {
try {
const domain = new URL(tab.url).hostname;
if (!domainGroups[domain]) {
domainGroups[domain] = [];
}
domainGroups[domain].push(tab);
} catch {
if (!domainGroups['unknown']) {
domainGroups['unknown'] = [];
}
domainGroups['unknown'].push(tab);
}
});
result += `📂 **Grouped tabs by domain:**\n`;
Object.entries(domainGroups).forEach(([domain, domainTabs]) => {
result += `• **${domain}:** ${domainTabs.length} tabs\n`;
domainTabs.slice(0, 3).forEach(tab => {
result += ` - ${tab.title}\n`;
});
if (domainTabs.length > 3) {
result += ` - ... and ${domainTabs.length - 3} more\n`;
}
});
} else if (strategy === "archive_old") {
// Find tabs that haven't been active recently
const currentTime = Date.now();
const oneHourAgo = currentTime - (60 * 60 * 1000);
// Since we don't have last accessed time, we'll use a heuristic
// based on tab loading status and position
const staleTabsToClose = tabs.filter(tab =>
tab.status === 'complete' &&
!tab.active &&
!tab.pinned &&
tab.index > 10 // Assume tabs at the end are less active
).slice(0, 10); // Limit to 10 tabs max
if (staleTabsToClose.length > 0) {
const tabIds = staleTabsToClose.map(tab => tab.id);
const closeResult = await callBrowserTool('tab_close', {
tab_ids: tabIds
});
if (closeResult.success) {
closedTabs = staleTabsToClose;
result += `✅ **Archived ${staleTabsToClose.length} old tabs:**\n`;
staleTabsToClose.forEach(tab => {
result += `${tab.title}\n`;
});
}
} else {
result += `✅ **No old tabs to archive**\n`;
}
}
// Final summary
const remainingTabs = tabs.length - closedTabs.length;
result += `\n📈 **Organization Results:**\n`;
result += `• **Tabs closed:** ${closedTabs.length}\n`;
result += `• **Tabs remaining:** ${remainingTabs}\n`;
result += `• **Organization strategy:** ${strategy}\n`;
if (remainingTabs > 15) {
result += `\n💡 **Recommendation:** Consider running additional organization strategies to further reduce tab count.`;
} else {
result += `\n✅ **Tab organization complete!** Your browsing session is now more organized.`;
}
return result;
} catch (error) {
throw new Error(`Tab organization workflow failed: ${error.message}`);
}
}
async function executeFillFormWorkflow(args) {
const { form_type = "auto" } = args;
try {
// Analyze page for form elements
const formAnalysis = await callBrowserTool('page_analyze', {
intent_hint: 'form submit',
phase: 'detailed',
focus_areas: ['forms', 'buttons'],
max_results: 10
});
if (!formAnalysis.elements || formAnalysis.elements.length === 0) {
throw new Error("No form elements found on current page");
}
// Categorize form elements
const formElements = {
inputs: formAnalysis.elements.filter(el => el.type === 'input'),
textareas: formAnalysis.elements.filter(el => el.type === 'textarea'),
selects: formAnalysis.elements.filter(el => el.type === 'select'),
buttons: formAnalysis.elements.filter(el => el.type === 'button')
};
// Analyze each form element for type and requirements
let result = `📝 **Form Analysis & Fill Assistant**\n\n`;
// Form overview
result += `🔍 **Form Elements Found:**\n`;
result += `• **Input fields:** ${formElements.inputs.length}\n`;
result += `• **Text areas:** ${formElements.textareas.length}\n`;
result += `• **Select dropdowns:** ${formElements.selects.length}\n`;
result += `• **Buttons:** ${formElements.buttons.length}\n`;
// Detailed element analysis
if (formElements.inputs.length > 0) {
result += `\n📊 **Input Field Analysis:**\n`;
formElements.inputs.forEach((input, index) => {
const elementState = formAnalysis.elements.find(el => el.id === input.id);
result += `${index + 1}. **${input.name}**\n`;
result += ` • Element ID: ${input.id}\n`;
result += ` • Ready: ${elementState?.ready ? 'Yes' : 'No'}\n`;
result += ` • Required: ${input.name.includes('*') ? 'Yes' : 'Unknown'}\n`;
// Suggest field type based on name
const fieldName = input.name.toLowerCase();
if (fieldName.includes('email')) {
result += ` • **Suggested type:** Email address\n`;
} else if (fieldName.includes('name')) {
result += ` • **Suggested type:** Name field\n`;
} else if (fieldName.includes('phone')) {
result += ` • **Suggested type:** Phone number\n`;
} else if (fieldName.includes('password')) {
result += ` • **Suggested type:** Password\n`;
} else {
result += ` • **Suggested type:** General text input\n`;
}
});
}
// Text area analysis
if (formElements.textareas.length > 0) {
result += `\n📝 **Text Area Analysis:**\n`;
formElements.textareas.forEach((textarea, index) => {
result += `${index + 1}. **${textarea.name}**\n`;
result += ` • Element ID: ${textarea.id}\n`;
result += ` • **Suggested use:** Long-form text input\n`;
});
}
// Submit buttons
if (formElements.buttons.length > 0) {
result += `\n🔘 **Submit Buttons:**\n`;
const submitButtons = formElements.buttons.filter(btn =>
btn.name.toLowerCase().includes('submit') ||
btn.name.toLowerCase().includes('send') ||
btn.name.toLowerCase().includes('save')
);
submitButtons.forEach((button, index) => {
result += `${index + 1}. **${button.name}** (ID: ${button.id})\n`;
});
}
// Form type detection
result += `\n🎯 **Detected Form Type:**\n`;
// Note: Page content detection would need to be done through browser tools
const contentLower = '';
let detectedType = 'unknown';
if (contentLower.includes('contact') || contentLower.includes('get in touch')) {
detectedType = 'contact';
} else if (contentLower.includes('register') || contentLower.includes('sign up')) {
detectedType = 'registration';
} else if (contentLower.includes('survey') || contentLower.includes('feedback')) {
detectedType = 'survey';
} else if (contentLower.includes('application') || contentLower.includes('apply')) {
detectedType = 'application';
}
result += `• **Auto-detected:** ${detectedType}\n`;
result += `• **User specified:** ${form_type}\n`;
// Filling recommendations
result += `\n💡 **Filling Recommendations:**\n`;
if (formElements.inputs.length > 0) {
result += `• Start with required fields (marked with *)\n`;
result += `• Use the element IDs provided for precise filling\n`;
result += `• Test form validation before final submission\n`;
}
// Ready-to-fill elements
const readyElements = formAnalysis.elements.filter(el => el.ready);
result += `\n✅ **Ready to Fill:** ${readyElements.length} elements are ready for interaction\n`;
if (readyElements.length > 0) {
result += `**Next steps:**\n`;
result += `1. Use element_fill with the provided Element IDs\n`;
result += `2. Fill required fields first\n`;
result += `3. Review form before submission\n`;
result += `4. Click appropriate submit button when ready\n`;
}
return result;
} catch (error) {
throw new Error(`Form analysis workflow failed: ${error.message}`);
}
}
// Start the server
startServer();