We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/davidteren/play-sound-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
MCP Server implementation for audio playback notifications.
This module implements the Model Context Protocol server using FastMCP,
providing the play_notification_sound tool for AI agents.
"""
import logging
from typing import Optional, Dict, Any
from mcp.server.fastmcp import FastMCP
from .config import ServerConfig
from .audio_player import AudioPlayer, PlaybackStatus, AFPlayBackend
logger = logging.getLogger(__name__)
class PlaySoundServer:
"""MCP Server for audio playback notifications."""
def __init__(self, config: ServerConfig):
"""Initialize the MCP server with configuration."""
self.config = config
self.audio_player = AudioPlayer(config)
# Initialize FastMCP server
self.app = FastMCP("play-sound-server")
self._setup_tools()
logger.info("PlaySoundServer initialized")
def _setup_tools(self) -> None:
"""Set up MCP tools."""
@self.app.tool()
async def play_notification_sound(
custom_sound_path: Optional[str] = None,
message: Optional[str] = None
) -> Dict[str, Any]:
"""
Play a notification sound to alert the user.
This tool plays an audio notification to get the user's attention,
typically used when an AI coding task has been completed.
Args:
custom_sound_path: Optional path to a custom audio file to play.
If not provided, uses the configured default or built-in sound.
message: Optional message to include in the response for context.
Returns:
Dictionary containing playback status, message, and details.
"""
logger.info(f"play_notification_sound called: custom_path={custom_sound_path}, message={message}")
try:
# Play the notification sound
result = await self.audio_player.play_notification(custom_sound_path)
# Prepare response
response = {
"success": result.status in [PlaybackStatus.SUCCESS, PlaybackStatus.FALLBACK_USED],
"status": result.status.value,
"message": result.message,
"backend_used": result.backend_used,
"fallback_used": result.fallback_used,
}
# Add user message if provided
if message:
response["user_message"] = message
# Add duration if available
if result.duration_ms:
response["duration_ms"] = result.duration_ms
# Log the result
if result.status == PlaybackStatus.SUCCESS:
logger.info(f"Notification played successfully using {result.backend_used}")
elif result.status == PlaybackStatus.FALLBACK_USED:
logger.info(f"Notification played using fallback with {result.backend_used}")
else:
logger.warning(f"Notification playback failed: {result.message}")
return response
except Exception as e:
logger.error(f"Error in play_notification_sound: {e}")
return {
"success": False,
"status": "error",
"message": f"Unexpected error: {str(e)}",
"backend_used": None,
"fallback_used": False,
}
@self.app.tool()
async def get_audio_status() -> Dict[str, Any]:
"""
Get the current audio system status and configuration.
Returns information about available audio backends, configuration,
and system capabilities.
Returns:
Dictionary containing audio system status and configuration.
"""
logger.debug("get_audio_status called")
try:
# Get backend information
backend_info = []
for backend in self.audio_player.backends:
backend_info.append({
"name": backend.name,
"available": backend.is_available(),
})
# Prepare status response
status = {
"backends_available": len(self.audio_player.backends),
"backends": backend_info,
"configuration": {
"volume_level": self.config.volume_level,
"enable_fallback": self.config.enable_fallback,
"custom_sound_configured": bool(self.config.custom_sound_path),
"audio_backend": self.config.audio_backend,
"playback_timeout_seconds": self.config.playback_timeout_seconds,
},
"default_sound_exists": self.audio_player._default_sound_path.exists(),
}
# Add custom sound info if configured
if self.config.custom_sound_path:
from pathlib import Path
custom_path = Path(self.config.custom_sound_path)
status["custom_sound"] = {
"path": str(custom_path),
"exists": custom_path.exists(),
"size_mb": round(custom_path.stat().st_size / (1024 * 1024), 2) if custom_path.exists() else None,
}
logger.debug(f"Audio status: {len(backend_info)} backends available")
return status
except Exception as e:
logger.error(f"Error in get_audio_status: {e}")
return {
"error": f"Failed to get audio status: {str(e)}",
"backends_available": 0,
"backends": [],
}
@self.app.tool()
async def test_audio_playback(
use_custom: bool = False
) -> Dict[str, Any]:
"""
Test audio playback functionality.
Performs a test of the audio system to verify it's working correctly.
Useful for troubleshooting audio issues.
Args:
use_custom: If True, test with custom sound (if configured).
If False, test with default sound.
Returns:
Dictionary containing test results and diagnostic information.
"""
logger.info(f"test_audio_playback called: use_custom={use_custom}")
try:
# Determine which sound to test
test_path = None
if use_custom and self.config.custom_sound_path:
test_path = self.config.custom_sound_path
# Run the test
result = await self.audio_player.play_notification(test_path)
# Prepare test results
test_result = {
"test_passed": result.status in [PlaybackStatus.SUCCESS, PlaybackStatus.FALLBACK_USED],
"status": result.status.value,
"message": result.message,
"backend_used": result.backend_used,
"fallback_used": result.fallback_used,
"test_type": "custom" if use_custom else "default",
}
logger.info(f"Audio test completed: {result.status.value}")
return test_result
except Exception as e:
logger.error(f"Error in test_audio_playback: {e}")
return {
"test_passed": False,
"status": "error",
"message": f"Test failed with error: {str(e)}",
"backend_used": None,
"fallback_used": False,
"test_type": "custom" if use_custom else "default",
}
@self.app.tool()
async def list_audio_devices() -> Dict[str, Any]:
"""
List available audio output devices on the system.
Returns information about all available audio output devices,
including which one is currently the default.
Returns:
Dictionary containing list of available audio devices with their properties.
"""
logger.info("list_audio_devices called")
try:
from .audio_player import AFPlayBackend
devices = await AFPlayBackend.get_available_audio_devices()
return {
"success": True,
"devices": devices,
"device_count": len(devices),
"current_configured_device": self.config.audio_device,
"message": f"Found {len(devices)} audio output devices"
}
except Exception as e:
logger.error(f"Error in list_audio_devices: {e}")
return {
"success": False,
"devices": [],
"device_count": 0,
"current_configured_device": self.config.audio_device,
"error": str(e),
"message": f"Failed to list audio devices: {str(e)}"
}
def run(self) -> None:
"""Run the MCP server."""
logger.info("Starting MCP Play Sound Server...")
try:
# Run the FastMCP server
self.app.run()
except KeyboardInterrupt:
logger.info("Server stopped by user")
except Exception as e:
logger.error(f"Server error: {e}")
raise
finally:
logger.info("MCP Play Sound Server stopped")
def get_server_info(self) -> Dict[str, Any]:
"""Get server information for debugging."""
return {
"name": "MCP Play Sound Server",
"version": "0.1.0",
"tools_count": 4, # We have 4 tools: play_notification_sound, get_audio_status, test_audio_playback, list_audio_devices
"backends_available": len(self.audio_player.backends),
"config": {
"volume_level": self.config.volume_level,
"enable_fallback": self.config.enable_fallback,
"custom_sound_configured": bool(self.config.custom_sound_path),
}
}