Skill: ryno2390/agent-mcp-bridge (version 1.0.0)

--- name: agent-mcp-bridge description: Set up and use an MCP message broker for direct inter-agent communication between OpenClaw and other AI agents (e.g. hermes-agent, Claude Code, any MCP-capable agent). Use when you need two AI agents on the same machine to exchange messages without human relay — replacing Discord copy-paste or txt file workarounds. Triggers on phrases like "set up agent communication", "MCP broker", "inter-agent messaging", "connect two agents", "agent bridge", "Isaac Hermes communication", or any request to enable direct agent-to-agent messaging. --- # Agent MCP Bridge A lightweight FastMCP + SQLite message broker that lets two AI agents communicate directly via MCP tools. Both agents connect to the same server as MCP clients — symmetric, no platform-specific adapters. ## Architecture ``` Agent A (OpenClaw) MCP Broker Agent B (hermes-agent / any) send_message() ──────► FastAPI+SQLite ◄────────── poll_messages() poll_messages() ◄────── localhost:8765 ──────────► send_message() ``` ## Quick Setup ### 1. Install and start the broker ```bash cp -r <skill-dir>/scripts/server/ ~/.openclaw/agent-bridge-mcp/ cd ~/.openclaw/agent-bridge-mcp ./start.sh # creates venv, installs deps, starts on port 8765 ``` ### 2. Register as a launchd service (macOS auto-start) ```bash cp <skill-dir>/references/launchd-plist.md ~/Library/LaunchAgents/ai.openclaw.agent-bridge.plist # Edit the plist to set correct paths, then: launchctl load ~/Library/LaunchAgents/ai.openclaw.agent-bridge.plist ``` ### 3. Add to OpenClaw config Add to `~/.openclaw/openclaw.json`: ```json { "mcp": { "servers": { "agent-bridge": { "url": "http://127.0.0.1:8765/mcp", "transport": "streamable-http" } } } } ``` ### 4. Connect the other agent Give the other agent the MCP URL: `http://127.0.0.1:8765/mcp` (streamable-http transport). They connect with their native MCP client support. ## MCP Tools | Tool | Parameters | Returns | Use when | |---|---|---|---| | `send_message` | `from_agent`, `to`, `subject`, `body`, `reply_to?` | `{message_id, timestamp}` | Sending a task or reply | | `poll_messages` | `agent_id`, `limit?` | list of message dicts | Checking your inbox | | `mark_read` | `message_id` | `{status}` | After processing a message | | `list_agents` | — | list of agent ids | Discovering who's active | ## Message format ```json { "id": "83223c09", "from_agent": "hermes", "to_agent": "isaac", "subject": "Research request", "body": "Please analyze...", "timestamp": "2026-03-31T16:58:31Z", "thread_id": "83223c09", "reply_to": null, "status": "pending" } ``` ## Fallback: Filesystem bridge If the MCP server is unavailable, use the filesystem bridge (zero infrastructure): - See `references/filesystem-bridge.md` for setup - Inbox/outbox dirs: `~/.openclaw/shared/{agent}-inbox/` ## Heartbeat integration (OpenClaw) Add to `HEARTBEAT.md` to auto-process incoming messages: ``` Check ~/.openclaw/shared/isaac-inbox/ for new .json files. If any exist: read, process, reply via hermes-inbox/, move to processed/. ``` ## Verification Test the full loop: ```bash # From Python from agent_bridge import AgentBridge bridge = AgentBridge("isaac") bridge.send("hermes", "Handshake test", "Can you receive this?") # Other agent polls and replies msgs = bridge.receive() ```

path: scripts/agent_bridge.py
content: """
agent_bridge.py — Isaac/Hermes shared filesystem message bus.

Usage (Isaac side):
    from agent_bridge import AgentBridge
    bridge = AgentBridge(agent_id="isaac")
    
    # Send a message to Hermes
    bridge.send("hermes", subject="Research request", body="Please analyze X...")
    
    # Check for new messages from Hermes
    messages = bridge.receive()
    for msg in messages:
        print(msg["from"], msg["subject"], msg["body"])
        bridge.mark_processed(msg)
"""
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Dict, Any, Optional


