@orellbuehler/homeassistant-mcp 0.3.0 → 0.5.0

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/README.md CHANGED
@@ -120,15 +120,19 @@ available immediately. Verify with `claude mcp list` (should show `homeassistant
120
120
 
121
121
  **Registries** (WebSocket)
122
122
 
123
- | Tool | Description |
124
- | ------------------------ | --------------------------------------------------------------------------- |
125
- | `list_registry_entities` | ALL registered entities, incl. disabled/unavailable (area, device, status). |
126
- | `list_devices` | Devices (id, name, manufacturer, model, area). |
127
- | `list_areas` | Areas (area_id, name, floor). |
128
- | `list_labels` | Labels (label_id, name, color, icon). |
123
+ | Tool | Description |
124
+ | ------------------------ | ---------------------------------------------------------------------------------------- |
125
+ | `list_registry_entities` | ALL registered entities, incl. disabled/unavailable (area, device, status). |
126
+ | `rename_entity` | Set an entity's registry `name` and/or `new_entity_id` (config edit, no device control). |
127
+ | `list_devices` | Devices (id, name, manufacturer, model, area). |
128
+ | `list_areas` | Areas (area_id, name, floor). |
129
+ | `list_labels` | Labels (label_id, name, color, icon). |
129
130
 
130
131
  `list_entities` (REST) shows entities that currently have state; `list_registry_entities`
131
132
  (WebSocket) shows everything registered, including disabled entities, with area/device grouping.
133
+ `rename_entity` writes the registry via `config/entity_registry/update` (requires an admin token):
134
+ `name` overrides the friendly name (null reverts to the integration's `original_name`) and
135
+ `new_entity_id` renames the entity_id within the same domain.
132
136
 
133
137
  **Energy dashboard** (WebSocket)
134
138
 
@@ -159,6 +163,26 @@ prefs, then save), so editing the Energy dashboard in the HA UI at the same mome
159
163
  Pass an `automation.*` or `script.*` entity id; the trace key is resolved automatically. Use these
160
164
  to debug **whether and how** an automation you authored actually ran.
161
165
 
166
+ **ZHA / Zigbee groups** (WebSocket)
167
+
168
+ | Tool | Description |
169
+ | -------------------------- | ----------------------------------------------------------------------------------- |
170
+ | `list_zha_groups` | Existing ZHA groups (group_id, name, members). |
171
+ | `list_zha_groupable` | Endpoints that can join a group (ieee + endpoint_id + their entities). |
172
+ | `create_zha_group` | Create a group on the coordinator; members by `entity_id` or `{ieee, endpoint_id}`. |
173
+ | `add_zha_group_members` | Add members to a group. |
174
+ | `remove_zha_group_members` | Remove members from a group. |
175
+ | `remove_zha_group` | Delete group(s) by id (member devices untouched). |
176
+
177
+ A ZHA group lives on the Zigbee coordinator and is exposed to HA as one group entity (e.g. a single
178
+ `light.*` that drives all members together via Zigbee multicast). This is **configuration
179
+ authoring**, not device control — like creating a helper, it changes what entities exist, not their
180
+ on/off state. The write tools call `zha/group/*` and **require an admin token**. Members must be
181
+ groupable ZHA endpoints (devices that support the Zigbee Groups cluster); `create_zha_group` /
182
+ `add_zha_group_members` accept `entity_id`s and resolve them to `{ieee, endpoint_id}` via
183
+ `list_zha_groupable`, or you can pass the `{ieee, endpoint_id}` pairs directly. The HA group entity
184
+ is created asynchronously, so read its final `entity_id` from the entity registry afterwards.
185
+
162
186
  ## Safety boundary
163
187
 
164
188
  - **No device control.** There is no generic `call_service`, no `set_state`, and no `fire_event`.
@@ -168,6 +192,9 @@ to debug **whether and how** an automation you authored actually ran.
168
192
  - **Energy dashboard config is writable** via `energy/save_prefs` (the `*_energy_*` tools). This is
169
193
  configuration authoring — the dashboard's sources and Individual-devices list — not device control,
170
194
  and it needs an admin token. No `call_service`, `set_state`, or `fire_event` is added.
195
+ - **ZHA groups are writable** via `zha/group/*` (the `*_zha_group*` tools). Creating/editing a Zigbee
196
+ group changes which group entities exist (config authoring), not any device's on/off state, and it
197
+ needs an admin token. Still no `call_service`, `set_state`, or `fire_event`.
171
198
  - **Read tools expose your home's data** (entity names, states, areas) to the agent/LLM. Use a
172
199
  scoped HA user if that matters to you.
173
200
  - Prefer `https` and a trusted network path to your instance.
package/dist/server.js CHANGED
@@ -8,8 +8,9 @@ import { registerReloadTools } from "./tools/reload.js";
8
8
  import { registerRegistryTools } from "./tools/registry.js";
9
9
  import { registerEnergyTools } from "./tools/energy.js";
10
10
  import { registerTraceTools } from "./tools/trace.js";
11
+ import { registerZhaTools } from "./tools/zha.js";
11
12
  export function createServer(client, wsClient) {
12
- const server = new McpServer({ name: "homeassistant-mcp", version: "0.3.0" });
13
+ const server = new McpServer({ name: "homeassistant-mcp", version: "0.5.0" });
13
14
  registerEntityTools(server, client);
14
15
  registerServiceTools(server, client);
15
16
  registerSystemTools(server, client);
@@ -19,5 +20,6 @@ export function createServer(client, wsClient) {
19
20
  registerRegistryTools(server, wsClient);
20
21
  registerEnergyTools(server, wsClient, client);
21
22
  registerTraceTools(server, wsClient, client);
23
+ registerZhaTools(server, wsClient);
22
24
  return server;
23
25
  }
@@ -31,6 +31,38 @@ export function registerRegistryTools(server, ws) {
31
31
  return err(e);
32
32
  }
33
33
  });
34
+ server.tool("rename_entity", "Rename an entity in the entity registry via the WebSocket API (config/entity_registry/update). Set 'name' to override the friendly name (pass null to revert to the integration's original_name) and/or 'new_entity_id' to change the entity_id itself (must keep the same domain). At least one of name or new_entity_id is required. This edits registry config only — it does not control any device. Returns the updated entity_entry.", {
35
+ entity_id: z
36
+ .string()
37
+ .describe("Current entity_id to rename, e.g. 'cover.storen_wohnzimmer_links'"),
38
+ name: z
39
+ .string()
40
+ .nullable()
41
+ .optional()
42
+ .describe("New friendly name; null reverts to the integration's original_name"),
43
+ new_entity_id: z
44
+ .string()
45
+ .optional()
46
+ .describe("New entity_id; must keep the same domain, e.g. 'cover.wohnzimmer_links'"),
47
+ }, async ({ entity_id, name, new_entity_id }) => {
48
+ try {
49
+ if (name === undefined && new_entity_id === undefined) {
50
+ throw new Error("Provide at least one of 'name' or 'new_entity_id' to rename the entity");
51
+ }
52
+ if (new_entity_id !== undefined && domainOf(new_entity_id) !== domainOf(entity_id)) {
53
+ throw new Error(`new_entity_id must stay in the '${domainOf(entity_id)}' domain (got '${new_entity_id}')`);
54
+ }
55
+ const payload = { entity_id };
56
+ if (name !== undefined)
57
+ payload.name = name;
58
+ if (new_entity_id !== undefined)
59
+ payload.new_entity_id = new_entity_id;
60
+ return ok(await ws.command("config/entity_registry/update", payload));
61
+ }
62
+ catch (e) {
63
+ return err(e);
64
+ }
65
+ });
34
66
  server.tool("list_devices", "List devices from the device registry via the WebSocket API (id, name, manufacturer, model, area_id). Useful for grouping entities by physical device when writing automations.", {}, async () => {
35
67
  try {
36
68
  const list = (await ws.command("config/device_registry/list"));
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { ok, err } from "../hass/format.js";
3
+ const memberSchema = z.union([
4
+ z.string(),
5
+ z.object({
6
+ ieee: z.string(),
7
+ endpoint_id: z.number().int().nonnegative(),
8
+ }),
9
+ ]);
10
+ async function resolveMembers(ws, members) {
11
+ const needLookup = members.some((m) => typeof m === "string");
12
+ let groupable = [];
13
+ if (needLookup) {
14
+ groupable = (await ws.command("zha/devices/groupable"));
15
+ }
16
+ return members.map((m) => {
17
+ if (typeof m !== "string")
18
+ return { ieee: m.ieee, endpoint_id: m.endpoint_id };
19
+ const hit = groupable.find((ep) => ep.entities?.some((e) => e.entity_id === m));
20
+ if (!hit) {
21
+ throw new Error(`'${m}' is not a groupable ZHA entity. A ZHA group member must be a ZHA entity on an endpoint that supports the Zigbee Groups cluster (see list_zha_groupable). Non-ZHA entities (Wi-Fi/Shelly/Hue-bridge/etc.) cannot join a ZHA group.`);
22
+ }
23
+ return { ieee: hit.device.ieee, endpoint_id: hit.endpoint_id };
24
+ });
25
+ }
26
+ export function registerZhaTools(server, ws) {
27
+ server.tool("list_zha_groups", "List existing ZHA (Zigbee) groups via the WebSocket API (zha/groups). Each group has a group_id, name and its members. A ZHA group lives on the Zigbee coordinator and is exposed to HA as a single group entity (e.g. one light.* that drives all members together via Zigbee multicast).", {}, async () => {
28
+ try {
29
+ return ok(await ws.command("zha/groups"));
30
+ }
31
+ catch (e) {
32
+ return err(e);
33
+ }
34
+ });
35
+ server.tool("list_zha_groupable", "List ZHA device endpoints that can be added to a ZHA group (zha/devices/groupable) — endpoints whose device supports the Zigbee Groups cluster. Returns, per endpoint, its ieee + endpoint_id and the entities on it, so you can choose group members by entity_id. Use this to see what create_zha_group / add_zha_group_members accept.", {}, async () => {
36
+ try {
37
+ const eps = (await ws.command("zha/devices/groupable"));
38
+ const endpoints = eps.map((ep) => ({
39
+ ieee: ep.device?.ieee ?? null,
40
+ endpoint_id: ep.endpoint_id,
41
+ device_name: ep.device?.user_given_name ?? ep.device?.name ?? null,
42
+ entities: (ep.entities ?? []).map((e) => e.entity_id),
43
+ }));
44
+ return ok({ count: endpoints.length, endpoints });
45
+ }
46
+ catch (e) {
47
+ return err(e);
48
+ }
49
+ });
50
+ server.tool("create_zha_group", "Create a ZHA (Zigbee) group on the coordinator (zha/group/add) and return the new group (group_id, name, members). This is configuration authoring — it does not switch anything; it tells the Zigbee network to treat the members as one group, and HA then exposes a single group entity (e.g. light.<name>) that controls them together via Zigbee multicast. 'members' is optional and accepts entity_ids (resolved to ieee+endpoint via list_zha_groupable) and/or explicit {ieee, endpoint_id} objects; only groupable ZHA entities are valid. The HA group entity is created asynchronously and may not be in this response — read its entity_id from the entity registry afterwards.", {
51
+ name: z.string().describe("Friendly name for the new group, e.g. 'Iris TV'"),
52
+ members: z
53
+ .array(memberSchema)
54
+ .optional()
55
+ .describe("Initial members: entity_ids (e.g. 'light.philips_iris_tv_left') and/or {ieee, endpoint_id} objects. Omit to create an empty group and add members later."),
56
+ }, async ({ name, members }) => {
57
+ try {
58
+ const payload = { group_name: name };
59
+ if (members && members.length)
60
+ payload.members = await resolveMembers(ws, members);
61
+ return ok(await ws.command("zha/group/add", payload));
62
+ }
63
+ catch (e) {
64
+ return err(e);
65
+ }
66
+ });
67
+ server.tool("add_zha_group_members", "Add members to an existing ZHA group (zha/group/members/add). 'members' accepts entity_ids (resolved via list_zha_groupable) and/or {ieee, endpoint_id} objects. Returns the updated group.", {
68
+ group_id: z.number().int().describe("Target group_id (from list_zha_groups)"),
69
+ members: z
70
+ .array(memberSchema)
71
+ .min(1)
72
+ .describe("Members to add: entity_ids and/or {ieee, endpoint_id} objects"),
73
+ }, async ({ group_id, members }) => {
74
+ try {
75
+ const resolved = await resolveMembers(ws, members);
76
+ return ok(await ws.command("zha/group/members/add", { group_id, members: resolved }));
77
+ }
78
+ catch (e) {
79
+ return err(e);
80
+ }
81
+ });
82
+ server.tool("remove_zha_group_members", "Remove members from an existing ZHA group (zha/group/members/remove). 'members' accepts entity_ids (resolved via list_zha_groupable) and/or {ieee, endpoint_id} objects. Returns the updated group.", {
83
+ group_id: z.number().int().describe("Target group_id (from list_zha_groups)"),
84
+ members: z
85
+ .array(memberSchema)
86
+ .min(1)
87
+ .describe("Members to remove: entity_ids and/or {ieee, endpoint_id} objects"),
88
+ }, async ({ group_id, members }) => {
89
+ try {
90
+ const resolved = await resolveMembers(ws, members);
91
+ return ok(await ws.command("zha/group/members/remove", { group_id, members: resolved }));
92
+ }
93
+ catch (e) {
94
+ return err(e);
95
+ }
96
+ });
97
+ server.tool("remove_zha_group", "Delete one or more ZHA groups by id (zha/group/remove). This removes the group from the Zigbee coordinator and its HA group entity; the member devices themselves are untouched. Returns the remaining groups.", {
98
+ group_ids: z.array(z.number().int()).min(1).describe("group_id(s) to delete"),
99
+ }, async ({ group_ids }) => {
100
+ try {
101
+ return ok(await ws.command("zha/group/remove", { group_ids }));
102
+ }
103
+ catch (e) {
104
+ return err(e);
105
+ }
106
+ });
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orellbuehler/homeassistant-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Model Context Protocol server for Home Assistant: introspect entities, services, events, registries, render templates and validate configuration to help AI agents author HA configs and automations.",
5
5
  "type": "module",
6
6
  "bin": {