From e26dc768fed7e528a78adbcdf225958f2eabe132 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars Date: Fri, 13 Jun 2025 23:21:32 +0200 Subject: [PATCH] opendia init --- README.md | 877 ++-------------------- opendia-extension/background.js | 438 +++++++++++ opendia-extension/content.js | 13 + opendia-extension/manifest.json | 44 ++ opendia-extension/popup.html | 191 +++++ opendia-extension/popup.js | 112 +++ opendia-mcp/.env.example | 4 + opendia-mcp/package-lock.json | 1230 +++++++++++++++++++++++++++++++ opendia-mcp/package.json | 12 + opendia-mcp/server.js | 418 +++++++++++ 10 files changed, 2540 insertions(+), 799 deletions(-) create mode 100644 opendia-extension/background.js create mode 100644 opendia-extension/content.js create mode 100644 opendia-extension/manifest.json create mode 100644 opendia-extension/popup.html create mode 100644 opendia-extension/popup.js create mode 100644 opendia-mcp/.env.example create mode 100644 opendia-mcp/package-lock.json create mode 100644 opendia-mcp/package.json create mode 100644 opendia-mcp/server.js diff --git a/README.md b/README.md index 4e89351..24ec9d7 100644 --- a/README.md +++ b/README.md @@ -1,802 +1,53 @@ -# Chrome Extension MCP Bridge +# OpenDia Browser Bridge -This project creates a Chrome Extension that exposes browser functions through the Model Context Protocol (MCP), allowing AI models to interact with browser capabilities. +OpenDia is a Chrome Extension that exposes browser functions through the Model Context Protocol (MCP), allowing AI models to interact with browser capabilities. -## Project Structure +## ⚠️ Security Warning -``` -chrome-mcp-extension/ -├── extension/ -│ ├── manifest.json -│ ├── background.js -│ ├── content.js -│ ├── popup.html -│ └── popup.js -├── mcp-server/ -│ ├── package.json -│ ├── server.js -│ └── .env -└── README.md -``` +**IMPORTANT**: This extension is provided as-is with no security guarantees. By using this extension, you acknowledge and accept the following risks: -## Chrome Extension +- The extension requires broad browser permissions to function +- It establishes WebSocket connections to localhost +- It allows external applications to control browser functions +- We cannot guarantee the security of data transmitted through the extension +- Use at your own risk and only in trusted environments -### manifest.json -```json -{ - "manifest_version": 3, - "name": "Browser MCP Bridge", - "version": "1.0.0", - "description": "Exposes browser functions through Model Context Protocol", - "permissions": [ - "tabs", - "activeTab", - "storage", - "bookmarks", - "history", - "downloads", - "cookies", - "webNavigation", - "scripting", - "nativeMessaging", - "contextMenus", - "notifications", - "alarms", - "clipboardRead", - "clipboardWrite" - ], - "host_permissions": [ - "" - ], - "background": { - "service_worker": "background.js" - }, - "action": { - "default_popup": "popup.html", - "default_title": "MCP Browser Bridge" - }, - "content_scripts": [ - { - "matches": [""], - "js": ["content.js"], - "run_at": "document_idle" - } - ], - "externally_connectable": { - "ids": ["*"], - "matches": ["http://localhost/*"] - } -} -``` +## Quick Start -### background.js -```javascript -// MCP Server connection configuration -const MCP_SERVER_URL = 'ws://localhost:3000'; -let mcpSocket = null; -let reconnectInterval = null; +### Prerequisites +- Node.js (v14 or higher) +- Google Chrome browser -// Initialize WebSocket connection to MCP server -function connectToMCPServer() { - if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return; - - mcpSocket = new WebSocket(MCP_SERVER_URL); - - mcpSocket.onopen = () => { - console.log('Connected to MCP server'); - clearInterval(reconnectInterval); - - // Register available browser functions - mcpSocket.send(JSON.stringify({ - type: 'register', - tools: getAvailableTools() - })); - }; - - mcpSocket.onmessage = async (event) => { - const message = JSON.parse(event.data); - await handleMCPRequest(message); - }; - - mcpSocket.onclose = () => { - console.log('Disconnected from MCP server'); - // Attempt to reconnect every 5 seconds - reconnectInterval = setInterval(connectToMCPServer, 5000); - }; - - mcpSocket.onerror = (error) => { - console.error('MCP connection error:', error); - }; -} +### Installation -// Define available browser tools for MCP -function getAvailableTools() { - return [ - { - name: 'browser_navigate', - description: 'Navigate to a URL in the active tab', - inputSchema: { - type: 'object', - properties: { - url: { type: 'string', description: 'URL to navigate to' } - }, - required: ['url'] - } - }, - { - name: 'browser_get_tabs', - description: 'Get all open tabs', - inputSchema: { - type: 'object', - properties: {} - } - }, - { - name: 'browser_create_tab', - description: 'Create a new tab', - inputSchema: { - type: 'object', - properties: { - url: { type: 'string', description: 'URL for the new tab' }, - active: { type: 'boolean', description: 'Whether to make the tab active' } - } - } - }, - { - name: 'browser_close_tab', - description: 'Close a tab by ID', - inputSchema: { - type: 'object', - properties: { - tabId: { type: 'number', description: 'ID of the tab to close' } - }, - required: ['tabId'] - } - }, - { - name: 'browser_execute_script', - description: 'Execute JavaScript in the active tab', - inputSchema: { - type: 'object', - properties: { - code: { type: 'string', description: 'JavaScript code to execute' } - }, - required: ['code'] - } - }, - { - name: 'browser_get_page_content', - description: 'Get the content of the active page', - inputSchema: { - type: 'object', - properties: { - selector: { type: 'string', description: 'CSS selector to get specific content' } - } - } - }, - { - name: 'browser_take_screenshot', - description: 'Take a screenshot of the active tab', - inputSchema: { - type: 'object', - properties: { - format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format' } - } - } - }, - { - name: 'browser_get_bookmarks', - description: 'Get browser bookmarks', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query for bookmarks' } - } - } - }, - { - name: 'browser_add_bookmark', - description: 'Add a bookmark', - inputSchema: { - type: 'object', - properties: { - title: { type: 'string', description: 'Bookmark title' }, - url: { type: 'string', description: 'Bookmark URL' } - }, - required: ['title', 'url'] - } - }, - { - name: 'browser_get_history', - description: 'Search browser history', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query' }, - maxResults: { type: 'number', description: 'Maximum number of results' } - } - } - }, - { - name: 'browser_get_cookies', - description: 'Get cookies for a domain', - inputSchema: { - type: 'object', - properties: { - domain: { type: 'string', description: 'Domain to get cookies for' } - } - } - }, - { - name: 'browser_fill_form', - description: 'Fill a form on the current page', - inputSchema: { - type: 'object', - properties: { - formData: { - type: 'object', - description: 'Key-value pairs of form field names/IDs and values' - } - }, - required: ['formData'] - } - }, - { - name: 'browser_click_element', - description: 'Click an element on the page', - inputSchema: { - type: 'object', - properties: { - selector: { type: 'string', description: 'CSS selector of element to click' } - }, - required: ['selector'] - } - } - ]; -} +1. **Set up the MCP Server** + ```bash + cd opendia-mcp + npm install + npm start + ``` -// Handle MCP requests -async function handleMCPRequest(message) { - const { id, method, params } = message; - - try { - let result; - - switch (method) { - case 'browser_navigate': - result = await navigateToUrl(params.url); - break; - case 'browser_get_tabs': - result = await getTabs(); - break; - case 'browser_create_tab': - result = await createTab(params); - break; - case 'browser_close_tab': - result = await closeTab(params.tabId); - break; - case 'browser_execute_script': - result = await executeScript(params.code); - break; - case 'browser_get_page_content': - result = await getPageContent(params.selector); - break; - case 'browser_take_screenshot': - result = await takeScreenshot(params.format); - break; - case 'browser_get_bookmarks': - result = await getBookmarks(params.query); - break; - case 'browser_add_bookmark': - result = await addBookmark(params); - break; - case 'browser_get_history': - result = await getHistory(params); - break; - case 'browser_get_cookies': - result = await getCookies(params.domain); - break; - case 'browser_fill_form': - result = await fillForm(params.formData); - break; - case 'browser_click_element': - result = await clickElement(params.selector); - break; - default: - throw new Error(`Unknown method: ${method}`); - } - - // Send success response - mcpSocket.send(JSON.stringify({ - id, - result - })); - } catch (error) { - // Send error response - mcpSocket.send(JSON.stringify({ - id, - error: { - message: error.message, - code: -32603 - } - })); - } -} +2. **Install the Chrome Extension** + - Open Chrome and go to `chrome://extensions/` + - Enable "Developer mode" in the top right + - Click "Load unpacked" and select the `opendia-extension` directory + - The extension icon will appear in your browser toolbar -// Browser function implementations -async function navigateToUrl(url) { - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - await chrome.tabs.update(activeTab.id, { url }); - return { success: true, tabId: activeTab.id }; -} +3. **Configure your MCP client** + Add the browser server to your MCP configuration: + ```json + { + "mcpServers": { + "opendia": { + "command": "node", + "args": ["/path/to/opendia/opendia-mcp/server.js"], + "env": {} + } + } + } + ``` -async function getTabs() { - const tabs = await chrome.tabs.query({}); - return tabs.map(tab => ({ - id: tab.id, - title: tab.title, - url: tab.url, - active: tab.active, - windowId: tab.windowId - })); -} - -async function createTab(params) { - const tab = await chrome.tabs.create({ - url: params.url || 'about:blank', - active: params.active !== false - }); - return { id: tab.id, windowId: tab.windowId }; -} - -async function closeTab(tabId) { - await chrome.tabs.remove(tabId); - return { success: true }; -} - -async function executeScript(code) { - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - const results = await chrome.scripting.executeScript({ - target: { tabId: activeTab.id }, - func: new Function(code), - }); - return results[0].result; -} - -async function getPageContent(selector) { - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - const results = await chrome.scripting.executeScript({ - target: { tabId: activeTab.id }, - func: (sel) => { - if (sel) { - const element = document.querySelector(sel); - return element ? element.innerText : null; - } - return document.body.innerText; - }, - args: [selector] - }); - return results[0].result; -} - -async function takeScreenshot(format = 'png') { - const dataUrl = await chrome.tabs.captureVisibleTab(null, { format }); - return { dataUrl, format }; -} - -async function getBookmarks(query) { - if (query) { - return await chrome.bookmarks.search(query); - } - return await chrome.bookmarks.getTree(); -} - -async function addBookmark(params) { - const bookmark = await chrome.bookmarks.create({ - title: params.title, - url: params.url - }); - return bookmark; -} - -async function getHistory(params) { - const historyItems = await chrome.history.search({ - text: params.query || '', - maxResults: params.maxResults || 100 - }); - return historyItems; -} - -async function getCookies(domain) { - const cookies = await chrome.cookies.getAll({ domain }); - return cookies; -} - -async function fillForm(formData) { - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - const results = await chrome.scripting.executeScript({ - target: { tabId: activeTab.id }, - func: (data) => { - for (const [key, value] of Object.entries(data)) { - const element = document.querySelector(`[name="${key}"], #${key}`); - if (element) { - element.value = value; - element.dispatchEvent(new Event('input', { bubbles: true })); - } - } - return { success: true, filled: Object.keys(data).length }; - }, - args: [formData] - }); - return results[0].result; -} - -async function clickElement(selector) { - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - const results = await chrome.scripting.executeScript({ - target: { tabId: activeTab.id }, - func: (sel) => { - const element = document.querySelector(sel); - if (element) { - element.click(); - return { success: true, clicked: sel }; - } - throw new Error(`Element not found: ${sel}`); - }, - args: [selector] - }); - return results[0].result; -} - -// Initialize connection when extension loads -connectToMCPServer(); - -// Handle extension icon click -chrome.action.onClicked.addListener(() => { - chrome.runtime.openOptionsPage(); -}); -``` - -### content.js -```javascript -// Content script for interacting with web pages -console.log('MCP Browser Bridge content script loaded'); - -// Listen for messages from the background script -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'getPageInfo') { - sendResponse({ - title: document.title, - url: window.location.href, - content: document.body.innerText - }); - } -}); -``` - -### popup.html -```html - - - - MCP Browser Bridge - - - -

