Turn any website into an MCP event stream.
A Chrome extension that maps DOM events to MCP Event Bus messages. AI agents can subscribe to browser events and publish responses that appear on websites—all running locally on your machine.
┌──────────────────────────────────────────────────────────────────┐
│ MCP MESH │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ EVENT BUS │ │
│ │ │ │
│ │ user.message.received ◄── bridge publishes │ │
│ │ agent.response.* ────────► bridge subscribes │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌─────────────┐ ┌────────┴────────┐ ┌─────────────────┐ │
│ │ Agents │ │ mesh-bridge │ │ Other MCPs │ │
│ │ │◄───│ │───►│ │ │
│ │ Subscribe │ │ DOM ↔ Events │ │ Can also │ │
│ │ Process │ │ │ │ subscribe/ │ │
│ │ Respond │ │ Domains: │ │ publish │ │
│ │ │ │ • WhatsApp ✅ │ │ │ │
│ └─────────────┘ └────────┬────────┘ └─────────────────┘ │
│ │ │
└──────────────────────────────┼────────────────────────────────────┘
│ WebSocket
┌──────────────▼──────────────┐
│ Chrome Extension │
│ │
│ • Observes DOM changes │
│ • Extracts structured data │
│ • Injects AI responses │
│ • Per-site content scripts │
└─────────────────────────────┘
- Extension observes DOM events (new messages, clicks, navigation)
- Bridge translates DOM events into Event Bus messages
- Agents (or any MCP) subscribe to events and process them
- Responses flow back through the bridge into the DOM
The AI never sees the DOM. It sees structured events like:
{ type: "user.message.received", text: "Hello", source: "whatsapp", chatId: "self" }In MCP Mesh, add a new Custom Command connection:
| Field | Value |
|---|---|
| Name | Mesh Bridge |
| Type | Custom Command |
| Command | bun |
| Arguments | run, start |
| Working Directory | /path/to/mesh-bridge |
cd mesh-bridge
bun installThen in Chrome:
- Go to
chrome://extensions - Enable Developer mode
- Click Load unpacked → select
extension/
Navigate to web.whatsapp.com and open your self-chat ("Message Yourself"). Send a message—the agent will respond!
The WhatsApp domain demonstrates the full pattern:
// Content script observes new messages
new MutationObserver(() => {
const lastMessage = getLastMessage();
if (isNewUserMessage(lastMessage)) {
socket.send(JSON.stringify({
type: "message",
domain: "whatsapp",
text: lastMessage,
chatId: getChatName()
}));
}
}).observe(messageContainer, { childList: true, subtree: true });// Server publishes to Event Bus
await callMeshTool(eventBusId, "EVENT_PUBLISH", {
type: "user.message.received",
data: {
text: message.text,
source: "whatsapp",
chatId: message.chatId
}
});// Content script receives response
socket.onmessage = (event) => {
const frame = JSON.parse(event.data);
if (frame.type === "send") {
// Inject into WhatsApp input
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.message.received" {
text: string; // Message content
source: string; // "whatsapp", "linkedin", etc.
chatId?: string; // Conversation ID
sender?: { name?: string };
}"agent.response.whatsapp" {
taskId: string;
chatId?: string;
text: string;
imageUrl?: string;
isFinal: boolean;
}
"agent.task.progress" {
taskId: string;
message: string;
}Any MCP can subscribe to user.message.* or publish agent.response.* events.
Create extension/domains/mysite/content.js:
const DOMAIN = "mysite";
let socket = new WebSocket("ws://localhost:9999");
socket.onopen = () => {
socket.send(JSON.stringify({ type: "connect", domain: DOMAIN, url: location.href }));
};
// Observe DOM → publish events
new MutationObserver(() => {
const data = extractFromDOM();
if (data) {
socket.send(JSON.stringify({ type: "message", domain: DOMAIN, ...data }));
}
}).observe(document.body, { childList: true, subtree: true });
// Subscribe to responses → mutate DOM
socket.onmessage = (e) => {
const frame = JSON.parse(e.data);
if (frame.type === "send") {
injectIntoDom(frame.text);
}
};Create server/domains/mysite/index.ts:
import type { Domain } from "../../core/domain.ts";
export const mysiteDomain: Domain = {
id: "mysite",
name: "My Site",
urlPatterns: [/mysite\.com/],
handleMessage: async (message, ctx) => {
await publishEvent("user.message.received", {
text: message.text,
source: "mysite",
chatId: message.chatId
});
}
};In server/main.ts:
import { mysiteDomain } from "./domains/mysite/index.ts";
registerDomain(mysiteDomain);Add to extension/manifest.json:
{
"content_scripts": [
{
"matches": ["https://mysite.com/*"],
"js": ["domains/mysite/content.js"]
}
]
}# WebSocket port (extension connects here)
WS_PORT=9999
# For standalone mode only
MESH_URL=http://localhost:3000
MESH_API_KEY=your-keymesh-bridge/
├── server/
│ ├── index.ts # Entry point
│ ├── websocket.ts # WebSocket server
│ ├── events.ts # Event types
│ ├── core/
│ │ ├── protocol.ts # Frame types
│ │ ├── mesh-client.ts
│ │ └── domain.ts # Domain interface
│ └── domains/
│ └── whatsapp/ # WhatsApp implementation
├── extension/
│ ├── manifest.json
│ ├── background.js
│ └── domains/
│ └── whatsapp/
│ └── content.js
└── docs/
└── ARCHITECTURE.md
# Run with hot reload
bun run dev
# Run tests
bun test
# Format code
bun run fmt| Approach | Tokens/interaction | Reliability |
|---|---|---|
| Screenshot + Vision | 1000-3000 | Fragile |
| DOM serialization | 2000-10000 | Fragile |
| Event-based | 50-100 | Stable |
Events are:
- Small: Structured data, not HTML noise
- Stable: Event types don't change when UI changes
- Composable: Any MCP can subscribe/publish
- Runs locally on your machine
- Uses your browser session (no credential sharing)
- Only processes self-chat in WhatsApp (never private conversations)
- Open source—audit the code yourself
| Domain | Status | Description |
|---|---|---|
| ✅ Ready | Self-chat AI interaction | |
| 🔜 Planned | Messaging & networking | |
| X/Twitter | 🔜 Planned | Compose, DMs |
| Gmail | 🔜 Planned | Compose, inbox |
| Custom | 📖 Guide | Add any site |
MIT