From c3c77c1e04308e4f633649deded8869c7a0a6c56 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars Date: Sat, 12 Jul 2025 20:30:15 +0200 Subject: [PATCH] hosted mode w/ Ngrok + SSE --- README.md | 48 +++++++ opendia-mcp/package-lock.json | 25 +++- opendia-mcp/package.json | 8 +- opendia-mcp/server.js | 236 ++++++++++++++++++++++++++++++---- 4 files changed, 291 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 308e1fa..cf360f3 100644 --- a/README.md +++ b/README.md @@ -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. +## 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 OpenDia gives AI models **17 powerful browser tools**: diff --git a/opendia-mcp/package-lock.json b/opendia-mcp/package-lock.json index 1fbe617..3d8dca1 100644 --- a/opendia-mcp/package-lock.json +++ b/opendia-mcp/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.3", "license": "MIT", "dependencies": { - "express": "^4.19.2", + "cors": "^2.8.5", + "express": "^4.21.2", "ws": "^8.18.0" }, "bin": { @@ -136,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", @@ -529,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", diff --git a/opendia-mcp/package.json b/opendia-mcp/package.json index 35f71c3..f1cfc59 100644 --- a/opendia-mcp/package.json +++ b/opendia-mcp/package.json @@ -8,6 +8,9 @@ }, "scripts": { "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": [ @@ -34,8 +37,9 @@ "url": "https://github.com/aaronjmars/opendia/issues" }, "dependencies": { - "ws": "^8.18.0", - "express": "^4.19.2" + "cors": "^2.8.5", + "express": "^4.21.2", + "ws": "^8.18.0" }, "engines": { "node": ">=16.0.0" diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 53a58cf..ba13830 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -3,6 +3,21 @@ const WebSocket = require("ws"); 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 const wss = new WebSocket.Server({ port: 3000 }); let chromeExtensionSocket = null; @@ -1131,10 +1146,74 @@ 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 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 +1234,145 @@ 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, 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" - ); +// START: Enhanced server startup with optional tunneling +async function startServer() { + console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features"); + + // 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"); -console.error( - "🔌 Waiting for Chrome Extension connection on ws://localhost:3000" -); -console.error("🎯 Features: Anti-detection bypass + intelligent automation"); +// Start the server +startServer();