Connect any website to MCP Mesh via the Event Bus.
A Chrome extension that maps DOM events to MCP Event Bus pub/sub—enabling AI agents to interact with any website. Think RPA, but powered by events and AI.
┌─────────────────────────────────────────────────────────────────┐
│ MCP MESH │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ EVENT BUS │ │
│ │ │ │
│ │ user.message.received ───────► agent.response.whatsapp │ │
│ │ (Bridge publishes) (Agent publishes) │ │
│ │ │ │
│ └─────────────▲─────────────────────────────┬───────────────┘ │
│ │ │ │
│ │ SUBSCRIBE │ PUBLISH │
│ │ │ │
│ ┌─────────────┴───────┐ ┌────────▼────────────┐ │
│ │ Pilot │ │ mesh-bridge │ │
│ │ (AI Agent) │ │ (DOM ↔ Events) │ │
│ │ │ │ │ │
│ │ Subscribes to: │ │ Subscribes to: │ │
│ │ user.message.* │ │ agent.response.* │ │
│ │ │ │ agent.task.* │ │
│ │ Publishes: │ │ │ │
│ │ agent.response.* │ │ Publishes: │ │
│ │ agent.task.* │ │ user.message.* │ │
│ └─────────────────────┘ └──────────┬──────────┘ │
│ │ │
└───────────────────────────────────────────────┼──────────────────┘
│ WebSocket
┌─────────────────▼─────────────────┐
│ Chrome Extension │
│ │
│ DOM Observation ──► Event Publish│
│ Event Subscribe ──► DOM Mutation │
│ │
│ Example: WhatsApp Web │
│ • New message → publish event │
│ • Response event → inject reply │
└───────────────────────────────────┘
The bridge is a thin layer that translates between:
- DOM events (user types, clicks, new elements appear)
- Event Bus messages (CloudEvents pub/sub)
It has no AI logic—agents subscribe to events and respond via events.
// 1. OBSERVE DOM → PUBLISH EVENT
// When user sends a message in WhatsApp...
const observer = new MutationObserver(() => {
const lastMessage = getLastMessage(); // Extract from DOM
if (isNewUserMessage(lastMessage)) {
// Publish to Event Bus via WebSocket → Bridge → Mesh
socket.send(JSON.stringify({
type: "message",
domain: "whatsapp",
text: lastMessage.text,
chatId: getChatName(),
}));
}
});
observer.observe(messageContainer, { childList: true, subtree: true });// 2. SUBSCRIBE TO EVENTS → MUTATE DOM
// When agent responds...
socket.onmessage = (event) => {
const frame = JSON.parse(event.data);
if (frame.type === "send") {
// Inject response into WhatsApp's input and send
const input = document.querySelector('[data-testid="conversation-compose-box-input"]');
input.focus();
document.execCommand("insertText", false, frame.text);
document.querySelector('[data-testid="send"]').click();
}
};// User sent a message via any interface
"user.message.received" {
text: "What's the weather like?",
source: "whatsapp", // or "linkedin", "x", "slack"...
chatId: "John Doe",
sender: { name: "John" },
metadata: { /* interface-specific data */ }
}// Task progress updates
"agent.task.progress" {
taskId: "abc123",
source: "whatsapp",
message: "Checking weather API...",
percent: 50
}
// Final response
"agent.response.whatsapp" {
taskId: "abc123",
chatId: "John Doe",
text: "It's 72°F and sunny ☀️",
isFinal: true
}
// Task completed
"agent.task.completed" {
taskId: "abc123",
response: "It's 72°F and sunny",
duration: 2340,
toolsUsed: ["WEATHER_API"]
}Add mesh-bridge as a Custom Command connection:
| Field | Value |
|---|---|
| Name | Mesh Bridge |
| Type | Custom Command |
| Command | bun |
| Arguments | run server |
| Working Directory | /path/to/mesh-bridge |
The mesh will start the bridge and pass authentication context.
- Open
chrome://extensions - Enable "Developer mode"
- Click "Load unpacked" → select
extension/ - Navigate to WhatsApp Web
Send yourself a message in WhatsApp. The bridge will:
- Detect the new message (DOM observation)
- Publish
user.message.receivedevent - Pilot agent receives and processes it
- Agent publishes
agent.response.whatsapp - Bridge receives and injects response into chat
A domain defines how to map a specific website's DOM to events.
Create extension/domains/linkedin/content.js:
const DOMAIN_ID = "linkedin";
const BRIDGE_URL = "ws://localhost:9999";
let socket = null;
// ============================================================================
// CONNECTION
// ============================================================================
function connect() {
socket = new WebSocket(BRIDGE_URL);
socket.onopen = () => {
// Announce our domain to the bridge
socket.send(JSON.stringify({
type: "connect",
domain: DOMAIN_ID,
url: window.location.href,
capabilities: ["messages", "notifications"],
}));
};
socket.onmessage = handleServerMessage;
socket.onclose = () => setTimeout(connect, 5000);
}
// ============================================================================
// DOM → EVENTS (Publishing)
// ============================================================================
function observeMessages() {
// Find LinkedIn's message container
const container = document.querySelector('.msg-overlay-list-bubble');
if (!container) {
setTimeout(observeMessages, 1000);
return;
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.classList?.contains('msg-s-message-list__event')) {
const text = node.querySelector('.msg-s-event-listitem__body')?.innerText;
const sender = node.querySelector('.msg-s-message-group__name')?.innerText;
if (text && isFromOther(node)) {
// Publish user message event
socket.send(JSON.stringify({
type: "message",
domain: DOMAIN_ID,
text,
chatId: getCurrentChatId(),
sender: { name: sender },
}));
}
}
}
}
});
observer.observe(container, { childList: true, subtree: true });
}
// ============================================================================
// EVENTS → DOM (Subscribing)
// ============================================================================
function handleServerMessage(event) {
const frame = JSON.parse(event.data);
switch (frame.type) {
case "connected":
console.log(`[${DOMAIN_ID}] Connected to bridge`);
observeMessages();
break;
case "send":
// Agent wants to send a response
injectMessage(frame.chatId, frame.text);
break;
case "navigate":
// Agent wants to navigate to a profile/page
window.location.href = frame.url;
break;
case "click":
// Agent wants to click something
document.querySelector(frame.selector)?.click();
break;
}
}
function injectMessage(chatId, text) {
const input = document.querySelector('.msg-form__contenteditable');
if (!input) return;
input.focus();
document.execCommand("insertText", false, text);
input.dispatchEvent(new Event("input", { bubbles: true }));
// Click send
setTimeout(() => {
document.querySelector('.msg-form__send-button')?.click();
}, 100);
}
// ============================================================================
// HELPERS
// ============================================================================
function getCurrentChatId() {
return document.querySelector('.msg-overlay-bubble-header__title')?.innerText || 'unknown';
}
function isFromOther(node) {
return !node.classList.contains('msg-s-message-list__event--sent');
}
// Start
connect();Create server/domains/linkedin/index.ts:
import type { Domain, DomainMessage, DomainContext } from "../../core/domain.ts";
import { EVENT_TYPES } from "../../events.ts";
export const linkedinDomain: Domain = {
id: "linkedin",
name: "LinkedIn",
urlPatterns: [/^https?:\/\/(www\.)?linkedin\.com/],
// Transform incoming WebSocket message to Event Bus event
async handleMessage(message: DomainMessage, ctx: DomainContext) {
// Publish to event bus - Pilot will pick it up
await ctx.meshClient.callTool("EVENT_PUBLISH", {
type: EVENT_TYPES.USER_MESSAGE,
data: {
text: message.text,
source: "linkedin",
chatId: message.chatId,
sender: message.sender,
},
});
// Progress and responses come via event subscriptions
// The bridge auto-routes them back to this domain
},
// Domain-specific tools (optional)
tools: [
{
name: "LINKEDIN_PROFILE",
description: "Get current LinkedIn profile info",
execute: async (input, ctx) => {
// Request profile data from content script
ctx.send({ type: "request_profile" });
// Response comes via event
return { success: true, message: "Profile requested" };
},
},
],
};In server/main.ts:
import { linkedinDomain } from "./domains/linkedin/index.ts";
registerDomain(linkedinDomain);In extension/manifest.json:
{
"content_scripts": [
{
"matches": ["https://www.linkedin.com/*"],
"js": ["domains/linkedin/content.js"]
}
]
}// Observe new messages and publish events
function observeMessages() {
const container = document.querySelector(MESSAGES_SELECTOR);
new MutationObserver((mutations) => {
const newMessages = extractNewMessages(mutations);
for (const msg of newMessages) {
if (shouldProcess(msg)) {
publishEvent("user.message.received", {
text: msg.text,
source: DOMAIN_ID,
chatId: msg.chatId,
});
}
}
}).observe(container, { childList: true, subtree: true });
}// Track button clicks and publish events
document.addEventListener("click", (e) => {
const button = e.target.closest("[data-action]");
if (button) {
publishEvent("user.action.click", {
action: button.dataset.action,
source: DOMAIN_ID,
context: extractContext(button),
});
}
});// Intercept form submissions
document.addEventListener("submit", (e) => {
const form = e.target;
const formData = new FormData(form);
publishEvent("user.action.submit", {
formId: form.id,
data: Object.fromEntries(formData),
source: DOMAIN_ID,
});
});// Observe URL changes (for SPAs)
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
publishEvent("user.navigation", {
url: location.href,
source: DOMAIN_ID,
});
}
}).observe(document, { subtree: true, childList: true });// Insert AI response into an input
function handleSendResponse(frame) {
const input = document.querySelector(INPUT_SELECTOR);
input.focus();
document.execCommand("insertText", false, frame.text);
input.dispatchEvent(new Event("input", { bubbles: true }));
}// Click a button on behalf of agent
function handleClick(frame) {
const element = document.querySelector(frame.selector);
if (element) {
element.click();
}
}// Display agent feedback in the UI
function handleNotification(frame) {
const toast = document.createElement("div");
toast.className = "mesh-bridge-toast";
toast.textContent = frame.message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}// Navigate to a different page
function handleNavigate(frame) {
window.location.href = frame.url;
}| Domain | Status | Description |
|---|---|---|
| ✅ Ready | Chat with AI via self-messages | |
| 🔜 Planned | AI-powered messaging | |
| X (Twitter) | 🔜 Planned | Tweet composition, DMs |
| Slack | 🔜 Planned | Workspace integration |
| Any Site | 🛠️ RPA | Add your own domain! |
# WebSocket port for extension connection
WS_PORT=9999
# Default AI model (used by agents)
DEFAULT_MODEL=anthropic/claude-sonnet-4
# Mesh connection (automatic when run via Mesh)
MESH_URL=http://localhost:3000
MESH_API_KEY=... # Optional if running inside mesh# Install dependencies
bun install
# Run the bridge server
bun run server
# Run tests
bun test- Architecture - Detailed architecture overview
- MCP Mesh Event Bus - Event bus documentation
- Pilot Agent - AI agent that processes events
MIT