MCP Browser Bridge

- -
-
- Checking connection... -
- -
- Server: ws://localhost:3000
- Available Tools: 0 -
- - - - -
-
Waiting for activity...
-
- - - - -``` - -### popup.js -```javascript -// Popup script for status display -let logContainer = document.getElementById('log'); -let statusIndicator = document.getElementById('statusIndicator'); -let statusText = document.getElementById('statusText'); -let toolCount = document.getElementById('toolCount'); - -// Check connection status -chrome.runtime.sendMessage({ action: 'getStatus' }, (response) => { - updateStatus(response?.connected || false); -}); - -// Update UI based on connection status -function updateStatus(connected) { - if (connected) { - statusIndicator.className = 'status-indicator connected'; - statusText.textContent = 'Connected to MCP server'; - } else { - statusIndicator.className = 'status-indicator disconnected'; - statusText.textContent = 'Disconnected from MCP server'; - } -} - -// Reconnect button -document.getElementById('reconnectBtn').addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'reconnect' }); - addLog('Attempting to reconnect...'); -}); - -// Test button -document.getElementById('testBtn').addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'test' }); - addLog('Sending test message...'); -}); - -// Add log entry -function addLog(message) { - const entry = document.createElement('div'); - entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; - logContainer.appendChild(entry); - logContainer.scrollTop = logContainer.scrollHeight; -} - -// Listen for updates from background script -chrome.runtime.onMessage.addListener((message) => { - if (message.type === 'statusUpdate') { - updateStatus(message.connected); - } else if (message.type === 'log') { - addLog(message.message); - } -}); -``` - -## MCP Server - -### package.json -```json -{ - "name": "browser-mcp-server", - "version": "1.0.0", - "description": "MCP server for browser automation", - "main": "server.js", - "scripts": { - "start": "node server.js", - "dev": "nodemon server.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "ws": "^8.16.0", - "express": "^4.18.2", - "dotenv": "^16.3.1" - }, - "devDependencies": { - "nodemon": "^3.0.2" - } -} -``` - -### server.js -```javascript -const WebSocket = require('ws'); -const express = require('express'); -const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); -const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); - -// WebSocket server for Chrome Extension -const wss = new WebSocket.Server({ port: 3000 }); -let chromeExtensionSocket = null; -let availableTools = []; - -// MCP Server setup -const server = new Server( - { - name: 'browser-mcp-server', - version: '1.0.0', - }, - { - capabilities: { - tools: {}, - }, - } -); - -// Handle Chrome Extension connections -wss.on('connection', (ws) => { - console.log('Chrome Extension connected'); - chromeExtensionSocket = ws; - - ws.on('message', (data) => { - const message = JSON.parse(data); - - if (message.type === 'register') { - availableTools = message.tools; - console.log(`Registered ${availableTools.length} browser tools`); - - // Register tools with MCP server - availableTools.forEach(tool => { - server.setRequestHandler(`tools/call/${tool.name}`, async (request) => { - return callBrowserTool(tool.name, request.params.arguments); - }); - }); - } else if (message.id) { - // Handle tool response - handleToolResponse(message); - } - }); - - ws.on('close', () => { - console.log('Chrome Extension disconnected'); - chromeExtensionSocket = null; - }); -}); - -// Tool call tracking -const pendingCalls = new Map(); - -// Call browser tool through Chrome Extension -async function callBrowserTool(toolName, args) { - if (!chromeExtensionSocket || chromeExtensionSocket.readyState !== WebSocket.OPEN) { - throw new Error('Chrome Extension not connected'); - } - - const callId = Date.now().toString(); - - return new Promise((resolve, reject) => { - pendingCalls.set(callId, { resolve, reject }); - - chromeExtensionSocket.send(JSON.stringify({ - id: callId, - method: toolName, - params: args - })); - - // Timeout after 30 seconds - setTimeout(() => { - if (pendingCalls.has(callId)) { - pendingCalls.delete(callId); - reject(new Error('Tool call timeout')); - } - }, 30000); - }); -} - -// Handle tool responses from Chrome Extension -function handleToolResponse(message) { - const pending = pendingCalls.get(message.id); - if (pending) { - pendingCalls.delete(message.id); - if (message.error) { - pending.reject(new Error(message.error.message)); - } else { - pending.resolve(message.result); - } - } -} - -// List available tools -server.setRequestHandler('tools/list', async () => { - return { - tools: availableTools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema - })) - }; -}); - -// Start MCP server -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.log('MCP Server running on stdio'); -} - -// Run the server -main().catch(console.error); - -// Optional: HTTP endpoint for health checks -const app = express(); -app.get('/health', (req, res) => { - res.json({ - status: 'ok', - chromeExtensionConnected: chromeExtensionSocket !== null, - availableTools: availableTools.length - }); -}); -app.listen(3001, () => { - console.log('Health check endpoint available at http://localhost:3001/health'); -}); -``` - -## Installation and Usage - -### 1. Install the Chrome Extension -1. Open Chrome and navigate to `chrome://extensions/` -2. Enable "Developer mode" -3. Click "Load unpacked" and select the `extension` directory -4. The extension icon should appear in your browser toolbar - -### 2. Set up the MCP Server -```bash -cd mcp-server -npm install -npm start -``` - -### 3. Configure your MCP client -Add the browser server to your MCP configuration: - -```json -{ - "mcpServers": { - "browser": { - "command": "node", - "args": ["/path/to/mcp-server/server.js"], - "env": {} - } - } -} -``` - -### 4. Available MCP Tools +## Available MCP Tools Once connected, the following tools will be available through MCP: @@ -814,23 +65,51 @@ Once connected, the following tools will be available through MCP: - **browser_fill_form**: Automatically fill form fields - **browser_click_element**: Click elements on the page -## Security Considerations +## Project Structure -1. **Permissions**: The extension requests broad permissions. Review and limit based on your needs. -2. **Local Only**: The WebSocket server runs on localhost only by default -3. **Content Security**: Be cautious when executing scripts or accessing sensitive data -4. **Authentication**: Consider adding authentication between the extension and MCP server +``` +opendia/ +├── opendia-extension/ # Chrome extension files +│ ├── manifest.json # Extension configuration +│ ├── background.js # Background service worker +│ ├── content.js # Content scripts +│ ├── popup.html # Extension popup UI +│ └── popup.js # Popup functionality +├── opendia-mcp/ # MCP server implementation +│ ├── package.json # Server dependencies +│ ├── server.js # MCP server logic +│ └── .env # Environment configuration +└── README.md +``` -## Extending the Bridge +## Contributing -To add new browser functions: +1. **Adding New Browser Functions** + - Add the tool definition in `getAvailableTools()` in background.js + - Implement the handler in `handleMCPRequest()` + - The tool will automatically be registered with the MCP server -1. Add the tool definition in `getAvailableTools()` in background.js -2. Implement the handler in `handleMCPRequest()` -3. The tool will automatically be registered with the MCP server +2. **Development Workflow** + - Modify extension files in the `opendia-extension` directory + - Reload the extension in Chrome to see changes + - Test new functionality through the MCP interface + +3. **Security Considerations** + - Review and limit permissions based on needs + - The WebSocket server runs on localhost only by default + - Be cautious when executing scripts or accessing sensitive data + - Consider adding authentication between the extension and MCP server ## Troubleshooting - **Extension not connecting**: Check that the MCP server is running on port 3000 - **Tools not available**: Verify the extension is loaded and check the popup for connection status -- **Permission errors**: Ensure the extension has the necessary permissions in manifest.json \ No newline at end of file +- **Permission errors**: Ensure the extension has the necessary permissions in manifest.json + +## License + +MIT License + +## Disclaimer + +This software is provided "as is", without warranty of any kind, express or implied. In no event shall the authors or copyright holders be liable for any claim, damages or other liability arising from the use of this software. diff --git a/opendia-extension/background.js b/opendia-extension/background.js new file mode 100644 index 0000000..d033168 --- /dev/null +++ b/opendia-extension/background.js @@ -0,0 +1,438 @@ +// MCP Server connection configuration +const MCP_SERVER_URL = 'ws://localhost:3000'; +let mcpSocket = null; +let reconnectInterval = null; +let reconnectAttempts = 0; + +// Initialize WebSocket connection to MCP server +function connectToMCPServer() { + if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return; + + mcpSocket = new WebSocket(MCP_SERVER_URL); + + mcpSocket.onopen = () => { + clearInterval(reconnectInterval); + + // Register available browser functions + mcpSocket.send(JSON.stringify({ + type: 'register', + tools: getAvailableTools() + })); + }; + + mcpSocket.onmessage = async (event) => { + const message = JSON.parse(event.data); + await handleMCPRequest(message); + }; + + mcpSocket.onclose = () => { + // Attempt to reconnect every 5 seconds + reconnectInterval = setInterval(connectToMCPServer, 5000); + }; + + mcpSocket.onerror = (error) => { + // Handle error silently in production + }; +} + +// Define available browser tools for MCP +function getAvailableTools() { + return [ + { + name: "browser_navigate", + description: "Navigate to a URL in the active tab", + inputSchema: { + type: "object", + properties: { + url: { type: "string", description: "URL to navigate to" }, + }, + required: ["url"], + }, + }, + { + name: "browser_get_tabs", + description: "Get all open tabs", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "browser_create_tab", + description: "Create a new tab", + inputSchema: { + type: "object", + properties: { + url: { type: "string", description: "URL for the new tab" }, + active: { + type: "boolean", + description: "Whether to make the tab active", + }, + }, + }, + }, + { + name: "browser_close_tab", + description: "Close a tab by ID", + inputSchema: { + type: "object", + properties: { + tabId: { type: "number", description: "ID of the tab to close" }, + }, + required: ["tabId"], + }, + }, + { + name: "browser_execute_script", + description: "Execute JavaScript in the active tab", + inputSchema: { + type: "object", + properties: { + code: { type: "string", description: "JavaScript code to execute" }, + }, + required: ["code"], + }, + }, + { + name: "browser_get_page_content", + description: "Get the content of the active page", + inputSchema: { + type: "object", + properties: { + selector: { + type: "string", + description: "CSS selector to get specific content", + }, + }, + }, + }, + { + name: "browser_take_screenshot", + description: "Take a screenshot of the active tab", + inputSchema: { + type: "object", + properties: { + format: { + type: "string", + enum: ["png", "jpeg"], + description: "Image format", + }, + }, + }, + }, + { + name: "browser_get_bookmarks", + description: "Get browser bookmarks", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query for bookmarks" }, + }, + }, + }, + { + name: "browser_add_bookmark", + description: "Add a bookmark", + inputSchema: { + type: "object", + properties: { + title: { type: "string", description: "Bookmark title" }, + url: { type: "string", description: "Bookmark URL" }, + }, + required: ["title", "url"], + }, + }, + { + name: "browser_get_history", + description: "Search browser history", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query" }, + maxResults: { + type: "number", + description: "Maximum number of results", + }, + }, + }, + }, + { + name: "browser_get_cookies", + description: "Get cookies for a domain", + inputSchema: { + type: "object", + properties: { + domain: { type: "string", description: "Domain to get cookies for" }, + }, + }, + }, + { + name: "browser_fill_form", + description: "Fill a form on the current page", + inputSchema: { + type: "object", + properties: { + formData: { + type: "object", + description: "Key-value pairs of form field names/IDs and values", + }, + }, + required: ["formData"], + }, + }, + { + name: "browser_click_element", + description: "Click an element on the page", + inputSchema: { + type: "object", + properties: { + selector: { + type: "string", + description: "CSS selector of element to click", + }, + }, + required: ["selector"], + }, + }, + ]; +} + +// Handle MCP requests +async function handleMCPRequest(message) { + const { id, method, params } = message; + + try { + let result; + + switch (method) { + case "browser_navigate": + result = await navigateToUrl(params.url); + break; + case "browser_get_tabs": + result = await getTabs(); + break; + case "browser_create_tab": + result = await createTab(params); + break; + case "browser_close_tab": + result = await closeTab(params.tabId); + break; + case "browser_execute_script": + result = await executeScript(params.code); + break; + case "browser_get_page_content": + result = await getPageContent(params.selector); + break; + case "browser_take_screenshot": + result = await takeScreenshot(params.format); + break; + case "browser_get_bookmarks": + result = await getBookmarks(params.query); + break; + case "browser_add_bookmark": + result = await addBookmark(params); + break; + case "browser_get_history": + result = await getHistory(params); + break; + case "browser_get_cookies": + result = await getCookies(params.domain); + break; + case "browser_fill_form": + result = await fillForm(params.formData); + break; + case "browser_click_element": + result = await clickElement(params.selector); + break; + default: + throw new Error(`Unknown method: ${method}`); + } + + // Send success response + mcpSocket.send( + JSON.stringify({ + id, + result, + }) + ); + } catch (error) { + // Send error response + mcpSocket.send( + JSON.stringify({ + id, + error: { + message: error.message, + code: -32603, + }, + }) + ); + } +} + +// Browser function implementations +async function navigateToUrl(url) { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + await chrome.tabs.update(activeTab.id, { url }); + return { success: true, tabId: activeTab.id }; +} + +async function getTabs() { + const tabs = await chrome.tabs.query({}); + return tabs.map((tab) => ({ + id: tab.id, + title: tab.title, + url: tab.url, + active: tab.active, + windowId: tab.windowId, + })); +} + +async function createTab(params) { + const tab = await chrome.tabs.create({ + url: params.url || "about:blank", + active: params.active !== false, + }); + return { id: tab.id, windowId: tab.windowId }; +} + +async function closeTab(tabId) { + await chrome.tabs.remove(tabId); + return { success: true }; +} + +async function executeScript(code) { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: new Function(code), + }); + return results[0].result; +} + +async function getPageContent(selector) { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: (sel) => { + if (sel) { + const element = document.querySelector(sel); + return element ? element.innerText : null; + } + return document.body.innerText; + }, + args: [selector], + }); + return results[0].result; +} + +async function takeScreenshot(format = "png") { + const dataUrl = await chrome.tabs.captureVisibleTab(null, { format }); + return { dataUrl, format }; +} + +async function getBookmarks(query) { + if (query) { + return await chrome.bookmarks.search(query); + } + return await chrome.bookmarks.getTree(); +} + +async function addBookmark(params) { + const bookmark = await chrome.bookmarks.create({ + title: params.title, + url: params.url, + }); + return bookmark; +} + +async function getHistory(params) { + const historyItems = await chrome.history.search({ + text: params.query || "", + maxResults: params.maxResults || 100, + }); + return historyItems; +} + +async function getCookies(domain) { + const cookies = await chrome.cookies.getAll({ domain }); + return cookies; +} + +async function fillForm(formData) { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: (data) => { + for (const [key, value] of Object.entries(data)) { + const element = document.querySelector(`[name="${key}"], #${key}`); + if (element) { + element.value = value; + element.dispatchEvent(new Event("input", { bubbles: true })); + } + } + return { success: true, filled: Object.keys(data).length }; + }, + args: [formData], + }); + return results[0].result; +} + +async function clickElement(selector) { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: (sel) => { + const element = document.querySelector(sel); + if (element) { + element.click(); + return { success: true, clicked: sel }; + } + throw new Error(`Element not found: ${sel}`); + }, + args: [selector], + }); + return results[0].result; +} + +// Initialize connection when extension loads +connectToMCPServer(); + +// Heartbeat to keep connection alive +setInterval(() => { + if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { + mcpSocket.send(JSON.stringify({ type: "ping", timestamp: Date.now() })); + } +}, 30000); // Every 30 seconds + +// Handle messages from popup +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "getStatus") { + sendResponse({ + connected: mcpSocket && mcpSocket.readyState === WebSocket.OPEN, + }); + } else if (request.action === "reconnect") { + connectToMCPServer(); + sendResponse({ success: true }); + } else if (request.action === "test") { + if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { + mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() })); + } + sendResponse({ success: true }); + } + return true; // Keep the message channel open +}); diff --git a/opendia-extension/content.js b/opendia-extension/content.js new file mode 100644 index 0000000..af3486f --- /dev/null +++ b/opendia-extension/content.js @@ -0,0 +1,13 @@ +// Content script for interacting with web pages +console.log('MCP Browser Bridge content script loaded'); + +// Listen for messages from the background script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'getPageInfo') { + sendResponse({ + title: document.title, + url: window.location.href, + content: document.body.innerText + }); + } +}); \ No newline at end of file diff --git a/opendia-extension/manifest.json b/opendia-extension/manifest.json new file mode 100644 index 0000000..7e62aae --- /dev/null +++ b/opendia-extension/manifest.json @@ -0,0 +1,44 @@ +{ + "manifest_version": 3, + "name": "OpenDia Browser Bridge", + "version": "1.0.0", + "description": "Exposes browser functions through Model Context Protocol", + "permissions": [ + "tabs", + "activeTab", + "storage", + "bookmarks", + "history", + "downloads", + "cookies", + "webNavigation", + "scripting", + "nativeMessaging", + "contextMenus", + "notifications", + "alarms", + "clipboardRead", + "clipboardWrite" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html", + "default_title": "OpenDia Browser Bridge" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "externally_connectable": { + "ids": ["*"], + "matches": ["http://localhost/*"] + } +} diff --git a/opendia-extension/popup.html b/opendia-extension/popup.html new file mode 100644 index 0000000..694401b --- /dev/null +++ b/opendia-extension/popup.html @@ -0,0 +1,191 @@ + + + + OpenDia Browser Bridge + + + +
+ +

