Deploy Echo Universal Host
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- README.md +62 -7
- app.py +17 -0
- mcp.json +25 -0
- requirements.txt +5 -0
- services/mcp_server/.DS_Store +0 -0
- services/mcp_server/__init__.py +2 -0
- services/mcp_server/__pycache__/__init__.cpython-310.pyc +0 -0
- services/mcp_server/__pycache__/__init__.cpython-311.pyc +0 -0
- services/mcp_server/__pycache__/main.cpython-310.pyc +0 -0
- services/mcp_server/__pycache__/main.cpython-311.pyc +0 -0
- services/mcp_server/audio_store/__init__.py +3 -0
- services/mcp_server/audio_store/__pycache__/__init__.cpython-310.pyc +0 -0
- services/mcp_server/audio_store/__pycache__/__init__.cpython-311.pyc +0 -0
- services/mcp_server/audio_store/__pycache__/storage.cpython-310.pyc +0 -0
- services/mcp_server/audio_store/__pycache__/storage.cpython-311.pyc +0 -0
- services/mcp_server/audio_store/storage.py +20 -0
- services/mcp_server/core/__init__.py +2 -0
- services/mcp_server/core/__pycache__/__init__.cpython-310.pyc +0 -0
- services/mcp_server/core/__pycache__/__init__.cpython-311.pyc +0 -0
- services/mcp_server/core/__pycache__/actions.cpython-311.pyc +0 -0
- services/mcp_server/core/__pycache__/config.cpython-310.pyc +0 -0
- services/mcp_server/core/__pycache__/config.cpython-311.pyc +0 -0
- services/mcp_server/core/__pycache__/player.cpython-311.pyc +0 -0
- services/mcp_server/core/__pycache__/tts.cpython-310.pyc +0 -0
- services/mcp_server/core/__pycache__/tts.cpython-311.pyc +0 -0
- services/mcp_server/core/actions.py +30 -0
- services/mcp_server/core/config.py +27 -0
- services/mcp_server/core/player.py +61 -0
- services/mcp_server/core/tts.py +46 -0
- services/mcp_server/main.py +47 -0
- services/mcp_server/requirements.txt +5 -0
- services/mcp_server/routes/__init__.py +2 -0
- services/mcp_server/routes/__pycache__/__init__.cpython-310.pyc +0 -0
- services/mcp_server/routes/__pycache__/__init__.cpython-311.pyc +0 -0
- services/mcp_server/routes/__pycache__/mcp.cpython-310.pyc +0 -0
- services/mcp_server/routes/__pycache__/mcp.cpython-311.pyc +0 -0
- services/mcp_server/routes/mcp.py +79 -0
- services/mcp_server/server.py +50 -0
- services/mcp_server/static/.DS_Store +0 -0
- services/mcp_server/static/audio/.DS_Store +0 -0
- services/mcp_server/websocket/__init__.py +3 -0
- services/mcp_server/websocket/__pycache__/__init__.cpython-310.pyc +0 -0
- services/mcp_server/websocket/__pycache__/__init__.cpython-311.pyc +0 -0
- services/mcp_server/websocket/__pycache__/manager.cpython-310.pyc +0 -0
- services/mcp_server/websocket/__pycache__/manager.cpython-311.pyc +0 -0
- services/mcp_server/websocket/manager.py +49 -0
- src/__init__.py +2 -0
- src/__pycache__/__init__.cpython-310.pyc +0 -0
- src/core/__init__.py +20 -0
- src/core/__pycache__/__init__.cpython-310.pyc +0 -0
README.md
CHANGED
|
@@ -1,13 +1,68 @@
|
|
| 1 |
---
|
| 2 |
-
title: Echo
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Echo - Universal MCP Host
|
| 3 |
+
emoji: 🔊
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.0.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
tags:
|
| 11 |
+
- mcp-in-action-track-consumer
|
| 12 |
+
- agents
|
| 13 |
+
- mcp
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# Echo: The Universal MCP Host
|
| 17 |
+
|
| 18 |
+
**"Visualize the Invisible."**
|
| 19 |
+
|
| 20 |
+
Echo is a Neo-Brutalist, bidirectional Web Client for the Model Context Protocol (MCP). It acts as a "Universal Host" that can load any MCP Server and give it a UI.
|
| 21 |
+
|
| 22 |
+
## 🏆 Hackathon Submission (Track 2)
|
| 23 |
+
* **Track**: MCP in Action (Consumer)
|
| 24 |
+
* **Tag**: `mcp-in-action-track-consumer`
|
| 25 |
+
|
| 26 |
+
## 🚀 Key Features
|
| 27 |
+
|
| 28 |
+
### 1. Visual MCP Manager
|
| 29 |
+
Replacing `mcp.json` editing with a GUI.
|
| 30 |
+
* **App Store Style**: Visualize your tools as cards.
|
| 31 |
+
* **One-Click Connect**: Instant connection to MCP servers.
|
| 32 |
+
* **Status Monitoring**: Real-time connection health.
|
| 33 |
+
|
| 34 |
+
### 2. Bidirectional State Protocol (Innovation)
|
| 35 |
+
Standard MCP is request/response. Echo introduces **Client State Broadcasting**.
|
| 36 |
+
* **Protocol Inspector**: Visualize JSON-RPC traffic in real-time.
|
| 37 |
+
* **Blocking State**: Echo broadcasts when it is blocked (e.g., waiting for user confirmation).
|
| 38 |
+
* **Proactive Voice**: Connected AgentBell servers can listen for this state and speak *before* acting.
|
| 39 |
+
|
| 40 |
+
### 3. Universal Host
|
| 41 |
+
Echo is server-agnostic.
|
| 42 |
+
* It connects to `stdio` servers (like `server-memory`).
|
| 43 |
+
* It connects to python servers (like `agentbell`).
|
| 44 |
+
* It provides a standard LLM interface (OpenAI) to interact with tools.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 🛠️ How to Use
|
| 49 |
+
|
| 50 |
+
1. **Enter your OpenAI API Key** in the settings (or via Environment Variable).
|
| 51 |
+
2. **Go to TOOLS**: See available servers.
|
| 52 |
+
3. **Click Connect** on "agentbell-voice" or "memory".
|
| 53 |
+
4. **Go to SESSION**: Chat with the agent.
|
| 54 |
+
* Try: "Create a memory entity called Test" (Tool Call)
|
| 55 |
+
* Try: "Delete all entities" (Triggers Blocking State -> Inspector Highlight)
|
| 56 |
+
5. **Open INSPECTOR**: Watch the JSON-RPC traffic live.
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 📦 Setup (Local)
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
git clone https://huggingface.co/spaces/MCP-1st-Birthday/echo-agent
|
| 64 |
+
cd echo-agent
|
| 65 |
+
pip install -r requirements.txt
|
| 66 |
+
python app.py
|
| 67 |
+
```
|
| 68 |
+
|
app.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# Load environment variables from .env file if present
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
# Ensure 'src' is in the Python path so we can import our modules
|
| 9 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), "src"))
|
| 10 |
+
|
| 11 |
+
from ui.layout import create_ui
|
| 12 |
+
|
| 13 |
+
if __name__ == "__main__":
|
| 14 |
+
# Launch the application
|
| 15 |
+
# server_name="0.0.0.0" allows external access if needed
|
| 16 |
+
ui = create_ui()
|
| 17 |
+
ui.launch(server_port=7860)
|
mcp.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/mcp.json",
|
| 3 |
+
"$comment": "Echo Universal Host - MCP Server Configuration",
|
| 4 |
+
"mcpServers": {
|
| 5 |
+
"agentbell-voice": {
|
| 6 |
+
"command": "python",
|
| 7 |
+
"args": [
|
| 8 |
+
"services/mcp_server/server.py"
|
| 9 |
+
],
|
| 10 |
+
"env": {
|
| 11 |
+
"PYTHONUNBUFFERED": "1",
|
| 12 |
+
"PYTHONPATH": "."
|
| 13 |
+
},
|
| 14 |
+
"_description": "AgentBell Voice MCP Server - Provides voice notification tools"
|
| 15 |
+
},
|
| 16 |
+
"memory": {
|
| 17 |
+
"command": "npx",
|
| 18 |
+
"args": [
|
| 19 |
+
"-y",
|
| 20 |
+
"@modelcontextprotocol/server-memory"
|
| 21 |
+
],
|
| 22 |
+
"_description": "Simple in-memory key-value store for testing MCP connections"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5.0.0
|
| 2 |
+
mcp[cli]
|
| 3 |
+
openai
|
| 4 |
+
python-dotenv
|
| 5 |
+
requests
|
services/mcp_server/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
services/mcp_server/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentBell MCP server package."""
|
| 2 |
+
|
services/mcp_server/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (188 Bytes). View file
|
|
|
services/mcp_server/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (206 Bytes). View file
|
|
|
services/mcp_server/__pycache__/main.cpython-310.pyc
ADDED
|
Binary file (1.61 kB). View file
|
|
|
services/mcp_server/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (2.68 kB). View file
|
|
|
services/mcp_server/audio_store/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio storage helpers."""
|
| 2 |
+
"""Audio storage helpers."""
|
| 3 |
+
|
services/mcp_server/audio_store/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (195 Bytes). View file
|
|
|
services/mcp_server/audio_store/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (211 Bytes). View file
|
|
|
services/mcp_server/audio_store/__pycache__/storage.cpython-310.pyc
ADDED
|
Binary file (816 Bytes). View file
|
|
|
services/mcp_server/audio_store/__pycache__/storage.cpython-311.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
services/mcp_server/audio_store/storage.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from uuid import uuid4
|
| 3 |
+
|
| 4 |
+
AUDIO_DIR = Path(__file__).resolve().parents[1] / "static" / "audio"
|
| 5 |
+
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def save_audio_bytes(audio_bytes: bytes, prefix: str = "notification") -> str:
|
| 9 |
+
"""
|
| 10 |
+
Persist audio bytes to disk and return the accessible URL path.
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
str: Relative URL (e.g., /static/audio/xxx.mp3)
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
filename = f"{prefix}-{uuid4().hex}.mp3"
|
| 17 |
+
filepath = AUDIO_DIR / filename
|
| 18 |
+
filepath.write_bytes(audio_bytes)
|
| 19 |
+
return f"/static/audio/{filename}"
|
| 20 |
+
|
services/mcp_server/core/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core utilities for the MCP server."""
|
| 2 |
+
|
services/mcp_server/core/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (198 Bytes). View file
|
|
|
services/mcp_server/core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (216 Bytes). View file
|
|
|
services/mcp_server/core/__pycache__/actions.cpython-311.pyc
ADDED
|
Binary file (1.43 kB). View file
|
|
|
services/mcp_server/core/__pycache__/config.cpython-310.pyc
ADDED
|
Binary file (1.01 kB). View file
|
|
|
services/mcp_server/core/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.48 kB). View file
|
|
|
services/mcp_server/core/__pycache__/player.cpython-311.pyc
ADDED
|
Binary file (3.67 kB). View file
|
|
|
services/mcp_server/core/__pycache__/tts.cpython-310.pyc
ADDED
|
Binary file (1.49 kB). View file
|
|
|
services/mcp_server/core/__pycache__/tts.cpython-311.pyc
ADDED
|
Binary file (2.03 kB). View file
|
|
|
services/mcp_server/core/actions.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from .config import Config
|
| 3 |
+
from .tts import generate_speech
|
| 4 |
+
from .player import play_audio_bytes
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger("agentbell.core")
|
| 7 |
+
|
| 8 |
+
def perform_voice_notify(text: str, config: Config) -> str:
|
| 9 |
+
"""
|
| 10 |
+
Core logic for voice notification:
|
| 11 |
+
1. Validates config.
|
| 12 |
+
2. Generates speech (TTS).
|
| 13 |
+
3. Plays audio locally.
|
| 14 |
+
"""
|
| 15 |
+
if not config.tts_configured:
|
| 16 |
+
logger.error("TTS not configured")
|
| 17 |
+
raise RuntimeError("ELEVENLABS_API_KEY not configured.")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
# 1. Generate Audio
|
| 21 |
+
audio_bytes = generate_speech(text, config)
|
| 22 |
+
|
| 23 |
+
# 2. Play Locally
|
| 24 |
+
play_audio_bytes(audio_bytes)
|
| 25 |
+
|
| 26 |
+
return "Voice notification played successfully."
|
| 27 |
+
except Exception as e:
|
| 28 |
+
logger.exception("Error in voice_notify core logic")
|
| 29 |
+
raise e
|
| 30 |
+
|
services/mcp_server/core/config.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class Config:
|
| 11 |
+
"""Holds runtime configuration for the MCP server."""
|
| 12 |
+
|
| 13 |
+
elevenlabs_api_key: str | None
|
| 14 |
+
elevenlabs_voice_id: str
|
| 15 |
+
|
| 16 |
+
@property
|
| 17 |
+
def tts_configured(self) -> bool:
|
| 18 |
+
return bool(self.elevenlabs_api_key)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load_config() -> Config:
|
| 22 |
+
"""Load configuration from environment variables."""
|
| 23 |
+
return Config(
|
| 24 |
+
elevenlabs_api_key=os.getenv("ELEVENLABS_API_KEY"),
|
| 25 |
+
elevenlabs_voice_id=os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM"),
|
| 26 |
+
)
|
| 27 |
+
|
services/mcp_server/core/player.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import platform
|
| 4 |
+
import subprocess
|
| 5 |
+
import tempfile
|
| 6 |
+
import threading
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _play_and_cleanup(cmd: list, temp_path: str):
|
| 13 |
+
"""Helper function to run the player command and cleanup afterwards."""
|
| 14 |
+
try:
|
| 15 |
+
# This blocks this background thread, but not the main server thread
|
| 16 |
+
subprocess.run(cmd, check=True)
|
| 17 |
+
except Exception as e:
|
| 18 |
+
logger.error(f"Background playback failed: {e}")
|
| 19 |
+
finally:
|
| 20 |
+
# Give a small buffer ensuring file handle is released
|
| 21 |
+
time.sleep(0.5)
|
| 22 |
+
if os.path.exists(temp_path):
|
| 23 |
+
try:
|
| 24 |
+
os.unlink(temp_path)
|
| 25 |
+
except Exception:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def play_audio_bytes(audio_data: bytes, **kwargs) -> None:
|
| 30 |
+
"""
|
| 31 |
+
Play audio bytes (MP3 or other supported formats) in a non-blocking way.
|
| 32 |
+
This ensures the MCP server responds immediately, allowing the Agent UI
|
| 33 |
+
to update (show buttons) WHILE the voice is starting to speak.
|
| 34 |
+
"""
|
| 35 |
+
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tf:
|
| 36 |
+
tf.write(audio_data)
|
| 37 |
+
temp_path = tf.name
|
| 38 |
+
|
| 39 |
+
system = platform.system()
|
| 40 |
+
cmd = []
|
| 41 |
+
|
| 42 |
+
if system == "Darwin":
|
| 43 |
+
cmd = ["afplay", temp_path]
|
| 44 |
+
elif system == "Linux":
|
| 45 |
+
if subprocess.call(["which", "paplay"], stdout=subprocess.DEVNULL) == 0:
|
| 46 |
+
cmd = ["paplay", temp_path]
|
| 47 |
+
elif subprocess.call(["which", "aplay"], stdout=subprocess.DEVNULL) == 0:
|
| 48 |
+
cmd = ["aplay", temp_path]
|
| 49 |
+
elif system == "Windows":
|
| 50 |
+
# Windows Powershell sound player might be tricky in background threads due to COM
|
| 51 |
+
# but let's try standard command execution
|
| 52 |
+
ps_cmd = f"(New-Object Media.SoundPlayer '{temp_path}').PlaySync()"
|
| 53 |
+
cmd = ["powershell", "-c", ps_cmd]
|
| 54 |
+
|
| 55 |
+
if cmd:
|
| 56 |
+
logger.info(f"Starting background playback on {system}...")
|
| 57 |
+
# Launch in a separate thread so we don't block the API response
|
| 58 |
+
thread = threading.Thread(target=_play_and_cleanup, args=(cmd, temp_path))
|
| 59 |
+
thread.start()
|
| 60 |
+
else:
|
| 61 |
+
logger.warning("No suitable player found, skipping playback.")
|
services/mcp_server/core/tts.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
import requests
|
| 5 |
+
|
| 6 |
+
from .config import Config
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TTSNotConfiguredError(RuntimeError):
|
| 12 |
+
"""Raised when text-to-speech is not configured properly."""
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def generate_speech(text: str, config: Config) -> bytes:
|
| 16 |
+
"""
|
| 17 |
+
Generate speech audio bytes using ElevenLabs.
|
| 18 |
+
|
| 19 |
+
Raises:
|
| 20 |
+
TTSNotConfiguredError: If the API key is missing.
|
| 21 |
+
requests.exceptions.RequestException: If the HTTP request fails.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
if not config.elevenlabs_api_key:
|
| 25 |
+
raise TTSNotConfiguredError("ELEVENLABS_API_KEY is not set.")
|
| 26 |
+
|
| 27 |
+
logger.info("Calling ElevenLabs TTS for text: %s", text)
|
| 28 |
+
|
| 29 |
+
tts_url = (
|
| 30 |
+
f"https://api.elevenlabs.io/v1/text-to-speech/{config.elevenlabs_voice_id}"
|
| 31 |
+
)
|
| 32 |
+
headers = {
|
| 33 |
+
"Accept": "audio/mpeg",
|
| 34 |
+
"Content-Type": "application/json",
|
| 35 |
+
"xi-api-key": config.elevenlabs_api_key,
|
| 36 |
+
}
|
| 37 |
+
payload = {
|
| 38 |
+
"text": text,
|
| 39 |
+
"model_id": "eleven_multilingual_v2",
|
| 40 |
+
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75},
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
response = requests.post(tts_url, json=payload, headers=headers, timeout=30)
|
| 44 |
+
response.raise_for_status()
|
| 45 |
+
return response.content
|
| 46 |
+
|
services/mcp_server/main.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
|
| 7 |
+
from .core.config import load_config
|
| 8 |
+
from .routes.mcp import get_router
|
| 9 |
+
from .websocket.manager import ConnectionManager
|
| 10 |
+
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger("agentbell.mcp")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def create_app() -> FastAPI:
|
| 16 |
+
config = load_config()
|
| 17 |
+
app = FastAPI(
|
| 18 |
+
title="AgentBell MCP Server",
|
| 19 |
+
description="A Model Context Protocol (MCP) server for real-time voice notifications.",
|
| 20 |
+
version="1.0.0",
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
static_dir = Path(__file__).resolve().parent / "static"
|
| 24 |
+
static_dir.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 26 |
+
|
| 27 |
+
manager = ConnectionManager()
|
| 28 |
+
|
| 29 |
+
app.include_router(get_router(config, manager))
|
| 30 |
+
|
| 31 |
+
if not config.tts_configured:
|
| 32 |
+
logger.warning(
|
| 33 |
+
"ELEVENLABS_API_KEY is not configured. /invoke calls will return an error."
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
@app.websocket("/ws")
|
| 37 |
+
async def websocket_endpoint(websocket):
|
| 38 |
+
await manager.handle_connection(websocket)
|
| 39 |
+
|
| 40 |
+
@app.get("/")
|
| 41 |
+
async def root():
|
| 42 |
+
return {"message": "AgentBell MCP Server is running."}
|
| 43 |
+
|
| 44 |
+
return app
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
app = create_app()
|
services/mcp_server/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
requests
|
| 4 |
+
python-dotenv
|
| 5 |
+
websockets
|
services/mcp_server/routes/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""API routes for the MCP server."""
|
| 2 |
+
|
services/mcp_server/routes/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
services/mcp_server/routes/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (214 Bytes). View file
|
|
|
services/mcp_server/routes/__pycache__/mcp.cpython-310.pyc
ADDED
|
Binary file (2.8 kB). View file
|
|
|
services/mcp_server/routes/__pycache__/mcp.cpython-311.pyc
ADDED
|
Binary file (4.03 kB). View file
|
|
|
services/mcp_server/routes/mcp.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from fastapi import APIRouter
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
from ..core.config import Config
|
| 6 |
+
from ..core.actions import perform_voice_notify
|
| 7 |
+
from ..websocket.manager import ConnectionManager
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class InvokeRequest(BaseModel):
|
| 13 |
+
tool_id: str
|
| 14 |
+
parameters: dict
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class InvokeResponse(BaseModel):
|
| 18 |
+
content: str
|
| 19 |
+
content_type: str = "text/plain"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_router(config: Config, manager: ConnectionManager) -> APIRouter:
|
| 23 |
+
router = APIRouter()
|
| 24 |
+
|
| 25 |
+
@router.get("/health")
|
| 26 |
+
async def health_check():
|
| 27 |
+
return {"status": "ok"}
|
| 28 |
+
|
| 29 |
+
@router.get("/tools")
|
| 30 |
+
async def get_tools():
|
| 31 |
+
# Keeping description consistent with server.py but optimized for JSON response
|
| 32 |
+
return {
|
| 33 |
+
"tools": [
|
| 34 |
+
{
|
| 35 |
+
"id": "voice_notify",
|
| 36 |
+
"description": (
|
| 37 |
+
"Speak a message to the user via local audio. "
|
| 38 |
+
"CRITICAL: Use this tool BEFORE ANY action that requires user confirmation/clicking "
|
| 39 |
+
"(e.g., file edits, deletes, terminal commands) regardless of task length. "
|
| 40 |
+
"Narrate your steps. Style: Concise (5-15 words), human-like, no raw code."
|
| 41 |
+
),
|
| 42 |
+
"parameters": [
|
| 43 |
+
{
|
| 44 |
+
"name": "text",
|
| 45 |
+
"type": "string",
|
| 46 |
+
"description": "The text to be spoken to the user.",
|
| 47 |
+
"required": True,
|
| 48 |
+
}
|
| 49 |
+
],
|
| 50 |
+
}
|
| 51 |
+
]
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
@router.post("/invoke", response_model=InvokeResponse)
|
| 55 |
+
async def invoke_tool(request: InvokeRequest):
|
| 56 |
+
if request.tool_id != "voice_notify":
|
| 57 |
+
return {"content": f"Error: Tool '{request.tool_id}' not found."}
|
| 58 |
+
|
| 59 |
+
text_to_speak = request.parameters.get("text")
|
| 60 |
+
if not text_to_speak:
|
| 61 |
+
return {"content": "Error: 'text' parameter is required."}
|
| 62 |
+
|
| 63 |
+
logger.info("Processing voice notification: %s", text_to_speak)
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
# 1. Play Audio (Core Logic)
|
| 67 |
+
result_msg = perform_voice_notify(text_to_speak, config)
|
| 68 |
+
|
| 69 |
+
# 2. Broadcast Event (For Gradio/Web Client)
|
| 70 |
+
# We fire and forget the websocket event
|
| 71 |
+
await manager.broadcast_event("voice_start", {"text": text_to_speak})
|
| 72 |
+
|
| 73 |
+
return {"content": result_msg}
|
| 74 |
+
|
| 75 |
+
except Exception as exc:
|
| 76 |
+
logger.exception("Failed to process speech: %s", exc)
|
| 77 |
+
return {"content": f"Error: Failed to process speech. Details: {exc}"}
|
| 78 |
+
|
| 79 |
+
return router
|
services/mcp_server/server.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from mcp.server.fastmcp import FastMCP
|
| 3 |
+
from services.mcp_server.core.config import load_config
|
| 4 |
+
from services.mcp_server.core.actions import perform_voice_notify
|
| 5 |
+
|
| 6 |
+
# Configure logging
|
| 7 |
+
logging.basicConfig(level=logging.INFO)
|
| 8 |
+
logger = logging.getLogger("agentbell.mcp")
|
| 9 |
+
|
| 10 |
+
# Create FastMCP server
|
| 11 |
+
mcp = FastMCP("AgentBell")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@mcp.tool()
|
| 15 |
+
def voice_notify(text: str) -> str:
|
| 16 |
+
"""
|
| 17 |
+
Speak a message to the user via local audio.
|
| 18 |
+
|
| 19 |
+
Use this tool to act as a "Voice Companion" for the user.
|
| 20 |
+
You should narrate your key steps, status, and requests for attention.
|
| 21 |
+
|
| 22 |
+
USAGE PROTOCOL:
|
| 23 |
+
1. WHEN TO SPEAK:
|
| 24 |
+
- **Before Actions**: BEFORE running terminal commands, editing files, or calling other tools, speak a concise summary of what you are about to do.
|
| 25 |
+
- **Start of Task**: When receiving a new request, briefly acknowledge the plan.
|
| 26 |
+
- **End of Task**: When a task is complete, summarize the result.
|
| 27 |
+
- **Blocks/Confirmations**: If an action might require user approval (e.g. `rm`, `install`), you MUST speak BEFORE calling that tool.
|
| 28 |
+
|
| 29 |
+
2. CONTENT GUIDELINES:
|
| 30 |
+
- **Language**: ALWAYS match the language of the user's last message/prompt.
|
| 31 |
+
- **Style**: Human-like, concise (5-15 words), intelligent summary.
|
| 32 |
+
- **Forbidden**: Do NOT read raw filenames, paths, URLs, or code blocks. Use general terms.
|
| 33 |
+
|
| 34 |
+
3. CRITICAL TIMING (PREVENT DEADLOCKS):
|
| 35 |
+
- You MUST call `voice_notify`, **WAIT** for it to return, and ONLY THEN call the subsequent action tool.
|
| 36 |
+
- **NEVER** call `voice_notify` and a blocking tool (like `run_terminal_cmd`) in the same parallel execution block.
|
| 37 |
+
"""
|
| 38 |
+
logger.info(f"AgentBell triggered: {text}")
|
| 39 |
+
config = load_config()
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
return perform_voice_notify(text, config)
|
| 43 |
+
except RuntimeError as e:
|
| 44 |
+
return f"Error: {e}"
|
| 45 |
+
except Exception as e:
|
| 46 |
+
return f"Error playing voice: {e}"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
mcp.run()
|
services/mcp_server/static/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
services/mcp_server/static/audio/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
services/mcp_server/websocket/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""WebSocket management utilities."""
|
| 2 |
+
"""WebSocket management utilities."""
|
| 3 |
+
|
services/mcp_server/websocket/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (202 Bytes). View file
|
|
|
services/mcp_server/websocket/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (218 Bytes). View file
|
|
|
services/mcp_server/websocket/__pycache__/manager.cpython-310.pyc
ADDED
|
Binary file (2.14 kB). View file
|
|
|
services/mcp_server/websocket/__pycache__/manager.cpython-311.pyc
ADDED
|
Binary file (3.59 kB). View file
|
|
|
services/mcp_server/websocket/manager.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
|
| 4 |
+
from fastapi import WebSocket, WebSocketDisconnect
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ConnectionManager:
|
| 10 |
+
"""Tracks active websocket connections and broadcasts events."""
|
| 11 |
+
|
| 12 |
+
def __init__(self) -> None:
|
| 13 |
+
self.active_connections: List[WebSocket] = []
|
| 14 |
+
|
| 15 |
+
async def register(self, websocket: WebSocket) -> None:
|
| 16 |
+
await websocket.accept()
|
| 17 |
+
self.active_connections.append(websocket)
|
| 18 |
+
logger.info("WebSocket connected. Total clients: %s", len(self.active_connections))
|
| 19 |
+
|
| 20 |
+
def unregister(self, websocket: WebSocket) -> None:
|
| 21 |
+
if websocket in self.active_connections:
|
| 22 |
+
self.active_connections.remove(websocket)
|
| 23 |
+
logger.info("WebSocket disconnected. Total clients: %s", len(self.active_connections))
|
| 24 |
+
|
| 25 |
+
async def broadcast_event(self, event_type: str, data: Dict[str, Any]) -> None:
|
| 26 |
+
"""Broadcast a generic JSON event to all clients."""
|
| 27 |
+
payload = {
|
| 28 |
+
"type": event_type,
|
| 29 |
+
**data
|
| 30 |
+
}
|
| 31 |
+
stale_connections: list[WebSocket] = []
|
| 32 |
+
for connection in self.active_connections:
|
| 33 |
+
try:
|
| 34 |
+
await connection.send_json(payload)
|
| 35 |
+
except Exception as exc: # pragma: no cover
|
| 36 |
+
logger.warning("Failed sending payload to websocket: %s", exc)
|
| 37 |
+
stale_connections.append(connection)
|
| 38 |
+
for connection in stale_connections:
|
| 39 |
+
self.unregister(connection)
|
| 40 |
+
|
| 41 |
+
async def handle_connection(self, websocket: WebSocket) -> None:
|
| 42 |
+
"""Accept and keep the websocket connection alive."""
|
| 43 |
+
await self.register(websocket)
|
| 44 |
+
try:
|
| 45 |
+
while True:
|
| 46 |
+
# We keep the websocket open; we do not expect client messages yet.
|
| 47 |
+
await websocket.receive_text()
|
| 48 |
+
except WebSocketDisconnect:
|
| 49 |
+
self.unregister(websocket)
|
src/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Echo Universal MCP Host - Source Package
|
| 2 |
+
|
src/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (155 Bytes). View file
|
|
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Module - MCP Connection, LLM, State Management
|
| 2 |
+
from .state import (
|
| 3 |
+
# Connection Management
|
| 4 |
+
get_connection,
|
| 5 |
+
ensure_connection,
|
| 6 |
+
get_all_connections,
|
| 7 |
+
connect_server,
|
| 8 |
+
disconnect_server,
|
| 9 |
+
is_connected,
|
| 10 |
+
# Inspector
|
| 11 |
+
get_inspector_logs,
|
| 12 |
+
add_inspector_log,
|
| 13 |
+
# Bidirectional Protocol
|
| 14 |
+
ClientState,
|
| 15 |
+
broadcast_client_state,
|
| 16 |
+
get_current_state
|
| 17 |
+
)
|
| 18 |
+
from .mcp_client import MCPConnection, create_connection_from_config
|
| 19 |
+
from .llm import get_llm, LLMRouter
|
| 20 |
+
|
src/core/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (621 Bytes). View file
|
|
|