SHARED_DIR = Path(__file__).parent


class AgentBridge:
    def __init__(self, agent_id: str, shared_dir: Path = None):
        self.agent_id = agent_id
        self.shared_dir = shared_dir or SHARED_DIR
        self.inbox = self.shared_dir / f"{agent_id}-inbox"
        self.processed = self.shared_dir / "processed"
        self.inbox.mkdir(parents=True, exist_ok=True)
        self.processed.mkdir(parents=True, exist_ok=True)

    def send(
        self,
        to: str,
        subject: str,
        body: str,
        thread_id: str = None,
        reply_to: str = None,
    ) -> str:
        """Write a message to the recipient's inbox. Returns message_id."""
        message_id = str(uuid.uuid4())[:8]
        ts = datetime.now(timezone.utc).isoformat()
        msg = {
            "message_id": message_id,
            "from": self.agent_id,
            "to": to,
            "subject": subject,
            "body": body,
            "timestamp": ts,
            "thread_id": thread_id or message_id,
            "reply_to": reply_to,
            "status": "pending",
        }
        outbox = self.shared_dir / f"{to}-inbox"
        outbox.mkdir(parents=True, exist_ok=True)
        filename = f"{ts.replace(':', '-')}_{message_id}.json"
        (outbox / filename).write_text(json.dumps(msg, indent=2))
        return message_id

    def receive(self) -> List[Dict[str, Any]]:
        """Read all pending messages from inbox. Returns list of message dicts."""
        messages = []
        for f in sorted(self.inbox.glob("*.json")):
            try:
                msg = json.loads(f.read_text())
                msg["_file"] = str(f)
                messages.append(msg)
            except Exception:
                pass
        return messages

    def mark_processed(self, msg: Dict[str, Any]) -> None:
        """Move a message file to processed/ after handling."""
        f = Path(msg.get("_file", ""))
        if f.exists():
            dest = self.processed / f.name
            f.rename(dest)

    def reply(self, original: Dict[str, Any], body: str) -> str:
        """Reply to a message. Returns new message_id."""
        return self.send(
            to=original["from"],
            subject=f"Re: {original['subject']}",
            body=body,
            thread_id=original.get("thread_id"),
            reply_to=original["message_id"],
        )

    def pending_count(self) -> int:
        return len(list(self.inbox.glob("*.json")))


path: scripts/server/server.py
content: #!/usr/bin/env python3
"""
Agent MCP Bridge — Inter-agent message broker via MCP protocol.

Exposes 4 MCP tools:
  - send_message(to, subject, body, reply_to?) → message_id
  - poll_messages(agent_id, limit?) → list of pending messages
  - mark_read(message_id) → bool
  - list_agents() → list of known agent ids

Uses SQLite for persistence. Zero external dependencies beyond fastmcp.

Run: python server.py
Or:  uvicorn server:app --port 8765
"""
import asyncio
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import aiosqlite
from fastmcp import FastMCP

DB_PATH = Path(__file__).parent / "messages.db"
mcp = FastMCP("agent-bridge", instructions="Inter-agent message broker for Isaac and Hermes")

