Model Context Protocol (MCP) server that enables Claude Desktop to control the entire BUControl room system via natural language commands.
This MCP server acts as a thin WebSocket wrapper around the BUControl system. Claude Desktop generates WindowCommand strings and control values directly - the server simply handles the WebSocket communication with the Node-RED bridge.
- Natural Language Control: Ask Claude to control your entire room in plain English
- Video Wall Control: Advanced multi-window display configurations with overlays and transparency
- Room Controls: Lights, volume, screen power, privacy glass, and DIDO output selection
- Real-time Status: Query current state of all devices and configurations
- Simple Integration: Claude generates commands, server handles communication
- Stateful Connection: Maintains persistent WebSocket connection for low latency
- Node.js 18.0.0 or higher
- BUControl system running (Node-RED + WebSocket bridge)
- Claude Desktop application
- Install dependencies:
cd packages/mcp-bucontrol-server
npm install- Configure Claude Desktop:
Edit your Claude Desktop MCP configuration file:
Windows: %APPDATA%\Claude\claude_desktop_config.json
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json
Add the BUControl server:
{
"mcpServers": {
"bucontrol": {
"command": "node",
"args": [
"c:\\BUControl\\bucontrol\\packages\\mcp-bucontrol-server\\index.js"
]
}
}
}Note: Use absolute paths. On Windows, escape backslashes or use forward slashes.
- Restart Claude Desktop
The MCP server will automatically:
- Fetch the WebSocket port from the Config API
- Connect to the WebSocket bridge
- Identify as a client
- Discover the BUControl Video Wall Controller component
- Subscribe to state updates
To use from an iPad or other device on your local network:
- Start the remote server:
cd packages/mcp-bucontrol-server
npm run start:remote- Note the network address shown in the output:
[MCP] Network: http://192.168.1.100:3100
From your iPad, use: http://192.168.1.100:3100/sse
-
On your iPad (using Claude.ai in browser or Claude app):
- The app should auto-detect MCP servers on your network
- Or manually add the server URL:
http://YOUR-IP:3100/sse
-
That's it! You can now control your room from your iPad using natural language.
Once configured, you can ask Claude naturally:
Video Wall Commands:
- "Show input 1 full screen on the video wall"
- "Display input 2 and input 3 side by side"
- "Put input 1 full screen with input 3 in the bottom right corner"
- "Show input 1 full screen with an overlay of input 3 at 60% opacity"
- "Display input 2 full screen with a small PIP of input 4 in the top right corner at 25% size"
Room Control Commands:
- "Turn on the screen"
- "Turn off the screen"
- "Dim the lights to 40%"
- "Set the lights to full brightness"
- "Turn the lights off"
- "Frost the privacy glass"
- "Clear the privacy glass"
- "Enable the DIDO output so I can see the video wall"
- "Set the volume to 0 dB"
- "Lower the volume to -20 dB"
- "Mute the audio" (sets to -100 dB)
- "Increase volume to -10 dB"
Combined Commands:
- "Dim the board room lights and pull input1 full screen on the video wall with an overlay of input 3 full screen as well but with opacity 60"
- "Turn on the screen, set lights to 75%, set volume to -10 dB, clear the privacy glass, and show input 2 full screen"
- "Prepare the room for a presentation: lights at 50%, screen on, privacy glass clear, volume at 0 dB, show input 1"
- "Set up for movie mode: lights at 20%, volume at -5 dB, frost the privacy glass, show input 1 full screen"
Status Queries:
- "What's currently displayed on the video wall?"
- "Which video sources are connected?"
- "What's the current lighting level?"
- "What's the current volume level?"
- "Is the screen on?"
- "Is the privacy glass frosted?"
- "Show me the status of all room controls"
-
You say: "Show input 1 full screen with input 3 overlay at 60% opacity"
-
Claude understands and generates the WindowCommand:
BV1:E:A1:2:W1S1X0Y0W100H100A0:W2S3X0Y0W100H100A60 -
MCP server sends command via WebSocket to Node-RED bridge
-
Bridge validates and forwards to Q-SYS Core
-
Q-SYS sends commands to Aurora DIDO hardware
-
Display updates with new configuration
The server exposes 13 tools to Claude Desktop:
Send WindowCommand to control video wall display.
Parameters:
command(string): WindowCommand in compact text format
Example:
{
"command": "BV1:E:A1:2:W1S1X0Y0W100H100A0:W2S3X0Y0W100H100A60"
}Get current hardware state from Aurora DIDO.
Returns:
- Current display configuration
- Active windows with geometry and transparency
- Source routing and audio configuration
List all video inputs and their connection status.
Returns:
- IN1-IN4 connection status
- VPX encoder information
- Source names/labels
Turn the HDMI display screen on or off.
Parameters:
enabled(boolean): true to turn on, false to turn off
Get the current screen power state.
Returns:
- enabled (boolean): whether screen is on or off
Control the privacy glass frosting.
Parameters:
frosted(boolean): true to frost (private), false to clear (transparent)
Get the current privacy glass state.
Returns:
- frosted (boolean): whether glass is frosted or clear
Enable or disable DIDO output on the video wall (required to see the display).
Parameters:
enabled(boolean): true to enable, false to disable
Get the current DIDO output state.
Returns:
- enabled (boolean): whether DIDO output is active
Set the room lighting level (Lutron LEAP Zone 1).
Parameters:
level(number): 0-100 (0=off, 100=full brightness)
Examples:
- Full: 100
- Normal: 75
- Dim: 40-50
- Very dim: 20-30
- Off: 0
Get the current room lighting level.
Returns:
- level (number): Current lighting level (0-100)
Set the main audio output volume level (Mixer_8x8_2 Output 1 Gain).
Parameters:
level(number): -100 to +10 dB
Examples:
- Maximum: +10 dB
- Normal/comfortable: 0 dB
- Quiet: -20 dB
- Very quiet: -40 dB
- Minimum: -100 dB (essentially muted)
Get the current main audio output volume level.
Returns:
- level (number): Current volume level in dB (-100 to +10)
- unit (string): "dB"
Claude generates commands in this format:
BV<version>:<flags>:<audio>:<count>:<window1>:<window2>:...[:<windowN>]
- BV<version>: Protocol identifier (always "BV1")
- <flags>: E=enabled, D=disabled
- <audio>: Audio output - A1, A2, A3, or A4 (maps to IN1-IN4)
- <count>: Number of windows (1-4)
- <windowN>: Window definition
W<id>S<src>X<x>Y<y>W<w>H<h>A<a>
- W<id>: Window ID (1-4, z-order, 1=bottom)
- S<src>: Source input (1-4 = IN1-IN4)
- X<x>: X position (0-100%)
- Y<y>: Y position (0-100%)
- W<w>: Width (1-100%)
- H<h>: Height (1-100%)
- A<a>: Alpha transparency (0=opaque, 100=transparent)
- Window 1 alpha MUST be 0 (opaque bottom layer)
- Geometry must not overflow: X+W ≤ 100, Y+H ≤ 100
- All values must be integers
Fullscreen:
BV1:E:A1:1:W1S1X0Y0W100H100A0
IN1 full screen, audio from IN1
Side-by-Side:
BV1:E:A2:2:W1S2X0Y0W50H100A0:W2S3X50Y0W50H100A0
IN2 left half, IN3 right half, audio from IN2
Picture-in-Picture:
BV1:E:A2:2:W1S2X0Y0W100H100A0:W2S3X70Y70W25H25A0
IN2 full screen, IN3 small corner (25% size), audio from IN2
Transparent Overlay:
BV1:E:A1:2:W1S1X0Y0W100H100A0:W2S3X0Y0W100H100A60
IN1 full screen opaque, IN3 full screen 60% transparent overlay
┌─────────────────────────────┐
│ Claude Desktop │
│ (Natural Language) │
└──────────┬──────────────────┘
│ MCP Protocol
↓
┌─────────────────────────────┐
│ MCP Server (This Package) │
│ - Generates WindowCommand │
│ - Manages WebSocket │
└──────────┬──────────────────┘
│ Socket.IO
↓
┌─────────────────────────────┐
│ WebSocket Bridge │
│ (Node-RED) │
│ Port: 3002 (dev)/3001 (prod)│
└──────────┬──────────────────┘
│
↓
┌─────────────────────────────┐
│ Q-SYS Core │
│ BUVideoController Plugin │
└──────────┬──────────────────┘
│ TCP
↓
┌─────────────────────────────┐
│ Aurora DIDO │
│ 192.168.100.67:6970 │
└──────────┬──────────────────┘
│
↓
Physical Display
The server automatically discovers configuration:
- Fetches port from Config API (
http://localhost:1881/api/config) - Connects to WebSocket bridge on discovered port
- Identifies with platform metadata
- Discovers BUControl Video Wall Controller component
- Subscribes to state updates
- Controller ID:
modular-controller-config - Config API Port:
1881(development) - WebSocket Port:
3002(development),3001(production) - Hostname:
localhost
Check Node-RED is running:
# Development
curl http://localhost:1881/api/config
# Should return:
# {"ports":{"websocket":3002,"nodeRed":1881,"frontend":5174}}Check Claude Desktop logs:
- Windows:
%APPDATA%\Claude\logs\ - macOS:
~/Library/Logs/Claude/ - Linux:
~/.config/Claude/logs/
Verify WebSocket connection: The server logs to stderr (visible in Claude Desktop logs):
[MCP] Connecting to http://localhost:3002...
[MCP] Connected to WebSocket bridge
[MCP] Client identified: <client-id>
[MCP] Found video controller: <component-name> (<component-id>)
[MCP] MCP server ready
Check command format: Commands must follow WindowCommand protocol exactly. Claude should generate valid commands based on the tool description.
If get_videowall_status returns "unknown":
- Hardware state is polled every 1 second from DIDO
- Wait a few seconds after connection
- Check Q-SYS Core is communicating with DIDO (192.168.100.67:6970)
npm run devChanges automatically restart the server.
You can test the WebSocket connection without Claude Desktop:
# Run test script from project root
node scripts/test-windowcommand-e2e.cjsThis validates the entire pipeline: WebSocket → Node-RED → Q-SYS → DIDO
See documentation:
docs/BUVIDEOCONTROLLER_DESIGN_SPEC.md- System architecturedocs/FLUTTER_WEBSOCKET_CLIENT.md- WebSocket protocol detailsscripts/test-windowcommand-e2e.cjs- E2E test examplesscripts/test-windowcommand.cjs- Validation logic examples
MIT
For issues or questions:
- Check logs in Claude Desktop logs directory
- Verify Node-RED WebSocket bridge is running
- Test with
test-windowcommand-e2e.cjsscript - Review protocol documentation in
docs/