MCP server for Control4 driver development — enables AI coding agents to build, deploy, test, and debug Lua drivers on real controllers
drivers-driverworks-c4-mcp-server
MCP server for Control4 Lua driver development. Enables AI coding agents to build, deploy, test, and debug drivers on real controllers over the local network.
Works with any MCP-compatible agent: OpenCode, Claude Code, Cursor, Windsurf, etc.
Requirements
Host Machine
- Node.js >= 20
- Python 3 (for driver packaging and scaffolding)
drivers-createc4zcloned as a sibling directory (i.e.,../drivers-createc4z/relative to this repo) — required forc4_build_driverdrivers-jumpstartcloned as a sibling directory (i.e.,../drivers-jumpstart/) — required forc4_scaffoldonly
Controller
- A Control4 controller reachable on your local network
- LRDB debugging requires the LRDB feature flag on the controller. The file
/etc/features/lrdbmust exist on the controller's filesystem. Without it,c4_debug_enablewill fail. You can create it via SSH:touch /etc/features/lrdb(requires root/admin access to the controller)
Installation
git clone https://github.com/snap-one/drivers-driverworks-c4-mcp-server.git
cd drivers-driverworks-c4-mcp-server
npm install
npm run build
Agent Configuration
OpenCode
Add to your OpenCode MCP config (.opencode/config.json or global config):
{
"mcp": {
"c4": {
"type": "stdio",
"command": "node",
"args": [
"/absolute/path/to/drivers-driverworks-c4-mcp-server/dist/index.js"
]
}
}
}
Claude Code
claude mcp add c4 node /absolute/path/to/drivers-driverworks-c4-mcp-server/dist/index.js
Cursor
Add to .cursor/mcp.json:
{
"mcpServers": {
"c4": {
"command": "node",
"args": [
"/absolute/path/to/drivers-driverworks-c4-mcp-server/dist/index.js"
]
}
}
}
Generic (any MCP client)
The server uses stdio transport. Run it as:
node /path/to/drivers-driverworks-c4-mcp-server/dist/index.js
Typical Workflow
1. c4_connect → authenticate to controller
2. c4_scaffold → generate a new driver skeleton (or start from existing source)
3. c4_build_driver → package source into a .c4z
4. c4_upload_and_install → install the driver into a room
5. c4_push_lua → inject Lua code for rapid iteration
6. c4_read_output → read print()/C4:ErrorLog() output
7. c4_eval_lua → inspect variables and state
8. Iterate on steps 5-7
9. c4_build_driver → rebuild with final code
10. c4_upload_and_update → push the updated .c4z
Tools
Connection
| Tool | Description |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| c4_connect | Connect to a controller. Params: host, user, password, optional port (default 443). Authenticates via local JWT and establishes a persistent connection. All other tools require this first. |
| c4_disconnect | Disconnect from the current controller. |
| c4_status | Check connection status — shows host, connected state, permissions. |
Drivers
| Tool | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| c4_list_drivers | List all items (drivers/agents) on the controller with id, name, type, and room. |
| c4_driver_info | Get detailed info for a device, including properties and optionally variables. Params: deviceId, optional includeProperties (default true), includeVariables (default false). |
| c4_list_rooms | List all rooms on the controller. |
| c4_build_driver | Package driver source into a .c4z using CreateC4Z.py. Params: name, srcDir, optional dstDir, encrypt. Always adds -d flag for C4:AllowExecute(true). |
| c4_upload_and_update | Upload a .c4z and update the existing driver in the project. Params: filepath. |
| c4_upload_and_install | Upload a .c4z and install it as a new driver in a room. Params: filepath, roomId. |
| c4_delete_driver | Delete a driver from the controller project. Params: deviceId. |
Lua
| Tool | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| c4_push_lua | Inject and execute Lua code on a running driver. Params: deviceId, code, optional bufferName. Auto-starts output capture. |
| c4_eval_lua | Evaluate a Lua expression and return the result. Params: deviceId, expression. Returns the tostring() of the result. |
| c4_read_output | Read captured print/dbg/C4:ErrorLog output. Params: optional deviceId, since (timestamp), limit, startCapture. |
| c4_clear_output | Clear the output buffer. Params: optional deviceId. |
| c4_get_property | Get a driver property value, or list all properties. Params: deviceId, optional name. |
| c4_set_property | Set a driver property via C4:UpdateProperty(). Params: deviceId, name, value. |
| c4_send_command | Send a command to a driver. Params: deviceId, command, optional params. |
Debug (LRDB)
Requires the LRDB feature flag on the controller (see Requirements).
| Tool | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| c4_debug_enable | Start an LRDB debug session on a driver. Connects a debug client over TCP. Params: deviceId. |
| c4_debug_disable | Stop a debug session and disconnect. Params: deviceId. |
| c4_debug_add_breakpoint | Set a breakpoint at a line in a Lua source file. Supports optional condition (Lua expression) and hit_condition (e.g. >=5, %3). Params: deviceId, file, line. |
| c4_debug_add_logpoint | Set a logpoint — logs a message without pausing. log_message can contain {expr} interpolation. Params: deviceId, file, line, log_message. |
| c4_debug_remove_breakpoints | Clear all breakpoints in a Lua source file. Params: deviceId, file. |
| c4_debug_get_breakpoints | List all breakpoints with hit counts. Params: deviceId. |
| c4_debug_continue | Resume execution after a breakpoint or pause. Params: deviceId. |
| c4_debug_step | Step to the next line (step over). Params: deviceId. |
| c4_debug_step_in | Step into the function call on the current line. Params: deviceId. |
| c4_debug_step_out | Step out of the current function. Params: deviceId. |
| c4_debug_pause | Pause the driver at the next opportunity. Params: deviceId. |
| c4_debug_get_stacktrace | Get the call stack when paused. Returns file, function, and line for each frame. Params: deviceId. |
| c4_debug_get_locals | Get local variables at the current breakpoint. Params: deviceId, optional stackLevel, depth. |
| c4_debug_get_upvalues | Get upvalue (closure) variables at the current breakpoint. Params: deviceId, optional stackLevel, depth. |
| c4_debug_eval | Evaluate a Lua expression in the debugger context with access to locals, upvalues, and globals. Params: deviceId, expression, optional stackLevel. |
| c4_debug_wait_for_pause | Wait for the debugger to pause (breakpoint hit, step complete, etc.). Returns pause reason and position. Params: deviceId, optional timeoutMs. |
Scaffolding
| Tool | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| c4_scaffold | Generate a new driver project from a design spec using JumpStart. Params: optional designPath or design (JSON object), outputDir, environmentPath. |
Resources
The server exposes reference documentation as MCP resources:
| Resource URI | Description |
| ----------------------------------- | --------------------------------------------------------------------------------- |
| c4://api-reference | Complete listing of all C4:* API functions available in the driver Lua sandbox. |
| c4://api-reference/{functionName} | Detailed docs for a specific API function (parameters, return values, examples). |
| c4://common-libraries/{name} | EmmyLua type definitions for Control4 common Lua libraries. |
| c4://docs | Index of all available DriverWorks development documentation topics. |
| c4://docs/{topic} | Full content of a documentation topic (e.g. driver-architecture, proxy-lifecycle, light-v2-reference, development-workflow). |
| c4://driver/{deviceId}/output | Live captured output from a specific driver. |
Development
# Run tests
npm test
# Unit tests only
npm run test:unit
# MCP protocol tests
npm run test:mcp
# Integration tests (requires a real controller)
npm run test:integration
# Type check
npm run typecheck
# Dev mode (tsx, no build needed)
npm run dev
Architecture
src/
├── index.ts # Entry point (stdio transport)
├── server.ts # createServer() — registers all tools & resources
├── types.ts # Shared TypeScript types
├── core/
│ ├── apiClient.js # Socket.IO client (adapted from nodeControl4APIClient)
│ ├── authService.ts # Local JWT auth + privilege escalation
│ ├── brokerClient.ts # Broker REST/socket API (items, drivers, commands, Lua)
│ ├── connection.ts # Singleton connection manager with output buffering
│ └── outputBuffer.ts # Ring buffer for driver output capture
├── debug/
│ ├── index.ts # LRDB debug module exports
│ ├── Client.ts # LRDB debug client (JSON-RPC 2.0)
│ ├── JsonRpc.ts # JSON-RPC framing
│ ├── TypedEventEmitter.ts
│ └── Adapter/
│ ├── index.ts
│ └── TcpAdapter.ts # TCP adapter for LRDB connection
├── tools/
│ ├── connection.ts # c4_connect, c4_disconnect, c4_status
│ ├── drivers.ts # c4_list_drivers, c4_driver_info, c4_list_rooms, c4_build_driver, c4_upload_*, c4_delete_driver
│ ├── lua.ts # c4_push_lua, c4_eval_lua, c4_read_output, c4_clear_output, c4_get/set_property, c4_send_command
│ ├── debug.ts # 16 debug tools: enable/disable, breakpoints, logpoints, stepping, stack/locals/upvalues, eval, wait
│ └── scaffold.ts # c4_scaffold
├── resources/
│ └── index.ts # MCP resources (API docs, libraries, dev docs, driver output)
data/
├── c4-api-completions.json # API completion data
└── c4-api-details.json # Detailed API function documentation
test/
├── unit/ # Unit tests (mocked broker)
├── mcp/ # MCP protocol tests (in-memory transport)
└── integration/ # Integration tests (real controller)
Connection Model
The server maintains a single persistent connection to one controller at a time. The connection lifecycle:
c4_connect— authenticates viaPOST /api/v1/localjwt, optionally escalates to SUPERUSER privileges, establishes socket.io connection to the broker.- Tools operate — all
c4_*tools use the active connection. If not connected, they return errors. c4_disconnect— tears down the socket.io connection and stops token refresh.
Token refresh happens automatically in the background. If the controller becomes unreachable, tools will return connection errors and the agent can reconnect.
Auth & Privilege Escalation
The server authenticates using the controller's local JWT endpoint. By default, local users may have read-only permissions. The server automatically escalates to SUPERUSER privileges by:
- Checking the JWT's
Permissionsclaim for/directoraccess - If missing, finding the Identity Agent via the items API
- Sending an
UPDATE_USERcommand withSUPERUSER: true - Re-authenticating immediately to get an elevated JWT
This escalation code can be removed for production deployments where users already have the correct permissions.