7 Commits

Author SHA1 Message Date
Aaron Elijah Mars
f57010971a Bump version to 1.0.4
Updated version in package.json and manifest.json for release 1.0.4

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 21:18:09 +02:00
Aaron Elijah Mars
e40805801a fix port assignment + license 2025-07-12 21:04:26 +02:00
Aaron Elijah Mars
c3c77c1e04 hosted mode w/ Ngrok + SSE 2025-07-12 20:30:15 +02:00
@aaronjmars
23eda72d91 Update README.md 2025-06-29 07:35:49 +02:00
@aaronjmars
b131e05513 Update README.md 2025-06-28 21:34:49 +02:00
Aaron Elijah Mars
019df3b3b0 v1 2025-06-28 21:28:52 +02:00
Aaron Elijah Mars
42e465045c v1 2025-06-28 21:27:42 +02:00
10 changed files with 893 additions and 112 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 OpenDia Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,9 +1,10 @@
# OpenDia <img src="opendia-extension/icon-128.png" alt="OpenDia" width="32" height="32">
> **The open alternative to Dia**
> Connect your browser to AI models. No browser switching needed—works seamlessly with any Chromium browser including Chrome & Arc.
**The open alternative to Dia**
Connect your browser to AI models. No browser switching needed—works seamlessly with any Chromium browser including Chrome & Arc.
[![npm version](https://badge.fury.io/js/opendia.svg)](https://badge.fury.io/js/opendia)
[![npm version](https://img.shields.io/npm/v/opendia)](https://www.npmjs.com/package/opendia)
[![GitHub release](https://img.shields.io/github/release/aaronjmars/opendia.svg)](https://github.com/aaronjmars/opendia/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 📺 See it in Action
@@ -15,7 +16,7 @@
OpenDia lets AI models control your browser automatically. **The key advantage? It leverages everything you already have**—your logged-in accounts, saved passwords, cookies, wallets, and browsing history. No need to start from scratch or switch contexts.
**🔑 Use Your Existing Digital Life:**
-**Logged-in accounts**: Post to Twitter / X, LinkedIn, Facebook with your existing sessions
-**Logged-in accounts**: Post to Twitter/X, LinkedIn, Facebook with your existing sessions
-**Browser data**: Access your bookmarks, history, and saved passwords
-**Extensions & wallets**: Use MetaMask, password managers, or any installed extensions
-**Cookies & sessions**: Stay authenticated across all your favorite sites
@@ -32,12 +33,11 @@ OpenDia lets AI models control your browser automatically. **The key advantage?
Works with **any Chromium-based browser**:
-**Google Chrome**
-**Arc Browser**
-**Arc**
-**Microsoft Edge**
-**Brave Browser**
-**Brave**
-**Opera**
-**Vivaldi**
-**Any Chromium variant**
-**Any Chromium based browser**
Perfect for **Cursor users** who want to automate their local testing and development workflows!
@@ -71,18 +71,13 @@ Perfect for **Cursor users** who want to automate their local testing and develo
## ⚡ Quick Start
### 1. Start the Server
```bash
npx opendia
```
### 2. Install the Browser Extension
### 1. Install the Browser Extension
1. Download from [releases](https://github.com/aaronjmars/opendia/releases)
2. Go to `chrome://extensions/` (or your browser's extension page)
3. Enable "Developer mode"
4. Click "Load unpacked" and select the extension folder
### 3. Connect to Your AI
### 2. Connect to Your AI
**For Claude Desktop**, add to your configuration:
```json
{
@@ -97,6 +92,64 @@ npx opendia
**For Cursor or other AI tools**, use the same configuration or follow their specific setup instructions.
## Usage Modes
### Local Mode (Default)
```bash
npx opendia
```
- Chrome extension: ws://localhost:5555 (auto-discovery enabled)
- Claude Desktop: stdio (existing config)
- Local SSE: http://localhost:5556/sse
### Port Configuration
```bash
# Use custom ports
npx opendia --port=6000 # Uses 6000 (WebSocket) + 6001 (HTTP)
npx opendia --ws-port=5555 --http-port=5556 # Specify individually
# Handle port conflicts
npx opendia --kill-existing # Safely terminate existing OpenDia processes
```
### Auto-Tunnel Mode
```bash
npx opendia --tunnel
```
- Automatically creates ngrok tunnel
- Copy URL for ChatGPT/online AI services
- Local functionality preserved
**Note**: For auto-tunneling to work, you need ngrok installed:
**macOS:**
```bash
brew install ngrok
```
**Windows:**
```bash
# Using Chocolatey
choco install ngrok
# Or download from https://ngrok.com/download
```
**Linux:**
```bash
# Ubuntu/Debian
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
# Or download from https://ngrok.com/download
```
Then get your free authtoken from https://dashboard.ngrok.com/get-started/your-authtoken and run:
```bash
ngrok config add-authtoken YOUR_TOKEN_HERE
```
## 🛠️ Capabilities
OpenDia gives AI models **17 powerful browser tools**:
@@ -188,8 +241,9 @@ cd opendia-mcp
npm install
npm start
# Load extension in your browser
# Load extension in your browser
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension
# Extension will auto-connect to server on localhost:5555
```
### Ways to Contribute

View File

@@ -1,13 +1,45 @@
// MCP Server connection configuration
const MCP_SERVER_URL = 'ws://localhost:3000';
let MCP_SERVER_URL = 'ws://localhost:5555'; // Default, will be auto-discovered
let mcpSocket = null;
let reconnectInterval = null;
let reconnectAttempts = 0;
let lastKnownPorts = { websocket: 5555, http: 5556 }; // Cache for port discovery
// 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;
}
// Initialize WebSocket connection to MCP server
function connectToMCPServer() {
async function connectToMCPServer() {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return;
// 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
}
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
mcpSocket = new WebSocket(MCP_SERVER_URL);
@@ -32,12 +64,20 @@ function connectToMCPServer() {
mcpSocket.onclose = () => {
console.log('❌ Disconnected from MCP server, will reconnect...');
reconnectAttempts++;
// Clear any existing reconnect interval
if (reconnectInterval) {
clearInterval(reconnectInterval);
}
// Attempt to reconnect every 5 seconds
reconnectInterval = setInterval(connectToMCPServer, 5000);
};
mcpSocket.onerror = (error) => {
console.log('⚠️ MCP WebSocket error:', error);
reconnectAttempts++;
};
}
@@ -1116,8 +1156,10 @@ async function getSelectedText(params) {
}
}
// Initialize connection when extension loads
connectToMCPServer();
// Initialize connection when extension loads (with delay for server startup)
setTimeout(() => {
connectToMCPServer();
}, 1000);
// Heartbeat to keep connection alive
setInterval(() => {
@@ -1141,6 +1183,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
} else if (request.action === "reconnect") {
connectToMCPServer();
sendResponse({ success: true });
} else if (request.action === "getPorts") {
sendResponse({
current: lastKnownPorts,
websocketUrl: MCP_SERVER_URL
});
} else if (request.action === "test") {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "OpenDia",
"version": "1.0.0",
"version": "1.0.4",
"description": "Browser automation through Model Context Protocol",
"icons": {
"16": "icon-16.png",

View File

@@ -255,9 +255,9 @@
<span id="statusText" class="tooltip">
Checking connection...
<span class="tooltip-content">
Make sure your MCP server is connected.
If it's the case, click on Reconnect.
If it still don't work, kill your 3000 port & try again.
Start server with: npx opendia
Auto-discovery will find the correct ports.
If issues persist, try: npx opendia --kill-existing
</span>
</span>
</div>
@@ -265,7 +265,7 @@
<div class="info">
<div class="info-row">
<span class="info-label">Server</span>
<span class="info-value" id="serverUrl">ws://localhost:3000</span>
<span class="info-value" id="serverUrl">Auto-Discovery</span>
</div>
<div class="info-row">
<span class="info-label">Available Tools</span>

View File

@@ -3,6 +3,7 @@ let statusIndicator = document.getElementById("statusIndicator");
let statusText = document.getElementById("statusText");
let toolCount = document.getElementById("toolCount");
let currentPage = document.getElementById("currentPage");
let serverUrl = document.getElementById("serverUrl");
// Get dynamic tool count from background script
function updateToolCount() {
@@ -59,16 +60,31 @@ function checkStatus() {
checkStatus();
setInterval(checkStatus, 2000);
// Update server URL display
function updateServerUrl() {
if (chrome.runtime?.id) {
chrome.runtime.sendMessage({ action: "getPorts" }, (response) => {
if (!chrome.runtime.lastError && response?.websocketUrl) {
serverUrl.textContent = response.websocketUrl;
}
});
}
}
// Update server URL periodically
updateServerUrl();
setInterval(updateServerUrl, 5000);
// Update UI based on connection status
function updateStatus(connected) {
if (connected) {
statusIndicator.className = "status-indicator connected";
statusText.innerHTML = `Connected to MCP server
<span class="tooltip-content">Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.</span>`;
<span class="tooltip-content">Connected successfully! Server auto-discovery is working. Default ports: WebSocket=5555, HTTP=5556</span>`;
} else {
statusIndicator.className = "status-indicator disconnected";
statusText.innerHTML = `Disconnected from MCP server
<span class="tooltip-content">Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.</span>`;
<span class="tooltip-content">Start server with: npx opendia. Auto-discovery will find the correct ports. If issues persist, try: npx opendia --kill-existing</span>`;
}
}

208
opendia-mcp/README.md Normal file
View File

@@ -0,0 +1,208 @@
# OpenDia <img src="opendia-extension/icon-128.png" alt="OpenDia" width="32" height="32">
> **The open alternative to Dia**
> Connect your browser to AI models. No browser switching needed—works seamlessly with any Chromium browser including Chrome & Arc.
[![npm version](https://badge.fury.io/js/opendia.svg)](https://badge.fury.io/js/opendia)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 📺 See it in Action
![OpenDia Demo](./preview.gif)
## 🚀 What is OpenDia?
OpenDia lets AI models control your browser automatically. **The key advantage? It leverages everything you already have**—your logged-in accounts, saved passwords, cookies, wallets, and browsing history. No need to start from scratch or switch contexts.
**🔑 Use Your Existing Digital Life:**
-**Logged-in accounts**: Post to Twitter / X, LinkedIn, Facebook with your existing sessions
-**Browser data**: Access your bookmarks, history, and saved passwords
-**Extensions & wallets**: Use MetaMask, password managers, or any installed extensions
-**Cookies & sessions**: Stay authenticated across all your favorite sites
-**Local testing**: Perfect for development with Cursor - test with real user sessions
**✨ Key Benefits:**
- 🔄 **Universal AI Support**: Works with Claude, ChatGPT, Cursor and even local models
- 🎯 **Anti-Detection**: Specialized bypasses for Twitter/X, LinkedIn, Facebook
- 📱 **Smart Automation**: AI understands your pages and finds the right elements
- 🛡️ **Privacy-First**: Everything runs locally, your data stays with you
-**Zero Setup**: Get started with one command
## 🌐 Browser Support
Works with **any Chromium-based browser**:
-**Google Chrome**
-**Arc Browser**
-**Microsoft Edge**
-**Brave Browser**
-**Opera**
-**Vivaldi**
-**Any Chromium variant**
Perfect for **Cursor users** who want to automate their local testing and development workflows!
## 🎬 What You Can Do
**Real workflows you can try today:**
### 📰 Content & Social Media
- **"Summarize all the articles I read today and post a Twitter thread about the key insights"**
- **"Find interesting articles related to AI from my bookmarks and create a reading list"**
- **"Read this article and post a thoughtful comment on the LinkedIn version"**
- **"Check my recent Twitter bookmarks and summarize the main themes"**
### 📧 Productivity & Research
- **"Browse my latest emails and tell me what needs urgent attention"**
- **"Find all the GitHub repos I visited this week and create a summary report"**
- **"Extract the main points from this research paper and save them to my notes"**
- **"Search my browsing history for that article about AI safety I read last month"**
### 🤖 Development & Testing (Perfect for Cursor!)
- **"Test my web app's signup flow and take screenshots at each step"**
- **"Fill out this form with test data and check if validation works"**
- **"Navigate through my app and check if all the buttons work properly"**
- **"Use my connected wallet to test this DeFi interface"**
### 🔄 Advanced Automation
- **"Open tabs for all my daily news sources and summarize the top stories"**
- **"Draft replies to my unread messages based on the context"**
- **"Monitor this webpage and notify me when the content changes"**
- **"Automatically bookmark interesting articles I'm reading"**
## ⚡ Quick Start
### 1. Start the Server
```bash
npx opendia
```
### 2. Install the Browser Extension
1. Download from [releases](https://github.com/aaronjmars/opendia/releases)
2. Go to `chrome://extensions/` (or your browser's extension page)
3. Enable "Developer mode"
4. Click "Load unpacked" and select the extension folder
### 3. Connect to Your AI
**For Claude Desktop**, add to your configuration:
```json
{
"mcpServers": {
"opendia": {
"command": "npx",
"args": ["opendia"]
}
}
}
```
**For Cursor or other AI tools**, use the same configuration or follow their specific setup instructions.
## 🛠️ Capabilities
OpenDia gives AI models **17 powerful browser tools**:
### 🎯 Smart Page Understanding
- **Analyze any webpage** - AI automatically finds buttons, forms, and interactive elements
- **Extract content intelligently** - Get clean text from articles, social posts, or search results
- **Understand context** - AI knows what type of page it's looking at and how to interact with it
### 🖱️ Natural Interactions
- **Click anything** - Buttons, links, menus - AI finds and clicks the right elements
- **Fill forms smartly** - Works even on complex sites like Twitter, LinkedIn, Facebook
- **Navigate seamlessly** - Go to pages, scroll, wait for content to load
- **Handle modern web apps** - Bypasses detection on social platforms
### 📑 Tab & Window Management
- **Multi-tab workflows** - Open, close, switch between tabs automatically
- **Organize your workspace** - Let AI manage your browser tabs efficiently
- **Coordinate complex tasks** - Work across multiple sites simultaneously
### 📊 Access Your Browser Data
- **Bookmarks & History** - Find that article you read last week
- **Current page content** - Get selected text, links, or full page content
- **Real-time information** - Work with whatever's currently on your screen
### 🛡️ Anti-Detection Features
- **Social media posting** - Bypass automation detection on Twitter/X, LinkedIn, Facebook
- **Natural interactions** - Mimics human behavior to avoid triggering security measures
- **Reliable automation** - Works consistently even on sites that block typical automation tools
## 💬 Example Prompts to Try
Once everything is set up, try asking your AI:
**Content Creation:**
> *"Read the article on this page and create a Twitter thread summarizing the main points"*
**Research & Analysis:**
> *"Look through my browser history from this week and find articles about machine learning. Summarize the key trends."*
**Social Media Management:**
> *"Check my Twitter bookmarks and organize them into categories. Create a summary of each category."*
**Productivity:**
> *"Open tabs for my usual morning reading sites and give me a briefing of today's top stories"*
**Development Testing:**
> *"Fill out this contact form with test data and check if the submission works properly"*
**Personal Assistant:**
> *"Find that GitHub repo I was looking at yesterday about React components and bookmark it for later"*
## 🏗️ How It Works
```mermaid
graph LR
A[AI Model] --> B[OpenDia Server]
B --> C[Browser Extension]
C --> D[Your Browser]
D --> E[Any Website]
```
1. **You ask** your AI to do something browser-related
2. **AI calls** OpenDia tools to understand and interact with pages
3. **OpenDia controls** your browser through the extension
4. **You get results** - AI can see what happened and respond intelligently
## 🔒 Security & Privacy
**Your data stays private**:
-**Everything runs locally** - No cloud processing of your browsing data
-**You control access** - Extension only works when you want it to
-**Open source** - Full transparency of what the code does
-**No tracking** - We don't collect or store any of your information
**Important**: This tool requires broad browser permissions to function. Only use with AI models you trust, and in environments where you're comfortable with browser automation.
## 🤝 Contributing
Love to have your help making OpenDia better!
### Quick Development Setup
```bash
git clone https://github.com/aaronjmars/opendia.git
cd opendia
# Start the server
cd opendia-mcp
npm install
npm start
# Load extension in your browser
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension
```
### Ways to Contribute
- 🐛 **Report bugs** via [GitHub Issues](https://github.com/aaronjmars/opendia/issues)
- 💡 **Share it on social medias**
- 🔧 **Add new browser capabilities**
- 📖 **Improve documentation**
- 🧪 **Test with different AI models**
## 📝 License
MIT License - see [LICENSE](LICENSE) for details.
---
**Ready to supercharge your browser with AI? Get started with `npx opendia`! 🚀**

View File

@@ -1,15 +1,23 @@
{
"name": "opendia-server",
"version": "1.0.0",
"name": "opendia",
"version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opendia-server",
"version": "1.0.0",
"name": "opendia",
"version": "1.0.3",
"license": "MIT",
"dependencies": {
"express": "^4.x.x",
"ws": "^8.x.x"
"cors": "^2.8.5",
"express": "^4.21.2",
"ws": "^8.18.0"
},
"bin": {
"opendia": "server.js"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/accepts": {
@@ -129,6 +137,19 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -522,6 +543,15 @@
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@@ -1,13 +1,51 @@
{
"name": "opendia-server",
"version": "1.0.0",
"description": "MCP Server for OpenDia Browser Bridge",
"name": "opendia",
"version": "1.0.4",
"description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook",
"main": "server.js",
"bin": {
"opendia": "./server.js"
},
"scripts": {
"start": "node server.js"
"start": "node server.js",
"tunnel": "node server.js --tunnel",
"sse-only": "node server.js --sse-only",
"tunnel-sse": "node server.js --tunnel --sse-only",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"mcp",
"browser",
"automation",
"ai",
"claude",
"chrome",
"extension",
"twitter",
"linkedin",
"facebook",
"anti-detection"
],
"author": "OpenDia Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/aaronjmars/opendia.git"
},
"homepage": "https://github.com/aaronjmars/opendia",
"bugs": {
"url": "https://github.com/aaronjmars/opendia/issues"
},
"dependencies": {
"ws": "^8.x.x",
"express": "^4.x.x"
}
}
"cors": "^2.8.5",
"express": "^4.21.2",
"ws": "^8.18.0"
},
"engines": {
"node": ">=16.0.0"
},
"files": [
"server.js",
"README.md"
]
}

509
opendia-mcp/server.js Normal file → Executable file
View File

@@ -2,9 +2,141 @@
const WebSocket = require("ws");
const express = require("express");
const net = require('net');
const { exec } = require('child_process');
// WebSocket server for Chrome Extension
const wss = new WebSocket.Server({ port: 3000 });
// 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}`);
console.error(`💡 Options:`);
console.error(` 1. Kill existing: npx opendia --kill-existing`);
console.error(` 2. Use different port: npx opendia --${portName.toLowerCase()}-port=${port + 1}`);
console.error(` 3. Check running processes: lsof -i:${port}`);
console.error(``);
console.error(`⏹️ Exiting to avoid conflicts...`);
process.exit(1);
} 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 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 Extension (will be initialized after port conflict resolution)
let wss = null;
let chromeExtensionSocket = null;
let availableTools = [];
@@ -1077,64 +1209,130 @@ function handleToolResponse(message) {
}
}
// Handle Chrome Extension connections
wss.on("connection", (ws) => {
console.error("Chrome Extension connected");
chromeExtensionSocket = ws;
// Setup WebSocket connection handlers
function setupWebSocketHandlers() {
wss.on("connection", (ws) => {
console.error("Chrome 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);
// 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("Chrome 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) {
console.error("Error processing message:", error);
res.status(500).json({
jsonrpc: "2.0",
id: req.body.id,
error: { code: -32603, message: error.message }
});
}
});
ws.on("close", () => {
console.error("Chrome 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: 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 = "";
process.stdin.on("data", async (chunk) => {
inputBuffer += chunk.toString();
if (!sseOnly) {
process.stdin.on("data", async (chunk) => {
inputBuffer += chunk.toString();
// Process complete lines
const lines = inputBuffer.split("\n");
@@ -1155,34 +1353,203 @@ process.stdin.on("data", async (chunk) => {
}
}
}
});
});
}
// Optional: HTTP endpoint for health checks
const app = express();
app.get("/health", (req, res) => {
// ADD: Health check endpoint (update existing one)
app.get('/health', (req, res) => {
res.json({
status: "ok",
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",
],
'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'
]
});
});
app.listen(3001, () => {
console.error("🎯 Enhanced Browser MCP Server with Anti-Detection Features");
console.error(
"Health check endpoint available at http://localhost:3001/health"
);
// ADD: Port discovery endpoint for Chrome 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`
});
});
console.error("🚀 Enhanced Browser MCP Server started");
console.error(
"🔌 Waiting for Chrome Extension connection on ws://localhost:3000"
);
console.error("🎯 Features: Anti-detection bypass + intelligent automation");
// 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}`);
// Handle --kill-existing flag
if (killExisting) {
console.error('🔧 Killing 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(`🔌 Chrome 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(' Kill existing: npx opendia --kill-existing');
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();
});
// Start the server
startServer();