mirror of
https://github.com/aaronjmars/opendia.git
synced 2025-12-29 16:16:00 +00:00
opendia init
This commit is contained in:
877
README.md
877
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": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "MCP Browser Bridge"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"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
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MCP Browser Bridge</title>
|
||||
<style>
|
||||
body {
|
||||
width: 350px;
|
||||
padding: 16px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.connected { background-color: #4CAF50; }
|
||||
.disconnected { background-color: #f44336; }
|
||||
.info {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
button {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
.log {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
margin-top: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>MCP Browser Bridge</h2>
|
||||
|
||||
<div class="status">
|
||||
<div class="status-indicator" id="statusIndicator"></div>
|
||||
<span id="statusText">Checking connection...</span>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>Server:</strong> <span id="serverUrl">ws://localhost:3000</span><br>
|
||||
<strong>Available Tools:</strong> <span id="toolCount">0</span>
|
||||
</div>
|
||||
|
||||
<button id="reconnectBtn">Reconnect</button>
|
||||
<button id="testBtn">Test Connection</button>
|
||||
|
||||
<div class="log" id="log">
|
||||
<div>Waiting for activity...</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user