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
Chrome's Manifest V3 suspends service workers after ~30 seconds of inactivity. Previously, this would break the extension when returning to a tab after being away.
Now: The content script monitors for context invalidation and automatically reloads the page when detected. No more "Extension context invalidated" errors requiring manual refresh.
The bridge now works seamlessly with Pilot's thread management. Conversations within 5 minutes are treated as the same thread, enabling natural follow-ups like:
- "draft this" → continues from previous research
- "yes" / "continue" → proceeds to next workflow step
- "new thread" → starts fresh
- 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