Kuk1 commited on
Commit
85dd3af
·
1 Parent(s): d387e95

Deploy Echo Universal Host

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +62 -7
  2. app.py +17 -0
  3. mcp.json +25 -0
  4. requirements.txt +5 -0
  5. services/mcp_server/.DS_Store +0 -0
  6. services/mcp_server/__init__.py +2 -0
  7. services/mcp_server/__pycache__/__init__.cpython-310.pyc +0 -0
  8. services/mcp_server/__pycache__/__init__.cpython-311.pyc +0 -0
  9. services/mcp_server/__pycache__/main.cpython-310.pyc +0 -0
  10. services/mcp_server/__pycache__/main.cpython-311.pyc +0 -0
  11. services/mcp_server/audio_store/__init__.py +3 -0
  12. services/mcp_server/audio_store/__pycache__/__init__.cpython-310.pyc +0 -0
  13. services/mcp_server/audio_store/__pycache__/__init__.cpython-311.pyc +0 -0
  14. services/mcp_server/audio_store/__pycache__/storage.cpython-310.pyc +0 -0
  15. services/mcp_server/audio_store/__pycache__/storage.cpython-311.pyc +0 -0
  16. services/mcp_server/audio_store/storage.py +20 -0
  17. services/mcp_server/core/__init__.py +2 -0
  18. services/mcp_server/core/__pycache__/__init__.cpython-310.pyc +0 -0
  19. services/mcp_server/core/__pycache__/__init__.cpython-311.pyc +0 -0
  20. services/mcp_server/core/__pycache__/actions.cpython-311.pyc +0 -0
  21. services/mcp_server/core/__pycache__/config.cpython-310.pyc +0 -0
  22. services/mcp_server/core/__pycache__/config.cpython-311.pyc +0 -0
  23. services/mcp_server/core/__pycache__/player.cpython-311.pyc +0 -0
  24. services/mcp_server/core/__pycache__/tts.cpython-310.pyc +0 -0
  25. services/mcp_server/core/__pycache__/tts.cpython-311.pyc +0 -0
  26. services/mcp_server/core/actions.py +30 -0
  27. services/mcp_server/core/config.py +27 -0
  28. services/mcp_server/core/player.py +61 -0
  29. services/mcp_server/core/tts.py +46 -0
  30. services/mcp_server/main.py +47 -0
  31. services/mcp_server/requirements.txt +5 -0
  32. services/mcp_server/routes/__init__.py +2 -0
  33. services/mcp_server/routes/__pycache__/__init__.cpython-310.pyc +0 -0
  34. services/mcp_server/routes/__pycache__/__init__.cpython-311.pyc +0 -0
  35. services/mcp_server/routes/__pycache__/mcp.cpython-310.pyc +0 -0
  36. services/mcp_server/routes/__pycache__/mcp.cpython-311.pyc +0 -0
  37. services/mcp_server/routes/mcp.py +79 -0
  38. services/mcp_server/server.py +50 -0
  39. services/mcp_server/static/.DS_Store +0 -0
  40. services/mcp_server/static/audio/.DS_Store +0 -0
  41. services/mcp_server/websocket/__init__.py +3 -0
  42. services/mcp_server/websocket/__pycache__/__init__.cpython-310.pyc +0 -0
  43. services/mcp_server/websocket/__pycache__/__init__.cpython-311.pyc +0 -0
  44. services/mcp_server/websocket/__pycache__/manager.cpython-310.pyc +0 -0
  45. services/mcp_server/websocket/__pycache__/manager.cpython-311.pyc +0 -0
  46. services/mcp_server/websocket/manager.py +49 -0
  47. src/__init__.py +2 -0
  48. src/__pycache__/__init__.cpython-310.pyc +0 -0
  49. src/core/__init__.py +20 -0
  50. src/core/__pycache__/__init__.cpython-310.pyc +0 -0
README.md CHANGED
@@ -1,13 +1,68 @@
1
  ---
2
- title: Echo Agent
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.0.1
8
  app_file: app.py
9
  pinned: false
10
- short_description: A Bidirectional MCP Client & Visual Manager.
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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