OpenDia Browser Bridge

+
+ +
+
+ Checking connection... +
+ +
+
+ Server + ws://localhost:3000 +
+
+ Available Tools + 0 +
+
+ +
+ + +
+ +
+
+ [System] + Waiting for activity... +
+
+ + + + diff --git a/opendia-extension/popup.js b/opendia-extension/popup.js new file mode 100644 index 0000000..afde824 --- /dev/null +++ b/opendia-extension/popup.js @@ -0,0 +1,112 @@ +// Popup script for status display +let logContainer = document.getElementById("log"); +let statusIndicator = document.getElementById("statusIndicator"); +let statusText = document.getElementById("statusText"); +let toolCount = document.getElementById("toolCount"); + +// Get initial tool count +const tools = 13; // Number of tools we expose +toolCount.textContent = tools; + +// Check connection status +function checkStatus() { + if (chrome.runtime?.id) { + chrome.runtime.sendMessage({ action: "getStatus" }, (response) => { + if (chrome.runtime.lastError) { + updateStatus(false); + addLog("Extension background script not responding", "error"); + } else { + updateStatus(response?.connected || false); + } + }); + } else { + updateStatus(false); + addLog("Extension context invalid", "error"); + } +} + +// Check status on load and periodically +checkStatus(); +setInterval(checkStatus, 2000); + +// Update UI based on connection status +function updateStatus(connected) { + if (connected) { + statusIndicator.className = "status-indicator connected"; + statusText.textContent = "Connected to MCP server"; + } else { + statusIndicator.className = "status-indicator disconnected"; + statusText.textContent = "Disconnected from MCP server"; + } +} + +// Reconnect button +document.getElementById("reconnectBtn").addEventListener("click", () => { + if (chrome.runtime?.id) { + chrome.runtime.sendMessage({ action: "reconnect" }, (response) => { + if (chrome.runtime.lastError) { + addLog(chrome.runtime.lastError.message, "error"); + } else { + addLog("Attempting to reconnect...", "info"); + setTimeout(checkStatus, 1000); + } + }); + } +}); + +// Test button +document.getElementById("testBtn").addEventListener("click", () => { + if (chrome.runtime?.id) { + chrome.runtime.sendMessage({ action: "test" }, (response) => { + if (chrome.runtime.lastError) { + addLog(chrome.runtime.lastError.message, "error"); + } else { + addLog("Sending test message...", "info"); + } + }); + } +}); + +// Add log entry +function addLog(message, type = "info") { + const entry = document.createElement("div"); + entry.className = "log-entry"; + + const time = document.createElement("span"); + time.className = "log-time"; + time.textContent = `[${new Date().toLocaleTimeString()}]`; + + const content = document.createElement("span"); + content.textContent = message; + + if (type === "error") { + content.style.color = "var(--error-color)"; + } else if (type === "success") { + content.style.color = "var(--success-color)"; + } + + entry.appendChild(time); + entry.appendChild(content); + logContainer.appendChild(entry); + + // Keep only last 20 entries + while (logContainer.children.length > 20) { + logContainer.removeChild(logContainer.firstChild); + } + + logContainer.scrollTop = logContainer.scrollHeight; +} + +// Listen for updates from background script +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === "statusUpdate") { + updateStatus(message.connected); + if (message.connected) { + addLog("Connected to MCP server", "success"); + } else { + addLog("Disconnected from MCP server", "error"); + } + } else if (message.type === "log") { + addLog(message.message, message.type || "info"); + } +}); diff --git a/opendia-mcp/.env.example b/opendia-mcp/.env.example new file mode 100644 index 0000000..19bd8fd --- /dev/null +++ b/opendia-mcp/.env.example @@ -0,0 +1,4 @@ +# MCP Server Configuration +PORT=3000 +HEALTH_PORT=3001 +NODE_ENV=development \ No newline at end of file diff --git a/opendia-mcp/package-lock.json b/opendia-mcp/package-lock.json new file mode 100644 index 0000000..a22242a --- /dev/null +++ b/opendia-mcp/package-lock.json @@ -0,0 +1,1230 @@ +{ + "name": "browser-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "browser-mcp-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.16.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "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", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/opendia-mcp/package.json b/opendia-mcp/package.json new file mode 100644 index 0000000..2ffe4ee --- /dev/null +++ b/opendia-mcp/package.json @@ -0,0 +1,12 @@ +{ + "name": "opendia-server", + "version": "1.0.0", + "description": "MCP Server for OpenDia Browser Bridge", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "ws": "^8.x.x" + } +} \ No newline at end of file diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js new file mode 100644 index 0000000..4c48d61 --- /dev/null +++ b/opendia-mcp/server.js @@ -0,0 +1,418 @@ +#!/usr/bin/env node + +const WebSocket = require("ws"); +const express = require("express"); + +// WebSocket server for Chrome Extension +const wss = new WebSocket.Server({ port: 3000 }); +let chromeExtensionSocket = null; +let availableTools = []; + +// Tool call tracking +const pendingCalls = new Map(); + +// Simple MCP protocol implementation over stdio +async function handleMCPRequest(request) { + const { method, params, id } = request; + + // Handle notifications (no id means it's a notification) + if (!id && method && method.startsWith("notifications/")) { + console.error(`Received notification: ${method}`); + return null; // No response needed for notifications + } + + // Handle requests that don't need implementation + if (id === undefined || id === null) { + return null; // No response for notifications + } + + try { + let result; + + switch (method) { + case "initialize": + // RESPOND IMMEDIATELY - don't wait for extension + console.error(`MCP client initializing: ${params?.clientInfo?.name || "unknown"}`); + result = { + protocolVersion: "2024-11-05", + capabilities: { + tools: {}, + }, + serverInfo: { + name: "browser-mcp-server", + version: "1.0.0", + }, + instructions: "Browser automation tools via Chrome Extension bridge. Extension may take a moment to connect." + }; + break; + + case "tools/list": + // Return tools even if extension not connected yet + if (availableTools.length > 0) { + result = { + tools: availableTools.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + }; + } else { + // Return static tools with note that extension is connecting + result = { + tools: getStaticTools().map(tool => ({ + ...tool, + description: tool.description + " (Extension connecting...)" + })) + }; + } + break; + + case "tools/call": + if (!chromeExtensionSocket || chromeExtensionSocket.readyState !== WebSocket.OPEN) { + // Extension not connected - return helpful error + result = { + content: [ + { + type: "text", + text: "❌ Chrome Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the extension folder\n4. Ensure the extension is active", + }, + ], + isError: true + }; + } else { + // Extension connected - try the tool call + try { + const toolResult = await callBrowserTool( + params.name, + params.arguments || {} + ); + result = { + content: [ + { + type: "text", + text: JSON.stringify(toolResult, null, 2), + }, + ], + isError: false + }; + } catch (error) { + result = { + content: [ + { + type: "text", + text: `❌ Tool execution failed: ${error.message}`, + }, + ], + isError: true + }; + } + } + break; + + case "resources/list": + // Return empty resources list + result = { resources: [] }; + break; + + case "prompts/list": + // Return empty prompts list + result = { prompts: [] }; + break; + + default: + throw new Error(`Unknown method: ${method}`); + } + + return { jsonrpc: "2.0", id, result }; + } catch (error) { + return { + jsonrpc: "2.0", + id, + error: { + code: -32603, + message: error.message, + }, + }; + } +} + +// Static tool definitions for when extension isn't connected +function getStaticTools() { + return [ + { + name: "browser_navigate", + description: "Navigate to a URL in the active tab", + inputSchema: { + type: "object", + properties: { + url: { type: "string", description: "URL to navigate to" } + }, + required: ["url"] + } + }, + { + name: "browser_get_tabs", + description: "Get all open browser tabs", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "browser_create_tab", + description: "Create a new browser tab", + inputSchema: { + type: "object", + properties: { + url: { type: "string", description: "URL for new tab" }, + active: { type: "boolean", description: "Make tab active" } + } + } + }, + { + name: "browser_close_tab", + description: "Close a tab by ID", + inputSchema: { + type: "object", + properties: { + tabId: { type: "integer", description: "Tab ID to close" } + }, + required: ["tabId"] + } + }, + { + name: "browser_execute_script", + description: "Execute JavaScript in active tab", + inputSchema: { + type: "object", + properties: { + code: { type: "string", description: "JavaScript code" } + }, + required: ["code"] + } + }, + { + name: "browser_get_page_content", + description: "Get page text content", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector (optional)" } + } + } + }, + { + name: "browser_take_screenshot", + description: "Take screenshot of active tab", + inputSchema: { + type: "object", + properties: { + format: { type: "string", enum: ["png", "jpeg"], description: "Image format" } + } + } + }, + { + name: "browser_get_bookmarks", + description: "Get browser bookmarks", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query" } + } + } + }, + { + name: "browser_add_bookmark", + description: "Add a bookmark", + inputSchema: { + type: "object", + properties: { + title: { type: "string", description: "Bookmark title" }, + url: { type: "string", description: "Bookmark URL" } + }, + required: ["title", "url"] + } + }, + { + name: "browser_get_history", + description: "Search browser history", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search query" }, + maxResults: { type: "integer", description: "Max results" } + } + } + }, + { + name: "browser_get_cookies", + description: "Get cookies for domain", + inputSchema: { + type: "object", + properties: { + domain: { type: "string", description: "Domain name" } + } + } + }, + { + name: "browser_fill_form", + description: "Fill form on current page", + inputSchema: { + type: "object", + properties: { + formData: { type: "object", description: "Form field data" } + }, + required: ["formData"] + } + }, + { + name: "browser_click_element", + description: "Click element on page", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector" } + }, + required: ["selector"] + } + } + ]; +} + +// Call browser tool through Chrome Extension +async function callBrowserTool(toolName, args) { + if ( + !chromeExtensionSocket || + chromeExtensionSocket.readyState !== WebSocket.OPEN + ) { + throw new Error( + "Chrome Extension not connected. Make sure the extension is installed and active." + ); + } + + const callId = Date.now().toString(); + + return new Promise((resolve, reject) => { + pendingCalls.set(callId, { resolve, reject }); + + chromeExtensionSocket.send( + JSON.stringify({ + id: callId, + method: toolName, + params: args, + }) + ); + + // Timeout after 30 seconds + setTimeout(() => { + if (pendingCalls.has(callId)) { + pendingCalls.delete(callId); + reject(new Error("Tool call timeout")); + } + }, 30000); + }); +} + +// Handle tool responses from Chrome Extension +function handleToolResponse(message) { + const pending = pendingCalls.get(message.id); + if (pending) { + pendingCalls.delete(message.id); + if (message.error) { + pending.reject(new Error(message.error.message)); + } else { + pending.resolve(message.result); + } + } +} + +// Handle Chrome Extension connections +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`); + } 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; + clearInterval(pingInterval); + }); + + ws.on("error", (error) => { + console.error("WebSocket error:", error); + }); + + ws.on("pong", () => { + // Extension is alive + }); +}); + +// Read from stdin +let inputBuffer = ""; +process.stdin.on("data", async (chunk) => { + inputBuffer += chunk.toString(); + + // Process complete lines + const lines = inputBuffer.split("\n"); + inputBuffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim()) { + try { + const request = JSON.parse(line); + const response = await handleMCPRequest(request); + + // Only send response if one was generated (not for notifications) + if (response) { + process.stdout.write(JSON.stringify(response) + "\n"); + } + } catch (error) { + console.error("Error processing request:", error); + } + } + } +}); + +// Optional: HTTP endpoint for health checks +const app = express(); +app.get("/health", (req, res) => { + res.json({ + status: "ok", + chromeExtensionConnected: chromeExtensionSocket !== null, + availableTools: availableTools.length, + }); +}); + +app.listen(3001, () => { + console.error( + "Health check endpoint available at http://localhost:3001/health" + ); +}); + +console.error("Browser MCP Server started"); +console.error("Waiting for Chrome Extension connection on ws://localhost:3000");