-
Notifications
You must be signed in to change notification settings - Fork 0
MCP Server
The MCP server runs on the Pi at port 5001 and exposes the lighting rig as a Model Context Protocol endpoint. Any MCP-capable LLM client — Claude Desktop, ChatGPT, Cursor, custom agent — can discover fixtures, scenes, and groups and issue commands without hand-rolling REST integrations.
Endpoint: http://lights.local:5001/mcp (Streamable HTTP transport)
The MCP server provides:
-
Discovery tools — List fixtures (with
.qxf-derived channel info), groups, saved scenes, templates, and live channel values - Action tools — Activate scenes, apply templates, adjust brightness/color, fade, generate AI scenes, save scenes, set raw channels
-
Workspace resource — One-shot context dump (
lights://workspace) for the LLM to load at session start -
Sibling-service deployment — runs as
lighting-mcp.service, ordered after the Flask control server so the rig boots fully wired
./lightsctl.sh mcp-installThis creates a Python venv on the Pi, installs mcp[cli] + httpx, copies the server code, and sets up lighting-mcp.service ordered after lighting-control.service.
The MCP server is a thin wrapper over the control server's REST API. It holds no QLC+ connection of its own — it makes HTTP calls into the Flask app on localhost:5000. This preserves the "single writer" invariant on the persistent QLC+ WebSocket and makes the MCP process stateless and crash-safe to restart.
LLM agent ──MCP/HTTP──▶ lighting-mcp.service ──HTTP──▶ lighting-control.service ──WS──▶ QLC+
:5001/mcp :5000 :9999
When an LLM calls adjust_color("warm"), the MCP server posts directly to POST /api/action on Flask, bypassing the natural-language AI interpreter — the LLM is already on the other end of the MCP socket, so re-running a structured tool call through another LLM would waste latency.
See MCP_SERVER.md for the full technical deep-dive.
| Tool | Returns |
|---|---|
get_status |
AI provider, QLC+ service, workspace, WebSocket state |
list_fixtures |
All fixtures with .qxf-derived channel_info
|
get_fixture_channels |
Per-channel role/preset/colour for one fixture |
list_groups |
Fixture groups (named subsets) |
list_scenes |
Saved scene functions in workspace |
list_templates |
Built-in templates (party, ambient, …) |
get_channel_values |
Live DMX channel snapshot |
| Tool | Effect |
|---|---|
activate_scene |
Apply existing saved scene by name or numeric ID |
apply_template |
Apply a built-in template, optionally to a list of groups |
adjust_brightness |
Set/nudge master/dimmer (0-255, '75%', '+30', '-20') |
adjust_color |
Set a color preset (red, warm, cool, …) with optional intensity |
fade |
Fade brightness to target over N seconds |
generate_scene |
AI-synthesize a scene from a description and apply live |
set_channel |
Direct DMX channel write (power-user escape hatch) |
save_scene |
Persist a scene XML (e.g. from generate_scene) to workspace |
snapshot_scene |
Capture current live state as a new saved scene |
blackout |
Instantly zero every channel on targeted fixtures (kill-all) |
batch_action |
Execute an ordered list of actions in a single round trip |
identify_fixture |
Flash a single fixture so the operator can locate it physically |
| Tool | Effect |
|---|---|
create_group |
New named subset from a fixture-ID list |
delete_group |
Remove a group |
update_group |
Rename, change description, or replace fixture list |
add_fixtures_to_group |
Append fixtures to an existing group |
remove_fixtures_from_group |
Remove fixtures from an existing group |
| Tool | Effect |
|---|---|
describe_scene |
Return per-fixture channel values for a saved scene |
delete_scene |
Remove a saved scene from the workspace |
rename_scene |
Rename a scene (and/or move its folder Path) |
duplicate_scene |
Copy a scene under a new name |
| URI | Payload |
|---|---|
lights://workspace |
One-shot dump: status + fixtures + groups + scenes + templates |
Add to your MCP config:
{
"mcpServers": {
"qlc-lights": {
"url": "http://lights.local:5001/mcp"
}
}
}npx @modelcontextprotocol/inspector http://lights.local:5001/mcpfrom mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async with streamablehttp_client("http://lights.local:5001/mcp") as (r, w, _):
async with ClientSession(r, w) as s:
await s.initialize()
tools = await s.list_tools()
result = await s.call_tool("adjust_color", {"color": "warm", "intensity": "70%"})Env vars (set in /home/<user>/mcp-server/.env, loaded by the systemd unit):
| Variable | Default | Notes |
|---|---|---|
CONTROL_URL |
http://localhost:5000 |
Flask backend URL |
MCP_HOST |
0.0.0.0 |
Bind address |
MCP_PORT |
5001 |
Listen port |
MCP_PATH |
/mcp |
Streamable HTTP mount path |
MCP_BEARER_TOKEN |
(unset) | Reserved for auth — scaffolded, not enforced |
MCP_HTTP_TIMEOUT |
30 |
Seconds for upstream Flask calls |
./lightsctl.sh mcp-status # systemctl status lighting-mcp.service
./lightsctl.sh mcp-logs # journalctl -u lighting-mcp.service -n 50
./lightsctl.sh mcp-restart # restart after .env or code changes
./lightsctl.sh mcp-uninstall # disable, remove unit, drop firewall ruleMCP_BEARER_TOKEN is plumbed through the systemd unit and read at startup, but not yet enforced. To enable, wrap mcp.streamable_http_app() with an ASGI middleware that checks the Authorization: Bearer … header. FastMCP also supports a full OAuth provider — overkill for a LAN rig, but the right choice if the endpoint is ever exposed off-network through the nginx/stunnel TLS proxy.