uad-console-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@uad-console-mcpwhat's the current monitor level?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Unofficial project. Not affiliated with, endorsed by, or supported by Universal Audio, Inc. UAD, Apollo, Unison, and Console are trademarks of Universal Audio, Inc. This software is an independent implementation built against the local API that the UA Mixer Engine already exposes on this machine.
uad-console-mcp
An MCP server that lets Claude (or any MCP client) read and control a Universal Audio Apollo
interface: channel names and faders, preamp gain, cue and aux sends, the control-room monitor,
talkback, clock and sample rate, UAD plugin inserts, meters, and whole-rig snapshots. It talks to
the UA Mixer Engine over its local TCP API on 127.0.0.1:4710, the same API the Console app
uses.
The Console app does not need to be open. The engine does need to be running: it starts with UAD Console and keeps running after you quit the app. Nothing leaves your machine.
Does this work for me?
Setup | Status |
Apollo x8 Gen 2, Thunderbolt | Live-tested against real hardware (UA Mixer Engine, UAD 11.7.1) |
Other Thunderbolt / USB Apollos (Twin, x4, x6, x8p, x16, Solo, Duo, Quad) | Expected to work. Same engine, same protocol, and discovery is fully dynamic: no channel index, monitor output, or slot count is hardcoded. Unverified, so please report back |
Multi-unit cascades (two or more Apollos) | Mock-tested against a derived two-device fixture. The code enumerates every |
macOS | Verified |
Windows | Protocol-compatible, unverified. The server is pure Python over plain TCP with nothing platform-specific in it, so it should work wherever the engine listens on |
Discovery is why "expected to work" is a real expectation and not a hope: the server walks the
engine's own tree, finds the monitor output by the presence of its CRMonitorLevel property,
reads each strip's IOType, and asks the engine which send buses and insert slots exist. A model
with a different channel count is a different tree, not a different code path.
Related MCP server: X32/M32 MCP Server
Safety: read this before you install
This server can make your speakers loud and can put 48 volts down a microphone cable. An LLM driving it will occasionally be confidently wrong. The safety layer exists because of that, and because of one measured fact about the engine:
The UA Mixer Engine does not enforce its own read-only flags. A
seton a property the engine itself reported asreadonly: truewas accepted and applied on real hardware. The engine will not stop a bad write. The gate insafety.pyis the only real barrier.
Three modes
| What it allows |
| Nothing is written, ever. Every write is refused before it reaches the socket. Good for a first look, and for letting an LLM inspect a live session during a take |
| Writes allowed only to an allowlist of 35 property names: names, faders, pans, mutes, solos, preamp controls, sends, plugin slots and parameters, monitor, talkback, clock, rate, and per-input sample-rate conversion ( |
| The allowlist is bypassed and |
In every mode a permanent denylist refuses device identity and engine internals (DeviceHwID,
MultiUnitOrder, AppCommand, MixerState, DeviceTypeID, anything under /pluginscenes, and
anything that looks like firmware, a bootloader, or a serial number).
Hearing and speaker rails
Monitor level is not treated as a normal property. It is railed three ways at once, and unmuting or un-dimming counts as a level event: releasing a mute on a stored level of 0 dB is exactly as loud as setting 0 dB.
Variable | Default | What it does |
|
|
|
|
| Hard ceiling on monitor level in dB. Requests above it are refused, not silently clamped. Unmuting onto an above-ceiling stored level is refused too |
|
| Largest increase allowed in one call, for requests at or below the ceiling. Bigger jumps are clamped down to this |
|
| Total increase allowed inside the rolling window |
|
| Length of that rolling window, in seconds |
|
| Global write rate limit (token bucket), so a confused agent cannot machine-gun the engine |
|
| Burst size for that rate limit |
|
| Off by default: writes to properties the engine marks read-only are refused. Only set this if you know exactly which property you mean |
|
| Where |
The ceiling and the step limit are not the same rail, and the order matters. The ceiling is checked first: a request above it is refused outright and nothing is written, not even a partial +6 dB move. The per-call step clamp only applies to requests that are already at or below the ceiling, where a too-large jump is trimmed to +6 dB and applied.
One consequence worth saying out loud: climbing back out of a very low monitor level through this server is deliberately slow (+6 dB per call, +10 dB per 30 s). For a big jump, use the physical monitor knob instead of waiting out the budget.
Every refusal names the variable or confirm flag that would unblock it, so an agent can tell you the remedy instead of retrying blindly:
Refused monitor level -3.0 dB: above the -6.0 dB ceiling. Ask for -6.0 dB or less,
or raise UAD_MCP_MAX_MONITOR_DB (hearing and speaker damage risk).48V phantom power and ribbon microphones
uad_set_preamp(phantom_48v=true) is refused unless the same call carries
confirm_condenser_mic=true. Phantom power can permanently destroy a ribbon microphone, and
some vintage dynamics. A model that has not been told what microphone is plugged in cannot
satisfy that flag honestly, which is the point:
Refused to switch 48V phantom power on at /devices/0/inputs/0/preamps/0/48V: it can
permanently damage ribbon microphones and some vintage dynamics. Confirm the connected
microphone is a condenser and pass confirm_condenser_mic=True.No warranty
This is MIT-licensed software with no warranty of any kind. It writes to real audio hardware
in real time. You are responsible for your ears, your monitors, your microphones, and your
session. Sudden or sustained loud audio causes permanent hearing damage. The rails here reduce
the chance of an accident; they are not hearing protection and they are not a substitute for your
own hand on the monitor knob. Do not run this during a take you cannot afford to lose, and start
in readonly mode.
Install
Requires Python 3.11 or newer, and the UAD software installed with the engine running. Not on PyPI yet, so run it straight from git with uv:
uvx --from git+https://github.com/fomoPhil/uad-console-mcp uad-console-mcpClaude Code:
claude mcp add uad-console -- uvx --from git+https://github.com/fomoPhil/uad-console-mcp uad-console-mcpClaude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"uad-console": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/fomoPhil/uad-console-mcp",
"uad-console-mcp"
],
"env": {
"UAD_MCP_MODE": "safe",
"UAD_MCP_MAX_MONITOR_DB": "-12"
}
}
}
}Verify it before you wire it up
--check is a doctor. It reads and never writes, so it is safe to run at any time:
$ uvx --from git+https://github.com/fomoPhil/uad-console-mcp uad-console-mcp --check
uad-console-mcp 0.1.0
engine: reachable at 127.0.0.1:4710
UAD version 11.7.1, protocol 1.0.0
clock: Internal, locked=True
sample rate: 48000 Hz
dsp load: 11.4% dsp, 8.0% program
device 0: Apollo x8 Gen 2 (Apollo x8 Gen 2) online: 28 inputs, 2 auxs, 26 outputs
monitor out: MONITOR at /devices/0/outputs/22
plugins: 215 installed
safety mode: safe (from UAD_MCP_MODE)
monitor rail: ceiling -6.0 dB, max +6.0 dB per call, +10.0 dB per 30 s
write limit: 10/s, burst 20
raw set tool: not registeredIt exits non-zero if the engine is unreachable, no device is found, or a device is offline.
Other flags: --dump-tree [FILE] (walk the whole tree into the test-fixture format, home
directory scrubbed out, - for stdout), --host / --port (defaults 127.0.0.1 and 4710),
--log-level (DEBUG, INFO, WARNING, ERROR, CRITICAL, default INFO, always on stderr),
and --version.
What a session looks like
Labelling an 8-channel ADAT expander and building the drummer's cue mix. Condensed, but these are the real calls and the real result shapes.
You: "My ADAT expander has kick, snare top, snare bottom and two overheads on the first five channels. Name them properly and give the drummer a headphone mix."
uad_get_status()
-> clock: {source: "Internal", locked: true, sample_rate: 48000}
devices: [{device: 0, name: "Apollo x8 Gen 2", online: true, cue_buses: 2,
channels: {input: 28, aux: 2, output: 26}}]
safety: {mode: "safe", max_monitor_db: -6.0}
uad_list_channels(kind="input")
-> ... {index: 8, name: "UG 1", io_type: "ADAT", fader_db: 0.0, mute: false, has_preamp: false}
{index: 9, name: "UG 2", io_type: "ADAT", ...}
{index: 10, name: "UG 3", io_type: "ADAT", ...} (8 ADAT strips, indices 8 to 15)The eight UG n strips are the expander. Renaming and unhiding them in one batch:
uad_set_channels(items=[
{target: "UG 1", name: "KICK IN", fader_db: 0, mute: false, hidden: false},
{target: "UG 2", name: "SNARE TOP", fader_db: 0, mute: false, hidden: false},
{target: "UG 3", name: "SNARE BTM", fader_db: 0, mute: false, hidden: false},
{target: "UG 4", name: "OH L", fader_db: 0, mute: false, hidden: false, pan: -1.0},
{target: "UG 5", name: "OH R", fader_db: 0, mute: false, hidden: false, pan: 1.0},
])
-> results[0]: {resolved: {device: 0, index: 8, name: "UG 1"}, ok: true,
writes: [{path: "/devices/0/inputs/8/Name", property: "Name",
verified: true, before: "UG 1", after: "KICK IN", warnings: []}, ...]}verified: true is a real read-back, not an assumption: the server sets the value, gets it again,
and compares.
uad_set_sends(items=[
{target: "KICK IN", send: "cue1", level_db: -6},
{target: "SNARE TOP", send: "cue1", level_db: -8},
{target: "SNARE BTM", send: "cue1", level_db: -12},
{target: "OH L", send: "cue1", level_db: -10, pan: -1.0},
{target: "OH R", send: "cue1", level_db: -10, pan: 1.0},
])
-> results[0]: {send: "cue1", resolved: {index: 8, name: "KICK IN"}, ok: true,
writes: [{path: "/devices/0/inputs/8/sends/2/Gain",
verified: true, before: -144.0, after: -6.0}]}The new names work immediately: a verified rename updates the cached topology in place, so the
resolver does not have to wait out its 30 second TTL. Send IDs are resolved from the engine too,
which is why cue1 landed on sends/2 here rather than on a guessed index.
uad_watch_meters(targets=["KICK IN","SNARE TOP","OH L","OH R"], duration_s=10, threshold_db=-40)
-> {triggered: true, triggered_channels: ["KICK IN"],
peaks_db: {"KICK IN": -8.4, "SNARE TOP": -11.2, "OH L": -19.0, "OH R": -18.6}, polls: 7}Claude: "Named and metered, kick peaking at -8 dBFS. The drummer's cue is built on cue1, and HP 1 is already fed from cue1. The headphone volume itself has to come off the front-panel knob: the protocol exposes no level property for the HP outputs."
Tool reference
21 tools in readonly and safe mode, 22 in full mode. Every write result carries
{verified, before, after, warnings} plus the resolved {device, index, name}, and every tool
returns an actionable sentence instead of a traceback when something fails.
Tool | One line |
| Engine (including |
| Every input, aux or output strip across every unit, grouped by device, with live fader, mute, solo, solo_safe, pan, hidden and stereo state, plus |
| Batch: |
| One mic/line input's |
| Batch: a channel's |
| Control room: |
| Talkback on/off and level, with an auto-off timer (default 30 s) so an open talkback mic cannot be forgotten |
| Panic button, no arguments: mute every monitor output and turn talkback off |
| Change clock source ( |
| Change the system sample rate. Needs |
| The installed UAD catalog: name, categories, Unison capability, authorization. Filterable |
| What is loaded where: every insert slot plus the Unison slot, with plugin, power state and preset |
| Load a plugin into an insert slot or |
| Clear an insert or Unison slot, reporting what was removed |
| A loaded plugin's parameters: names, normalized 0-1 values, and the engine's own display text |
| Batch-set a loaded plugin's parameters by name, using normalized 0-1 values |
| Recall a preset by name, validated against that plugin's preset browser when the engine lists it |
| One-shot meter read (level, peak, clip) with at-floor channels called out |
| Poll meters for up to |
|
|
| Read any node or property in the engine tree verbatim, with its metadata. One argument, |
|
|
Reading state that no tool returns directly
Four things are readable but easy to miss, because the obvious tool does not return them.
A send's current level. There is no send-read tool. Read the send nodes with uad_raw_get
before you write, so you know what you are changing:
uad_raw_get(path="/devices/0/inputs/0/sends")
-> {kind: "node", children: ["0","1","2","3","4","5"]} one node per send
uad_raw_get(path="/devices/0/inputs/0/sends/2")
-> {properties: {ID: {value: "cue1"}, Gain: {value: -144.0, min: -144.0, max: 12.0},
Pan: {value: 0.0}, Bypass: {value: false}}}The node's ID is what uad_set_sends calls send: aux1, aux2, cue1 through cue4. Gain
is the level in dB, from -144 to +12, and -144.0 means the send is off, which is the default on
a fresh strip. All four cueN nodes exist in the tree even on a device that reports cue_buses: 2,
so a write to cue3 there verifies happily while reaching no real output: uad_set_sends adds a
warning saying exactly that, and an agent should relay it rather than reporting success.
The monitor level. uad_get_status does not carry it. Read it from
uad_list_channels(kind="output"): the strip with is_monitor: true has monitor_level_db
(the engine's CRMonitorLevel). Write it with uad_set_monitor(level_db=...), through the rails.
active is not a fault light. Every strip reports a read-only active flag, which is engine
state, not user state. active: false is normal on the right leg of every stereo pair, on
unused strips, and on some outputs: on the captured x8 Gen 2 tree, S/PDIF R and the working
MONITOR output both report active: false. It is not writable and there is nothing to fix. The
writable switch is enabled (EnabledByUser), set with
uad_set_channels(items=[{"target": N, "enabled": true}]).
SRConvert, per-input sample-rate conversion. Each digital input strip (S/PDIF on the x8 Gen 2)
carries a boolean SRConvert, which lets that input arrive at a different sample rate from the
Apollo's. Read it with uad_raw_get(path="/devices/0/inputs/16/SRConvert"). No dedicated tool
writes it: it is on the safe-mode allowlist, which is what lets uad_snapshot capture and restore
it, but changing it live needs uad_raw_set in full mode, or Console.
And for any "no sound" question, check engine.audio_streaming in uad_get_status first. If it
is false, the engine is passing no audio at all and no meter anywhere will move, so hunting through
channels, sends and the monitor path is wasted effort.
Resources
URI | Contents |
| Live |
| Live |
| The cannot-do list, with a workaround for each item (Markdown) |
Prompts
setup_vocal_tracking, onboard_adat_expander, troubleshoot_clock, panic_reset. Prose
versions of all four, plus a gain-staging guide, live in docs/RECIPES.md.
Not supported
These are hard limits of the API this server talks to, not a to-do list. The uad://limitations
resource says the same thing to the model at runtime.
Not supported | Why, and what to do instead |
Headphone output level | The HP outputs expose no level property at all. On the captured x8 Gen 2 tree, |
Saving a Console session file | There is no session save or recall in this API, and writes under |
Re-patching the I/O matrix |
|
Dante | No Dante tools, and no Dante routing nodes in the tree this server walks. The x8 Gen 2 reports a read-only |
Other UA local ports | Only TCP 4710 is implemented. Whatever the UAD software may offer on other local ports (community projects mention a helper on 4720) is out of scope: not reverse-engineered, not used, not tested |
Remote or networked engines |
|
Creating or deleting channels | The strip list is fixed by the hardware model |
Audio itself | No capture, no playback, no analysis. Meters are polled at about 3 Hz, |
Per-plugin DSP cost | Only the totals are exposed ( |
Monitor level above the ceiling | By design: |
Writing | The property is allowlisted so snapshots can restore it, but no tool exposes it. Read it with |
Companion skills
Two Claude skills ship in skills/. They are optional. The server works without them,
but they carry the studio judgement that does not belong in a tool description.
Skill | What it is for |
| Planning and configuring a rig from scratch: the interview questions to ask, which physical input suits which source, the clock master decision for ADAT expanders and other digital gear, ADAT sample-rate and S/MUX channel limits, multi-Apollo cascades, then channel naming, hiding and a baseline snapshot |
| Day-to-day operation: tracking setup with a Unison preamp and honest gain staging, insert chains, cue and headphone mixes for one or several performers, podcast and streaming layouts, troubleshooting decision trees ("no sound from channel 3", clicks and pops, a strip that vanished), and snapshot-before-you-experiment habits |
Each skill is a self-contained directory: a SKILL.md plus a references/ folder it points at
when a step needs the detail. Install by copying both into your personal skills folder:
mkdir -p ~/.claude/skills
cp -R skills/apollo-onboarding skills/apollo-studio-tasks ~/.claude/skills/See skills/README.md for details.
For agent-facing rules (hard prohibitions, canonical tool sequences, an error table and a studio glossary), see AGENTS.md.
Contributing
The most useful contribution is a tree dump from an Apollo model nobody has tested yet. That is exactly how a model moves from "expected to work" to "verified".
uad-console-mcp --dump-tree ~/Desktop/my-apollo-tree.jsonIt only reads. The walk takes about 11 seconds for roughly 1,900 nodes, and your home directory is scrubbed out of every string before the file is written (preset browser paths otherwise leak your login name). Open an issue titled with your model, attach the JSON, and say which OS and UAD version you are on. From that fixture the test suite can prove the code handles your channel count, your monitor output index and your send layout without ever touching your hardware.
Bug reports are far more useful with the --check output plus the exact tool call and result.
Dev setup
git clone https://github.com/fomoPhil/uad-console-mcp
cd uad-console-mcp
uv sync
uv run pytest # 500 tests, all against a mock engine, no hardware needed
uv run ruff check .A further 7 tests in tests/live/ are marked live: they need a real engine and a real Apollo,
and are excluded by default (addopts = -m 'not live'). Run them deliberately, never in CI:
UAD_MCP_LIVE=1 uv run pytest -m liveTwo rules in this codebase are not negotiable:
Never probe by writing against real hardware. The engine accepts writes to its own read-only properties. All write-behaviour tests run against
tests/mock_engine.py.Never hardcode a channel index, a monitor output index, or a slot count. Discovery reads them from the tree. The derived
twinfixture exists specifically to catch this.
The wire protocol is documented in docs/PROTOCOL.md, and the module contracts in docs/SPEC.md.
Acknowledgements and attribution
This is an independent implementation, informed by published community descriptions of the engine's local API. Credit where it is due:
raduvarga/UA-Midi-Control for the original public write-up of how the UA Mixer Engine's TCP API works. That write-up is the reason this project was possible at all.
Kalskiid/ua-commander (MIT) for the keepalive and monitor-discovery patterns.
uaaccess and open-apollo were referenced for orientation. Both are GPL-licensed, and no code or data from either was copied into this project.
The test fixture (
tests/fixtures/apollo_x8_gen2_tree.json) is our own read-only capture from our own Apollo x8 Gen 2.
Universal Audio has not been involved in this project. Please do not send them support requests about it.
License
MIT. See LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/fomoPhil/uad-console-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server