ha-ai-mcp-server 0.3.8
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/dist/adapters/addon/addon.adapter.d.ts +22 -0
- package/dist/adapters/addon/addon.adapter.d.ts.map +1 -0
- package/dist/adapters/addon/addon.adapter.js +92 -0
- package/dist/adapters/addon/addon.adapter.js.map +1 -0
- package/dist/adapters/browser/playwright.adapter.d.ts +27 -0
- package/dist/adapters/browser/playwright.adapter.d.ts.map +1 -0
- package/dist/adapters/browser/playwright.adapter.js +156 -0
- package/dist/adapters/browser/playwright.adapter.js.map +1 -0
- package/dist/adapters/ha/ha-rest.adapter.d.ts +19 -0
- package/dist/adapters/ha/ha-rest.adapter.d.ts.map +1 -0
- package/dist/adapters/ha/ha-rest.adapter.js +70 -0
- package/dist/adapters/ha/ha-rest.adapter.js.map +1 -0
- package/dist/adapters/ha/ha-ws.adapter.d.ts +29 -0
- package/dist/adapters/ha/ha-ws.adapter.d.ts.map +1 -0
- package/dist/adapters/ha/ha-ws.adapter.js +178 -0
- package/dist/adapters/ha/ha-ws.adapter.js.map +1 -0
- package/dist/browser/renderer.d.ts +3 -0
- package/dist/browser/renderer.d.ts.map +1 -0
- package/dist/browser/renderer.js +3 -0
- package/dist/browser/renderer.js.map +1 -0
- package/dist/clients/addon-client.d.ts +3 -0
- package/dist/clients/addon-client.d.ts.map +1 -0
- package/dist/clients/addon-client.js +3 -0
- package/dist/clients/addon-client.js.map +1 -0
- package/dist/clients/ha-rest.d.ts +3 -0
- package/dist/clients/ha-rest.d.ts.map +1 -0
- package/dist/clients/ha-rest.js +3 -0
- package/dist/clients/ha-rest.js.map +1 -0
- package/dist/clients/ha-ws.d.ts +3 -0
- package/dist/clients/ha-ws.d.ts.map +1 -0
- package/dist/clients/ha-ws.js +3 -0
- package/dist/clients/ha-ws.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/core/config.d.ts +30 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +27 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/errors.d.ts +18 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +29 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/logger.d.ts +13 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +25 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/domain/models/automation.d.ts +19 -0
- package/dist/domain/models/automation.d.ts.map +1 -0
- package/dist/domain/models/automation.js +2 -0
- package/dist/domain/models/automation.js.map +1 -0
- package/dist/domain/models/dashboard.d.ts +21 -0
- package/dist/domain/models/dashboard.d.ts.map +1 -0
- package/dist/domain/models/dashboard.js +2 -0
- package/dist/domain/models/dashboard.js.map +1 -0
- package/dist/domain/models/entity.d.ts +13 -0
- package/dist/domain/models/entity.d.ts.map +1 -0
- package/dist/domain/models/entity.js +2 -0
- package/dist/domain/models/entity.js.map +1 -0
- package/dist/domain/models/system.d.ts +35 -0
- package/dist/domain/models/system.d.ts.map +1 -0
- package/dist/domain/models/system.js +2 -0
- package/dist/domain/models/system.js.map +1 -0
- package/dist/domain/ports/addon-client.port.d.ts +31 -0
- package/dist/domain/ports/addon-client.port.d.ts.map +1 -0
- package/dist/domain/ports/addon-client.port.js +2 -0
- package/dist/domain/ports/addon-client.port.js.map +1 -0
- package/dist/domain/ports/ha-client.port.d.ts +17 -0
- package/dist/domain/ports/ha-client.port.d.ts.map +1 -0
- package/dist/domain/ports/ha-client.port.js +2 -0
- package/dist/domain/ports/ha-client.port.js.map +1 -0
- package/dist/domain/ports/renderer.port.d.ts +7 -0
- package/dist/domain/ports/renderer.port.d.ts.map +1 -0
- package/dist/domain/ports/renderer.port.js +2 -0
- package/dist/domain/ports/renderer.port.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/services/automation.service.d.ts +20 -0
- package/dist/services/automation.service.d.ts.map +1 -0
- package/dist/services/automation.service.js +118 -0
- package/dist/services/automation.service.js.map +1 -0
- package/dist/services/dashboard.service.d.ts +22 -0
- package/dist/services/dashboard.service.d.ts.map +1 -0
- package/dist/services/dashboard.service.js +72 -0
- package/dist/services/dashboard.service.js.map +1 -0
- package/dist/services/system.service.d.ts +19 -0
- package/dist/services/system.service.d.ts.map +1 -0
- package/dist/services/system.service.js +83 -0
- package/dist/services/system.service.js.map +1 -0
- package/dist/tools/automation.d.ts +33 -0
- package/dist/tools/automation.d.ts.map +1 -0
- package/dist/tools/automation.js +246 -0
- package/dist/tools/automation.js.map +1 -0
- package/dist/tools/dashboard.d.ts +33 -0
- package/dist/tools/dashboard.d.ts.map +1 -0
- package/dist/tools/dashboard.js +166 -0
- package/dist/tools/dashboard.js.map +1 -0
- package/dist/tools/system.d.ts +44 -0
- package/dist/tools/system.d.ts.map +1 -0
- package/dist/tools/system.js +306 -0
- package/dist/tools/system.js.map +1 -0
- package/dist/tools/types.d.ts +26 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +39 -0
- package/src/adapters/addon/addon.adapter.ts +113 -0
- package/src/adapters/browser/playwright.adapter.ts +186 -0
- package/src/adapters/ha/ha-rest.adapter.ts +82 -0
- package/src/adapters/ha/ha-ws.adapter.ts +212 -0
- package/src/browser/renderer.ts +7 -0
- package/src/clients/addon-client.ts +2 -0
- package/src/clients/ha-rest.ts +2 -0
- package/src/clients/ha-ws.ts +2 -0
- package/src/config.ts +1 -0
- package/src/core/config.ts +39 -0
- package/src/core/errors.ts +28 -0
- package/src/core/logger.ts +27 -0
- package/src/domain/models/automation.ts +19 -0
- package/src/domain/models/dashboard.ts +23 -0
- package/src/domain/models/entity.ts +13 -0
- package/src/domain/models/system.ts +37 -0
- package/src/domain/ports/addon-client.port.ts +35 -0
- package/src/domain/ports/ha-client.port.ts +16 -0
- package/src/domain/ports/renderer.port.ts +7 -0
- package/src/index.ts +103 -0
- package/src/services/automation.service.ts +142 -0
- package/src/services/dashboard.service.ts +83 -0
- package/src/services/system.service.ts +94 -0
- package/src/tools/automation.ts +300 -0
- package/src/tools/dashboard.ts +206 -0
- package/src/tools/system.ts +368 -0
- package/src/tools/types.ts +30 -0
- package/tests/clients.test.ts +522 -0
- package/tests/e2e-smoke.test.ts +620 -0
- package/tests/renderer.test.ts +286 -0
- package/tests/skills.test.ts +189 -0
- package/tests/tools.test.ts +530 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolClients, McpToolResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export const listAutomationsSchema = {
|
|
6
|
+
domain: z
|
|
7
|
+
.enum(["automation", "script", "scene"])
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Domain to list (defaults to 'automation')"),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const readAutomationSchema = {
|
|
13
|
+
automation_id: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("Automation ID or alias to locate in automations.yaml"),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const writeAutomationSchema = {
|
|
19
|
+
automation_id: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("Automation ID to insert or update"),
|
|
22
|
+
yaml_code: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("YAML configuration block for the automation"),
|
|
25
|
+
label: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Optional snapshot label for safety backup"),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const triggerAutomationSchema = {
|
|
32
|
+
entity_id: z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("Automation entity_id to trigger (e.g. 'automation.evening_lights')"),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function splitYamlBlocks(yamlContent: string): string[] {
|
|
38
|
+
if (!yamlContent.trim()) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const lines = yamlContent.split("\n");
|
|
42
|
+
const blocks: string[] = [];
|
|
43
|
+
let currentBlock: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (line.startsWith("- ") && currentBlock.length > 0) {
|
|
47
|
+
blocks.push(currentBlock.join("\n").trim());
|
|
48
|
+
currentBlock = [line];
|
|
49
|
+
} else {
|
|
50
|
+
currentBlock.push(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (currentBlock.length > 0) {
|
|
55
|
+
const trimmed = currentBlock.join("\n").trim();
|
|
56
|
+
if (trimmed) {
|
|
57
|
+
blocks.push(trimmed);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return blocks;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function escapeRegex(str: string): string {
|
|
65
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function blockMatchesIdOrAlias(block: string, targetId: string): boolean {
|
|
69
|
+
const idRegex = new RegExp(`(^|\\n)\\s*-\\s*id:\\s*['"]?${escapeRegex(targetId)}['"]?(\\s|$)|(^|\\n)\\s*id:\\s*['"]?${escapeRegex(targetId)}['"]?(\\s|$)`, "i");
|
|
70
|
+
const aliasRegex = new RegExp(`(^|\\n)\\s*alias:\\s*['"]?${escapeRegex(targetId)}['"]?(\\s|$)`, "i");
|
|
71
|
+
return idRegex.test(block) || aliasRegex.test(block);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handleAutomationList(
|
|
75
|
+
clients: ToolClients,
|
|
76
|
+
args: { domain?: "automation" | "script" | "scene" }
|
|
77
|
+
): Promise<McpToolResult> {
|
|
78
|
+
try {
|
|
79
|
+
const domain = args.domain || "automation";
|
|
80
|
+
const states = await clients.restClient.getStates();
|
|
81
|
+
|
|
82
|
+
const filtered = states
|
|
83
|
+
.filter((s) => s.entity_id.startsWith(`${domain}.`))
|
|
84
|
+
.map((s) => ({
|
|
85
|
+
entity_id: s.entity_id,
|
|
86
|
+
state: s.state,
|
|
87
|
+
friendly_name: s.attributes?.friendly_name || s.entity_id,
|
|
88
|
+
last_triggered: s.attributes?.last_triggered || null,
|
|
89
|
+
description: s.attributes?.description,
|
|
90
|
+
mode: s.attributes?.mode,
|
|
91
|
+
current: s.attributes?.current,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: JSON.stringify(filtered, null, 2),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
return {
|
|
104
|
+
isError: true,
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `Failed to list automations: ${error.message || String(error)}`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function handleAutomationRead(
|
|
116
|
+
clients: ToolClients,
|
|
117
|
+
args: { automation_id: string }
|
|
118
|
+
): Promise<McpToolResult> {
|
|
119
|
+
try {
|
|
120
|
+
const fileRes = await clients.addonClient.readFile("automations.yaml");
|
|
121
|
+
const blocks = splitYamlBlocks(fileRes.content);
|
|
122
|
+
const targetId = args.automation_id.trim();
|
|
123
|
+
|
|
124
|
+
const matchedBlock = blocks.find((b) => blockMatchesIdOrAlias(b, targetId));
|
|
125
|
+
|
|
126
|
+
if (!matchedBlock) {
|
|
127
|
+
return {
|
|
128
|
+
isError: true,
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `Automation "${targetId}" not found in automations.yaml`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: matchedBlock,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
return {
|
|
148
|
+
isError: true,
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: `Failed to read automation: ${error.message || String(error)}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function handleAutomationWrite(
|
|
160
|
+
clients: ToolClients,
|
|
161
|
+
args: { automation_id: string; yaml_code: string; label?: string }
|
|
162
|
+
): Promise<McpToolResult> {
|
|
163
|
+
try {
|
|
164
|
+
let existingContent = "";
|
|
165
|
+
try {
|
|
166
|
+
const fileRes = await clients.addonClient.readFile("automations.yaml");
|
|
167
|
+
existingContent = fileRes.content;
|
|
168
|
+
} catch {
|
|
169
|
+
existingContent = "";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const blocks = splitYamlBlocks(existingContent);
|
|
173
|
+
const targetId = args.automation_id.trim();
|
|
174
|
+
let formattedNewBlock = args.yaml_code.trim();
|
|
175
|
+
|
|
176
|
+
if (!formattedNewBlock.startsWith("- ")) {
|
|
177
|
+
const lines = formattedNewBlock.split("\n");
|
|
178
|
+
const firstLine = `- ${lines[0]}`;
|
|
179
|
+
const restLines = lines.slice(1).map((l) => (l.trim() ? ` ${l}` : l));
|
|
180
|
+
formattedNewBlock = [firstLine, ...restLines].join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const existingIndex = blocks.findIndex((b) => blockMatchesIdOrAlias(b, targetId));
|
|
184
|
+
|
|
185
|
+
if (existingIndex >= 0) {
|
|
186
|
+
blocks[existingIndex] = formattedNewBlock;
|
|
187
|
+
} else {
|
|
188
|
+
blocks.push(formattedNewBlock);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const updatedYaml = blocks.join("\n\n") + "\n";
|
|
192
|
+
const label = args.label || `Update automation ${targetId}`;
|
|
193
|
+
|
|
194
|
+
const writeRes = await clients.addonClient.writeFile("automations.yaml", updatedYaml, {
|
|
195
|
+
validateYaml: true,
|
|
196
|
+
label,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await clients.restClient.callService("automation", "reload");
|
|
201
|
+
} catch (reloadErr: any) {
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Automation "${targetId}" written to automations.yaml (Snapshot: ${writeRes.snapshot_id}), but service reload failed: ${reloadErr.message}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: `Automation "${targetId}" successfully saved and automations reloaded. Snapshot ID: ${writeRes.snapshot_id}.`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
} catch (error: any) {
|
|
221
|
+
return {
|
|
222
|
+
isError: true,
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: `Failed to write automation: ${error.message || String(error)}`,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function handleAutomationTrigger(
|
|
234
|
+
clients: ToolClients,
|
|
235
|
+
args: { entity_id: string }
|
|
236
|
+
): Promise<McpToolResult> {
|
|
237
|
+
try {
|
|
238
|
+
const entityId = args.entity_id.trim();
|
|
239
|
+
const domain = entityId.split(".")[0] || "automation";
|
|
240
|
+
|
|
241
|
+
await clients.restClient.callService(domain, "trigger", { entity_id: entityId });
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: `Successfully triggered ${entityId}`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
} catch (error: any) {
|
|
252
|
+
return {
|
|
253
|
+
isError: true,
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: `Failed to trigger ${args.entity_id}: ${error.message || String(error)}`,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function registerAutomationTools(server: McpServer, clients: ToolClients): void {
|
|
265
|
+
server.registerTool(
|
|
266
|
+
"ha_automation_list",
|
|
267
|
+
{
|
|
268
|
+
description: "List all Home Assistant automations, scripts, or scenes with their current states.",
|
|
269
|
+
inputSchema: listAutomationsSchema,
|
|
270
|
+
},
|
|
271
|
+
async (args) => handleAutomationList(clients, args)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
server.registerTool(
|
|
275
|
+
"ha_automation_read",
|
|
276
|
+
{
|
|
277
|
+
description: "Read the YAML configuration block of a specific automation from automations.yaml.",
|
|
278
|
+
inputSchema: readAutomationSchema,
|
|
279
|
+
},
|
|
280
|
+
async (args) => handleAutomationRead(clients, args)
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
server.registerTool(
|
|
284
|
+
"ha_automation_write",
|
|
285
|
+
{
|
|
286
|
+
description: "Safely create or update an automation in automations.yaml with automated backup snapshot and service reload.",
|
|
287
|
+
inputSchema: writeAutomationSchema,
|
|
288
|
+
},
|
|
289
|
+
async (args) => handleAutomationWrite(clients, args)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
server.registerTool(
|
|
293
|
+
"ha_automation_trigger",
|
|
294
|
+
{
|
|
295
|
+
description: "Trigger a Home Assistant automation or script entity immediately.",
|
|
296
|
+
inputSchema: triggerAutomationSchema,
|
|
297
|
+
},
|
|
298
|
+
async (args) => handleAutomationTrigger(clients, args)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ToolClients, McpToolResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export const getDashboardConfigSchema = {
|
|
6
|
+
dashboard_slug: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Dashboard slug (e.g. 'lovelace', 'dashboard-energy', or omit for default)"),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const saveDashboardConfigSchema = {
|
|
13
|
+
config_yaml: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("Lovelace configuration content in YAML or JSON format"),
|
|
16
|
+
dashboard_slug: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Dashboard slug (e.g. 'lovelace', 'dashboard-test')"),
|
|
20
|
+
label: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Optional backup/snapshot label description"),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const renderScreenshotSchema = {
|
|
27
|
+
url_path: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe("Lovelace dashboard URL path to render (e.g. 'lovelace/0', 'dashboard-energy')"),
|
|
30
|
+
device_preset: z
|
|
31
|
+
.enum(["desktop", "tablet", "mobile"])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Device viewport preset ('desktop' = 1920x1080, 'tablet' = 768x1024, 'mobile' = 375x812)"),
|
|
34
|
+
dark_mode: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Render with dark mode color scheme emulation"),
|
|
38
|
+
element_selector: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Optional CSS selector to capture an isolated card or component (e.g. '#card-light')"),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function handleDashboardGetConfig(
|
|
45
|
+
clients: ToolClients,
|
|
46
|
+
args: { dashboard_slug?: string }
|
|
47
|
+
): Promise<McpToolResult> {
|
|
48
|
+
try {
|
|
49
|
+
const slug = args.dashboard_slug?.trim() || null;
|
|
50
|
+
let config: any;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (clients.wsClient) {
|
|
54
|
+
config = await clients.wsClient.getLovelaceConfig(slug);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
const fallbackPath = slug && slug !== "lovelace" ? `dashboards/${slug}.yaml` : "ui-lovelace.yaml";
|
|
58
|
+
const fileRes = await clients.addonClient.readFile(fallbackPath);
|
|
59
|
+
config = fileRes.content;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (config === undefined || config === null) {
|
|
63
|
+
const fallbackPath = slug && slug !== "lovelace" ? `dashboards/${slug}.yaml` : "ui-lovelace.yaml";
|
|
64
|
+
const fileRes = await clients.addonClient.readFile(fallbackPath);
|
|
65
|
+
config = fileRes.content;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const outputText = typeof config === "string" ? config : JSON.stringify(config, null, 2);
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: outputText,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
return {
|
|
79
|
+
isError: true,
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `Failed to get dashboard configuration: ${error.message || String(error)}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleDashboardSaveConfig(
|
|
91
|
+
clients: ToolClients,
|
|
92
|
+
args: { config_yaml: string; dashboard_slug?: string; label?: string }
|
|
93
|
+
): Promise<McpToolResult> {
|
|
94
|
+
try {
|
|
95
|
+
const slug = args.dashboard_slug?.trim() || "lovelace";
|
|
96
|
+
const filePath = slug && slug !== "lovelace" ? `dashboards/${slug}.yaml` : "ui-lovelace.yaml";
|
|
97
|
+
const label = args.label || `Dashboard update (${slug})`;
|
|
98
|
+
|
|
99
|
+
const writeRes = await clients.addonClient.writeFile(filePath, args.config_yaml, {
|
|
100
|
+
validateYaml: true,
|
|
101
|
+
label,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const parsedJson = JSON.parse(args.config_yaml);
|
|
106
|
+
if (typeof parsedJson === "object" && parsedJson !== null) {
|
|
107
|
+
await clients.wsClient.saveLovelaceConfig(parsedJson, slug === "lovelace" ? null : slug);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Not JSON or WS save skipped; YAML file write succeeded
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: `Dashboard configuration successfully saved for "${slug}". Snapshot ID: ${writeRes.snapshot_id} (${writeRes.bytes_written} bytes written to ${writeRes.path}).`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
} catch (error: any) {
|
|
122
|
+
return {
|
|
123
|
+
isError: true,
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `Failed to save dashboard configuration: ${error.message || String(error)}`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function handleDashboardRenderScreenshot(
|
|
135
|
+
clients: ToolClients,
|
|
136
|
+
args: {
|
|
137
|
+
url_path: string;
|
|
138
|
+
device_preset?: "desktop" | "tablet" | "mobile";
|
|
139
|
+
dark_mode?: boolean;
|
|
140
|
+
element_selector?: string;
|
|
141
|
+
}
|
|
142
|
+
): Promise<McpToolResult> {
|
|
143
|
+
try {
|
|
144
|
+
const result = await clients.renderer.captureDashboard({
|
|
145
|
+
urlPath: args.url_path,
|
|
146
|
+
devicePreset: args.device_preset,
|
|
147
|
+
darkMode: args.dark_mode,
|
|
148
|
+
elementSelector: args.element_selector,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "image",
|
|
155
|
+
data: result.base64Png,
|
|
156
|
+
mimeType: "image/png",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: `Dashboard screenshot rendered for ${result.url} (${result.width}x${result.height})${
|
|
161
|
+
args.element_selector ? ` [selector: ${args.element_selector}]` : ""
|
|
162
|
+
}`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
return {
|
|
168
|
+
isError: true,
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: `Failed to capture dashboard screenshot: ${error.message || String(error)}`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function registerDashboardTools(server: McpServer, clients: ToolClients): void {
|
|
180
|
+
server.registerTool(
|
|
181
|
+
"ha_dashboard_get_config",
|
|
182
|
+
{
|
|
183
|
+
description: "Retrieve Home Assistant Lovelace dashboard configuration via WebSocket or storage file.",
|
|
184
|
+
inputSchema: getDashboardConfigSchema,
|
|
185
|
+
},
|
|
186
|
+
async (args) => handleDashboardGetConfig(clients, args)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
server.registerTool(
|
|
190
|
+
"ha_dashboard_save_config",
|
|
191
|
+
{
|
|
192
|
+
description: "Validate, snapshot, and save Home Assistant Lovelace dashboard configuration.",
|
|
193
|
+
inputSchema: saveDashboardConfigSchema,
|
|
194
|
+
},
|
|
195
|
+
async (args) => handleDashboardSaveConfig(clients, args)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
server.registerTool(
|
|
199
|
+
"ha_dashboard_render_screenshot",
|
|
200
|
+
{
|
|
201
|
+
description: "Render a high-resolution screenshot of a Home Assistant dashboard or card via headless browser.",
|
|
202
|
+
inputSchema: renderScreenshotSchema,
|
|
203
|
+
},
|
|
204
|
+
async (args) => handleDashboardRenderScreenshot(clients, args)
|
|
205
|
+
);
|
|
206
|
+
}
|