Labsco
randybritsch logo

Control4 MCP Server

2

from randybritsch

A safe-by-default MCP server that exposes your Control4 home automation (lights, scenes, locks, thermostats, and media) as structured tools over HTTP and Claude Desktop STDIO for reliable AI-powered control on your local network.

🔥🔥🔥✓ VerifiedFreeQuick setup

c4-mcp

<!-- mcp-name: io.github.randybritsch/c4-mcp -->

Turn your Control4 system into a Model Context Protocol (MCP) toolset, so any MCP-capable client (Claude Desktop, custom agents, scripts) can query rooms/devices and safely run automations.

CI License: MIT

Why this is cool

  • Works with real MCP clients: HTTP transport for dev/scripts + STDIO JSON-RPC for clients like Claude Desktop.
  • One integration point for many clients: use the same toolset from Claude Desktop, scripts, or your own agents without rewriting Control4 logic.
  • Structured tool schemas = fewer mistakes: explicit inputs/outputs (room/device IDs, levels, setpoints, etc.) reduce ambiguity vs. prompt-only automations.
  • Safe-by-default controls: optional write guardrails, read-only mode, and allow/deny lists for state-changing tools.
  • Session memory for follow-ups: enables natural multi-step flows like “turn on the basement lights… now dim those lights”.
  • Smarter “lights” semantics: room-based lighting ops avoid accidentally targeting fans/heaters/outlets.
  • One-command validation: an end-to-end runner exercises HTTP + STDIO so you can ship changes with confidence.
  • Tunable performance: inventory caching + env-configurable timeouts for slower Control4 projects.
  • Credentials stay local: keep config.json on your machine (gitignored) and choose local-only STDIO or LAN HTTP based on your risk tolerance.

What you can do

  • Discover rooms/devices by name, category, and room (plus resolvers for “best-effort” name-based calls).
  • Activate scenes, control shades, query variables/commands, and (optionally) change state (lights/locks/thermostat/media).
  • Use it as a local “home automation brain” for chat + agents without hard-coding your project’s device IDs.

Non-negotiable rules

  1. c4-mcp must remain decoupled from any specific client app (including c4-mcp-app).

    • Integration is via MCP over HTTP/STDIO only.
    • No shared code or cross-repo imports; clients should treat c4-mcp as an external dependency.
  2. The client (AI/app) owns command interpretation; c4-mcp owns execution + safety.

    • The client decides which tools to call and with what arguments (and in what sequence).
    • c4-mcp validates inputs, enforces guardrails, executes tool calls, and returns structured results/ambiguity.

Example prompts (copy/paste)

These work well in MCP clients like Claude Desktop (the client will call tools under the hood):

Copy & paste — that's it
List all rooms.

Show me the lights in the Basement.

Turn on the basement lights.

Now dim those lights to 30%.

Activate the "Movie Time" scene in the Living Room.

Which doors are currently unlocked?

Advanced prompts (hard in the stock Control4 app UI)

These are examples of the kind of cross-device, conditional, and multi-step requests that are awkward (or not possible) to do purely in the standard Control4 app UI without building custom automation logic elsewhere.

Note: prompts that change state (lights/locks/thermostat/media) require C4_WRITES_ENABLED=true. Read-only prompts (inventory/status/reporting) work fine with the safe default C4_WRITES_ENABLED=false.

Copy & paste — that's it
Run a “Good Night” sweep: turn off all lights except Hallway (10%), lock all exterior doors, set Downstairs thermostat to 68°F, then report what succeeded/failed.

If any door is unlocked, lock it — but do NOT lock the Garage door.

Find anything in the Basement that is currently on (lights, outlets), list it, then turn off everything except the dehumidifier outlet.

I’m leaving: turn off all AV devices, activate the “Away” scene, and confirm the house is secured (all locks locked).

The Basement lights are on — tell me which specific loads are on and turn off only the ones that are above 50%.

Compare the Living Room lights vs. Kitchen lights: which room is brighter right now? Then set them to match.

Do a safety check: list any unlocked doors, any lights left on in the Basement, and the current thermostat setpoints for each zone.

Tip: If you run with C4_WRITE_GUARDRAILS=true and C4_WRITES_ENABLED=false, you’ll get a safe read-only experience until you explicitly enable writes.

Ambiguity & disambiguation (recommended)

Name-based tools can legitimately return multiple matches (e.g., “Basement” might match several rooms). In that case, c4-mcp returns a structured failure with an ambiguous marker and a candidate list.

Recommended client pattern:

  1. Call the name-based tool with include_candidates=true (or accept the default if the tool always includes them).
  2. If the response indicates ambiguity, show the candidates to the user and let them pick.
  3. Re-call the tool with require_unique=true and a more specific scope (e.g., room_id / room_name, or exact device_name).

