@mp-consulting/homebridge-ai-kit 2.0.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 +273 -0
- package/config.schema.json +131 -0
- package/dist/agent/run-agent.d.ts +61 -0
- package/dist/agent/run-agent.d.ts.map +1 -0
- package/dist/agent/run-agent.js +92 -0
- package/dist/agent/run-agent.js.map +1 -0
- package/dist/bin/homebridge-ai-kit.d.ts +3 -0
- package/dist/bin/homebridge-ai-kit.d.ts.map +1 -0
- package/dist/bin/homebridge-ai-kit.js +61 -0
- package/dist/bin/homebridge-ai-kit.js.map +1 -0
- package/dist/bin/homebridge-mcp-server.d.ts +3 -0
- package/dist/bin/homebridge-mcp-server.d.ts.map +1 -0
- package/dist/bin/homebridge-mcp-server.js +5 -0
- package/dist/bin/homebridge-mcp-server.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/create-server.d.ts +17 -0
- package/dist/mcp/create-server.d.ts.map +1 -0
- package/dist/mcp/create-server.js +42 -0
- package/dist/mcp/create-server.js.map +1 -0
- package/dist/mcp/homebridge-client.d.ts +105 -0
- package/dist/mcp/homebridge-client.d.ts.map +1 -0
- package/dist/mcp/homebridge-client.js +341 -0
- package/dist/mcp/homebridge-client.js.map +1 -0
- package/dist/mcp/http.d.ts +26 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +124 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/live.d.ts +46 -0
- package/dist/mcp/live.d.ts.map +1 -0
- package/dist/mcp/live.js +105 -0
- package/dist/mcp/live.js.map +1 -0
- package/dist/mcp/prompts.d.ts +4 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +21 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/regex-search.d.ts +11 -0
- package/dist/mcp/regex-search.d.ts.map +1 -0
- package/dist/mcp/regex-search.js +53 -0
- package/dist/mcp/regex-search.js.map +1 -0
- package/dist/mcp/resources.d.ts +16 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +65 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/stdio.d.ts +11 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +56 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools/accessories.d.ts +15 -0
- package/dist/mcp/tools/accessories.d.ts.map +1 -0
- package/dist/mcp/tools/accessories.js +179 -0
- package/dist/mcp/tools/accessories.js.map +1 -0
- package/dist/mcp/tools/child-bridges.d.ts +3 -0
- package/dist/mcp/tools/child-bridges.d.ts.map +1 -0
- package/dist/mcp/tools/child-bridges.js +28 -0
- package/dist/mcp/tools/child-bridges.js.map +1 -0
- package/dist/mcp/tools/config.d.ts +7 -0
- package/dist/mcp/tools/config.d.ts.map +1 -0
- package/dist/mcp/tools/config.js +111 -0
- package/dist/mcp/tools/config.js.map +1 -0
- package/dist/mcp/tools/helpers.d.ts +31 -0
- package/dist/mcp/tools/helpers.d.ts.map +1 -0
- package/dist/mcp/tools/helpers.js +49 -0
- package/dist/mcp/tools/helpers.js.map +1 -0
- package/dist/mcp/tools/logs.d.ts +7 -0
- package/dist/mcp/tools/logs.d.ts.map +1 -0
- package/dist/mcp/tools/logs.js +122 -0
- package/dist/mcp/tools/logs.js.map +1 -0
- package/dist/mcp/tools/plugin-jobs.d.ts +11 -0
- package/dist/mcp/tools/plugin-jobs.d.ts.map +1 -0
- package/dist/mcp/tools/plugin-jobs.js +75 -0
- package/dist/mcp/tools/plugin-jobs.js.map +1 -0
- package/dist/mcp/tools/plugins.d.ts +3 -0
- package/dist/mcp/tools/plugins.d.ts.map +1 -0
- package/dist/mcp/tools/plugins.js +74 -0
- package/dist/mcp/tools/plugins.js.map +1 -0
- package/dist/mcp/tools/server.d.ts +3 -0
- package/dist/mcp/tools/server.d.ts.map +1 -0
- package/dist/mcp/tools/server.js +69 -0
- package/dist/mcp/tools/server.js.map +1 -0
- package/dist/mcp/tools/system.d.ts +3 -0
- package/dist/mcp/tools/system.d.ts.map +1 -0
- package/dist/mcp/tools/system.js +9 -0
- package/dist/mcp/tools/system.js.map +1 -0
- package/dist/mcp/types.d.ts +75 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +5 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/platform.d.ts +28 -0
- package/dist/plugin/platform.d.ts.map +1 -0
- package/dist/plugin/platform.js +76 -0
- package/dist/plugin/platform.js.map +1 -0
- package/dist/plugin/snippets.d.ts +27 -0
- package/dist/plugin/snippets.d.ts.map +1 -0
- package/dist/plugin/snippets.js +33 -0
- package/dist/plugin/snippets.js.map +1 -0
- package/homebridge-ui/public/index.html +164 -0
- package/homebridge-ui/public/js/app.js +200 -0
- package/homebridge-ui/public/lib/ai.css +738 -0
- package/homebridge-ui/public/lib/bootstrap-icons.min.css +5 -0
- package/homebridge-ui/public/lib/bootstrap.bundle.min.js +6 -0
- package/homebridge-ui/public/lib/bootstrap.min.css +5 -0
- package/homebridge-ui/public/lib/fonts/bootstrap-icons.woff +0 -0
- package/homebridge-ui/public/lib/fonts/bootstrap-icons.woff2 +0 -0
- package/homebridge-ui/public/lib/kit.css +994 -0
- package/homebridge-ui/public/lib/kit.js +794 -0
- package/homebridge-ui/server.js +28 -0
- package/package.json +94 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CharacteristicInfo, RegisterTools } from '../types.js';
|
|
2
|
+
type Value = string | number | boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Check `value` against a characteristic's metadata (format, range, step,
|
|
5
|
+
* valid values, write permission) and coerce it to the expected type,
|
|
6
|
+
* e.g. "50" → 50 or 1 → true. Returns an error message when it cannot be set.
|
|
7
|
+
*/
|
|
8
|
+
export declare function checkCharacteristicValue(info: CharacteristicInfo, value: Value): {
|
|
9
|
+
value: Value;
|
|
10
|
+
} | {
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const register: RegisterTools;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=accessories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessories.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/accessories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBhF,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvC;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAkDrH;AAED,eAAO,MAAM,QAAQ,EAAE,aA0ItB,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { READ, errorResult, handle, jsonResult } from './helpers.js';
|
|
3
|
+
function compactAccessory(acc) {
|
|
4
|
+
return {
|
|
5
|
+
uniqueId: acc.uniqueId,
|
|
6
|
+
serviceName: acc.serviceName,
|
|
7
|
+
type: acc.type,
|
|
8
|
+
manufacturer: acc.accessoryInformation?.Manufacturer ?? null,
|
|
9
|
+
model: acc.accessoryInformation?.Model ?? null,
|
|
10
|
+
values: acc.values ?? {},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const INTEGER_FORMATS = new Set(['int', 'uint8', 'uint16', 'uint32', 'uint64']);
|
|
14
|
+
/**
|
|
15
|
+
* Check `value` against a characteristic's metadata (format, range, step,
|
|
16
|
+
* valid values, write permission) and coerce it to the expected type,
|
|
17
|
+
* e.g. "50" → 50 or 1 → true. Returns an error message when it cannot be set.
|
|
18
|
+
*/
|
|
19
|
+
export function checkCharacteristicValue(info, value) {
|
|
20
|
+
const name = info.type;
|
|
21
|
+
if (info.canWrite === false) {
|
|
22
|
+
return { error: `${name} is read-only.` };
|
|
23
|
+
}
|
|
24
|
+
const format = info.format?.toLowerCase();
|
|
25
|
+
if (format === 'bool') {
|
|
26
|
+
if (typeof value === 'boolean') {
|
|
27
|
+
return { value };
|
|
28
|
+
}
|
|
29
|
+
if (value === 1 || value === 0 || value === '1' || value === '0' || value === 'true' || value === 'false') {
|
|
30
|
+
return { value: value === 1 || value === '1' || value === 'true' };
|
|
31
|
+
}
|
|
32
|
+
return { error: `${name} expects true or false, got ${JSON.stringify(value)}.` };
|
|
33
|
+
}
|
|
34
|
+
if (format === 'string' || format === 'data' || format === 'tlv8') {
|
|
35
|
+
const s = String(value);
|
|
36
|
+
if (info.maxLen !== undefined && s.length > info.maxLen) {
|
|
37
|
+
return { error: `${name} accepts at most ${info.maxLen} characters.` };
|
|
38
|
+
}
|
|
39
|
+
return { value: s };
|
|
40
|
+
}
|
|
41
|
+
if (format === 'float' || (format && INTEGER_FORMATS.has(format))) {
|
|
42
|
+
const n = Number(value);
|
|
43
|
+
if ((typeof value === 'string' && value.trim() === '') || !Number.isFinite(n)) {
|
|
44
|
+
return { error: `${name} expects a number, got ${JSON.stringify(value)}.` };
|
|
45
|
+
}
|
|
46
|
+
if (format !== 'float' && !Number.isInteger(n)) {
|
|
47
|
+
return { error: `${name} expects a whole number, got ${n}.` };
|
|
48
|
+
}
|
|
49
|
+
const range = [info.minValue !== undefined ? `min ${info.minValue}` : '', info.maxValue !== undefined ? `max ${info.maxValue}` : '']
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(', ');
|
|
52
|
+
if ((info.minValue !== undefined && n < info.minValue) || (info.maxValue !== undefined && n > info.maxValue)) {
|
|
53
|
+
return { error: `${name} must be within range (${range}), got ${n}.` };
|
|
54
|
+
}
|
|
55
|
+
if (info.validValues?.length && !info.validValues.includes(n)) {
|
|
56
|
+
return { error: `${name} must be one of ${info.validValues.join(', ')}, got ${n}.` };
|
|
57
|
+
}
|
|
58
|
+
if (info.minStep) {
|
|
59
|
+
const base = info.minValue ?? 0;
|
|
60
|
+
const steps = (n - base) / info.minStep;
|
|
61
|
+
if (Math.abs(steps - Math.round(steps)) > 1e-6) {
|
|
62
|
+
const nearest = base + Math.round(steps) * info.minStep;
|
|
63
|
+
return { error: `${name} moves in steps of ${info.minStep}; try ${Number(nearest.toFixed(6))}.` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { value: n };
|
|
67
|
+
}
|
|
68
|
+
return { value };
|
|
69
|
+
}
|
|
70
|
+
export const register = (tool, client) => {
|
|
71
|
+
tool('list_accessories', {
|
|
72
|
+
title: 'List accessories',
|
|
73
|
+
description: 'List all Homebridge accessories with their current state (on/off, brightness, temperature, etc.)',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
room: z.string().optional().describe('Filter by room name (case-insensitive)'),
|
|
76
|
+
type: z.string().optional().describe("Filter by accessory type (e.g. 'Lightbulb', 'Switch', 'Thermostat')"),
|
|
77
|
+
manufacturer: z.string().optional().describe('Filter by manufacturer (case-insensitive, contains match)'),
|
|
78
|
+
excludeManufacturer: z.string().optional().describe('Exclude accessories from this manufacturer (case-insensitive, contains match)'),
|
|
79
|
+
name: z.string().optional().describe('Filter by service name (case-insensitive, contains match)'),
|
|
80
|
+
},
|
|
81
|
+
annotations: READ,
|
|
82
|
+
}, handle('listing accessories', async ({ room, type, manufacturer, excludeManufacturer, name }) => {
|
|
83
|
+
const [all, layout] = await Promise.all([
|
|
84
|
+
client.getAccessories(),
|
|
85
|
+
room ? client.getAccessoryLayout() : undefined,
|
|
86
|
+
]);
|
|
87
|
+
let accessories = all;
|
|
88
|
+
// Room filter: resolve UIDs from layout
|
|
89
|
+
if (room && layout) {
|
|
90
|
+
const matchedRoom = layout.find((r) => r.name.toLowerCase() === room.toLowerCase());
|
|
91
|
+
if (!matchedRoom) {
|
|
92
|
+
return errorResult(`Room not found: "${room}". Available rooms: ${layout.map((r) => r.name).join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
const roomUids = new Set(matchedRoom.services.map((s) => s.uniqueId));
|
|
95
|
+
accessories = accessories.filter((a) => roomUids.has(a.uniqueId));
|
|
96
|
+
}
|
|
97
|
+
if (type) {
|
|
98
|
+
const t = type.toLowerCase();
|
|
99
|
+
accessories = accessories.filter((a) => a.type?.toLowerCase() === t);
|
|
100
|
+
}
|
|
101
|
+
if (manufacturer) {
|
|
102
|
+
const mfr = manufacturer.toLowerCase();
|
|
103
|
+
accessories = accessories.filter((a) => a.accessoryInformation?.Manufacturer?.toLowerCase().includes(mfr));
|
|
104
|
+
}
|
|
105
|
+
if (excludeManufacturer) {
|
|
106
|
+
const excl = excludeManufacturer.toLowerCase();
|
|
107
|
+
accessories = accessories.filter((a) => !a.accessoryInformation?.Manufacturer?.toLowerCase().includes(excl));
|
|
108
|
+
}
|
|
109
|
+
if (name) {
|
|
110
|
+
const n = name.toLowerCase();
|
|
111
|
+
accessories = accessories.filter((a) => a.serviceName?.toLowerCase().includes(n));
|
|
112
|
+
}
|
|
113
|
+
return jsonResult(accessories.map(compactAccessory));
|
|
114
|
+
}));
|
|
115
|
+
tool('get_accessory', {
|
|
116
|
+
title: 'Get accessory',
|
|
117
|
+
description: 'Get detailed information about a specific accessory by its uniqueId. Use list_accessories first to find the uniqueId.',
|
|
118
|
+
inputSchema: { uniqueId: z.string().min(1).describe('The unique identifier of the accessory') },
|
|
119
|
+
annotations: READ,
|
|
120
|
+
}, handle('getting accessory', async ({ uniqueId }) => jsonResult(await client.getAccessory(uniqueId))));
|
|
121
|
+
tool('set_accessory', {
|
|
122
|
+
title: 'Control accessory',
|
|
123
|
+
description: 'Control a Homebridge accessory — turn it on/off, set brightness, color temperature, etc. ' +
|
|
124
|
+
'Use list_accessories first to find the uniqueId and available characteristicTypes. ' +
|
|
125
|
+
'The value is checked against the characteristic (format, min/max, step, valid values, writable) before it is sent.',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
uniqueId: z.string().min(1).describe('The unique identifier of the accessory'),
|
|
128
|
+
characteristicType: z
|
|
129
|
+
.string()
|
|
130
|
+
.min(1)
|
|
131
|
+
.describe("The characteristic to set (e.g. 'On', 'Brightness', 'ColorTemperature', 'Hue', 'Saturation', 'TargetTemperature', 'TargetDoorState')"),
|
|
132
|
+
value: z
|
|
133
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
134
|
+
.describe('The value to set (e.g. true/false for On, 0-100 for Brightness)'),
|
|
135
|
+
},
|
|
136
|
+
// Changes physical device state (locks, garage doors) but doesn't destroy data.
|
|
137
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
138
|
+
}, handle('setting accessory', async ({ uniqueId, characteristicType, value }) => {
|
|
139
|
+
const accessory = await client.getAccessory(uniqueId);
|
|
140
|
+
const characteristics = accessory?.serviceCharacteristics;
|
|
141
|
+
let toSend = value;
|
|
142
|
+
if (characteristics?.length) {
|
|
143
|
+
const info = characteristics.find((c) => c.type.toLowerCase() === characteristicType.toLowerCase());
|
|
144
|
+
if (!info) {
|
|
145
|
+
const writable = characteristics.filter((c) => c.canWrite !== false).map((c) => c.type);
|
|
146
|
+
return errorResult(`${accessory.serviceName ?? uniqueId} has no characteristic "${characteristicType}". Writable: ${writable.join(', ') || 'none'}.`);
|
|
147
|
+
}
|
|
148
|
+
const checked = checkCharacteristicValue(info, value);
|
|
149
|
+
if ('error' in checked) {
|
|
150
|
+
return errorResult(checked.error);
|
|
151
|
+
}
|
|
152
|
+
toSend = checked.value;
|
|
153
|
+
characteristicType = info.type;
|
|
154
|
+
}
|
|
155
|
+
return jsonResult(await client.setAccessoryCharacteristic(uniqueId, characteristicType, toSend));
|
|
156
|
+
}));
|
|
157
|
+
tool('get_accessory_layout', {
|
|
158
|
+
title: 'Get room layout',
|
|
159
|
+
description: 'Get the accessories room layout as configured in the Homebridge UI.',
|
|
160
|
+
annotations: READ,
|
|
161
|
+
}, handle('getting layout', async () => {
|
|
162
|
+
const [layout, accessories] = await Promise.all([client.getAccessoryLayout(), client.getAccessories()]);
|
|
163
|
+
const accessoryMap = new Map(accessories.map((acc) => [acc.uniqueId, acc]));
|
|
164
|
+
const enriched = layout.map((room) => ({
|
|
165
|
+
name: room.name,
|
|
166
|
+
services: room.services.map((svc) => {
|
|
167
|
+
const acc = accessoryMap.get(svc.uniqueId);
|
|
168
|
+
return {
|
|
169
|
+
uniqueId: svc.uniqueId,
|
|
170
|
+
serviceName: acc?.serviceName ?? svc.customName ?? 'Unknown',
|
|
171
|
+
type: acc?.type ?? 'Unknown',
|
|
172
|
+
manufacturer: acc?.accessoryInformation?.Manufacturer ?? null,
|
|
173
|
+
};
|
|
174
|
+
}),
|
|
175
|
+
}));
|
|
176
|
+
return jsonResult(enriched);
|
|
177
|
+
}));
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=accessories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessories.js","sourceRoot":"","sources":["../../../src/mcp/tools/accessories.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-bridges.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/child-bridges.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAUjD,eAAO,MAAM,QAAQ,EAAE,aA8BtB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { READ, handle, jsonResult, pick, textResult } from './helpers.js';
|
|
3
|
+
const FIELDS = ['username', 'name', 'plugin', 'identifier', 'status', 'paired', 'pid', 'port', 'manuallyStopped'];
|
|
4
|
+
const deviceId = z
|
|
5
|
+
.string()
|
|
6
|
+
.regex(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$|^[0-9A-Fa-f]{12}$/, 'must be a child bridge username like 0E:3C:12:AB:CD:EF')
|
|
7
|
+
.describe('The child bridge username (deviceId) from list_child_bridges');
|
|
8
|
+
export const register = (tool, client) => {
|
|
9
|
+
tool('list_child_bridges', {
|
|
10
|
+
title: 'List child bridges',
|
|
11
|
+
description: 'List the child bridges (plugins running in their own process) with their status (ok, pending, down) and whether they were stopped manually.',
|
|
12
|
+
annotations: READ,
|
|
13
|
+
}, handle('listing child bridges', async () => jsonResult((await client.getChildBridges()).map((b) => pick(b, FIELDS)))));
|
|
14
|
+
const control = (action, title, description) => tool(`${action}_child_bridge`, {
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
inputSchema: { deviceId },
|
|
18
|
+
// Starting only brings accessories back; stopping and restarting interrupt them.
|
|
19
|
+
annotations: { readOnlyHint: false, destructiveHint: action !== 'start', idempotentHint: true, openWorldHint: false },
|
|
20
|
+
}, handle(`${action === 'stop' ? 'stopp' : action}ing child bridge`, async ({ deviceId }) => {
|
|
21
|
+
await client.controlChildBridge(action, deviceId);
|
|
22
|
+
return textResult(`Child bridge ${deviceId}: ${action} requested. Check list_child_bridges for its status.`);
|
|
23
|
+
}));
|
|
24
|
+
control('restart', 'Restart child bridge', 'Restart one child bridge without restarting Homebridge. Its accessories are briefly unavailable.');
|
|
25
|
+
control('stop', 'Stop child bridge', 'Stop one child bridge. Its accessories stay unavailable until it is started again.');
|
|
26
|
+
control('start', 'Start child bridge', 'Start a child bridge that was stopped.');
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=child-bridges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-bridges.js","sourceRoot":"","sources":["../../../src/mcp/tools/child-bridges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,CAAU,CAAC;AAE3H,MAAM,QAAQ,GAAG,CAAC;KACf,MAAM,EAAE;KACR,KAAK,CAAC,wDAAwD,EAAE,wDAAwD,CAAC;KACzH,QAAQ,CAAC,8DAA8D,CAAC,CAAC;AAE5E,MAAM,CAAC,MAAM,QAAQ,GAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;IACtD,IAAI,CACF,oBAAoB,EACpB;QACE,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,6IAA6I;QAC1J,WAAW,EAAE,IAAI;KAClB,EACD,MAAM,CAAC,uBAAuB,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,MAAM,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CACtH,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,MAAoC,EAAE,KAAa,EAAE,WAAmB,EAAE,EAAE,CAC3F,IAAI,CACF,GAAG,MAAM,eAAe,EACxB;QACE,KAAK;QACL,WAAW;QACX,WAAW,EAAE,EAAE,QAAQ,EAAE;QACzB,iFAAiF;QACjF,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE;KACtH,EACD,MAAM,CAAC,GAAG,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,kBAAkB,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;QACvF,MAAM,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAClD,OAAO,UAAU,CAAC,gBAAgB,QAAQ,KAAK,MAAM,sDAAsD,CAAC,CAAC;IAC/G,CAAC,CAAC,CACH,CAAC;IAEJ,OAAO,CAAC,SAAS,EAAE,sBAAsB,EAAE,kGAAkG,CAAC,CAAC;IAC/I,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,oFAAoF,CAAC,CAAC;IAC3H,OAAO,CAAC,OAAO,EAAE,oBAAoB,EAAE,wCAAwC,CAAC,CAAC;AACnF,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegisterTools } from '../types.js';
|
|
2
|
+
type Block = Record<string, unknown>;
|
|
3
|
+
/** Deep merge: objects merge key by key, `null` deletes a key, anything else (arrays included) replaces. */
|
|
4
|
+
export declare function mergePatch(target: Block, patch: Block): Block;
|
|
5
|
+
export declare const register: RegisterTools;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAcjD,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAMrC,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,CAY7D;AAMD,eAAO,MAAM,QAAQ,EAAE,aA4FtB,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { REDACTED, containsRedacted, redactSecrets, restoreSecrets } from '@mp-consulting/homebridge-ai-core';
|
|
3
|
+
import { READ, errorResult, handle, jsonResult, textResult } from './helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* The minimum shape Homebridge needs. Extra keys pass through untouched; this
|
|
6
|
+
* only stops an empty or truncated object from replacing the whole file.
|
|
7
|
+
*/
|
|
8
|
+
const configSchema = z.looseObject({
|
|
9
|
+
bridge: z.looseObject({}).describe('The bridge block (name, username, port, pin, ...)'),
|
|
10
|
+
accessories: z.array(z.looseObject({})).optional(),
|
|
11
|
+
platforms: z.array(z.looseObject({})).optional(),
|
|
12
|
+
});
|
|
13
|
+
function isBlock(v) {
|
|
14
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
/** Deep merge: objects merge key by key, `null` deletes a key, anything else (arrays included) replaces. */
|
|
17
|
+
export function mergePatch(target, patch) {
|
|
18
|
+
const out = { ...target };
|
|
19
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
20
|
+
if (value === null) {
|
|
21
|
+
delete out[key];
|
|
22
|
+
}
|
|
23
|
+
else if (isBlock(value) && isBlock(out[key])) {
|
|
24
|
+
out[key] = mergePatch(out[key], value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
out[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function describeBlock(b) {
|
|
33
|
+
return [b.platform && `platform=${b.platform}`, b.accessory && `accessory=${b.accessory}`, b.name && `name=${b.name}`].filter(Boolean).join(' ');
|
|
34
|
+
}
|
|
35
|
+
export const register = (tool, client) => {
|
|
36
|
+
tool('get_config', {
|
|
37
|
+
title: 'Read config.json',
|
|
38
|
+
description: 'Read the current Homebridge config.json file content. Returns the full configuration including bridge settings, accessories, and platforms. ' +
|
|
39
|
+
`Passwords, tokens, API keys and the bridge pin are replaced by "${REDACTED}"; leave those placeholders as-is when calling update_config ` +
|
|
40
|
+
'and the real values are kept.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
includeSecrets: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Return real secret values instead of placeholders. Only use when the user explicitly asks to see a credential.'),
|
|
46
|
+
},
|
|
47
|
+
annotations: READ,
|
|
48
|
+
}, handle('getting config', async ({ includeSecrets }) => {
|
|
49
|
+
const config = await client.getConfig();
|
|
50
|
+
return jsonResult(includeSecrets ? config : redactSecrets(config));
|
|
51
|
+
}));
|
|
52
|
+
tool('update_config', {
|
|
53
|
+
title: 'Write config.json',
|
|
54
|
+
description: 'Update the Homebridge config.json file. You must provide the FULL config object — it replaces the entire file. ' +
|
|
55
|
+
`Use get_config first to read the current config, then modify and pass back the complete object. "${REDACTED}" placeholders ` +
|
|
56
|
+
'are swapped back to the current real values before saving.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
config: configSchema.describe('The complete config.json object to write'),
|
|
59
|
+
},
|
|
60
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
61
|
+
}, handle('updating config', async ({ config }) => {
|
|
62
|
+
const toWrite = containsRedacted(config) ? restoreSecrets(config, await client.getConfig()) : config;
|
|
63
|
+
const result = await client.updateConfig(toWrite);
|
|
64
|
+
return result
|
|
65
|
+
? jsonResult(result)
|
|
66
|
+
: textResult('Config updated successfully. A Homebridge restart may be required for changes to take effect.');
|
|
67
|
+
}));
|
|
68
|
+
tool('patch_config', {
|
|
69
|
+
title: 'Edit one config block',
|
|
70
|
+
description: 'Change one platform or accessory block in config.json without sending the whole file. Identify the block by `platform` or `accessory` ' +
|
|
71
|
+
'(plus `name` when several blocks share it). `patch` is deep-merged: objects merge, arrays and values replace, and null removes a key. ' +
|
|
72
|
+
`"${REDACTED}" placeholders in the patch keep the current secret. Returns the updated block (secrets redacted).`,
|
|
73
|
+
inputSchema: {
|
|
74
|
+
platform: z.string().min(1).optional().describe("The block's `platform` value, e.g. 'Camera-ffmpeg'"),
|
|
75
|
+
accessory: z.string().min(1).optional().describe("The block's `accessory` value, for accessory plugins"),
|
|
76
|
+
name: z.string().min(1).optional().describe("The block's `name`, to pick one of several blocks of the same platform"),
|
|
77
|
+
patch: z.looseObject({}).describe('Keys to change'),
|
|
78
|
+
},
|
|
79
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
80
|
+
}, handle('patching config', async ({ platform, accessory, name, patch }) => {
|
|
81
|
+
if (!platform === !accessory) {
|
|
82
|
+
return errorResult('Pass exactly one of `platform` or `accessory`.');
|
|
83
|
+
}
|
|
84
|
+
const config = await client.getConfig();
|
|
85
|
+
const listKey = platform ? 'platforms' : 'accessories';
|
|
86
|
+
const idKey = platform ? 'platform' : 'accessory';
|
|
87
|
+
const id = (platform ?? accessory);
|
|
88
|
+
const list = Array.isArray(config[listKey]) ? config[listKey] : [];
|
|
89
|
+
const matches = list
|
|
90
|
+
.map((b, index) => ({ b, index }))
|
|
91
|
+
.filter(({ b }) => isBlock(b) && b[idKey] === id && (name === undefined || b.name === name));
|
|
92
|
+
if (matches.length === 0) {
|
|
93
|
+
return errorResult(`No ${idKey} block "${id}"${name ? ` named "${name}"` : ''} in config.json.`);
|
|
94
|
+
}
|
|
95
|
+
if (matches.length > 1) {
|
|
96
|
+
return errorResult(`Several ${idKey} blocks match "${id}": ${matches.map(({ b }) => describeBlock(b)).join('; ')}. Pass \`name\` to pick one.`);
|
|
97
|
+
}
|
|
98
|
+
const { b, index } = matches[0];
|
|
99
|
+
const current = b;
|
|
100
|
+
let updated = mergePatch(current, patch);
|
|
101
|
+
if (containsRedacted(updated)) {
|
|
102
|
+
updated = restoreSecrets(updated, current);
|
|
103
|
+
}
|
|
104
|
+
updated[idKey] = id;
|
|
105
|
+
const nextList = [...list];
|
|
106
|
+
nextList[index] = updated;
|
|
107
|
+
await client.updateConfig({ ...config, [listKey]: nextList });
|
|
108
|
+
return jsonResult({ updated: redactSecrets(updated), note: 'Saved. A Homebridge (or child bridge) restart may be required.' });
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/mcp/tools/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
|
+
import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
export interface ToolConfig<Args extends ZodRawShapeCompat | undefined> {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema?: Args;
|
|
8
|
+
/** Required so every tool states whether it reads or writes. */
|
|
9
|
+
annotations: ToolAnnotations & {
|
|
10
|
+
readOnlyHint: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Registers a tool, or silently skips it when the server runs read-only and the tool writes. */
|
|
14
|
+
export type ToolRegistrar = <Args extends ZodRawShapeCompat | undefined = undefined>(name: string, config: ToolConfig<Args>, cb: ToolCallback<Args>) => void;
|
|
15
|
+
export declare function createRegistrar(server: McpServer, { readOnly }?: {
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
}): ToolRegistrar;
|
|
18
|
+
/** Reads Homebridge state, changes nothing. */
|
|
19
|
+
export declare const READ: ToolConfig<undefined>['annotations'];
|
|
20
|
+
/** Reads from the npm registry via Homebridge. */
|
|
21
|
+
export declare const READ_REGISTRY: ToolConfig<undefined>['annotations'];
|
|
22
|
+
export declare function textResult(text: string): CallToolResult;
|
|
23
|
+
/** Compact JSON: pretty-printing costs ~25% more tokens for the same data. */
|
|
24
|
+
export declare function jsonResult(data: unknown): CallToolResult;
|
|
25
|
+
export declare function errorResult(text: string): CallToolResult;
|
|
26
|
+
export declare function errorMessage(error: unknown): string;
|
|
27
|
+
/** Wraps a tool handler so a thrown error becomes an `isError` result prefixed with `action`. */
|
|
28
|
+
export declare function handle<A extends unknown[]>(action: string, fn: (...args: A) => Promise<CallToolResult>): (...args: A) => Promise<CallToolResult>;
|
|
29
|
+
/** Copies only the listed keys that are present on `obj`. */
|
|
30
|
+
export declare function pick<T extends object, K extends string>(obj: T, keys: readonly K[]): Partial<Record<K, unknown>>;
|
|
31
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AACvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1F,MAAM,WAAW,UAAU,CAAC,IAAI,SAAS,iBAAiB,GAAG,SAAS;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,gEAAgE;IAChE,WAAW,EAAE,eAAe,GAAG;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1D;AAED,iGAAiG;AACjG,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,SAAS,iBAAiB,GAAG,SAAS,GAAG,SAAS,EACjF,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,EACxB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,KACnB,IAAI,CAAC;AAEV,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,QAAgB,EAAE,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,aAAa,CAOnH;AAID,+CAA+C;AAC/C,eAAO,MAAM,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,aAAa,CAAgD,CAAC;AAEvG,kDAAkD;AAClD,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,aAAa,CAA+C,CAAC;AAI/G,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAEvD;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAExD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAExD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,iGAAiG;AACjG,wBAAgB,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,EACxC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,cAAc,CAAC,GAC1C,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,cAAc,CAAC,CAQzC;AAED,6DAA6D;AAC7D,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAQhH"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function createRegistrar(server, { readOnly = false } = {}) {
|
|
2
|
+
return (name, config, cb) => {
|
|
3
|
+
if (readOnly && !config.annotations.readOnlyHint) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
server.registerTool(name, config, cb);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
// ── Annotation presets ─────────────────────────────────────────────
|
|
10
|
+
/** Reads Homebridge state, changes nothing. */
|
|
11
|
+
export const READ = { readOnlyHint: true, openWorldHint: false };
|
|
12
|
+
/** Reads from the npm registry via Homebridge. */
|
|
13
|
+
export const READ_REGISTRY = { readOnlyHint: true, openWorldHint: true };
|
|
14
|
+
// ── Results ────────────────────────────────────────────────────────
|
|
15
|
+
export function textResult(text) {
|
|
16
|
+
return { content: [{ type: 'text', text }] };
|
|
17
|
+
}
|
|
18
|
+
/** Compact JSON: pretty-printing costs ~25% more tokens for the same data. */
|
|
19
|
+
export function jsonResult(data) {
|
|
20
|
+
return textResult(JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
export function errorResult(text) {
|
|
23
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
export function errorMessage(error) {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
|
28
|
+
/** Wraps a tool handler so a thrown error becomes an `isError` result prefixed with `action`. */
|
|
29
|
+
export function handle(action, fn) {
|
|
30
|
+
return async (...args) => {
|
|
31
|
+
try {
|
|
32
|
+
return await fn(...args);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return errorResult(`Error ${action}: ${errorMessage(error)}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Copies only the listed keys that are present on `obj`. */
|
|
40
|
+
export function pick(obj, keys) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
if (key in obj) {
|
|
44
|
+
out[key] = obj[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../src/mcp/tools/helpers.ts"],"names":[],"mappings":"AAmBA,MAAM,UAAU,eAAe,CAAC,MAAiB,EAAE,EAAE,QAAQ,GAAG,KAAK,KAA6B,EAAE;IAClG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;QAC1B,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC;YACjD,OAAO;QACT,CAAC;QACD,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IACxC,CAAC,CAAC;AACJ,CAAC;AAED,sEAAsE;AAEtE,+CAA+C;AAC/C,MAAM,CAAC,MAAM,IAAI,GAAyC,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AAEvG,kDAAkD;AAClD,MAAM,CAAC,MAAM,aAAa,GAAyC,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AAE/G,sEAAsE;AAEtE,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,MAAM,CACpB,MAAc,EACd,EAA2C;IAE3C,OAAO,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,SAAS,MAAM,KAAK,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,IAAI,CAAqC,GAAM,EAAE,IAAkB;IACjF,MAAM,GAAG,GAAgC,EAAE,CAAC;IAC5C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;YACf,GAAG,CAAC,GAAG,CAAC,GAAI,GAA+B,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegisterTools } from '../types.js';
|
|
2
|
+
/** Cap on how much of the log we keep in memory, in bytes. The rest is streamed past. */
|
|
3
|
+
export declare const MAX_BYTES: number;
|
|
4
|
+
/** Wall-clock budget for a regex search, so a pathological pattern can't hang the server. */
|
|
5
|
+
export declare const SEARCH_BUDGET_MS = 5000;
|
|
6
|
+
export declare const register: RegisterTools;
|
|
7
|
+
//# sourceMappingURL=logs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/logs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AASjD,yFAAyF;AACzF,eAAO,MAAM,SAAS,QAAmB,CAAC;AAE1C,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAwCrC,eAAO,MAAM,QAAQ,EAAE,aA+FtB,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RegexTimeoutError, regexSearch } from '../regex-search.js';
|
|
3
|
+
import { READ, errorResult, handle, textResult } from './helpers.js';
|
|
4
|
+
// The UI strips colour codes server-side, but a custom log path or an older UI
|
|
5
|
+
// can still return them, so strip defensively before matching *and* displaying.
|
|
6
|
+
// eslint-disable-next-line no-control-regex
|
|
7
|
+
const ANSI_REGEX = /\u001B\[[0-9;]*m/g;
|
|
8
|
+
/** Cap on how much of the log we keep in memory, in bytes. The rest is streamed past. */
|
|
9
|
+
export const MAX_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
/** Wall-clock budget for a regex search, so a pathological pattern can't hang the server. */
|
|
11
|
+
export const SEARCH_BUDGET_MS = 5000;
|
|
12
|
+
function stripAnsi(s) {
|
|
13
|
+
return s.replace(ANSI_REGEX, '');
|
|
14
|
+
}
|
|
15
|
+
function splitLines(text) {
|
|
16
|
+
const lines = text.split('\n');
|
|
17
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
18
|
+
lines.pop();
|
|
19
|
+
}
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the log and return its most recent lines, ANSI-stripped. Lines are
|
|
24
|
+
* stripped before they are handed back so that searching and displaying both
|
|
25
|
+
* operate on the same text the user sees.
|
|
26
|
+
*/
|
|
27
|
+
async function readLogTail(client) {
|
|
28
|
+
const { text, truncated } = await client.getLogTail(MAX_BYTES);
|
|
29
|
+
const lines = splitLines(text).map(stripAnsi);
|
|
30
|
+
// The first line of a truncated read is the tail end of an earlier line.
|
|
31
|
+
if (truncated) {
|
|
32
|
+
lines.shift();
|
|
33
|
+
}
|
|
34
|
+
return { lines, truncated };
|
|
35
|
+
}
|
|
36
|
+
function truncationNote(truncated) {
|
|
37
|
+
return truncated ? `Log is large; only the most recent ${MAX_BYTES / 1024 / 1024} MB was read.` : '';
|
|
38
|
+
}
|
|
39
|
+
export const register = (tool, client) => {
|
|
40
|
+
tool('get_recent_logs', {
|
|
41
|
+
title: 'Recent log lines',
|
|
42
|
+
description: 'Return the most recent lines from the Homebridge log (ANSI-stripped). Requires an hb-service based Homebridge install.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
lines: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(5000)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Number of lines to return from the tail (default 200, max 5000).'),
|
|
51
|
+
},
|
|
52
|
+
annotations: READ,
|
|
53
|
+
}, handle('reading Homebridge log', async ({ lines }) => {
|
|
54
|
+
const n = lines ?? 200;
|
|
55
|
+
const { lines: all, truncated } = await readLogTail(client);
|
|
56
|
+
const body = all.slice(-n).join('\n');
|
|
57
|
+
const note = truncationNote(truncated);
|
|
58
|
+
return textResult(note && body ? `${note}\n\n${body}` : note || body);
|
|
59
|
+
}));
|
|
60
|
+
tool('search_logs', {
|
|
61
|
+
title: 'Search logs',
|
|
62
|
+
description: 'Search the Homebridge log for matching lines. Returns up to `limit` most recent matches (ANSI-stripped). ' +
|
|
63
|
+
'Useful for finding errors, warnings, or events involving a specific device.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
pattern: z.string().min(1).describe('Substring or regex pattern to match against each log line.'),
|
|
66
|
+
regex: z
|
|
67
|
+
.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Treat pattern as a JavaScript regex (default: false, treats pattern as a literal substring).'),
|
|
70
|
+
caseSensitive: z.boolean().optional().describe('Case-sensitive match (default: false).'),
|
|
71
|
+
limit: z
|
|
72
|
+
.number()
|
|
73
|
+
.int()
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(2000)
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('Maximum matches to return, taken from the most recent (default 100, max 2000).'),
|
|
78
|
+
},
|
|
79
|
+
annotations: READ,
|
|
80
|
+
}, handle('searching Homebridge log', async ({ pattern, regex, caseSensitive, limit }) => {
|
|
81
|
+
const max = limit ?? 100;
|
|
82
|
+
const cs = caseSensitive ?? false;
|
|
83
|
+
const flags = cs ? '' : 'i';
|
|
84
|
+
const label = regex ? `/${pattern}/${flags}` : JSON.stringify(pattern) + (cs ? ' (case-sensitive)' : '');
|
|
85
|
+
if (regex) {
|
|
86
|
+
try {
|
|
87
|
+
new RegExp(pattern, flags);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return errorResult(`Invalid regex ${label}: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const { lines, truncated } = await readLogTail(client);
|
|
94
|
+
let matches;
|
|
95
|
+
if (regex) {
|
|
96
|
+
try {
|
|
97
|
+
matches = (await regexSearch(lines, pattern, flags, SEARCH_BUDGET_MS)).map((i) => lines[i]);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof RegexTimeoutError) {
|
|
101
|
+
return errorResult(`Search for ${label} exceeded ${SEARCH_BUDGET_MS}ms and was stopped. Try a simpler pattern or a literal substring.`);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Substring search is linear, so it is safe on the main thread.
|
|
108
|
+
const needle = cs ? pattern : pattern.toLowerCase();
|
|
109
|
+
matches = lines.filter((line) => (cs ? line : line.toLowerCase()).includes(needle));
|
|
110
|
+
}
|
|
111
|
+
const taken = matches.slice(-max);
|
|
112
|
+
const parts = [`Showing ${taken.length} of ${matches.length} match${matches.length === 1 ? '' : 'es'} for ${label}.`];
|
|
113
|
+
const note = truncationNote(truncated);
|
|
114
|
+
if (note) {
|
|
115
|
+
parts.push(note);
|
|
116
|
+
}
|
|
117
|
+
const header = parts.join(' ');
|
|
118
|
+
const body = taken.join('\n');
|
|
119
|
+
return textResult(body ? `${header}\n\n${body}` : header);
|
|
120
|
+
}));
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=logs.js.map
|