hosted mode w/ Ngrok + SSE

This commit is contained in:
Aaron Elijah Mars 2025-07-12 20:30:15 +02:00
parent 23eda72d91
commit c3c77c1e04
4 changed files with 291 additions and 26 deletions

View File

@ -93,6 +93,54 @@ 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:3000
- Claude Desktop: stdio (existing config)
- Local SSE: http://localhost:3001/sse
### 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**:

View File

@ -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",

View File

@ -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"

View File

@ -3,6 +3,21 @@
const WebSocket = require("ws"); const WebSocket = require("ws");
const express = require("express"); const express = require("express");
// ADD: New imports for SSE transport
const cors = require('cors');
const { createServer } = require('http');
const { spawn } = require('child_process');
// ADD: 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');
// ADD: Express app setup
const app = express();
app.use(cors());
app.use(express.json());
// WebSocket server for Chrome Extension // WebSocket server for Chrome Extension
const wss = new WebSocket.Server({ port: 3000 }); const wss = new WebSocket.Server({ port: 3000 });
let chromeExtensionSocket = null; let chromeExtensionSocket = null;
@ -1131,9 +1146,73 @@ wss.on("connection", (ws) => {
}); });
}); });
// 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 +1234,145 @@ 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,
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, () => { // START: Enhanced server startup with optional tunneling
console.error("🎯 Enhanced Browser MCP Server with Anti-Detection Features"); async function startServer() {
console.error( console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features");
"Health check endpoint available at http://localhost:3001/health"
); // Start HTTP server
const httpServer = app.listen(3001, () => {
console.error("🌐 HTTP/SSE server running on port 3001");
console.error("🔌 Waiting for Chrome Extension connection on ws://localhost:3000");
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', '3001', '--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 3001');
console.error(' 2. Use the ngrok URL + /sse');
console.error('');
console.error('💡 Or use local URL:');
console.error(' 🔗 http://localhost:3001/sse');
console.error('');
}
} else {
console.error('');
console.error('🏠 LOCAL MODE:');
console.error('🔗 SSE endpoint: http://localhost:3001/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:3001/sse');
} else {
console.error('📡 Transport: Hybrid (stdio + SSE)');
console.error('💡 Claude Desktop: Works with existing config');
console.error('💡 Online AI: Use SSE endpoint above');
}
}
// 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();
}); });
console.error("🚀 Enhanced Browser MCP Server started"); // Start the server
console.error( startServer();
"🔌 Waiting for Chrome Extension connection on ws://localhost:3000"
);
console.error("🎯 Features: Anti-detection bypass + intelligent automation");