@orellbuehler/homeassistant-mcp 0.5.1 → 0.7.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 +57 -3
- package/dist/server.js +7 -1
- package/dist/tools/automations.js +62 -0
- package/dist/tools/hacs.js +52 -0
- package/dist/tools/resources.js +56 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,10 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
authors automations (create/replace/delete via the config API), can reload reloadable domains after
|
|
16
|
+
YAML edits, can configure the Energy dashboard (sources and the Individual-devices list), manage
|
|
17
|
+
Lovelace resources, and prune HACS repositories. It deliberately has **no tools to turn devices
|
|
18
|
+
on/off, set states, or fire events**.
|
|
18
19
|
|
|
19
20
|
## Install
|
|
20
21
|
|
|
@@ -118,6 +119,21 @@ available immediately. Verify with `claude mcp list` (should show `homeassistant
|
|
|
118
119
|
| -------- | ------------------------------------------------------------------------------------------ |
|
|
119
120
|
| `reload` | Reload a reloadable domain so YAML edits apply without a restart (`all`, `automation`, …). |
|
|
120
121
|
|
|
122
|
+
**Automations** (REST, `/api/config/automation`)
|
|
123
|
+
|
|
124
|
+
| Tool | Description |
|
|
125
|
+
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
126
|
+
| `get_automation_config` | Stored config (triggers/conditions/actions/mode) of one automation. |
|
|
127
|
+
| `upsert_automation` | Create a new automation or replace an existing one's config (validated by HA). |
|
|
128
|
+
| `delete_automation` | Delete a UI-managed automation. |
|
|
129
|
+
|
|
130
|
+
All three accept the `automation.*` entity id (the internal id is resolved automatically) or the
|
|
131
|
+
internal id itself. Writes require an **admin token**; HA validates the config, stores it in
|
|
132
|
+
`automations.yaml` and reloads automations automatically. `upsert_automation` replaces the whole
|
|
133
|
+
config, so edit via `get_automation_config` → modify → `upsert_automation`. Only automations with
|
|
134
|
+
an `id` (UI-created / `automations.yaml`) are reachable — automations defined elsewhere in YAML are
|
|
135
|
+
not. Debug the result with the trace tools below.
|
|
136
|
+
|
|
121
137
|
**Registries** (WebSocket)
|
|
122
138
|
|
|
123
139
|
| Tool | Description |
|
|
@@ -183,18 +199,56 @@ groupable ZHA endpoints (devices that support the Zigbee Groups cluster); `creat
|
|
|
183
199
|
`list_zha_groupable`, or you can pass the `{ieee, endpoint_id}` pairs directly. The HA group entity
|
|
184
200
|
is created asynchronously, so read its final `entity_id` from the entity registry afterwards.
|
|
185
201
|
|
|
202
|
+
**Lovelace resources** (WebSocket)
|
|
203
|
+
|
|
204
|
+
| Tool | Description |
|
|
205
|
+
| -------------------------- | -------------------------------------------------------------------------- |
|
|
206
|
+
| `list_lovelace_resources` | Registered dashboard resources (id, type, url), e.g. `/hacsfiles/…`. |
|
|
207
|
+
| `create_lovelace_resource` | Register a JS/CSS URL loaded on every dashboard (defaults to module). |
|
|
208
|
+
| `update_lovelace_resource` | Change a resource's url and/or type by id. |
|
|
209
|
+
| `delete_lovelace_resource` | Remove a resource by id (e.g. the dangling one left after a HACS removal). |
|
|
210
|
+
|
|
211
|
+
Resources are the JS/CSS bundles loaded into every dashboard (Settings → Dashboards → Resources).
|
|
212
|
+
The write tools call `lovelace/resources/*`, **require an admin token**, and only work when the
|
|
213
|
+
resource registry is in **storage mode** (`lovelace: mode: storage`); they are disabled if Lovelace
|
|
214
|
+
is globally in YAML mode. This is config authoring, not device control.
|
|
215
|
+
|
|
216
|
+
**HACS** (WebSocket)
|
|
217
|
+
|
|
218
|
+
| Tool | Description |
|
|
219
|
+
| ------------------------ | ------------------------------------------------------------------------------- |
|
|
220
|
+
| `list_hacs_repositories` | HACS repositories (installed-only by default): id, category, versions, paths. |
|
|
221
|
+
| `remove_hacs_repository` | Uninstall a repository by id — deletes its downloaded files and unregisters it. |
|
|
222
|
+
|
|
223
|
+
`list_hacs_repositories` maps each repo to `id` + `local_path`/`file_name`, so you can tell which
|
|
224
|
+
`/hacsfiles/…` Lovelace resource belongs to a plugin. `remove_hacs_repository` calls
|
|
225
|
+
`hacs/repository/remove` (**admin token**) and deletes the plugin's files; when HACS manages
|
|
226
|
+
resources (storage mode) it also drops the plugin's Lovelace resource for you, so verify with
|
|
227
|
+
`list_lovelace_resources` and only `delete_lovelace_resource` one that's left behind. Removing an
|
|
228
|
+
integration you still reference in YAML will break that config, so check usage first.
|
|
229
|
+
|
|
186
230
|
## Safety boundary
|
|
187
231
|
|
|
188
232
|
- **No device control.** There is no generic `call_service`, no `set_state`, and no `fire_event`.
|
|
189
233
|
The server cannot turn things on/off.
|
|
190
234
|
- **`reload` is restricted** to a fixed allowlist of `*.reload` / `homeassistant.reload_*` services.
|
|
191
235
|
It restarts reloadable domains (e.g. re-reads `automations.yaml`) but cannot control devices.
|
|
236
|
+
- **Automation configs are writable** via the `/api/config/automation` REST API
|
|
237
|
+
(`upsert_automation` / `delete_automation`). This is the server's core purpose — authoring
|
|
238
|
+
automations — and needs an admin token. Be aware that an automation, once written, runs its
|
|
239
|
+
actions whenever its triggers fire, so review configs before writing. The server still cannot run
|
|
240
|
+
services directly: no `call_service`, `set_state`, or `fire_event`.
|
|
192
241
|
- **Energy dashboard config is writable** via `energy/save_prefs` (the `*_energy_*` tools). This is
|
|
193
242
|
configuration authoring — the dashboard's sources and Individual-devices list — not device control,
|
|
194
243
|
and it needs an admin token. No `call_service`, `set_state`, or `fire_event` is added.
|
|
195
244
|
- **ZHA groups are writable** via `zha/group/*` (the `*_zha_group*` tools). Creating/editing a Zigbee
|
|
196
245
|
group changes which group entities exist (config authoring), not any device's on/off state, and it
|
|
197
246
|
needs an admin token. Still no `call_service`, `set_state`, or `fire_event`.
|
|
247
|
+
- **Lovelace resources are writable** via `lovelace/resources/*` (the `*_lovelace_resource` tools) —
|
|
248
|
+
which JS/CSS bundles load into dashboards. Config authoring, admin token, storage-mode only.
|
|
249
|
+
- **HACS repositories are removable** via `hacs/repository/remove` (`remove_hacs_repository`). This
|
|
250
|
+
uninstalls a plugin/integration/theme and deletes its files (admin token); there is no install tool
|
|
251
|
+
(the server never downloads third-party code). Still no `call_service`, `set_state`, or `fire_event`.
|
|
198
252
|
- **Read tools expose your home's data** (entity names, states, areas) to the agent/LLM. Use a
|
|
199
253
|
scoped HA user if that matters to you.
|
|
200
254
|
- Prefer `https` and a trusted network path to your instance.
|
package/dist/server.js
CHANGED
|
@@ -5,21 +5,27 @@ import { registerSystemTools } from "./tools/system.js";
|
|
|
5
5
|
import { registerTemplateTools } from "./tools/templates.js";
|
|
6
6
|
import { registerHistoryTools } from "./tools/history.js";
|
|
7
7
|
import { registerReloadTools } from "./tools/reload.js";
|
|
8
|
+
import { registerAutomationTools } from "./tools/automations.js";
|
|
8
9
|
import { registerRegistryTools } from "./tools/registry.js";
|
|
9
10
|
import { registerEnergyTools } from "./tools/energy.js";
|
|
10
11
|
import { registerTraceTools } from "./tools/trace.js";
|
|
11
12
|
import { registerZhaTools } from "./tools/zha.js";
|
|
13
|
+
import { registerHacsTools } from "./tools/hacs.js";
|
|
14
|
+
import { registerResourceTools } from "./tools/resources.js";
|
|
12
15
|
export function createServer(client, wsClient) {
|
|
13
|
-
const server = new McpServer({ name: "homeassistant-mcp", version: "0.
|
|
16
|
+
const server = new McpServer({ name: "homeassistant-mcp", version: "0.7.0" });
|
|
14
17
|
registerEntityTools(server, client);
|
|
15
18
|
registerServiceTools(server, client);
|
|
16
19
|
registerSystemTools(server, client);
|
|
17
20
|
registerTemplateTools(server, client);
|
|
18
21
|
registerHistoryTools(server, client);
|
|
19
22
|
registerReloadTools(server, client);
|
|
23
|
+
registerAutomationTools(server, client);
|
|
20
24
|
registerRegistryTools(server, wsClient);
|
|
21
25
|
registerEnergyTools(server, wsClient, client);
|
|
22
26
|
registerTraceTools(server, wsClient, client);
|
|
23
27
|
registerZhaTools(server, wsClient);
|
|
28
|
+
registerHacsTools(server, wsClient);
|
|
29
|
+
registerResourceTools(server, wsClient);
|
|
24
30
|
return server;
|
|
25
31
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, err } from "../hass/format.js";
|
|
3
|
+
async function resolveId(automation, client) {
|
|
4
|
+
if (!automation.startsWith("automation."))
|
|
5
|
+
return automation;
|
|
6
|
+
const state = (await client.fetch(`/api/states/${encodeURIComponent(automation)}`));
|
|
7
|
+
const id = state.attributes?.id;
|
|
8
|
+
if (id === undefined || id === null || id === "") {
|
|
9
|
+
throw new Error(`Automation ${automation} has no 'id' attribute, so it is not stored in automations.yaml and cannot be read or edited via the config API (it is defined elsewhere in YAML without an id)`);
|
|
10
|
+
}
|
|
11
|
+
return String(id);
|
|
12
|
+
}
|
|
13
|
+
const configPath = (id) => `/api/config/automation/config/${encodeURIComponent(id)}`;
|
|
14
|
+
export function registerAutomationTools(server, client) {
|
|
15
|
+
server.tool("get_automation_config", "Get the stored configuration of an automation (alias, description, triggers, conditions, actions, mode) from automations.yaml via GET /api/config/automation/config/{id}. Accepts the automation.* entity id (the internal id is resolved automatically) or the internal id itself. Only automations with an 'id' (UI-created, stored in automations.yaml) are accessible.", {
|
|
16
|
+
automation: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe("automation.* entity id, or the automation's internal id (the 'id' attribute)"),
|
|
19
|
+
}, async ({ automation }) => {
|
|
20
|
+
try {
|
|
21
|
+
const id = await resolveId(automation, client);
|
|
22
|
+
return ok(await client.fetch(configPath(id)));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return err(e);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.tool("upsert_automation", "Create a new automation or replace an existing one via POST /api/config/automation/config/{id} (requires an admin token). Home Assistant validates the config, writes it to automations.yaml and reloads automations automatically — no separate reload needed. The config REPLACES the stored one entirely, so to edit call get_automation_config first and send back the modified object. Omit 'automation' to create a new one (an id is generated and returned); its entity id becomes automation.<slugified alias>. Note that once its triggers fire, an automation runs its actions — review the config before writing.", {
|
|
29
|
+
config: z
|
|
30
|
+
.record(z.unknown())
|
|
31
|
+
.describe("Full automation config as stored in automations.yaml: alias, description?, triggers, conditions?, actions, mode? (the legacy singular trigger/condition/action keys are also accepted)"),
|
|
32
|
+
automation: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Existing automation.* entity id or internal id to replace; omit to create a new automation"),
|
|
36
|
+
}, async ({ config, automation }) => {
|
|
37
|
+
try {
|
|
38
|
+
const id = automation ? await resolveId(automation, client) : String(Date.now());
|
|
39
|
+
const result = await client.fetch(configPath(id), {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: JSON.stringify(config),
|
|
42
|
+
});
|
|
43
|
+
return ok({ id, created: !automation, result });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return err(e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
server.tool("delete_automation", "Delete an automation from automations.yaml via DELETE /api/config/automation/config/{id} (requires an admin token). Accepts the automation.* entity id or the internal id. Only UI-managed automations (those stored in automations.yaml) can be deleted; Home Assistant reloads automations automatically afterwards.", {
|
|
50
|
+
automation: z
|
|
51
|
+
.string()
|
|
52
|
+
.describe("automation.* entity id, or the automation's internal id (the 'id' attribute)"),
|
|
53
|
+
}, async ({ automation }) => {
|
|
54
|
+
try {
|
|
55
|
+
const id = await resolveId(automation, client);
|
|
56
|
+
return ok(await client.fetch(configPath(id), { method: "DELETE" }));
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return err(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -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. When HACS manages resources (storage mode) it also removes the plugin's /hacsfiles/… Lovelace resource for you; verify with list_lovelace_resources and use delete_lovelace_resource only if one is left behind. 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 for manually-registered resources (e.g. /local/…) or a /hacsfiles/… resource that HACS did not auto-remove 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orellbuehler/homeassistant-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|