This is how higher-level apps can support natural commands like “turn on the basement lights” while still being deterministic and safe.

Direct HTTP examples (no MCP client required)

If you’re not using an MCP client yet, you can still call the server directly.

List tools:

  • GET http://127.0.0.1:3333/mcp/list

Synology/Compose note:

  • Inside Docker/Compose, c4-mcp commonly listens on :3333.
  • On the NAS/LAN, it’s often published as host port :3334 → container :3333.
    • Example: GET http://<NAS_IP>:3334/mcp/list

Call a tool (example: list rooms):

PowerShell:

Copy & paste — that's it
$base = 'http://127.0.0.1:3333'  # or: http://<NAS_IP>:3334
Invoke-RestMethod -Method Post -Uri ($base + '/mcp/call') -ContentType 'application/json' -Body (
	@{ kind = 'tool'; name = 'c4_list_rooms'; args = @{} } | ConvertTo-Json -Depth 10
)

curl:

Copy & paste — that's it
curl -s http://127.0.0.1:3333/mcp/call \
	-H "Content-Type: application/json" \
	-d '{"kind":"tool","name":"c4_list_rooms","args":{}}'

PowerShell tips (Windows)

1) /mcp/list returns a tool map (not an array).

In PowerShell, tools is a PSCustomObject where each property name is a tool name.

Copy & paste — that's it
$r = Invoke-RestMethod -Method Get -Uri 'http://127.0.0.1:3333/mcp/list' -TimeoutSec 10
$toolNames = $r.tools.PSObject.Properties.Name | Sort-Object
"tools_count=$($r.tools.PSObject.Properties.Count)"
$toolNames | Select-Object -First 25

2) Quick start/stop on Windows (detached, logs captured).

This avoids confusion around multiple terminals / Ctrl+C and makes it easy to inspect server logs.

Copy & paste — that's it
# Safe-by-default: guardrails on, writes off
$env:C4_WRITE_GUARDRAILS='true'
$env:C4_WRITES_ENABLED='false'
$env:PYTHONUTF8='1'

