Indigo MCP Server Plugin
by mlamoure
README.md
# Indigo MCP Server Plugin
A Model Context Protocol (MCP) server plugin that lets AI assistants like Claude search, understand, and
control your [Indigo](https://www.indigodomo.com/) home automation system through natural language.
- "Find all light switches in the bedroom"
- "What devices are currently on?"
- "Turn on the garage lights" / "Execute the bedtime scene"
- "What trigger turned off the porch light last night?"
## Requirements
- **Indigo Domotics** 2025.2 or later (ships Python 3.13)
- **macOS** 10.15 (Catalina) or later
- **Apple Silicon** (M-series) only — LanceDB 0.30+ no longer ships Intel Mac wheels
- **OpenAI API key** for semantic search ([get one](https://platform.openai.com/api-keys)). Only device
names, types, and descriptions are sent, to generate embeddings (minimal cost) — see
[Privacy & Security](#privacy--security).
- **Node.js** (for Claude Desktop only, which connects via `npx mcp-remote`): `brew install node`
## Installation
1. Install the **MCP Server** plugin in Indigo via the Plugin Manager.
2. Enter your **OpenAI API key** in the plugin's preferences.
3. Add a new **MCP Server** device in Indigo (this creates the actual server; one per install).
4. Wait for the plugin to **index your database** — the first run takes a while.
5. **Connect an MCP client** — see below.
Optional integrations, all off by default:
- **InfluxDB** — required for the `analyze_historical_data` tool. Works with InfluxDB 1.x and
InfluxDB 3 (via its v1-compatibility API): use an `https://` URL to enable TLS, and for v3 enter a
database token as the password (the username is ignored by v3).
- **Event Webhooks** — real-time outbound push notifications; requires a server you run, **not** stock
Claude Desktop. See [Event Subscriptions & Webhooks](#event-subscriptions--webhooks). *(v2026.1.0)*
- **LangSmith** — AI prompt tracing for debugging; most people don't need it.
## Connecting an MCP Client
### 1. Get an API key
Every connection authenticates with an Indigo API key sent as `Authorization: Bearer <key>`. Two kinds:
- **Reflector API key** — from your Indigo Reflector settings. Use for remote/HTTPS access.
- **Local secret** — for local/LAN access. Add one to
`/Library/Application Support/Perceptive Automation/Indigo <VERSION>/Preferences/secrets.json`
([format](https://wiki.indigodomo.com/doku.php?id=indigo_2024.2_documentation:indigo_web_server#local_secrets)),
then restart the Indigo Web Server.
### 2. Pick your endpoint URL
The endpoint path is always `/message/com.vtmikel.mcp_server/mcp/`. Choose the base by where you connect
from (default Web Server port is `8176`):
| Access | Endpoint URL | Key |
|--------|--------------|-----|
| Same machine as Indigo | `http://localhost:8176/message/com.vtmikel.mcp_server/mcp/` | Local secret |
| Another machine on your LAN | `http://<indigo-ip>:8176/message/com.vtmikel.mcp_server/mcp/` | Local secret |
| Remote (outside your network) | `https://<your-reflector>.indigodomo.net/message/com.vtmikel.mcp_server/mcp/` | Reflector key |
For HTTPS on the LAN with a self-signed certificate, use the `https://<indigo-host>:8176/...` URL and see
the self-signed note in the client examples below.
### 3. Configure your client
**VS Code, Cursor, Claude Code** support direct HTTP transport — simpler and more reliable. Add to your MCP
settings (`.vscode/mcp.json`, Cursor MCP settings, or `~/.claude.json` / project `.mcp.json`):
```json
{
"mcpServers": {
"indigo": {
"type": "http",
"url": "http://localhost:8176/message/com.vtmikel.mcp_server/mcp/",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
```
Swap the `url` for the LAN or Reflector variant from the table above.
**Claude Desktop** does not support direct HTTP, so it proxies through `mcp-remote`. Add to
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"indigo": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://your-reflector.indigodomo.net/message/com.vtmikel.mcp_server/mcp/",
"--header", "Authorization:Bearer YOUR_API_KEY"
]
}
}
}
```
- **Plain HTTP** (local/LAN): add `"--allow-http"` to `args`.
- **Self-signed HTTPS** (LAN): add `"env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }` to the server block.
> **Why the difference?** `mcp-remote` requests OAuth endpoints that Indigo doesn't implement; direct HTTP
> transport avoids that entirely, so prefer it whenever your client supports it.
### HTTP transport notes
The endpoint uses the MCP streamable-HTTP transport over the Indigo Web Server: `POST` carries all
messages and `GET` returns `405` (no server→client SSE stream).
**Supported protocol revisions** (both served concurrently on the same endpoint):
| Revision | Style | Notes |
|----------|-------|-------|
| `2026-07-28` | Stateless ("modern") | Per-request `_meta` metadata, `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers, `server/discover`. No sessions. |
| `2025-11-25`, `2025-06-18` | Session-based ("legacy") | `initialize` handshake + `Mcp-Session-Id` header; sessions expire after 2 hours idle (current Indigo Web Server versions reject the `DELETE` teardown before it reaches the plugin). **Deprecated** — legacy support will be removed in a future release once MCP clients have migrated to `2026-07-28`. |
A request carrying the modern per-request metadata is served statelessly; an `initialize` request
selects the legacy session flow. Server-push features (`subscriptions/listen`, `listChanged`
notifications) are not offered — Indigo Web Server plugin responses are one-shot, so there is no
SSE channel to deliver them on. `Origin`-header validation is likewise not enforced: the Indigo Web
Server authenticates every request before the plugin sees it, which already defeats DNS rebinding.
## What's Possible
What the plugin can and can't do with each kind of Indigo entity.
| Entity | Read | Create | Edit definition | Control / run | Delete |
|--------|------|--------|-----------------|---------------|--------|
| **Devices** | ✓ full — state, properties, model, type, address | ✗ | ✗ | ✓ on/off, brightness, RGB/RGBW color, thermostat setpoints & modes | ✗ |
| **Variables** | ✓ full — value, folder | ✓ `variable_create` | ~ value only | — | ✗ |
| **Action groups** | ✓ full — every action step (scripts, plugin configs) | ✗ | ~ name / description | ✓ execute, duplicate, move | ✓ *gated* |
| **Triggers** | ✓ full — event, condition tree, action steps | ✗ | ~ name / description, event settings | ✓ enable/disable (timed auto-revert), execute, duplicate, move, remove delayed actions | ✓ *gated* |
| **Schedules** | ✓ full — timing, next run, condition tree, action steps | ✗ | ~ name / description | ✓ enable/disable (timed auto-revert), execute, duplicate, move, remove delayed actions | ✓ *gated* |
**Legend:** ✓ supported · ~ partial (see the cell) · ✗ not possible · — not applicable.
- Action steps, conditions, and schedule timing are not editable via MCP — change those in the Indigo UI.
- To make a variant: duplicate an automation (`control_trigger` / `control_schedule` /
`control_action_group`, `action: "duplicate"`), then edit the copy.
- **Delete** requires the *Allow AI to delete automations* preference (off by default) and `confirm=true`.
## Available Tools
Most list and search tools paginate with `limit` (default 50, max 500) and `offset`, and return
`total_count` / `has_more` for navigation.
### Search and query
- **search_entities** — natural-language search across devices, variables, action groups, triggers, and schedules
- **list_devices** — all devices, with optional state filtering
- **list_variables** / **list_variable_folders** — variables (with values) and their folders
- **list_action_groups** — action groups / scenes
- **get_devices_by_state** — devices matching state conditions
- **get_devices_by_type** — devices of a type (dimmer, relay, sensor, …)
- **get_device_by_id** / **get_variable_by_id** / **get_action_group_by_id** — exact lookups
### Automation introspection *(v2026.6.0)*
Inspect triggers, schedules, and action groups in full — including the action steps and condition trees
that Indigo's scripting API does not expose (read from the server's database file, refreshed within
minutes of a change).
- **list_triggers** — triggers with a one-line summary of what each watches; filter by name/type/enabled/folder
- **list_schedules** — schedules with **next execution time** and a timing summary
- **get_trigger_details** / **get_schedule_details** / **get_action_group_details** — the full explanation of
one automation: its event/timing, condition tree (or embedded Python condition script), and every action
step (device commands, variable writes, nested action groups, embedded Python, plugin actions with config),
IDs resolved to names
- **find_automation_references** — reverse lookup: which automations watch, act on, set, or condition-read a
device/variable/action group — including indirect paths through nested action groups, heuristic id/name
matches inside embedded Python (condition scripts and action scripts), cross-checked against Indigo's own
dependency graph
### Investigation *(v2026.6.0)*
- **investigate_event** — "what caused this?" Finds a device's state-change in the log, collects the
automations that fired around it, and ranks candidate causes by structural evidence (does it actually act
on that device, directly or through action-group chains?) plus temporal proximity.
- **query_event_log** — read the event log, newest first. With no filters it returns the recent tail from
Indigo's live log; add `query`/`regex`/`types`/`start_time`/`end_time` to scan the full historical daily
log files instead. Each entry is `{timestamp, type, message}`.
### Automation control *(v2026.6.0)*
- **control_trigger** / **control_schedule** / **control_action_group** — lifecycle actions: `enable`/`disable`
(with a `duration_seconds` auto-revert — "disable this trigger for 2 hours"), `execute`, `duplicate`,
`move_to_folder`, `remove_delayed_actions`, and `delete`. (Action groups support execute/duplicate/move/delete
only.) **Delete** requires `confirm=true` **and** the *Allow AI to delete automations* preference (off by
default); every other action is always available.
- **update_trigger** — edit a trigger's name/description and its event settings (watched device/variable,
comparison, value), returning a before/after diff.
- **update_schedule** / **update_action_group** — edit name/description only.
Action steps, conditions, and schedule timing are read-only in Indigo's scripting API — change those in the
Indigo UI. Since there's no API to author actions from scratch, `duplicate` (via `control_trigger`) followed by
`update_trigger` is the supported way to make a trigger variant.
### Device control
- **device_turn_on** / **device_turn_off** — power state
- **device_set_brightness** — dimmer level (0–100 or 0–1)
- **device_set_rgb_color** / **device_set_rgb_percent** / **device_set_hex_color** / **device_set_named_color**
(954 XKCD colors + aliases) / **device_set_white_levels** — RGB / RGBW control
- **thermostat_set_heat_setpoint** / **thermostat_set_cool_setpoint** / **thermostat_set_hvac_mode** /
**thermostat_set_fan_mode** — thermostat control
### Variables, actions, and system
- **variable_create** / **variable_update** — create or update variables
- **action_execute_group** — run an action group / scene
- **list_plugins** / **get_plugin_by_id** / **get_plugin_status** / **restart_plugin** — plugin management
- **analyze_historical_data** — AI analysis of device/variable history (requires InfluxDB). Pass
`time_range_hours` for "last N hours" questions; `time_range_days` cannot express windows shorter
than a day. Numeric sensors also get a current/min/max/mean/trend summary, and a warning when the
value never moved during the window — usually the sign of a sensor that has stopped reporting.
### Event subscriptions *(v2026.1.0, only when webhooks are enabled)*
- **create_event_subscription** — POST a JSON event to your webhook URL when device/variable conditions match
- **list_event_subscriptions** — active subscriptions with delivery health stats (or one by ID)
- **delete_event_subscription** — delete a subscription (cancels pending dwell timers)
See [Event Subscriptions & Webhooks](#event-subscriptions--webhooks) for the full guide.
## Plugin-Provided Tools *(v2026.8.1)*
Other Indigo plugins can contribute their own MCP tools to this server. A provider plugin
ships a small manifest in its bundle; this server discovers it automatically, lists the
tools to AI clients under the provider's prefix (e.g. `autolights_update_zone` from the
[Auto Lights](https://github.com/mlamoure/indigo-auto-lights) plugin), and forwards calls
to the provider. No configuration is needed on this server.
- **Write gate**: the plugin config checkbox "Allow plugin-provided tools to make changes"
(default on) controls whether provider *write* tools work; read tools always do. Takes
effect immediately.
- **Refresh**: provider tools register automatically when the provider starts or updates.
The menu item **Plugins → MCP Server → Rescan Plugin-Provided MCP Tools** forces a
re-discovery.
- **Client cache caveat**: `tools/list` responses carry a 1-hour cache hint, so an
already-connected MCP client may not see newly installed provider tools until it starts
a fresh session.
### Add MCP tools to your plugin
Three steps — full details and rules in
[docs/mcp-provider-manifest.md](docs/mcp-provider-manifest.md):
**1. Ship a manifest** at `Contents/Resources/mcp-manifest.json` describing your tools:
```json
{
"manifest_version": 1,
"provider": {"plugin_id": "com.example.myplugin", "display_name": "My Plugin"},
"tools": [
{
"name": "get_status",
"description": "Return the plugin's current status.",
"write": false,
"inputSchema": {"type": "object", "properties": {}, "required": []}
}
]
}
```
**2. Answer tool calls** — declare a hidden action in `Actions.xml`:
```xml
<Action id="mcp_tool_invoke" uiPath="hidden">
<Name>MCP Tool Invocation Endpoint</Name>
<CallbackMethod>handle_mcp_tool_invoke</CallbackMethod>
</Action>
```
and implement the callback in `plugin.py` (arguments arrive as a JSON string; reply with a
JSON-string envelope):
```python
def handle_mcp_tool_invoke(self, action, dev=None, caller_waiting_for_result=True):
import json
tool = action.props.get("tool", "")
args = json.loads(action.props.get("arguments", "{}"))
if tool == "get_status":
return json.dumps({"status": "ok", "result": {"state": "running"}})
return json.dumps({"status": "error",
"error": {"type": "not_found", "message": f"unknown tool {tool}"}})
```
**3. Announce yourself** (recommended) — one line in your `startup()` so your tools
register the moment your plugin starts, without an MCP Server restart:
```python
try:
indigo.server.broadcastToSubscribers("mcp_tools_updated")
except Exception:
pass
```
Your plugin stays fully functional for users who don't have the MCP Server installed — the
manifest is inert data, the hidden action is never called, and the broadcast is a no-op.
## Event Subscriptions & Webhooks
*Added in v2026.1.0.* Event subscriptions let an MCP client ask Indigo to notify it the next time something
happens — "the next time the front door opens", "if the temperature goes above 80°F", "if the garage door
stays open for 10 minutes".
> ### ⚠️ This is an *outbound* webhook — you must run your own server
>
> When a subscription's conditions match, the plugin sends an **HTTP POST** to a URL **you provide**. It is a
> **sender only** — there is no built-in receiver. **This will not work with stock Claude Desktop** or most
> off-the-shelf MCP clients, which have no way to receive a proactive notification. It's meant for custom
> agents / automation systems that own a persistent HTTP endpoint (for example, [OpenClaw](https://openclaw.ai/)).
Enable it under **Plugins → MCP Server → Configure → Enable Event Webhooks** (the three tools are hidden
until then).
### Creating a subscription
`create_event_subscription` accepts:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `webhook_url` | string | **yes** | HTTP(S) endpoint **you run** that events are POSTed to. |
| `entity_type` | `"device"` \| `"variable"` | **yes** | What kind of entity to watch. |
| `conditions` | object | **yes** | State conditions that trigger the webhook (see operators below). |
| `entity_id` | integer | no | A specific device/variable ID, or omit to watch **all** entities of that type. |
| `auth` | object | no | `{ "mode": "none"\|"bearer"\|"hmac", "token": "…", "verify_ssl": true }` (see Authentication). |
| `duration_seconds` | integer (≥1) | no | **Dwell time** — the condition must stay matched this long before firing. If it reverts first, nothing is sent. |
| `max_fires` | integer (≥1) | no | Auto-delete the subscription after this many successful deliveries. Use `1` for a one-shot notification. Omit for unlimited. |
| `description` | string | no | Human-readable label for the subscription. |
A webhook fires on the **transition into** a matching state (not repeatedly while it stays matched). Multiple
conditions are combined with **AND**.
```python
# Notify me once, the next time the front door opens
create_event_subscription(
webhook_url="https://my-server.example.com/indigo-hook",
entity_type="device", entity_id=12345,
conditions={"onState": True}, max_fires=1,
description="Front door opened",
)
# Alert me if the garage door stays open for 10 minutes
create_event_subscription(
webhook_url="https://my-server.example.com/indigo-hook",
entity_type="device", entity_id=67890,
conditions={"onState": True}, duration_seconds=600,
description="Garage left open",
)
```
### Condition operators
Conditions match against device/variable state keys (including third-party plugin states). Use simple
equality, or an operator object per key:
```jsonc
{ "onState": true } // equality
{ "brightness": { "gt": 50 } } // single operator
{ "temperatureInput1": { "gt": 80 }, "onState": true } // AND of multiple keys
```
| Operator | Meaning |
|----------|---------|
| `eq` / `ne` | equal to / not equal to |
| `gt` / `gte` | greater than / greater than or equal |
| `lt` / `lte` | less than / less than or equal |
| `contains` | substring is contained in the value |
| `regex` | value matches the regular expression |
**Variables** match on their `value` key. Indigo stores every value as a **string**, but booleans and
numbers in your conditions are coerced automatically, so `{ "value": true }`, `{ "value": { "eq": "open" } }`,
and `{ "value": { "gt": 50 } }` all work. To fire on *every* change regardless of the new value, use
`{ "any_change": true }` — variables only, and not combinable with `duration_seconds`.
### Authentication
Set via the `auth` parameter; your receiver should validate it so only your Indigo server can post to your
endpoint.
- **`none`** (default) — no auth headers.
- **`bearer`** — adds `Authorization: Bearer <token>`.
- **`hmac`** — adds `X-Webhook-Signature: sha256=<hexdigest>` (`HMAC-SHA256(token, raw_body_bytes)`) and
`X-Webhook-Timestamp: <unix-seconds>`. Verify on the receiver:
```python
import hmac, hashlib
expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-Webhook-Signature"])
```
Set `"verify_ssl": false` only if your receiver uses a self-signed certificate.
### The webhook payload
Each delivery is a `POST` with `Content-Type: application/json`, the headers `X-Event-Id`, `X-Event-Type`
(`device.state_changed` | `variable.value_changed`), and `X-Subscription-Id` (plus any auth headers), and a
body like:
```json
{
"event_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"schema_version": "1.0",
"dedupe_key": "indigo:device:12345:state:onState:True",
"source": { "system": "indigo", "plugin": "com.vtmikel.mcp_server", "host": "my-indigo-mac" },
"timestamp": "2026-06-01T15:30:45.123456+00:00",
"event_type": "device.state_changed",
"entity": { "kind": "device", "id": 12345, "name": "Front Door", "device_type": "…" },
"state": { "changed_keys": ["onState"], "old": { "onState": false }, "new": { "onState": true } },
"trigger": { "subscription_id": "…", "conditions_matched": { "onState": true } },
"human": { "title": "Front Door state changed", "summary": "Front Door: onState=true" }
}
```
Variable changes use `event_type: "variable.value_changed"`, `entity.kind: "variable"`, and a `state` of
`{ "changed_keys": ["value"], "old": { "value": "…" }, "new": { "value": "…" } }`.
### Delivery behavior
- **At-least-once** — retries mean an event can arrive more than once; your receiver **must deduplicate by
`event_id`** (or `dedupe_key`).
- **Retries** — up to 4 attempts (1 + 3 retries), 10s timeout each, exponential backoff (~1s/2s/4s), on `5xx`
and network errors. A `4xx` is a permanent rejection and is not retried. Success is any `2xx` — return
`200` promptly.
- **Persisted across restarts** — subscriptions are saved (`0600`) to
`…/Preferences/Plugins/com.vtmikel.mcp_server/subscriptions.json` and reloaded on startup, so they survive
restarts and upgrades. The file **contains your webhook auth tokens** (required so authenticated webhooks
can re-authenticate). Pending dwell timers are not persisted — a held condition re-arms on its next
matching transition.
### Managing subscriptions in a browser *(v2026.3.0)*
When webhooks are enabled, the plugin serves a page that **lists active subscriptions and lets you remove
them** (create/edit stays with the MCP tools; auth tokens are never shown).

- **URL:** `http://<your-indigo-host>:8176/message/com.vtmikel.mcp_server/events_ui/` — served by the Indigo
Web Server under the **same authentication** as the rest of IWS (open it from a browser logged into Indigo).
- **Plugins → MCP Server → Print Event Subscriptions Web UI URL** prints the local/LAN/Reflector URLs to the log.
### Minimal example receiver
Any HTTPS endpoint reachable from your Indigo host works. A dependency-free Python receiver to test with:
```python
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
# (Optional) verify X-Webhook-Signature here if using HMAC auth.
event = json.loads(body)
# Dedupe by event_id — at-least-once delivery means retries can repeat.
print(f"{event['event_type']} {event['event_id']}: {event['human']['summary']}")
self.send_response(200) # any 2xx = success
self.end_headers()
HTTPServer(("0.0.0.0", 8888), Handler).serve_forever()
```
## Tips for Better Results
- **Be specific** — include location and device type in queries.
- **Use device Notes** — descriptions in the Notes field are included in the AI's context.
- **State vs. search** — use `list_devices({"onState": true})` for state queries, `search_entities("lights")`
for discovery.
## Privacy & Security
**Sent to OpenAI** (only to generate search embeddings, stored locally on your Indigo server): device
name/description/model/type/address, variable name/description, and action-group name/description — sent on
install and when entities are added or changed. **Never sent:** device states or values, credentials, URLs,
IP/network configuration, or historical/usage data.
**Network** — every MCP connection requires Bearer-token authentication. Local HTTP stays on your LAN; use the
Indigo Reflector for encrypted remote access. For self-signed HTTPS on the LAN, set
`NODE_TLS_REJECT_UNAUTHORIZED=0` (Claude Desktop) as shown above.
## Support
- **Issues:** the project repository
- **Questions:** [Indigo Domotics Forum](https://forums.indigodomo.com/viewforum.php?f=274)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues