Skip to main content
Glama
nickbeentjes

mac-audio-router-mcp

by nickbeentjes
README.md
# 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

A3.5/5.0

Scored across 16 tools

Disambiguation4/5

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).

Naming Consistency5/5

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.

Tool Count4/5

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.

Completeness4/5

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.

Maintenance

ActivityInactive
ResponsivenessNo issues