crestron-mcp 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +298 -0
- package/LICENSE +21 -0
- package/PROTOCOL.md +650 -0
- package/README.md +81 -0
- package/dist/config.js +71 -0
- package/dist/connection.js +728 -0
- package/dist/index.js +285 -0
- package/mcpb/manifest.json +76 -0
- package/package.json +55 -0
package/AGENT_GUIDE.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Crestron control: guide for an AI assistant
|
|
2
|
+
|
|
3
|
+
You have tools to query and control a Crestron AV system (lighting, AV, HVAC,
|
|
4
|
+
shades, etc.) over MCP. This document tells you how to use them well. Read it
|
|
5
|
+
before acting on the system.
|
|
6
|
+
|
|
7
|
+
## What you can do
|
|
8
|
+
|
|
9
|
+
Discover the rooms and devices, read the current state of any device, set
|
|
10
|
+
values, and smoothly ramp (fade) analog levels. You cannot add or remove
|
|
11
|
+
devices, change wiring, or configure the processor; you operate what the
|
|
12
|
+
installer exposed.
|
|
13
|
+
|
|
14
|
+
## Connecting (Claude Code / CLI)
|
|
15
|
+
|
|
16
|
+
The MCP server is a self-contained binary (no runtime to install). Register it
|
|
17
|
+
once, pointing at the processor:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
claude mcp add crestron \
|
|
21
|
+
-e CRESTRON_HOST=<processor-ip> \
|
|
22
|
+
-e CRESTRON_KEY=<secure-key> \
|
|
23
|
+
-s user \
|
|
24
|
+
-- /path/to/crestron-mcp-<platform>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or a project `.mcp.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"crestron": {
|
|
33
|
+
"command": "/path/to/crestron-mcp-<platform>",
|
|
34
|
+
"env": { "CRESTRON_HOST": "<processor-ip>", "CRESTRON_KEY": "<secure-key>" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Auth is config, not your concern at runtime: `CRESTRON_KEY` (recommended, secure
|
|
41
|
+
key, enables TLS automatically), or `CRESTRON_AUTH` (password mode), or neither
|
|
42
|
+
(open mode). If a call returns "authentication required", the credentials are
|
|
43
|
+
missing or wrong in the config, tell the user; you can't fix it from a tool call.
|
|
44
|
+
|
|
45
|
+
## The tools
|
|
46
|
+
|
|
47
|
+
- `discover_crestron_system()` - rooms, categories, device counts. Call this first.
|
|
48
|
+
- `list_crestron_rooms()` - rooms with device counts.
|
|
49
|
+
- `list_crestron_devices(room?, category?)` - devices, optionally filtered. Room
|
|
50
|
+
matching is case-insensitive and accepts the room name or id.
|
|
51
|
+
- `query_crestron_device(device_id)` - live state of one device: value plus whether
|
|
52
|
+
it's idle / ramping / pulsing, and any pending scheduled action (see Reading state).
|
|
53
|
+
- `get_crestron_time()` - the processor's current time (epoch ms + ISO 8601).
|
|
54
|
+
- `control_crestron_device(device_id, value, delay_ms?)` - set a device's value,
|
|
55
|
+
optionally after a delay.
|
|
56
|
+
- `set_crestron_devices([{device_id, value, duration_ms?, delay_ms?}, ...])` - apply a
|
|
57
|
+
scene in one call; each entry can fade (duration_ms, analog) and/or wait (delay_ms).
|
|
58
|
+
- `pulse_crestron_device(device_id, pulse_ms, delay_ms?)` - momentarily pulse a
|
|
59
|
+
DIGITAL device on then off (a simulated button press), optionally after a delay.
|
|
60
|
+
- `ramp_crestron_device(device_id, value, duration_ms)` - fade an ANALOG device
|
|
61
|
+
to a value over a duration.
|
|
62
|
+
- `cancel_crestron_device(device_id)` - stop a ramp/pulse in progress and clear any
|
|
63
|
+
pending delayed action on a device, without otherwise changing its value.
|
|
64
|
+
- `get_room_status(room_name)` - every device in a room with its state.
|
|
65
|
+
- `activate_crestron_license(license_key)` - license the processor with a key the user
|
|
66
|
+
provides (see Licensing / activation).
|
|
67
|
+
- `get_crestron_license_status()` - is it licensed now, is it a time-limited trial, how
|
|
68
|
+
much trial time remains, which trial of how many (`trial_seq` / `trial_max`), the MAC,
|
|
69
|
+
and a buy URL.
|
|
70
|
+
- `start_crestron_trial()` - start a free 7-day trial (no payment), if available.
|
|
71
|
+
|
|
72
|
+
## Licensing / activation
|
|
73
|
+
|
|
74
|
+
The processor must be licensed before it accepts any control or query command. If it
|
|
75
|
+
isn't, every tool returns an error that explains the fix and includes the processor's
|
|
76
|
+
**activation code (its MAC)**. There are two ways forward, both done in chat:
|
|
77
|
+
|
|
78
|
+
- **Free trial** - call `start_crestron_trial()`. It contacts Solution AV's licensing server,
|
|
79
|
+
which mints a signed trial bound to the processor's MAC and counts it there (that count is
|
|
80
|
+
what enforces the 3-per-processor limit). No account, no card, nothing to paste; the only
|
|
81
|
+
data sent is the processor's MAC, and the signed trial is then stored on the processor. The
|
|
82
|
+
result tells you how many of the 3 one-week trials remain. If asked, describe it accurately
|
|
83
|
+
(issued and counted online by the licensing server, then stored on the box); do not claim
|
|
84
|
+
nothing happens online. Offer this first to a user just trying it out.
|
|
85
|
+
- **Buy a license** - the user gets a key at the buy link (perpetual, AUD $249 inc GST,
|
|
86
|
+
bound to the MAC), pastes it in chat, and you call `activate_crestron_license(license_key)`.
|
|
87
|
+
|
|
88
|
+
**Always hand over the buy link itself.** Whenever buying is on the table (an unlicensed
|
|
89
|
+
box, or trials exhausted), give the user the full purchase URL with their MAC already
|
|
90
|
+
filled in, exactly as it appears in the result (the unlicensed-error guidance, or `buy_url`
|
|
91
|
+
on a `trials_exhausted` result). Offer it up front: don't shorten it to the bare domain, and
|
|
92
|
+
don't wait for the user to ask "where do I buy". The MAC-prefilled link is the single most
|
|
93
|
+
useful thing you can give them, so lead with it.
|
|
94
|
+
|
|
95
|
+
On success, retry whatever they originally asked for. The license (or trial) is stored
|
|
96
|
+
**on the processor**, so it stays licensed for every client and across reboots. A purchased
|
|
97
|
+
key is not a secret (it only works on that one processor, bound to its MAC), so it's fine
|
|
98
|
+
to receive it in chat.
|
|
99
|
+
|
|
100
|
+
**Trials and nudging.** A trial is a license with an expiry; `get_crestron_license_status`
|
|
101
|
+
reports `time_limited`, `remaining_human` (e.g. "2 days"), and, when present, `trial_seq` /
|
|
102
|
+
`trial_max` (e.g. 2 and 3). Weave these into conversation **naturally and sparingly** - e.g.
|
|
103
|
+
"you're on your 2nd of 3 free weeks, about 5 days left" when it's relevant, and as the time
|
|
104
|
+
gets low (under ~2 days) gently offer to start the next free trial or buy a license. The
|
|
105
|
+
sequence is a soft conversion cue (the 3rd trial is the last free one - mention that when on
|
|
106
|
+
it), but nudge, don't nag. When a trial lapses, control commands return the same unlicensed error again; just
|
|
107
|
+
offer the next trial or the buy link. Either way the underlying AV keeps running - licensing
|
|
108
|
+
only gates this natural-language layer.
|
|
109
|
+
|
|
110
|
+
**Describing how it works.** You can't see inside the tools. The trial result names its own
|
|
111
|
+
provenance (`issued_by`, `data_sent`, `stored`), so answer "how/where" questions from those:
|
|
112
|
+
starting a trial fetches a signed license from Solution AV's licensing server (only the
|
|
113
|
+
processor MAC is sent), which the processor then verifies and stores. Do not invent internal
|
|
114
|
+
mechanism: there is no on-box "trial function", and it is not "all local / nothing online". If
|
|
115
|
+
asked something a tool result doesn't tell you, say you don't have the internals rather than
|
|
116
|
+
guessing.
|
|
117
|
+
|
|
118
|
+
## Device model
|
|
119
|
+
|
|
120
|
+
Each device has an `id`, a human `name`, a `type`, an `access` mode, a `room`,
|
|
121
|
+
and (sometimes) a `category`.
|
|
122
|
+
|
|
123
|
+
IDs follow `<room>_<type><index>`, where type is:
|
|
124
|
+
- `d` = digital (on/off)
|
|
125
|
+
- `a` = analog (level 0-65535)
|
|
126
|
+
- `s` = serial (text)
|
|
127
|
+
|
|
128
|
+
The same index across types is usually the same physical function in different
|
|
129
|
+
facets, e.g. `Lounge_d3` (hallway light on/off), `Lounge_a3` (its dim level),
|
|
130
|
+
`Lounge_s3` (its text). Do not assume this, discover the actual devices and use
|
|
131
|
+
the exact ids you find.
|
|
132
|
+
|
|
133
|
+
## Value formats
|
|
134
|
+
|
|
135
|
+
- **digital**: `"0"` (off) or `"1"` (on).
|
|
136
|
+
- **analog**: `"0"` to `"65535"`. Treat as 0-100%: 0% = `"0"`, 50% = `"32768"`,
|
|
137
|
+
100% = `"65535"`. Convert a percentage the user asks for to this range.
|
|
138
|
+
- **serial**: arbitrary text (e.g. `"HDMI 1"`, `"play"`).
|
|
139
|
+
|
|
140
|
+
Values are always passed as strings.
|
|
141
|
+
|
|
142
|
+
## Ramps
|
|
143
|
+
|
|
144
|
+
`ramp_crestron_device` is ANALOG only and fades smoothly over `duration_ms`
|
|
145
|
+
(e.g. "fade the lounge lights to 50% over 3 seconds" -> ramp `Lounge_a3` to
|
|
146
|
+
`32768` over `3000`). It also takes an optional `delay_ms` to start the fade later
|
|
147
|
+
("fade down in 30 seconds, over 2 seconds" -> ramp with `duration_ms:2000,
|
|
148
|
+
delay_ms:30000`). For an instant analog change use `control_crestron_device`.
|
|
149
|
+
Ramping a digital or serial device is rejected.
|
|
150
|
+
|
|
151
|
+
## Scenes
|
|
152
|
+
|
|
153
|
+
`set_crestron_devices` applies many devices in one call, and each entry can carry its
|
|
154
|
+
own timing: `duration_ms` (fade, analog only - ramps instead of snapping) and/or
|
|
155
|
+
`delay_ms` (wait before it runs). So a scene can mix instant, fading, and delayed
|
|
156
|
+
actions. Prefer it over many separate calls when the user describes a named scene or
|
|
157
|
+
a multi-device change. Example "movie night": fade the lights down over 2s while the
|
|
158
|
+
screen drops and the projector switches on ->
|
|
159
|
+
`set_crestron_devices([{device_id:"Lounge_a3", value:"6553", duration_ms:2000},
|
|
160
|
+
{device_id:"Lounge_d5", value:"1"}, {device_id:"Lounge_d6", value:"1"}])`.
|
|
161
|
+
|
|
162
|
+
## Pulses (momentary presses)
|
|
163
|
+
|
|
164
|
+
`pulse_crestron_device` is DIGITAL only. It drives the device on for `pulse_ms`
|
|
165
|
+
then back off - a simulated momentary button press. Use it whenever the action is
|
|
166
|
+
a *press/trigger* rather than a steady on/off state: doorbells, gate/garage
|
|
167
|
+
triggers, projector or display power buttons, "tap"/"press"/"trigger" requests.
|
|
168
|
+
Use `control_crestron_device` instead when the user wants something to stay on or
|
|
169
|
+
off. Pulsing an analog or serial device is rejected. Typical `pulse_ms` is a few
|
|
170
|
+
hundred ms (e.g. `500`); follow the user if they specify a duration.
|
|
171
|
+
|
|
172
|
+
## Delays
|
|
173
|
+
|
|
174
|
+
`control_crestron_device` and `pulse_crestron_device` take an optional `delay_ms`:
|
|
175
|
+
the processor performs the action that many milliseconds later (e.g. "turn the
|
|
176
|
+
porch light on in 30 seconds" -> `control_crestron_device("porch_d1", "1", 30000)`).
|
|
177
|
+
The tool returns as soon as the action is *scheduled*, not when it runs, so don't
|
|
178
|
+
immediately query to confirm a delayed action - it hasn't happened yet. For
|
|
179
|
+
fade-over-time use `ramp_crestron_device` (delay is "wait, then act"; ramp is
|
|
180
|
+
"act gradually").
|
|
181
|
+
|
|
182
|
+
**Last command wins.** Each device holds only ONE pending scheduled action. Any new
|
|
183
|
+
command on that device (an immediate set/pulse/ramp, another scheduled action, or a
|
|
184
|
+
cancel) replaces the pending one. So you don't need to track timers: to change your
|
|
185
|
+
mind about a scheduled action, just issue the new intent, or `cancel_crestron_device`
|
|
186
|
+
to drop it entirely. The slot is per device id, so a pending action on `Lounge_a3`
|
|
187
|
+
(dim) is independent of one on `Lounge_d3` (on/off).
|
|
188
|
+
|
|
189
|
+
## Cancelling / stopping
|
|
190
|
+
|
|
191
|
+
`cancel_crestron_device(device_id)` halts whatever is happening on a device without
|
|
192
|
+
otherwise changing its value: it stops a ramp in progress (leaving the level where
|
|
193
|
+
it is), releases a pulse in progress to off, and clears any pending delayed action.
|
|
194
|
+
A device that is simply on (set high normally) stays on. Use it for "stop the fade",
|
|
195
|
+
"stop ringing the bell", "cancel that timer". It's safe to call when nothing is
|
|
196
|
+
running. To stop activity on every facet of a device, cancel each id (`_d`, `_a`,
|
|
197
|
+
`_s`) you care about.
|
|
198
|
+
|
|
199
|
+
## Reading state
|
|
200
|
+
|
|
201
|
+
`query_crestron_device` returns the **live picture** of a device, not just a value:
|
|
202
|
+
|
|
203
|
+
- `state: "idle"` - resting at `value`.
|
|
204
|
+
- `state: "ramping"` - analog fade in progress: `target` and `completes_at` (epoch
|
|
205
|
+
ms) plus `remaining_ms` (how long until it arrives).
|
|
206
|
+
- `state: "pulsing"` - digital momentary in progress: `releases_at` plus `remaining_ms`.
|
|
207
|
+
- `pending: { action, value, fires_at, in_ms }` - a delayed set/pulse is scheduled
|
|
208
|
+
(present on top of any state; `in_ms` is how long until it fires).
|
|
209
|
+
|
|
210
|
+
So one read distinguishes "13020 and resting" from "13020 and climbing to 60000"
|
|
211
|
+
from "0 now but a pulse fires in 8s". To verify a cancel, catch a pulse, or time a
|
|
212
|
+
fade, read once and look at `state` / `remaining_ms` / `pending`, don't poll a clock.
|
|
213
|
+
The `*_at` fields are epoch ms matching `get_crestron_time`; `remaining_ms` / `in_ms`
|
|
214
|
+
are self-contained, so prefer them for "how long to wait" (no clock needed).
|
|
215
|
+
|
|
216
|
+
Value accuracy still depends on the installer wiring real device state back to the
|
|
217
|
+
processor's feedback. If `value` consistently reads `0`/blank right after successful
|
|
218
|
+
writes, feedback isn't wired on that system, control still works, but you can't
|
|
219
|
+
confirm state by reading; tell the user rather than assuming the command failed.
|
|
220
|
+
(`state`/`pending`/ramp/pulse info comes from the processor itself, so it's accurate
|
|
221
|
+
regardless of feedback wiring.)
|
|
222
|
+
|
|
223
|
+
## Confirmation after an action
|
|
224
|
+
|
|
225
|
+
Every `control_crestron_device`, `ramp_crestron_device`, `pulse_crestron_device`, and
|
|
226
|
+
`set_crestron_devices` result already carries the outcome, so you usually do not need a
|
|
227
|
+
separate query:
|
|
228
|
+
|
|
229
|
+
- `status` - a short phrase: `"now 50000"`, `"fading to 0, ~10s left"`,
|
|
230
|
+
`"pulsing, releases in ~500ms"`, `"scheduled to set 1 in ~30s"`, or, on a
|
|
231
|
+
discrepancy, `"feedback reads 47000 (set 50000)"`.
|
|
232
|
+
- `confirmed` - the full live state (the same shape `query_crestron_device` returns).
|
|
233
|
+
|
|
234
|
+
Read these instead of assuming success. A mismatch in `status` (or `confirmed.value` not
|
|
235
|
+
matching what you set) is real signal: surface it rather than reporting "done".
|
|
236
|
+
|
|
237
|
+
One nuance for fades: the confirmation is taken the instant the action starts, so a ramp
|
|
238
|
+
reports the in-flight picture (`"fading to X, ~Ns left"`) and its target, not the final
|
|
239
|
+
resting value. That is deliberate (we never block for a multi-minute fade). To confirm a
|
|
240
|
+
long fade actually landed, re-query after `remaining_ms` (or `completes_at`) has passed;
|
|
241
|
+
the target in the status already tells you where it is heading.
|
|
242
|
+
|
|
243
|
+
Tuning (host-side): an immediate set waits a short settle (~350ms) for the feedback join
|
|
244
|
+
to move before reading back; ramps, pulses, and scheduled actions read instantly because
|
|
245
|
+
the processor tracks them itself. The host can set `CRESTRON_SETTLE_MS`, or disable the
|
|
246
|
+
read-back entirely with `CRESTRON_CONFIRM=0`.
|
|
247
|
+
|
|
248
|
+
## Time
|
|
249
|
+
|
|
250
|
+
`get_crestron_time()` returns the processor's clock as `epoch_ms` and `iso`. You
|
|
251
|
+
rarely need it just to wait (use `remaining_ms` / `in_ms` from a query instead), but
|
|
252
|
+
it's there to turn an absolute `*_at` into a human time, or whenever you need the
|
|
253
|
+
system's real date/time.
|
|
254
|
+
|
|
255
|
+
## Access modes
|
|
256
|
+
|
|
257
|
+
- `readwrite`: query and control both work.
|
|
258
|
+
- `readonly`: query works; control is rejected (`ACCESS_DENIED`).
|
|
259
|
+
- `writeonly`: control works; query is rejected.
|
|
260
|
+
|
|
261
|
+
Check `access` before trying to set a read-only device.
|
|
262
|
+
|
|
263
|
+
## Recommended workflow
|
|
264
|
+
|
|
265
|
+
1. `discover_crestron_system()` (or `list_crestron_rooms`) to learn the layout.
|
|
266
|
+
2. `list_crestron_devices(room)` to get exact ids for the room you care about.
|
|
267
|
+
3. Act with `control_crestron_device` / `ramp_crestron_device` using exact ids.
|
|
268
|
+
4. The action result already confirms the outcome (`status` / `confirmed`); query
|
|
269
|
+
separately only to re-check a long fade after it completes, or to read an unrelated device.
|
|
270
|
+
5. Prefer the user's words mapped to discovered ids; never invent an id.
|
|
271
|
+
|
|
272
|
+
## Examples
|
|
273
|
+
|
|
274
|
+
- "Turn on the lounge hallway light" -> `control_crestron_device("Lounge_d3", "1")`
|
|
275
|
+
- "Dim the lounge lights to 50%" -> `control_crestron_device("Lounge_a3", "32768")`
|
|
276
|
+
- "Fade the bedroom lights to 75% over 4 seconds" ->
|
|
277
|
+
`ramp_crestron_device("Bedroom_a3", "49152", 4000)`
|
|
278
|
+
- "Ring the front doorbell" -> `pulse_crestron_device("Lounge_d8", 500)`
|
|
279
|
+
- "Tap the projector power button" -> `pulse_crestron_device("Lounge_d5", 300)`
|
|
280
|
+
- "Turn the porch light on in 30 seconds" ->
|
|
281
|
+
`control_crestron_device("Lounge_d1", "1", 30000)`
|
|
282
|
+
- "Movie night" (lights fade, screen + projector on) ->
|
|
283
|
+
`set_crestron_devices([{device_id:"Lounge_a3", value:"6553", duration_ms:2000},
|
|
284
|
+
{device_id:"Lounge_d5", value:"1"}, {device_id:"Lounge_d6", value:"1"}])`
|
|
285
|
+
- "Fade the lounge down in 30 seconds over 2 seconds" ->
|
|
286
|
+
`ramp_crestron_device("Lounge_a3", "0", 2000, 30000)`
|
|
287
|
+
- "Stop the lounge fade" -> `cancel_crestron_device("Lounge_a3")`
|
|
288
|
+
- "Actually, cancel that" (after scheduling something) ->
|
|
289
|
+
`cancel_crestron_device(<that device id>)`
|
|
290
|
+
- "What's on in the dining room?" -> `get_room_status("Dining")`
|
|
291
|
+
- "Set the bedroom source to HDMI 1" -> `control_crestron_device("Bedroom_s4", "HDMI 1")`
|
|
292
|
+
|
|
293
|
+
## Errors
|
|
294
|
+
|
|
295
|
+
Tool results may include an `error` string or an `ERROR:<code>:<message>`. Common
|
|
296
|
+
ones: device not found (check the id with a list call), `ACCESS_DENIED` (wrong
|
|
297
|
+
access mode), `authentication required` (config issue, not yours to fix). Surface
|
|
298
|
+
the cause to the user rather than retrying blindly.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Solution AV Automation Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|