New-Item -ItemType Directory -Force -Path logs | Out-Null
$p = Start-Process -FilePath .\.venv\Scripts\python.exe -ArgumentList @('app.py') -PassThru -WindowStyle Hidden `
  -RedirectStandardOutput 'logs\http_server_out.txt' -RedirectStandardError 'logs\http_server_err.txt'
$p.Id | Set-Content -Encoding ascii 'logs\http_server.pid'
"started_pid=$($p.Id)"

# Sanity check
Test-NetConnection 127.0.0.1 -Port 3333 | Select-Object TcpTestSucceeded

Stop it later:

Copy & paste — that's it
Stop-Process -Id (Get-Content .\logs\http_server.pid)

If /mcp/list hangs or errors, check logs/http_server_err.txt.

Hosting on a NAS (Synology) — LAN only

The HTTP server is designed to run locally. To run it on a NAS and reach it from other machines on your LAN:

  • Bind to all interfaces with C4_BIND_HOST=0.0.0.0 (default is localhost-only).
  • Keep it LAN-only using Synology firewall rules (recommended) or a VPN (for remote access later).

Docker (recommended on Synology)

This repo includes a Dockerfile and docker-compose.yml.

On Synology (Container Manager), run a compose project that:

  • Publishes port 3333 to your LAN.
  • Mounts your real config.json (keep credentials off git).
  • Keeps writes off by default: C4_WRITES_ENABLED=false.

Before you start the compose project, create your local config file:

  • Copy config.example.jsonconfig.json and fill in values (this repo ignores config.json).

docker-compose.yml already sets C4_BIND_HOST=0.0.0.0.

LAN-only note: do not expose port 3333 to the internet. Use Synology Firewall to allow only your LAN subnet (e.g. 192.168.0.0/16) to reach TCP 3333.

Troubleshooting Synology builds

If Container Manager fails with an error like:

unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /volume1/...

That means Docker cannot find or access the folder you selected as the build context (the folder that should contain your Dockerfile and source code).

Fix:

  • Put the repo files on the NAS under a real shared-folder path, e.g. /volume1/docker/c4-mcp/.
  • Ensure that folder contains at least: Dockerfile, docker-compose.yml, requirements.txt, app.py, and the Python modules.
  • In Container Manager, create the Compose project using that exact folder as the project path (don’t point it at just /volume1/docker/ unless the files are actually there).
  • If your shared folder isn’t on volume1, use the correct volume (e.g. /volume2/...).

Host/port env vars

  • C4_BIND_HOST (default 127.0.0.1)
  • C4_PORT (default 3333)

Example (LAN): C4_BIND_HOST=0.0.0.0 and C4_PORT=3333

Security / publishing note (read this)

This project talks to your Control4 system using credentials (and often a local controller IP).

  • Never commit real credentials. Keep config.json local-only (it is ignored by .gitignore).
  • If you accidentally committed credentials at any point, rotate them immediately and rewrite git history before making the repo public.

Public GitHub checklist (do this before you publish)

  • Ensure config.json is not in git history. At minimum it should not be tracked in your current tree.
    • Quick check: git ls-files config.json should return nothing.
    • If it was ever committed: rotate your Control4 password and rewrite history (e.g., git filter-repo), then force-push.
  • Prefer C4_CONFIG_PATH (pointing to a file outside the repo) for the safest setup.

Registry metadata

This section is meant to be copy/paste-friendly for MCP registries and "server list" directories.

  • Name: c4-mcp
  • Category: Home Automation / Control4
  • Repo: https://github.com/randybritsch/c4-mcp
  • License: MIT
  • Transports:
    • STDIO (JSON-RPC): claude_stdio_server.py (for Claude Desktop and other stdio-based MCP clients)
      • HTTP: app.py (defaults to http://127.0.0.1:3333; override with C4_BIND_HOST/C4_PORT; endpoints: /mcp/list, /mcp/call)
  • Configuration / secrets:
    • Recommended: set C4_CONFIG_PATH to a local config.json that contains host, username, password (keep this file gitignored)
    • Optional: set C4_HOST (non-secret) to override host from config.json
    • Optional: set C4_USERNAME/C4_PASSWORD (secret) via OS env vars (must be provided together)
  • Safety defaults (recommended):
    • For read-only-by-default runs: C4_WRITE_GUARDRAILS=true + C4_WRITES_ENABLED=false
    • Optional filters: C4_WRITE_ALLOWLIST / C4_WRITE_DENYLIST
      • Scheduler Agent writes are additionally gated: c4_scheduler_set_enabled requires C4_SCHEDULER_WRITES_ENABLED=true

Python version note (important)

This project depends on flask-mcp-server, which in turn depends on pydantic/pydantic-core. At the time of writing, Python 3.14 will not work out-of-the-box on Windows because pydantic-core does not ship wheels for it yet.

Use Python 3.12 (recommended) or another version with pydantic-core wheels available.

Run

  • Start server: python app.py
  • Verify MCP is up: GET http://127.0.0.1:3333/mcp/list

End-to-end validation (one command)

This starts the HTTP server in read-only guardrails mode, runs the HTTP validator suite, runs both STDIO validators, and then stops the server.

Windows (PowerShell):

  • .\.venv\Scripts\python.exe tools\run_e2e.py

macOS / Linux (bash/zsh):

  • ./.venv/bin/python tools/run_e2e.py

If you already have the server running and only want to run validators:

Windows (PowerShell):

  • .\.venv\Scripts\python.exe tools\run_e2e.py --no-server --base-url http://127.0.0.1:3333

macOS / Linux (bash/zsh):

  • ./.venv/bin/python tools/run_e2e.py --no-server --base-url http://127.0.0.1:3333

Discover IDs

Device and room ids are specific to your Control4 project. Use:

  • c4_list_rooms
  • c4_find_rooms / c4_resolve_room (search by name)
  • c4_list_devices (by category)
  • c4_list_devices category: shades (best-effort discovery)
  • c4_list_devices category: scenes (best-effort; based on UI Buttons)
  • c4_find_devices / c4_resolve_device (search by name; optional category/room filters)
  • c4_resolve (resolve room + device together; device resolution can be scoped to the resolved room)

Shades / blinds (best-effort)

If your project has shades/blinds, try:

  • c4_shade_list
  • c4_shade_get_state (returns position 0-100 when available)
  • c4_shade_open / c4_shade_close / c4_shade_stop
  • c4_shade_set_position

Troubleshooting:

  • Use c4_item_commands(device_id) to see the exact command names your shade driver exposes.
  • Use c4_item_variables(device_id) to inspect which variable contains position/level.

Then pass the discovered ids into tools like c4_media_watch_launch_app or the scripts in tools/.

If you prefer name-based calls (no ids), use c4_media_watch_launch_app_by_name.

Lighting scenes (best-effort)

Control4 "scenes" vary by project. In many projects they show up as UI Button devices.

Try:

  • c4_scene_list (alias of c4_uibutton_list)
  • c4_scene_activate(device_id) (alias of c4_uibutton_activate)
  • c4_scene_activate_by_name(scene_name, room_name=...) (best-effort resolver + activate)

Troubleshooting:

  • Use c4_item_commands(device_id) to see which command(s) a given scene/button supports.
  • Read-only validator: .\.venv\Scripts\python.exe tools\validate_scenes.py --show-commands