@orellbuehler/homeassistant-mcp 0.5.0 → 0.6.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
@@ -12,9 +12,9 @@ AI agents.
12
12
  It is built to **help an agent author and validate Home Assistant configuration and automations** —
13
13
  not to control your home. It tells the agent what exists live (entities, services, events, areas,
14
14
  devices), validates the agent's work (render Jinja2 templates, check config, read the error log),
15
- can reload reloadable domains after YAML edits, and can configure the Energy dashboard (sources and
16
- the Individual-devices list). It deliberately has **no tools to turn devices on/off, set states, or
17
- fire events**.
15
+ can reload reloadable domains after YAML edits, can configure the Energy dashboard (sources and the
16
+ Individual-devices list), manage Lovelace resources, and prune HACS repositories. It deliberately has
17
+ **no tools to turn devices on/off, set states, or fire events**.
18
18
 
19
19
  ## Install
20
20
 
@@ -183,6 +183,33 @@ groupable ZHA endpoints (devices that support the Zigbee Groups cluster); `creat
183
183
  `list_zha_groupable`, or you can pass the `{ieee, endpoint_id}` pairs directly. The HA group entity
184
184
  is created asynchronously, so read its final `entity_id` from the entity registry afterwards.
185
185
 
186
+ **Lovelace resources** (WebSocket)
187
+
188
+ | Tool | Description |
189
+ | -------------------------- | -------------------------------------------------------------------------- |
190
+ | `list_lovelace_resources` | Registered dashboard resources (id, type, url), e.g. `/hacsfiles/…`. |
191
+ | `create_lovelace_resource` | Register a JS/CSS URL loaded on every dashboard (defaults to module). |
192
+ | `update_lovelace_resource` | Change a resource's url and/or type by id. |
193
+ | `delete_lovelace_resource` | Remove a resource by id (e.g. the dangling one left after a HACS removal). |
194
+
195
+ Resources are the JS/CSS bundles loaded into every dashboard (Settings → Dashboards → Resources).
196
+ The write tools call `lovelace/resources/*`, **require an admin token**, and only work when the
197
+ resource registry is in **storage mode** (`lovelace: mode: storage`); they are disabled if Lovelace
198
+ is globally in YAML mode. This is config authoring, not device control.
199
+
200
+ **HACS** (WebSocket)
201
+
202
+ | Tool | Description |
203
+ | ------------------------ | ------------------------------------------------------------------------------- |
204
+ | `list_hacs_repositories` | HACS repositories (installed-only by default): id, category, versions, paths. |
205
+ | `remove_hacs_repository` | Uninstall a repository by id — deletes its downloaded files and unregisters it. |
206
+
207
+ `list_hacs_repositories` maps each repo to `id` + `local_path`/`file_name`, so you can tell which
208
+ `/hacsfiles/…` Lovelace resource belongs to a plugin. `remove_hacs_repository` calls
209
+ `hacs/repository/remove` (**admin token**) and deletes the plugin's files, but not its Lovelace
210
+ resource — pair it with `delete_lovelace_resource` to fully clean up a frontend plugin. Removing an
211
+ integration you still reference in YAML will break that config, so check usage first.
212
+
186
213
  ## Safety boundary
187
214
 
188
215
  - **No device control.** There is no generic `call_service`, no `set_state`, and no `fire_event`.
@@ -195,6 +222,11 @@ is created asynchronously, so read its final `entity_id` from the entity registr
195
222
  - **ZHA groups are writable** via `zha/group/*` (the `*_zha_group*` tools). Creating/editing a Zigbee
196
223
  group changes which group entities exist (config authoring), not any device's on/off state, and it
197
224
  needs an admin token. Still no `call_service`, `set_state`, or `fire_event`.
225
+ - **Lovelace resources are writable** via `lovelace/resources/*` (the `*_lovelace_resource` tools) —
226
+ which JS/CSS bundles load into dashboards. Config authoring, admin token, storage-mode only.
227
+ - **HACS repositories are removable** via `hacs/repository/remove` (`remove_hacs_repository`). This
228
+ uninstalls a plugin/integration/theme and deletes its files (admin token); there is no install tool
229
+ (the server never downloads third-party code). Still no `call_service`, `set_state`, or `fire_event`.
198
230
  - **Read tools expose your home's data** (entity names, states, areas) to the agent/LLM. Use a
199
231
  scoped HA user if that matters to you.
200
232
  - Prefer `https` and a trusted network path to your instance.
package/dist/server.js CHANGED
@@ -9,8 +9,10 @@ import { registerRegistryTools } from "./tools/registry.js";
9
9
  import { registerEnergyTools } from "./tools/energy.js";
10
10
  import { registerTraceTools } from "./tools/trace.js";
11
11
  import { registerZhaTools } from "./tools/zha.js";
12
+ import { registerHacsTools } from "./tools/hacs.js";
13
+ import { registerResourceTools } from "./tools/resources.js";
12
14
  export function createServer(client, wsClient) {
13
- const server = new McpServer({ name: "homeassistant-mcp", version: "0.5.0" });
15
+ const server = new McpServer({ name: "homeassistant-mcp", version: "0.6.0" });
14
16
  registerEntityTools(server, client);
15
17
  registerServiceTools(server, client);
16
18
  registerSystemTools(server, client);
@@ -21,5 +23,7 @@ export function createServer(client, wsClient) {
21
23
  registerEnergyTools(server, wsClient, client);
22
24
  registerTraceTools(server, wsClient, client);
23
25
  registerZhaTools(server, wsClient);
26
+ registerHacsTools(server, wsClient);
27
+ registerResourceTools(server, wsClient);
24
28
  return server;
25
29
  }
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { ok, err } from "../hass/format.js";
3
+ function summarize(r) {
4
+ return {
5
+ id: r.id,
6
+ name: r.name,
7
+ full_name: r.full_name,
8
+ category: r.category,
9
+ installed_version: r.installed_version,
10
+ available_version: r.available_version,
11
+ pending_upgrade: r.pending_upgrade,
12
+ local_path: r.local_path,
13
+ file_name: r.file_name,
14
+ domain: r.domain,
15
+ };
16
+ }
17
+ export function registerHacsTools(server, ws) {
18
+ server.tool("list_hacs_repositories", "List HACS repositories (hacs/repositories/list) — the frontend plugins, integrations and themes HACS manages. Defaults to installed-only, trimmed to the useful fields (id, name, full_name, category, versions, local_path, file_name, domain). Use the returned id with remove_hacs_repository. For a frontend plugin, local_path/file_name tell you which /hacsfiles/… Lovelace resource points at it (see list_lovelace_resources), so you can delete the dangling resource after removing the plugin.", {
19
+ installed_only: z
20
+ .boolean()
21
+ .optional()
22
+ .describe("Only repositories that are installed. Default true; set false to include the whole known store (large)."),
23
+ category: z
24
+ .string()
25
+ .optional()
26
+ .describe("Filter by HACS category, e.g. 'integration', 'plugin', 'theme'. Omit for all categories."),
27
+ }, async ({ installed_only, category }) => {
28
+ try {
29
+ const payload = category ? { categories: [category] } : {};
30
+ const repos = (await ws.command("hacs/repositories/list", payload));
31
+ const filtered = installed_only === false ? repos : repos.filter((r) => r.installed);
32
+ const repositories = filtered.map(summarize);
33
+ return ok({ count: repositories.length, repositories });
34
+ }
35
+ catch (e) {
36
+ return err(e);
37
+ }
38
+ });
39
+ server.tool("remove_hacs_repository", "Uninstall a HACS repository by id (hacs/repository/remove) — deletes its downloaded files from the config (for a plugin, the JS under www/community/…) and unregisters it from HACS. Requires an admin token. This does NOT remove the plugin's Lovelace resource entry; after removing a frontend plugin, also delete its /hacsfiles/… resource with delete_lovelace_resource. Get the id from list_hacs_repositories. Removing an installed integration you still reference in YAML will break that config, so confirm nothing uses it first.", {
40
+ repository_id: z
41
+ .string()
42
+ .describe("The repository id from list_hacs_repositories (a numeric string, e.g. '172733314')."),
43
+ }, async ({ repository_id }) => {
44
+ try {
45
+ await ws.command("hacs/repository/remove", { repository: repository_id });
46
+ return ok({ repository_id, removed: true });
47
+ }
48
+ catch (e) {
49
+ return err(e);
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { ok, err } from "../hass/format.js";
3
+ const resType = z
4
+ .enum(["module", "css", "js", "html"])
5
+ .describe("Resource type. Lovelace cards use 'module'; 'css' for stylesheets. Rarely 'js'/'html'.");
6
+ export function registerResourceTools(server, ws) {
7
+ server.tool("list_lovelace_resources", "List registered Lovelace (dashboard) resources (lovelace/resources) — the JS/CSS URLs, e.g. /hacsfiles/… bundles, loaded into every dashboard. Each item has an id (use it with update/delete), a type (module/css/js/html) and a url. Only works when the resource registry is in storage mode (lovelace: mode: storage); it is disabled if Lovelace is globally in YAML mode.", {}, async () => {
8
+ try {
9
+ return ok(await ws.command("lovelace/resources"));
10
+ }
11
+ catch (e) {
12
+ return err(e);
13
+ }
14
+ });
15
+ server.tool("create_lovelace_resource", "Register a new Lovelace resource (lovelace/resources/create) so a card bundle/stylesheet loads on every dashboard. Config authoring (like adding it under Settings → Dashboards → Resources); requires an admin token and storage-mode resources. Returns the created resource incl. its new id.", {
16
+ url: z
17
+ .string()
18
+ .describe("Resource URL, e.g. '/hacsfiles/my-card/my-card.js' or '/local/aurora-cards.js'."),
19
+ res_type: resType.optional().describe("Defaults to 'module' (the type Lovelace cards use)."),
20
+ }, async ({ url, res_type }) => {
21
+ try {
22
+ return ok(await ws.command("lovelace/resources/create", { url, res_type: res_type ?? "module" }));
23
+ }
24
+ catch (e) {
25
+ return err(e);
26
+ }
27
+ });
28
+ server.tool("update_lovelace_resource", "Update an existing Lovelace resource (lovelace/resources/update) — change its url and/or type. Config authoring; requires an admin token and storage-mode resources. Get the resource_id from list_lovelace_resources.", {
29
+ resource_id: z.string().describe("The resource id from list_lovelace_resources."),
30
+ url: z.string().optional().describe("New URL. Omit to keep the current one."),
31
+ res_type: resType.optional().describe("New type. Omit to keep the current one."),
32
+ }, async ({ resource_id, url, res_type }) => {
33
+ try {
34
+ const payload = { resource_id };
35
+ if (url !== undefined)
36
+ payload.url = url;
37
+ if (res_type !== undefined)
38
+ payload.res_type = res_type;
39
+ return ok(await ws.command("lovelace/resources/update", payload));
40
+ }
41
+ catch (e) {
42
+ return err(e);
43
+ }
44
+ });
45
+ server.tool("delete_lovelace_resource", "Delete a Lovelace resource (lovelace/resources/delete) by id — removes the JS/CSS URL from every dashboard. Config authoring; requires an admin token and storage-mode resources. Use this to clear the dangling /hacsfiles/… resource left behind after remove_hacs_repository. Get the resource_id from list_lovelace_resources.", {
46
+ resource_id: z.string().describe("The resource id from list_lovelace_resources."),
47
+ }, async ({ resource_id }) => {
48
+ try {
49
+ await ws.command("lovelace/resources/delete", { resource_id });
50
+ return ok({ resource_id, deleted: true });
51
+ }
52
+ catch (e) {
53
+ return err(e);
54
+ }
55
+ });
56
+ }
package/dist/tools/zha.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { z } from "zod";
2
2
  import { ok, err } from "../hass/format.js";
3
+ // An entity_id can be listed either on the groupable endpoint itself or, on some
4
+ // HA versions where the endpoint list comes back empty/null, only under the
5
+ // device. Check both.
6
+ function endpointHasEntity(ep, entityId) {
7
+ const onEndpoint = ep.entities?.some((e) => e?.entity_id === entityId);
8
+ const onDevice = ep.device?.entities?.some((e) => e?.entity_id === entityId);
9
+ return Boolean(onEndpoint || onDevice);
10
+ }
11
+ function endpointEntityIds(ep) {
12
+ const refs = [...(ep.entities ?? []), ...(ep.device?.entities ?? [])];
13
+ return [
14
+ ...new Set(refs.filter((e) => !!e?.entity_id).map((e) => e.entity_id)),
15
+ ];
16
+ }
3
17
  const memberSchema = z.union([
4
18
  z.string(),
5
19
  z.object({
@@ -16,7 +30,7 @@ async function resolveMembers(ws, members) {
16
30
  return members.map((m) => {
17
31
  if (typeof m !== "string")
18
32
  return { ieee: m.ieee, endpoint_id: m.endpoint_id };
19
- const hit = groupable.find((ep) => ep.entities?.some((e) => e.entity_id === m));
33
+ const hit = groupable.find((ep) => endpointHasEntity(ep, m));
20
34
  if (!hit) {
21
35
  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
36
  }
@@ -39,7 +53,7 @@ export function registerZhaTools(server, ws) {
39
53
  ieee: ep.device?.ieee ?? null,
40
54
  endpoint_id: ep.endpoint_id,
41
55
  device_name: ep.device?.user_given_name ?? ep.device?.name ?? null,
42
- entities: (ep.entities ?? []).map((e) => e.entity_id),
56
+ entities: endpointEntityIds(ep),
43
57
  }));
44
58
  return ok({ count: endpoints.length, endpoints });
45
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orellbuehler/homeassistant-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": {