mac-audio-router-mcp
# mac-audio-router-mcp
An MCP server that gives AI agents full control over macOS audio routing, device management, volume, and multi-zone playback.
Built for environments where an AI assistant needs to manage audio across multiple outputs (HDMI TVs, Bluetooth speakers, AirPlay devices, satellite speakers) and multiple microphone inputs — without any manual intervention.
> **Status:** Early release. Tested on macOS 15 (Sequoia) with Apple Silicon. Contributions welcome.
## Installation
```shell
npm install mac-audio-router-mcp
```
For full device switching (recommended):
```shell
brew install switchaudio-osx
```
### Prerequisites
- macOS 12+ (Monterey or later)
- Node.js 18+
- Optional: [SwitchAudioSource](https://github.com/deweller/switchaudio-osx) for device switching beyond built-in outputs
## Quickstart
Add to your MCP client configuration:
```json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": ["mac-audio-router-mcp"]
}
}
}
```
For Claude Desktop, add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": ["mac-audio-router-mcp"]
}
}
}
```
For OpenClaw, add to your gateway config:
```json5
{
tools: [{
type: "mcp",
command: "npx",
args: ["mac-audio-router-mcp"]
}]
}
```
The agent can now discover and call audio tools. Try:
> "What audio devices are connected?"
>
> "Route the audio output to the Bluetooth speaker."
>
> "Set the volume to 40%."
>
> "Say 'dinner is ready' on the salon speaker, then switch back to HDMI."
## Tools
### Status & Discovery
| Tool | Description |
|------|-------------|
| `get_audio_status` | Full system snapshot: devices, zones, processes, volume, routing |
| `list_audio_devices` | All connected input/output devices with transport type (Bluetooth, HDMI, USB, AirPlay, built-in) |
| `list_audio_zones` | Configured audio zones and their assignments |
| `list_audio_processes` | Processes currently using audio hardware |
### Routing
| Tool | Parameters | Description |
|------|-----------|-------------|
| `set_output_device` | `device_name` | Route system output to a named device |
| `set_input_device` | `device_name` | Set the active microphone |
| `activate_zone` | `zone_id` | Apply a pre-configured zone's routing, volume, and device settings |
### Volume
| Tool | Parameters | Description |
|------|-----------|-------------|
| `get_volume` | — | Current volume level (0–100) |
| `set_volume` | `level` | Set volume (0–100) |
| `mute` | `muted` | Mute or unmute output |
### Playback
| Tool | Parameters | Description |
|------|-----------|-------------|
| `play_audio` | `file_path`, `volume?` | Play a WAV/MP3/AAC/AIFF file |
| `speak_text` | `text`, `voice?`, `rate?` | Text-to-speech via macOS `say` |
| `route_and_play` | `device_name`, `action`, `content`, `volume?`, `restore_device?` | Atomic: switch device, play/speak, optionally restore |
### Native Daemon (low-latency)
These tools require the `audiod` native daemon (CoreAudio HAL in C, sub-millisecond response):
| Tool | Parameters | Description |
|------|-----------|-------------|
| `hog_device` | `device_name`, `release?` | Take/release exclusive access to a device (prevents other apps using it) |
| `set_device_volume` | `device_name`, `level` | Set volume on a specific device, not just the system default |
### Zone Management
| Tool | Parameters | Description |
|------|-----------|-------------|
| `configure_zone` | `zone_id`, `name`, `description?`, `output_device?`, `input_device?`, `volume?` | Create or update a named audio zone |
| `activate_zone` | `zone_id` | Switch all routing to match a zone's configuration |
## Native Daemon
For sub-millisecond audio control, build and run the native `audiod` daemon. It talks directly to CoreAudio HAL in C — no AppleScript, no SwitchAudioSource, no subprocess spawning.
```shell
cd native
make
./audiod # listens on /tmp/audiod.sock
```
The MCP server auto-detects the daemon at startup. When connected, all device operations go through the Unix socket instead of system commands.
**Latency comparison:**
| Operation | System commands | Native daemon |
|-----------|---------------:|-------------:|
| List devices | ~2,000ms | ~0.2ms |
| Switch output | ~200ms | ~0.2ms |
| Get volume | ~150ms | ~0.1ms |
| Set volume | ~150ms | ~0.1ms |
The daemon protocol is newline-delimited JSON over a Unix socket:
```shell
# List all devices
echo '{"cmd":"list_devices"}' | nc -U /tmp/audiod.sock
# Switch output
echo '{"cmd":"set_output","name":"SAMSUNG"}' | nc -U /tmp/audiod.sock
# Set volume (0-100)
echo '{"cmd":"set_volume","level":60}' | nc -U /tmp/audiod.sock
# Per-device volume
echo '{"cmd":"set_volume","device":"Mac mini Speakers","level":40}' | nc -U /tmp/audiod.sock
# Take exclusive mic access
echo '{"cmd":"hog","device":"AIWA","release":false}' | nc -U /tmp/audiod.sock
```
Every response includes `_us` (microseconds elapsed) for profiling.
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ AI Agent (Claude, GPT, etc.) │
│ "Route TTS to the Bluetooth speaker in the salon" │
└──────────────┬──────────────────────────────────────┘
│ MCP (stdio / JSON-RPC)
┌──────────────▼──────────────────────────────────────┐
│ mac-audio-router-mcp (TypeScript) │
│ │
│ Tools: │
│ ├─ get_audio_status (system snapshot) │
│ ├─ list_audio_devices (CoreAudio enumeration) │
│ ├─ set_output_device (route output) │
│ ├─ set_input_device (select mic) │
│ ├─ set_volume / mute (volume control) │
│ ├─ configure_zone (multi-room setup) │
│ ├─ activate_zone (switch routing preset) │
│ ├─ play_audio (file playback) │
│ ├─ speak_text (TTS) │
│ └─ route_and_play (atomic route + play) │
│ │
│ Primary: Unix socket to audiod daemon │
│ Fallback: system commands (osascript, afplay, say) │
└──────────────┬──────────────────────────────────────┘
│ Unix domain socket (/tmp/audiod.sock)
┌──────────────▼──────────────────────────────────────┐
│ audiod (C, ~500 lines) │
│ │
│ CoreAudio HAL direct access: │
│ ├─ AudioObjectGetPropertyData (enumeration) │
│ ├─ AudioObjectSetPropertyData (routing) │
│ ├─ kAudioDevicePropertyVolumeScalar (volume) │
│ ├─ kAudioDevicePropertyHogMode (exclusive lock) │
│ └─ Device change notifications (auto-refresh) │
│ │
│ Response times: 0.1–0.3ms typical │
└──────────────┬──────────────────────────────────────┘
│
┌──────────────▼──────────────────────────────────────┐
│ macOS CoreAudio │
│ │
│ Devices: │
│ ├─ Built-in speakers / headphone jack │
│ ├─ HDMI / DisplayPort (TVs, monitors) │
│ ├─ Bluetooth (speakers, headphones) │
│ ├─ AirPlay (HomePod, Apple TV, smart speakers) │
│ ├─ USB audio interfaces │
│ └─ Virtual (Aggregate, BlackHole, Loopback) │
└─────────────────────────────────────────────────────┘
```
## Multi-Zone Example
Configure zones for a vessel, smart home, or studio — then let the agent switch between them:
```typescript
// The agent can do this via tool calls:
// 1. Configure zones
configure_zone({ zone_id: "salon", name: "Salon", output_device: "AIWA AWWS01", volume: 60 })
configure_zone({ zone_id: "bridge", name: "Bridge", output_device: "Samsung TV", volume: 40 })
configure_zone({ zone_id: "cockpit", name: "Cockpit", output_device: "JBL Clip", volume: 80 })
// 2. Route TTS to a specific zone
route_and_play({
device_name: "AIWA AWWS01",
action: "speak",
content: "Anchor watch: wind has shifted to 15 knots from the northwest.",
restore_device: "Samsung TV"
})
// 3. Switch zones
activate_zone({ zone_id: "bridge" })
```
## Extending
### Custom Device Matching
The server identifies device transport types (Bluetooth, HDMI, etc.) by name pattern matching. To add custom patterns, edit the `inferTransportType` function in `src/audio.ts`.
### Persistent Zone Configuration
Zones are stored in memory by default. To persist across restarts, set the `MAC_AUDIO_ROUTER_ZONES` environment variable to a JSON file path:
```json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": ["mac-audio-router-mcp"],
"env": {
"MAC_AUDIO_ROUTER_ZONES": "/path/to/zones.json"
}
}
}
}
```
### AirPlay & Apple TV
AirPlay devices appear as standard output devices in macOS. The agent can route to them using `set_output_device` with the AirPlay device name. For Apple TV, ensure the Mac is connected via AirPlay in System Settings first.
### Satellite / Multi-Room
For complex multi-room setups:
1. Use macOS Aggregate Devices or Multi-Output Devices (via Audio MIDI Setup) to create virtual devices that span multiple physical outputs
2. Configure each as a zone
3. The agent can then activate zones to control entire room groupings
## Development
```shell
git clone https://github.com/nickbeentjes/mac-audio-router-mcp.git
cd mac-audio-router-mcp
npm install
npm run build
```
Test with the MCP Inspector:
```shell
npm run inspect
```
Run directly:
```shell
node build/index.js
```
## Troubleshooting
| Issue | Solution |
|-------|----------|
| `set_output_device` fails | Install SwitchAudioSource: `brew install switchaudio-osx` |
| Bluetooth device not listed | Pair the device in System Settings > Bluetooth first |
| AirPlay device not listed | Connect to it once via System Settings > Sound > Output |
| Volume doesn't change | Some HDMI devices control volume independently |
| `get_audio_status` is slow | `system_profiler` can take 2-3s; install SwitchAudioSource for faster enumeration |
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## License
Released under the [MIT License](LICENSE).
TDQS
Scored across 16 tools
Most tools target distinct resources and actions (device routing, volume, zones, playback, listing). Some overlap exists between set_volume/set_device_volume and play_audio vs route_and_play, but descriptions clarify the distinction (default vs specific device, atomic combined operation).
All tools use consistent snake_case verb_noun or verb forms (set_output_device, configure_zone, list_audio_devices, play_audio). The list_* family is uniform and predictable throughout.
16 tools is slightly heavy but each maps to a genuine audio-routing operation. The set is well-scoped with no obviously redundant tools, though it sits near the upper end of the comfortable range.
Covers the core lifecycle: device selection, volume, mute, zone create/update/activate, playback, TTS, status, and discovery listing. Minor gap: no delete_zone operation to remove configured zones, but otherwise the surface is solid.