使用说明
eos-mcp
An MCP server for controlling ETC Eos family lighting consoles (Eos, Gio, Ion, ETCnomad) over OSC. It exposes the console's command line, playback, selection, levels, colour, position, faders, direct selects, and macros as MCP tools, and mirrors the console's live status so a model can read back what it just did.
Implements the OSC surface documented in Using OSC with Eos.
This drives real lights. Commands take effect immediately on whatever the console is connected to. Point it at ETCnomad offline, or a console in Blind, until you trust it.
How it works
Two independent UDP paths:
commands -> EOS_PORT_TX (console's "OSC UDP RX Port")
eos-mcp ──────────────────────────────────────────────────────► Eos console
◄──────────────────────────────────────────────────────
status <- EOS_PORT_RX (console's "OSC UDP TX Port")
Status is push-based: the server knows only what the console has sent it. Nothing is
polled. Call sync_state once at the start of a session to ask Eos to publish its current
state, and again whenever data looks stale.
eos_mcp/
├── app.py FastMCP instance
├── server.py entry point: logging, listener startup, mcp.run()
├── config.py EosConfig, validated from the environment
├── state.py console state + lock + snapshot()
├── errors.py EosConfigError / EosValidationError / EosSendError
├── logging_setup.py stderr logging (stdout carries the MCP protocol)
├── osc/
│ ├── address.py validates anything interpolated into an OSC address
│ ├── client.py EosClient - lazy socket, structured send errors
│ └── listener.py OscListener - receives status, folds it into state
└── tools/ one module per area of the console
Prerequisites
- An Eos family console or ETCnomad
uv— no separate Python install or clone needed;uvfetches Python and the server itself.
Setup
1. Enable OSC on the console. Browser → Setup → System Settings → System → Show Control → OSC:
| Setting | Value |
|---|---|
| OSC RX | Enabled |
| OSC TX | Enabled |
| OSC UDP RX Port | 8000 |
| OSC UDP TX Port | 8001 |
| OSC UDP TX IP Address | the machine's LAN IP — not 127.0.0.1 |
OSC UDP RX Port must match this server's EOS_PORT_TX, and OSC UDP TX Port must match its
EOS_PORT_RX. They are named from the console's point of view, so they look crossed over.
OSC UDP TX IP Address must be a real interface address, never 127.0.0.1. Eos does not
bind its transmit socket to loopback, so a loopback address there silently sends to nobody —
even when the console and this server are the same machine. Use the machine's LAN IP
(ipconfig getifaddr en0 on macOS, ipconfig on Windows). Receiving is unaffected: this
server binds 0.0.0.0, and commands sent to Eos on 127.0.0.1 work normally.
This asymmetry is the single most common setup failure, and it looks like a working setup: Eos reports TX as enabled, commands land, and every query stays empty.
These are show file settings, not console-wide ones. Loading a different show — an incoming company's file, for instance — reverts them, and they have to be set again.
2. Add it to your MCP client.
For Claude Desktop, add this to claude_desktop_config.json
(Settings → Developer → Edit Config):
{
"mcpServers": {
"eos": {
"command": "uvx",
"args": ["--from", "git+https://github.com/MaybeItsAdam/eos-mcp", "eos-mcp"],
"env": {
"EOS_IP": "127.0.0.1",
"EOS_PORT_TX": "8000",
"EOS_PORT_RX": "8001"
}
}
}
}
uvx fetches and caches the server straight from GitHub on first launch — nothing to clone or
install by hand. Restart Claude Desktop after editing the config. Any MCP client that launches a
command works the same way; drop the command/args/env above into its config format.
To pin to a specific commit instead of tracking main, append @<commit-sha> to the repo URL,
e.g. "git+https://github.com/MaybeItsAdam/eos-mcp@<sha>".
The server also publishes a system_instructions prompt covering how to sync state and when
to confirm before acting.
Configuration
All optional. Defaults suit ETCnomad running on the same machine.
| Variable | Default | Description |
|---|---|---|
EOS_IP |
127.0.0.1 |
Console address to send commands to. |
EOS_PORT_TX |
8000 |
Port commands are sent to. Match the console's OSC UDP RX Port. |
EOS_PORT_RX |
8001 |
Port status is received on. Match the console's OSC UDP TX Port. |
EOS_RX_HOST |
0.0.0.0 |
Local interface the listener binds to. |
EOS_LOG_LEVEL |
INFO |
DEBUG logs every OSC message sent and received. |
Invalid values are rejected at startup with a message naming the variable.
Tools
Every tool returns a structured result with an ok field. ok: false means the command never
reached the console, and error says why — check it rather than assuming success.
Diagnostics — get_connection_health, sync_state, get_show_info,
get_channel_parameters
get_channel_parameters reports what a channel is doing — every live parameter
value — as opposed to what has been assigned to it. It is how you tell a running
effect from one that was recorded and does nothing. Eos has undo, covering Record,
Update and Delete: press_key("undo").
Show data — list_show_targets, get_show_inventory, audit_show
Enumeration answers "what already exists?". Eos replaces an occupied target
without warning and offers no undo, so check before recording. audit_show reports
duplicate labels, labels differing only by case, and unlabelled targets.
Status — get_active_cue, get_pending_cue, get_live_blind_state, get_command_line,
get_selection, get_faders, get_direct_selects, get_system_state
Command line — command_line (anything the console can do, including destructive
operations)
Levels — set_level, apply_modifier, set_parameter, set_parameter_mod, set_dmx
Selection — select_channel (accepts "5" or "1 Thru 10"), select_group,
select_effect, select_curve, select_pixel_map, select_address_target,
open_magic_sheet
Playback — fire_cue (point cues supported), go_cue, stop_back_cue, reset_osc
Colour and position — set_color_rgb, set_color_hs, set_color_xy, set_pan_tilt,
set_xyz
Faders and direct selects — configure_fader_bank, configure_direct_selects,
load_to_fader, set_fader, control_fader_button, press_direct_select
Assigning a target to a fader is not command line syntax — Fader 6 Sub 5 is a
syntax error. Eos loads a fader by putting the target on the command line and pressing
that fader's Load button, which is what load_to_fader sends.
A bank must be created before Eos sends any labels or levels for it, so
get_faders and get_direct_selects stay empty until configure_fader_bank /
configure_direct_selects has been called. sync_state creates a default bank of ten.
Presets — fire_preset, fire_palette, recall_snapshot, bump_sub
Keys and macros — press_key, press_softkey, fire_macro
Encoders — wheel_level, wheel_parameter, switch_parameter
Cue list banks — config_cue_list_bank, page_cue_list_bank, select_cue_list_bank_cue,
reset_cue_list_bank
Argument ranges and valid values are declared in each tool's schema, so out-of-range input is rejected before it reaches the console.
Troubleshooting
Start with get_connection_health — it distinguishes the failure modes below.
Queries say "no data reported yet". Nothing has been requested. Call sync_state. If it
reports no reply, the console is not sending. Check, in this order:
- OSC UDP TX IP Address is the machine running this server (
127.0.0.1if that is the console). Blank or stale is the usual culprit — Eos shows TX as enabled and transmits to nobody, so the console's own settings screen looks correct. - OSC TX is enabled.
- OSC UDP TX Port equals
EOS_PORT_RX.
The two directions are independent, and the failure is asymmetric: commands still land, so the console visibly responds while every query stays empty. To tell a wrong port from silence altogether, listen on the port directly — nothing arriving on any port means TX is off or misdirected, rather than mismatched:
nc -ulp 8001 # or: python3 -c "import socket;s=socket.socket(2,2);s.bind(('',8001));print(s.recvfrom(9999))"
Because these live in the show file, loading a different show silently reverts them.
get_connection_health reports a bind error. Another process holds EOS_PORT_RX, often a
second copy of this server. Stop it, or set EOS_PORT_RX to a free port and change the
console's OSC UDP TX Port to match.
Commands return ok: false with a send error. The console is unreachable at EOS_IP.
Check the address, and that both machines are on the same network. UDP is fire-and-forget, so a
successful send only means the packet left this machine.
Commands report ok: true but nothing happens. The packets are leaving this machine, but
the console is not acting on them: check OSC RX is enabled and that its OSC UDP RX Port
equals EOS_PORT_TX. Also confirm the console is in Live rather than Blind —
get_live_blind_state.
Firewall. Both ports are UDP. On Windows, allow the Eos application and the Python interpreter through the firewall.
Nothing works and there is no log output. Set EOS_LOG_LEVEL=DEBUG. Logs go to stderr;
your MCP client may hide them.
Development
git clone https://github.com/MaybeItsAdam/eos-mcp
cd eos-mcp
uv sync --all-groups
uv run pytest # test suite; no console required
uv run ruff check .
uv run ruff format .
uv run mypy
To point a client at your local checkout instead of GitHub, use
"command": "uv", "args": ["--directory", "/absolute/path/to/eos-mcp", "run", "eos_server.py"]
in place of the uvx block above.
The tests build genuine OSC datagrams and run them through the real python-osc dispatcher, and the integration tests bind a listener on an ephemeral port, so the receive path is covered without hardware.
Two rules the test suite enforces mechanically:
- Never write to stdout. Under the stdio transport stdout carries JSON-RPC frames; a stray
print()corrupts the session. Useeos_mcp.logging_setup.get_logger. (tests/test_no_stdout.py) - Validate anything interpolated into an OSC address. A
/in a tool argument redirects the message to a different address on a live console. Use the helpers ineos_mcp/osc/address.py.
Licence
MIT — see LICENSE.