async def get_db(db_path: Optional[Path] = None):
    """Get database connection. Optionally use a custom path for testing."""
    path = db_path or DB_PATH
    db = await aiosqlite.connect(path)
    db.row_factory = aiosqlite.Row
    await db.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id TEXT PRIMARY KEY,
            from_agent TEXT NOT NULL,
            to_agent TEXT NOT NULL,
            subject TEXT DEFAULT '',
            body TEXT NOT NULL,
            timestamp TEXT NOT NULL,
            thread_id TEXT,
            reply_to TEXT,
            status TEXT DEFAULT 'pending'
        )
    """)
    await db.commit()
    return db

@mcp.tool()
async def send_message(
    from_agent: str,
    to: str,
    subject: str,
    body: str,
    reply_to: Optional[str] = None,
) -> dict:
    """
    Send a message from one agent to another.
    Returns the message_id of the sent message.
    """
    message_id = str(uuid.uuid4())[:8]
    ts = datetime.now(timezone.utc).isoformat()
    thread_id = reply_to or message_id
    
    db = await get_db()
    try:
        await db.execute(
            "INSERT INTO messages VALUES (?,?,?,?,?,?,?,?,?)",
            (message_id, from_agent, to, subject, body, ts, thread_id, reply_to, "pending")
        )
        await db.commit()
    finally:
        await db.close()
    
    return {"message_id": message_id, "timestamp": ts, "status": "sent"}

@mcp.tool()
async def poll_messages(agent_id: str, limit: int = 10) -> list:
    """
    Poll for pending messages addressed to agent_id.
    Returns list of message dicts (oldest first).
    """
    db = await get_db()
    try:
        cursor = await db.execute(
            "SELECT * FROM messages WHERE to_agent=? AND status='pending' ORDER BY timestamp ASC LIMIT ?",
            (agent_id, limit)
        )
        rows = await cursor.fetchall()
        return [dict(row) for row in rows]
    finally:
        await db.close()

@mcp.tool()
async def mark_read(message_id: str) -> dict:
    """
    Mark a message as read/processed.
    Returns success status.
    """
    db = await get_db()
    try:
        await db.execute(
            "UPDATE messages SET status='read' WHERE id=?",
            (message_id,)
        )
        await db.commit()
        return {"message_id": message_id, "status": "read"}
    finally:
        await db.close()

@mcp.tool()
async def list_agents() -> list:
    """
    List all agent ids that have sent or received messages.
    """
    db = await get_db()
    try:
        cursor = await db.execute(
            "SELECT DISTINCT from_agent as agent FROM messages "
            "UNION SELECT DISTINCT to_agent FROM messages ORDER BY agent"
        )
        rows = await cursor.fetchall()
        return [row[0] for row in rows]
    finally:
        await db.close()

# Create ASGI app for uvicorn
app = mcp.http_app(transport="streamable-http")

if __name__ == "__main__":
    # Run with streamable-http transport
    mcp.run(transport="streamable-http", host="127.0.0.1", port=8765)


path: scripts/server/requirements.txt
content: fastmcp>=2.0.0
uvicorn>=0.30.0
aiosqlite>=0.20.0


path: scripts/server/start.sh
content: #!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"

# Install deps if needed
if [ ! -d ".venv" ]; then
    python3 -m venv .venv
    .venv/bin/pip install -q -r requirements.txt
fi

echo "Starting Agent MCP Bridge on http://127.0.0.1:8765"
.venv/bin/python server.py


path: references/filesystem-bridge.md
content: # Filesystem Bridge (Fallback)

Zero-infrastructure fallback when the MCP server is unavailable. Both agents read/write JSON files to a shared directory.

## Setup

```bash
mkdir -p ~/.openclaw/shared/isaac-inbox \
         ~/.openclaw/shared/hermes-inbox \
         ~/.openclaw/shared/processed
```

## Usage (Python)

```python
from agent_bridge import AgentBridge

bridge = AgentBridge("isaac", shared_dir=Path("~/.openclaw/shared").expanduser())

# Send
bridge.send("hermes", "Subject", "Body text")

# Receive
for msg in bridge.receive():
    print(msg["from"], msg["subject"], msg["body"])
    bridge.mark_processed(msg)
```

## File format

Files are named `{ISO8601-timestamp}_{message_id}.json` and placed in `{recipient}-inbox/`.

## Limitations vs MCP broker

- Requires polling (no push)
- No persistence across machines
- Manual cleanup needed for processed/ dir
- No threading/reply-chain tracking beyond thread_id field


path: references/launchd-plist.md
content: <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>ai.openclaw.agent-bridge</string>

    <key>ProgramArguments</key>
    <array>
        <!-- Update this path to match your actual venv location -->
        <string>/Users/YOUR_USERNAME/.openclaw/agent-bridge-mcp/.venv/bin/python</string>
        <string>/Users/YOUR_USERNAME/.openclaw/agent-bridge-mcp/server.py</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/Users/YOUR_USERNAME/.openclaw/agent-bridge-mcp</string>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/Users/YOUR_USERNAME/.openclaw/agent-bridge-mcp/bridge.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/YOUR_USERNAME/.openclaw/agent-bridge-mcp/bridge.log</string>
</dict>
</plist>


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)