mirror of
https://github.com/aaronjmars/opendia.git
synced 2025-12-17 09:46:01 +00:00
hosted mode w/ Ngrok + SSE
This commit is contained in:
parent
23eda72d91
commit
c3c77c1e04
48
README.md
48
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**:
|
||||
|
||||
25
opendia-mcp/package-lock.json
generated
25
opendia-mcp/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,8 +1146,72 @@ 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 = "";
|
||||
if (!sseOnly) {
|
||||
process.stdin.on("data", async (chunk) => {
|
||||
inputBuffer += chunk.toString();
|
||||
|
||||
@ -1156,33 +1235,144 @@ 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");
|
||||
|
||||
console.error("🚀 Enhanced Browser MCP Server started");
|
||||
console.error(
|
||||
"🔌 Waiting for Chrome Extension connection on ws://localhost:3000"
|
||||
);
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user