mirror of
https://github.com/aaronjmars/opendia.git
synced 2025-12-29 16:16:00 +00:00
Compare commits
3 Commits
23eda72d91
...
v1.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f57010971a | ||
|
|
e40805801a | ||
|
|
c3c77c1e04 |
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
66
README.md
66
README.md
@@ -33,12 +33,11 @@ OpenDia lets AI models control your browser automatically. **The key advantage?
|
|||||||
|
|
||||||
Works with **any Chromium-based browser**:
|
Works with **any Chromium-based browser**:
|
||||||
- ✅ **Google Chrome**
|
- ✅ **Google Chrome**
|
||||||
- ✅ **Arc Browser**
|
- ✅ **Arc**
|
||||||
- ✅ **Microsoft Edge**
|
- ✅ **Microsoft Edge**
|
||||||
- ✅ **Brave Browser**
|
- ✅ **Brave**
|
||||||
- ✅ **Opera**
|
- ✅ **Opera**
|
||||||
- ✅ **Vivaldi**
|
- ✅ **Any Chromium based browser**
|
||||||
- ✅ **Any Chromium variant**
|
|
||||||
|
|
||||||
Perfect for **Cursor users** who want to automate their local testing and development workflows!
|
Perfect for **Cursor users** who want to automate their local testing and development workflows!
|
||||||
|
|
||||||
@@ -93,6 +92,64 @@ Perfect for **Cursor users** who want to automate their local testing and develo
|
|||||||
|
|
||||||
**For Cursor or other AI tools**, use the same configuration or follow their specific setup instructions.
|
**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
|
## 🛠️ Capabilities
|
||||||
|
|
||||||
OpenDia gives AI models **17 powerful browser tools**:
|
OpenDia gives AI models **17 powerful browser tools**:
|
||||||
@@ -186,6 +243,7 @@ npm start
|
|||||||
|
|
||||||
# Load extension in your browser
|
# Load extension in your browser
|
||||||
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension
|
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension
|
||||||
|
# Extension will auto-connect to server on localhost:5555
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ways to Contribute
|
### Ways to Contribute
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
// MCP Server connection configuration
|
// 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 mcpSocket = null;
|
||||||
let reconnectInterval = null;
|
let reconnectInterval = null;
|
||||||
let reconnectAttempts = 0;
|
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
|
// Initialize WebSocket connection to MCP server
|
||||||
function connectToMCPServer() {
|
async function connectToMCPServer() {
|
||||||
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return;
|
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);
|
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
|
||||||
mcpSocket = new WebSocket(MCP_SERVER_URL);
|
mcpSocket = new WebSocket(MCP_SERVER_URL);
|
||||||
|
|
||||||
@@ -32,12 +64,20 @@ function connectToMCPServer() {
|
|||||||
|
|
||||||
mcpSocket.onclose = () => {
|
mcpSocket.onclose = () => {
|
||||||
console.log('❌ Disconnected from MCP server, will reconnect...');
|
console.log('❌ Disconnected from MCP server, will reconnect...');
|
||||||
|
reconnectAttempts++;
|
||||||
|
|
||||||
|
// Clear any existing reconnect interval
|
||||||
|
if (reconnectInterval) {
|
||||||
|
clearInterval(reconnectInterval);
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to reconnect every 5 seconds
|
// Attempt to reconnect every 5 seconds
|
||||||
reconnectInterval = setInterval(connectToMCPServer, 5000);
|
reconnectInterval = setInterval(connectToMCPServer, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
mcpSocket.onerror = (error) => {
|
mcpSocket.onerror = (error) => {
|
||||||
console.log('⚠️ MCP WebSocket error:', error);
|
console.log('⚠️ MCP WebSocket error:', error);
|
||||||
|
reconnectAttempts++;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,8 +1156,10 @@ async function getSelectedText(params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize connection when extension loads
|
// Initialize connection when extension loads (with delay for server startup)
|
||||||
connectToMCPServer();
|
setTimeout(() => {
|
||||||
|
connectToMCPServer();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Heartbeat to keep connection alive
|
// Heartbeat to keep connection alive
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@@ -1141,6 +1183,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
} else if (request.action === "reconnect") {
|
} else if (request.action === "reconnect") {
|
||||||
connectToMCPServer();
|
connectToMCPServer();
|
||||||
sendResponse({ success: true });
|
sendResponse({ success: true });
|
||||||
|
} else if (request.action === "getPorts") {
|
||||||
|
sendResponse({
|
||||||
|
current: lastKnownPorts,
|
||||||
|
websocketUrl: MCP_SERVER_URL
|
||||||
|
});
|
||||||
} else if (request.action === "test") {
|
} else if (request.action === "test") {
|
||||||
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
|
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
|
||||||
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));
|
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "OpenDia",
|
"name": "OpenDia",
|
||||||
"version": "1.0.0",
|
"version": "1.0.4",
|
||||||
"description": "Browser automation through Model Context Protocol",
|
"description": "Browser automation through Model Context Protocol",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icon-16.png",
|
"16": "icon-16.png",
|
||||||
|
|||||||
@@ -255,9 +255,9 @@
|
|||||||
<span id="statusText" class="tooltip">
|
<span id="statusText" class="tooltip">
|
||||||
Checking connection...
|
Checking connection...
|
||||||
<span class="tooltip-content">
|
<span class="tooltip-content">
|
||||||
Make sure your MCP server is connected.
|
Start server with: npx opendia
|
||||||
If it's the case, click on Reconnect.
|
Auto-discovery will find the correct ports.
|
||||||
If it still don't work, kill your 3000 port & try again.
|
If issues persist, try: npx opendia --kill-existing
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Server</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Available Tools</span>
|
<span class="info-label">Available Tools</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ let statusIndicator = document.getElementById("statusIndicator");
|
|||||||
let statusText = document.getElementById("statusText");
|
let statusText = document.getElementById("statusText");
|
||||||
let toolCount = document.getElementById("toolCount");
|
let toolCount = document.getElementById("toolCount");
|
||||||
let currentPage = document.getElementById("currentPage");
|
let currentPage = document.getElementById("currentPage");
|
||||||
|
let serverUrl = document.getElementById("serverUrl");
|
||||||
|
|
||||||
// Get dynamic tool count from background script
|
// Get dynamic tool count from background script
|
||||||
function updateToolCount() {
|
function updateToolCount() {
|
||||||
@@ -59,16 +60,31 @@ function checkStatus() {
|
|||||||
checkStatus();
|
checkStatus();
|
||||||
setInterval(checkStatus, 2000);
|
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
|
// Update UI based on connection status
|
||||||
function updateStatus(connected) {
|
function updateStatus(connected) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
statusIndicator.className = "status-indicator connected";
|
statusIndicator.className = "status-indicator connected";
|
||||||
statusText.innerHTML = `Connected to MCP server
|
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 {
|
} else {
|
||||||
statusIndicator.className = "status-indicator disconnected";
|
statusIndicator.className = "status-indicator disconnected";
|
||||||
statusText.innerHTML = `Disconnected from MCP server
|
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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
opendia-mcp/package-lock.json
generated
25
opendia-mcp/package-lock.json
generated
@@ -9,7 +9,8 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.19.2",
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.21.2",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -136,6 +137,19 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -529,6 +543,15 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opendia",
|
"name": "opendia",
|
||||||
"version": "1.0.3",
|
"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",
|
"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",
|
"main": "server.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"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"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -34,8 +37,9 @@
|
|||||||
"url": "https://github.com/aaronjmars/opendia/issues"
|
"url": "https://github.com/aaronjmars/opendia/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2"
|
"express": "^4.21.2",
|
||||||
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
|
|||||||
@@ -2,9 +2,141 @@
|
|||||||
|
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const net = require('net');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
// WebSocket server for Chrome Extension
|
// ADD: New imports for SSE transport
|
||||||
const wss = new WebSocket.Server({ port: 3000 });
|
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 chromeExtensionSocket = null;
|
||||||
let availableTools = [];
|
let availableTools = [];
|
||||||
|
|
||||||
@@ -1077,8 +1209,9 @@ function handleToolResponse(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Chrome Extension connections
|
// Setup WebSocket connection handlers
|
||||||
wss.on("connection", (ws) => {
|
function setupWebSocketHandlers() {
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
console.error("Chrome Extension connected");
|
console.error("Chrome Extension connected");
|
||||||
chromeExtensionSocket = ws;
|
chromeExtensionSocket = ws;
|
||||||
|
|
||||||
@@ -1129,11 +1262,76 @@ wss.on("connection", (ws) => {
|
|||||||
ws.on("pong", () => {
|
ws.on("pong", () => {
|
||||||
// Extension is alive
|
// 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
|
// Read from stdin
|
||||||
let inputBuffer = "";
|
let inputBuffer = "";
|
||||||
process.stdin.on("data", async (chunk) => {
|
if (!sseOnly) {
|
||||||
|
process.stdin.on("data", async (chunk) => {
|
||||||
inputBuffer += chunk.toString();
|
inputBuffer += chunk.toString();
|
||||||
|
|
||||||
// Process complete lines
|
// Process complete lines
|
||||||
@@ -1155,34 +1353,203 @@ process.stdin.on("data", async (chunk) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Optional: HTTP endpoint for health checks
|
// ADD: Health check endpoint (update existing one)
|
||||||
const app = express();
|
app.get('/health', (req, res) => {
|
||||||
app.get("/health", (req, res) => {
|
|
||||||
res.json({
|
res.json({
|
||||||
status: "ok",
|
status: 'ok',
|
||||||
chromeExtensionConnected: chromeExtensionSocket !== null,
|
chromeExtensionConnected: chromeExtensionSocket !== null,
|
||||||
availableTools: availableTools.length,
|
availableTools: availableTools.length,
|
||||||
|
transport: sseOnly ? 'sse-only' : 'hybrid',
|
||||||
|
tunnelEnabled: enableTunnel,
|
||||||
|
ports: {
|
||||||
|
websocket: WS_PORT,
|
||||||
|
http: HTTP_PORT
|
||||||
|
},
|
||||||
features: [
|
features: [
|
||||||
"Anti-detection bypass for Twitter/X, LinkedIn, Facebook",
|
'Anti-detection bypass for Twitter/X, LinkedIn, Facebook',
|
||||||
"Two-phase intelligent page analysis",
|
'Two-phase intelligent page analysis',
|
||||||
"Smart content extraction with summarization",
|
'Smart content extraction with summarization',
|
||||||
"Element state detection and interaction readiness",
|
'Element state detection and interaction readiness',
|
||||||
"Performance analytics and token optimization",
|
'Performance analytics and token optimization',
|
||||||
],
|
'SSE transport for online AI services'
|
||||||
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(3001, () => {
|
// ADD: Port discovery endpoint for Chrome extension
|
||||||
console.error("🎯 Enhanced Browser MCP Server with Anti-Detection Features");
|
app.get('/ports', (req, res) => {
|
||||||
console.error(
|
res.json({
|
||||||
"Health check endpoint available at http://localhost:3001/health"
|
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");
|
// START: Enhanced server startup with port conflict resolution
|
||||||
console.error(
|
async function startServer() {
|
||||||
"🔌 Waiting for Chrome Extension connection on ws://localhost:3000"
|
console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features");
|
||||||
);
|
console.error(`📊 Default ports: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`);
|
||||||
console.error("🎯 Features: Anti-detection bypass + intelligent automation");
|
|
||||||
|
// 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();
|
||||||
|
|||||||
Reference in New Issue
Block a user