@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1

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.
Files changed (281) hide show
  1. package/.env.example +1 -1
  2. package/README.md +11 -2
  3. package/dist/omadaClient/client.d.ts +6 -1
  4. package/dist/omadaClient/client.js +12 -1
  5. package/dist/omadaClient/client.js.map +1 -1
  6. package/dist/omadaClient/diagnose.d.ts +68 -0
  7. package/dist/omadaClient/diagnose.js +164 -0
  8. package/dist/omadaClient/diagnose.js.map +1 -0
  9. package/dist/omadaClient/index.d.ts +6 -0
  10. package/dist/omadaClient/index.js +15 -0
  11. package/dist/omadaClient/index.js.map +1 -1
  12. package/dist/omadaClient/macGroups.d.ts +58 -0
  13. package/dist/omadaClient/macGroups.js +124 -0
  14. package/dist/omadaClient/macGroups.js.map +1 -0
  15. package/dist/tools/infra/actions.d.ts +1 -1
  16. package/dist/tools/infra/actions.js +1 -1
  17. package/dist/tools/omada/index.js +6 -0
  18. package/dist/tools/omada/index.js.map +1 -1
  19. package/dist/tools/omada/macGroups.d.ts +19 -0
  20. package/dist/tools/omada/macGroups.js +77 -0
  21. package/dist/tools/omada/macGroups.js.map +1 -0
  22. package/dist/tools/omada/namespace.js +8 -0
  23. package/dist/tools/omada/namespace.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +5 -1
  27. package/.codex/review-rules.md +0 -16
  28. package/.dockerignore +0 -8
  29. package/.gitattributes +0 -30
  30. package/.github/workflows/ci.yml +0 -193
  31. package/AGENTS.md +0 -389
  32. package/Dockerfile +0 -50
  33. package/docker-compose.yml +0 -23
  34. package/docs/CHANGELOG.md +0 -341
  35. package/docs/COVERAGE.md +0 -219
  36. package/docs/QUICK_START.md +0 -240
  37. package/eslint.config.js +0 -33
  38. package/n8n/ai-agent-safety-monitor.json +0 -355
  39. package/openapi.json +0 -1259
  40. package/scripts/check-version-sync.mjs +0 -69
  41. package/scripts/generate-jwt.ts +0 -121
  42. package/src/config.ts +0 -440
  43. package/src/haClient/automations.ts +0 -261
  44. package/src/haClient/calendars.ts +0 -94
  45. package/src/haClient/config.ts +0 -52
  46. package/src/haClient/devices.ts +0 -653
  47. package/src/haClient/entities.ts +0 -128
  48. package/src/haClient/events.ts +0 -689
  49. package/src/haClient/history.ts +0 -73
  50. package/src/haClient/index.ts +0 -336
  51. package/src/haClient/request.ts +0 -142
  52. package/src/haClient/services.ts +0 -45
  53. package/src/haClient/states.ts +0 -60
  54. package/src/haClient/updates.ts +0 -58
  55. package/src/haClient/wsCommand.ts +0 -44
  56. package/src/index.ts +0 -164
  57. package/src/localAI/index.ts +0 -88
  58. package/src/localAI/prompts.ts +0 -50
  59. package/src/localAI/providers/base.ts +0 -120
  60. package/src/localAI/providers/ollama.ts +0 -78
  61. package/src/localAI/providers/openai.ts +0 -86
  62. package/src/localAI/types.ts +0 -67
  63. package/src/omadaClient/auth.ts +0 -178
  64. package/src/omadaClient/client.ts +0 -337
  65. package/src/omadaClient/device.ts +0 -118
  66. package/src/omadaClient/index.ts +0 -310
  67. package/src/omadaClient/network.ts +0 -284
  68. package/src/omadaClient/request.ts +0 -365
  69. package/src/omadaClient/security.ts +0 -51
  70. package/src/omadaClient/site.ts +0 -344
  71. package/src/ownerQuestions/service.ts +0 -536
  72. package/src/permissions/index.ts +0 -143
  73. package/src/resources/index.ts +0 -213
  74. package/src/server/auth.ts +0 -175
  75. package/src/server/chatFace.ts +0 -789
  76. package/src/server/chatSlice.ts +0 -57
  77. package/src/server/common.ts +0 -170
  78. package/src/server/eventSubscription.ts +0 -343
  79. package/src/server/http.ts +0 -626
  80. package/src/server/instructions.ts +0 -97
  81. package/src/server/rateLimiter.ts +0 -227
  82. package/src/server/stdio.ts +0 -41
  83. package/src/server/stream.ts +0 -205
  84. package/src/tools/ai/analyzeSensors.ts +0 -33
  85. package/src/tools/ai/index.ts +0 -20
  86. package/src/tools/common.ts +0 -442
  87. package/src/tools/homeassistant/askOwner.ts +0 -133
  88. package/src/tools/homeassistant/automationControls.ts +0 -66
  89. package/src/tools/homeassistant/calendar.ts +0 -97
  90. package/src/tools/homeassistant/callService.ts +0 -25
  91. package/src/tools/homeassistant/controlClimate.ts +0 -68
  92. package/src/tools/homeassistant/controlCover.ts +0 -48
  93. package/src/tools/homeassistant/controlFan.ts +0 -44
  94. package/src/tools/homeassistant/controlLight.ts +0 -38
  95. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  96. package/src/tools/homeassistant/createAutomation.ts +0 -35
  97. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  98. package/src/tools/homeassistant/entityAction.ts +0 -35
  99. package/src/tools/homeassistant/entityGuard.ts +0 -28
  100. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  101. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  102. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  103. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  104. package/src/tools/homeassistant/getHistory.ts +0 -24
  105. package/src/tools/homeassistant/getState.ts +0 -24
  106. package/src/tools/homeassistant/getStates.ts +0 -46
  107. package/src/tools/homeassistant/getVersion.ts +0 -21
  108. package/src/tools/homeassistant/index.ts +0 -121
  109. package/src/tools/homeassistant/listAutomations.ts +0 -35
  110. package/src/tools/homeassistant/listEntities.ts +0 -44
  111. package/src/tools/homeassistant/listPersons.ts +0 -46
  112. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  113. package/src/tools/homeassistant/searchEntities.ts +0 -58
  114. package/src/tools/homeassistant/sendNotification.ts +0 -179
  115. package/src/tools/homeassistant/systemTools.ts +0 -52
  116. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  117. package/src/tools/index.ts +0 -78
  118. package/src/tools/infra/actions.ts +0 -95
  119. package/src/tools/infra/index.ts +0 -138
  120. package/src/tools/omada/blockClient.ts +0 -28
  121. package/src/tools/omada/cyclePoePort.ts +0 -32
  122. package/src/tools/omada/deleteClient.ts +0 -27
  123. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  124. package/src/tools/omada/getClient.ts +0 -14
  125. package/src/tools/omada/getDevice.ts +0 -14
  126. package/src/tools/omada/getFirewallSetting.ts +0 -14
  127. package/src/tools/omada/getInternetInfo.ts +0 -14
  128. package/src/tools/omada/getLanNetworkList.ts +0 -14
  129. package/src/tools/omada/getLanProfileList.ts +0 -14
  130. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  131. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  132. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  133. package/src/tools/omada/getSsidDetail.ts +0 -22
  134. package/src/tools/omada/getSsidList.ts +0 -21
  135. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  136. package/src/tools/omada/getThreatList.ts +0 -42
  137. package/src/tools/omada/getWlanGroupList.ts +0 -14
  138. package/src/tools/omada/graph.ts +0 -187
  139. package/src/tools/omada/index.ts +0 -153
  140. package/src/tools/omada/listBlockedClients.ts +0 -17
  141. package/src/tools/omada/listClients.ts +0 -14
  142. package/src/tools/omada/listClientsActivity.ts +0 -25
  143. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  144. package/src/tools/omada/listDevices.ts +0 -14
  145. package/src/tools/omada/listDevicesStats.ts +0 -41
  146. package/src/tools/omada/listMostActiveClients.ts +0 -14
  147. package/src/tools/omada/listSites.ts +0 -18
  148. package/src/tools/omada/namespace.ts +0 -1164
  149. package/src/tools/omada/searchDevices.ts +0 -20
  150. package/src/tools/omada/setClientRateLimit.ts +0 -26
  151. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  152. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  153. package/src/tools/omada/setSsidEnabled.ts +0 -38
  154. package/src/tools/omada/unblockClient.ts +0 -26
  155. package/src/tools/types.ts +0 -11
  156. package/src/tools/utils/pagination-schema.ts +0 -27
  157. package/src/types/ai/index.ts +0 -11
  158. package/src/types/ai/types.ts +0 -67
  159. package/src/types/homeassistant/automations.ts +0 -43
  160. package/src/types/homeassistant/calendars.ts +0 -26
  161. package/src/types/homeassistant/config.ts +0 -14
  162. package/src/types/homeassistant/entities.ts +0 -14
  163. package/src/types/homeassistant/errors.ts +0 -33
  164. package/src/types/homeassistant/haConfig.ts +0 -7
  165. package/src/types/homeassistant/history.ts +0 -13
  166. package/src/types/homeassistant/index.ts +0 -46
  167. package/src/types/homeassistant/request.ts +0 -6
  168. package/src/types/homeassistant/serviceCall.ts +0 -18
  169. package/src/types/homeassistant/states.ts +0 -58
  170. package/src/types/homeassistant/updates.ts +0 -28
  171. package/src/types/index.ts +0 -12
  172. package/src/types/omada/activeClientInfo.ts +0 -17
  173. package/src/types/omada/clientActivity.ts +0 -32
  174. package/src/types/omada/clientBlockStatus.ts +0 -11
  175. package/src/types/omada/clientPastConnection.ts +0 -82
  176. package/src/types/omada/index.ts +0 -14
  177. package/src/types/omada/omadaApiResponse.ts +0 -5
  178. package/src/types/omada/omadaClientInfo.ts +0 -11
  179. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  180. package/src/types/omada/omadaDeviceStats.ts +0 -31
  181. package/src/types/omada/omadaSiteSummary.ts +0 -5
  182. package/src/types/omada/oswStackDetail.ts +0 -11
  183. package/src/types/omada/paginatedResult.ts +0 -6
  184. package/src/types/omada/rateLimitProfile.ts +0 -58
  185. package/src/types/omada/threatInfo.ts +0 -38
  186. package/src/types/omada/tokenResult.ts +0 -6
  187. package/src/types.ts +0 -2
  188. package/src/utils/config-validations.ts +0 -68
  189. package/src/utils/jwt.ts +0 -112
  190. package/src/utils/logger.ts +0 -121
  191. package/src/utils/plainText.ts +0 -48
  192. package/src/utils/redact.ts +0 -59
  193. package/src/utils/sanitizeError.ts +0 -18
  194. package/src/utils/tlsDispatcher.ts +0 -69
  195. package/src/version.ts +0 -5
  196. package/tests/bdd/automation-creation.spec.ts +0 -214
  197. package/tests/bdd/automation-management.spec.ts +0 -267
  198. package/tests/bdd/calendar.spec.ts +0 -190
  199. package/tests/bdd/configuration.spec.ts +0 -283
  200. package/tests/bdd/device-control.spec.ts +0 -344
  201. package/tests/bdd/entity-listing.spec.ts +0 -208
  202. package/tests/bdd/entity-search.spec.ts +0 -183
  203. package/tests/bdd/history-retrieval.spec.ts +0 -195
  204. package/tests/bdd/notifications.spec.ts +0 -238
  205. package/tests/bdd/service-calls.spec.ts +0 -144
  206. package/tests/bdd/state-retrieval.spec.ts +0 -204
  207. package/tests/bench/hotpaths.bench.ts +0 -68
  208. package/tests/config.test.ts +0 -355
  209. package/tests/features/automation-creation.feature +0 -36
  210. package/tests/features/automation-management.feature +0 -54
  211. package/tests/features/calendar.feature +0 -37
  212. package/tests/features/configuration.feature +0 -70
  213. package/tests/features/device-control.feature +0 -100
  214. package/tests/features/entity-listing.feature +0 -51
  215. package/tests/features/entity-search.feature +0 -44
  216. package/tests/features/history-retrieval.feature +0 -38
  217. package/tests/features/notifications.feature +0 -37
  218. package/tests/features/service-calls.feature +0 -35
  219. package/tests/features/state-retrieval.feature +0 -37
  220. package/tests/fixtures/openapi-default-slice.json +0 -290
  221. package/tests/haClient/automationTrace.test.ts +0 -59
  222. package/tests/haClient/automations.test.ts +0 -149
  223. package/tests/haClient/config.test.ts +0 -117
  224. package/tests/haClient/entities.test.ts +0 -188
  225. package/tests/haClient/events.test.ts +0 -455
  226. package/tests/haClient/eventsTls.test.ts +0 -52
  227. package/tests/haClient/history.test.ts +0 -228
  228. package/tests/haClient/index.test.ts +0 -243
  229. package/tests/haClient/request.test.ts +0 -217
  230. package/tests/haClient/services.test.ts +0 -120
  231. package/tests/haClient/states.test.ts +0 -119
  232. package/tests/haClient/updates.test.ts +0 -193
  233. package/tests/index.test.ts +0 -160
  234. package/tests/integration/homeassistant.test.ts +0 -144
  235. package/tests/integration/omada.test.ts +0 -167
  236. package/tests/integration/server.test.ts +0 -151
  237. package/tests/localAI/index.test.ts +0 -177
  238. package/tests/localAI/ollama.test.ts +0 -187
  239. package/tests/localAI/openai.test.ts +0 -257
  240. package/tests/omadaClient/actions.test.ts +0 -161
  241. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  242. package/tests/omadaClient/ntp.test.ts +0 -381
  243. package/tests/omadaClient/site.test.ts +0 -37
  244. package/tests/ownerQuestions/service.test.ts +0 -499
  245. package/tests/ownerQuestions/tools.test.ts +0 -186
  246. package/tests/permissions/index.test.ts +0 -217
  247. package/tests/resources/index.test.ts +0 -288
  248. package/tests/scripts/generateJwt.test.ts +0 -44
  249. package/tests/security/accessControl.test.ts +0 -223
  250. package/tests/security/authentication.test.ts +0 -117
  251. package/tests/security/configuration.test.ts +0 -170
  252. package/tests/security/cryptoTransport.test.ts +0 -172
  253. package/tests/security/dependencies.test.ts +0 -152
  254. package/tests/security/injection.test.ts +0 -117
  255. package/tests/security/insecureDesign.test.ts +0 -121
  256. package/tests/security/logging.test.ts +0 -131
  257. package/tests/security/resourceConsumption.test.ts +0 -142
  258. package/tests/server/auth.test.ts +0 -249
  259. package/tests/server/chatFace.test.ts +0 -320
  260. package/tests/server/common.test.ts +0 -84
  261. package/tests/server/http.test.ts +0 -561
  262. package/tests/server/instructions.test.ts +0 -152
  263. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  264. package/tests/server/stdio.test.ts +0 -99
  265. package/tests/server/stream.test.ts +0 -285
  266. package/tests/server/toolVisibility.http.test.ts +0 -111
  267. package/tests/server/toolVisibility.test.ts +0 -129
  268. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  269. package/tests/tools/common.test.ts +0 -123
  270. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  271. package/tests/tools/infra/invokeAction.test.ts +0 -280
  272. package/tests/tools/omada/graph.test.ts +0 -385
  273. package/tests/tools/omada/handlers.test.ts +0 -897
  274. package/tests/tools/omada/namespace.test.ts +0 -483
  275. package/tests/tools/registry.test.ts +0 -168
  276. package/tests/utils/config-validations.test.ts +0 -352
  277. package/tests/utils/jwt.test.ts +0 -115
  278. package/tests/utils/logger.test.ts +0 -327
  279. package/tests/utils/redact.test.ts +0 -105
  280. package/tsconfig.json +0 -24
  281. package/vitest.config.ts +0 -38
@@ -1,442 +0,0 @@
1
- /**
2
- * Common tool utilities
3
- * Shared helpers for all MCP tools
4
- */
5
-
6
- import type { CallToolResult, ServerContext } from '@modelcontextprotocol/server';
7
- import { z } from 'zod';
8
- import { logger } from '../utils/logger.js';
9
- import { redactSecrets } from '../utils/redact.js';
10
- import { Permission, hasPermission, getPermissionNames } from '../permissions/index.js';
11
-
12
- // Re-export Permission for tool files
13
- export { Permission };
14
-
15
- /**
16
- * Local-trust flag for the stdio transport (L3 / OWASP A04:2021).
17
- *
18
- * Tool calls normally carry a permission mask in `authInfo.extra.permissions`,
19
- * populated by the HTTP auth middleware (bearer mask, or 0xFF for the loopback
20
- * `none` mode). When that mask is ABSENT we fail closed — deny — instead of the
21
- * old default-allow `?? 0xFF`.
22
- *
23
- * The one legitimate no-auth path is stdio (Claude Desktop): there is no per-request
24
- * auth and the process owner is the trusted local user. `startStdioServer` opts into
25
- * full trust by calling `setLocalFullTrust(true)`. HTTP never enables it.
26
- */
27
- let localFullTrust = false;
28
-
29
- export function setLocalFullTrust(enabled: boolean): void {
30
- localFullTrust = enabled;
31
- }
32
-
33
- /**
34
- * Extra context passed to tool handlers
35
- * Permissions are extracted from authInfo.extra.permissions (MCP SDK standard)
36
- */
37
- export type ToolExtra = ServerContext;
38
-
39
- /**
40
- * Resolve the caller's permission mask from the MCP SDK auth context.
41
- *
42
- * L3: fail closed when the mask is absent — only the trusted-local stdio path
43
- * (setLocalFullTrust) grants full permissions (0xFF) without an explicit mask.
44
- * HTTP requests always carry a mask (bearer-derived, or 0xFF for loopback `none`).
45
- *
46
- * Exposed so tools that gate per-resource (e.g. the Omada resource-graph
47
- * `omada_read`/`omada_browse`) can apply the same extraction + fail-closed
48
- * semantics as `wrapToolHandler`, instead of re-reading `authInfo` ad hoc.
49
- */
50
- export function getCallerPermissions(extra: ToolExtra): number {
51
- return (extra.http?.authInfo?.extra?.permissions as number | undefined) ?? (localFullTrust ? 0xFF : 0);
52
- }
53
-
54
- /**
55
- * Convert any value to a CallToolResult
56
- */
57
- export function toToolResult(value: unknown, isError = false): CallToolResult {
58
- // Every tool response passes here: secrets returned by Home Assistant or Omada
59
- // (Wi-Fi password, RADIUS/VPN keys, access tokens) never reach the client.
60
- const safe = redactSecrets(value);
61
- const text = typeof safe === 'string' ? safe : JSON.stringify(safe, null, 2);
62
- return {
63
- content: text ? [{ type: 'text' as const, text }] : [],
64
- isError,
65
- };
66
- }
67
-
68
- /**
69
- * Safely serialize a value for logging
70
- */
71
- export function safeSerialize(value: unknown): string {
72
- try {
73
- return JSON.stringify(value);
74
- } catch {
75
- return '[unserializable]';
76
- }
77
- }
78
-
79
- /**
80
- * Static permission bit each tool declares via `wrapToolHandler`, keyed by tool
81
- * name. Lets the server hide tools a caller can never use from `tools/list`.
82
- * Tools with no static bit (e.g. `omada_read`/`omada_browse`, which gate per
83
- * resource path) are absent and therefore always listed.
84
- */
85
- const toolPermissions = new Map<string, number>();
86
-
87
- /** The static permission a tool requires, or `undefined` when it declares none. */
88
- export function getToolRequiredPermission(name: string): number | undefined {
89
- return toolPermissions.get(name);
90
- }
91
-
92
- /**
93
- * Visibility rules for tools that gate per call rather than with one static bit
94
- * (e.g. `omada_read`, whose permission depends on the resource path). The rule
95
- * answers "can this caller use the tool at all?".
96
- */
97
- const toolVisibilityRules = new Map<string, (callerPermissions: number) => boolean>();
98
-
99
- /** Declare when a tool without a static permission should be listed to a caller. */
100
- export function setToolVisibilityRule(name: string, isUsable: (callerPermissions: number) => boolean): void {
101
- toolVisibilityRules.set(name, isUsable);
102
- }
103
-
104
- /**
105
- * Whether `tools/list` should offer `name` to a caller: false when the caller lacks
106
- * the tool's static permission, or when its visibility rule says the caller can use
107
- * none of it. Tools with neither stay visible (their handlers still enforce RBAC).
108
- */
109
- export function isToolVisibleTo(name: string, callerPermissions: number): boolean {
110
- const required = toolPermissions.get(name);
111
- if (required !== undefined && !hasPermission(callerPermissions, required)) {
112
- return false;
113
- }
114
- const rule = toolVisibilityRules.get(name);
115
- return rule ? rule(callerPermissions) : true;
116
- }
117
-
118
- /**
119
- * Wrap a tool handler with logging, error handling, and permission checking
120
- * Includes session ID and args in logs for debugging
121
- *
122
- * @param name - Tool name for logging
123
- * @param handler - The actual tool handler function
124
- * @param requiredPermission - Optional permission mask required to use this tool
125
- */
126
- export function wrapToolHandler<T>(
127
- name: string,
128
- handler: (args: T, extra: ToolExtra) => Promise<CallToolResult>,
129
- requiredPermission?: number
130
- ): (args: T, extra: ToolExtra) => Promise<CallToolResult> {
131
- if (requiredPermission !== undefined) {
132
- toolPermissions.set(name, requiredPermission);
133
- }
134
- return async (args: T, extra: ToolExtra): Promise<CallToolResult> => {
135
- const sessionId = extra.sessionId ?? 'unknown-session';
136
- // Extract permissions from MCP SDK authInfo.extra.permissions.
137
- // L3: fail closed when the mask is absent — only the trusted-local stdio path
138
- // (setLocalFullTrust) grants full permissions without an explicit mask.
139
- const userPermissions = getCallerPermissions(extra);
140
-
141
- // Check permissions if required
142
- if (requiredPermission !== undefined && !hasPermission(userPermissions, requiredPermission)) {
143
- const required = getPermissionNames(requiredPermission);
144
- const has = getPermissionNames(userPermissions);
145
- logger.warn('Permission denied', { tool: name, sessionId, required, has });
146
- return toToolResult({
147
- error: 'Permission denied',
148
- message: `Tool '${name}' requires permission: ${required.join(', ')}`,
149
- required,
150
- has,
151
- }, true);
152
- }
153
-
154
- // Arguments can carry message text, entity names or MAC addresses: they are
155
- // logged at debug level only, never in normal (info) operation.
156
- logger.info('Tool invoked', { tool: name, sessionId });
157
- logger.debug('Tool arguments', { tool: name, sessionId, args: safeSerialize(args) });
158
-
159
- try {
160
- const result = await handler(args, extra);
161
- logger.info('Tool completed', { tool: name, sessionId });
162
- return result;
163
- } catch (error) {
164
- logger.error('Tool failed', {
165
- tool: name,
166
- sessionId,
167
- error: error instanceof Error ? error.message : String(error),
168
- });
169
- return toToolResult({
170
- error: `Failed to execute ${name}`,
171
- message: error instanceof Error ? error.message : String(error),
172
- }, true);
173
- }
174
- };
175
- }
176
-
177
- /**
178
- * Common Zod schemas for tool inputs
179
- */
180
-
181
- // Empty schema for tools with no inputs
182
- export const emptySchema = z.object({});
183
-
184
- // Entity ID schema
185
- export const entityIdSchema = z.object({
186
- entity_id: z.string().describe('The entity ID (e.g., "light.living_room", "sensor.temperature")'),
187
- });
188
-
189
- // Domain schema
190
- export const domainSchema = z.object({
191
- domain: z.string().describe('Domain name (e.g., "light", "sensor", "switch", "climate")'),
192
- });
193
-
194
- // Search query schema
195
- export const searchSchema = z.object({
196
- query: z.string().describe('Search query (searches in entity_id and friendly_name)'),
197
- });
198
-
199
- // History schema
200
- export const historySchema = z.object({
201
- entity_id: z.string().describe('The entity ID to get history for'),
202
- hours: z.number().optional().describe('Number of hours of history to retrieve (default: 24)'),
203
- });
204
-
205
- // Entity action schema
206
- export const entityActionSchema = z.object({
207
- entity_id: z.string().describe('The entity ID to perform the action on'),
208
- action: z.enum(['turn_on', 'turn_off', 'toggle']).describe('The action to perform'),
209
- });
210
-
211
- // List entities filter schema
212
- export const listEntitiesSchema = z.object({
213
- domain: z.string().optional().describe('Filter by domain (e.g., "light", "switch", "sensor")'),
214
- search: z.string().optional().describe('Search query to filter by entity_id or friendly_name'),
215
- state: z.string().optional().describe('Filter by state (e.g., "on", "off", "unavailable")'),
216
- limit: z.number().optional().describe('Maximum number of entities to return'),
217
- });
218
-
219
- // Automation state filter schema
220
- export const automationFilterSchema = z.object({
221
- state: z.enum(['on', 'off']).optional().describe('Filter by automation state (enabled/disabled)'),
222
- });
223
-
224
- // System log schema
225
- export const systemLogSchema = z.object({
226
- hours: z.number().optional().describe('Number of hours of history to retrieve (default: 24, max: 168)'),
227
- entity_id: z.string().optional().describe('Optional entity ID to filter logs for a specific entity'),
228
- limit: z.number().optional().describe('Maximum number of entries to return (default: 100)'),
229
- });
230
-
231
- // Automation ID schema
232
- export const automationIdSchema = z.object({
233
- automation_id: z.string().describe('The unique ID of the automation'),
234
- });
235
-
236
- // Service call schema
237
- export const serviceCallSchema = z.object({
238
- domain: z.string().describe('Service domain (e.g., "light", "switch", "climate", "notify")'),
239
- service: z.string().describe('Service name (e.g., "turn_on", "turn_off", "set_temperature")'),
240
- service_data: z.record(z.string(), z.unknown()).optional().describe('Optional service data'),
241
- target: z.object({
242
- entity_id: z.union([z.string(), z.array(z.string())]).optional(),
243
- device_id: z.union([z.string(), z.array(z.string())]).optional(),
244
- area_id: z.union([z.string(), z.array(z.string())]).optional(),
245
- }).optional().describe('Optional target'),
246
- });
247
-
248
- // Analyze sensors schema
249
- export const analyzeSensorsSchema = z.object({
250
- sensors: z.record(z.string(), z.unknown()).describe('Sensor data to analyze. Object with entity_id keys and {state, unit, attributes} values.'),
251
- });
252
-
253
- // Trigger automation schema
254
- export const triggerAutomationSchema = z.object({
255
- entity_id: z.string().describe('The automation entity ID (e.g., "automation.turn_on_lights")'),
256
- variables: z.record(z.string(), z.unknown()).optional().describe('Optional variables to pass to the automation'),
257
- });
258
-
259
- // Create automation schema
260
- export const createAutomationSchema = z.object({
261
- alias: z.string().describe('Friendly name for the automation'),
262
- description: z.string().optional().describe('Optional description of what the automation does'),
263
- mode: z.enum(['single', 'restart', 'queued', 'parallel']).optional().describe('Execution mode (default: single)'),
264
- trigger: z.unknown().describe('Trigger(s) that start the automation. Each trigger needs a "platform" property.'),
265
- condition: z.unknown().optional().describe('Optional condition(s) that must be true for actions to run.'),
266
- action: z.unknown().describe('Action(s) to perform when triggered. Each action typically has "service" and "target" properties.'),
267
- });
268
-
269
- // Control light schema
270
- export const controlLightSchema = z.object({
271
- entity_id: z.string().describe('The light entity ID (e.g., "light.living_room")'),
272
- action: z.enum(['turn_on', 'turn_off', 'toggle']).describe('The action to perform'),
273
- brightness_pct: z.number().optional().describe('Brightness percentage (0-100)'),
274
- color_temp_kelvin: z.number().optional().describe('Color temperature in Kelvin (e.g., 2700 for warm, 6500 for cool)'),
275
- rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional().describe('RGB color as [red, green, blue] (0-255 each)'),
276
- transition: z.number().optional().describe('Transition time in seconds'),
277
- });
278
-
279
- // Control climate schema
280
- export const controlClimateSchema = z.object({
281
- entity_id: z.string().describe('The climate entity ID (e.g., "climate.living_room")'),
282
- temperature: z.number().optional().describe('Target temperature'),
283
- hvac_mode: z.enum(['off', 'heat', 'cool', 'heat_cool', 'auto', 'dry', 'fan_only']).optional().describe('HVAC mode'),
284
- fan_mode: z.string().optional().describe('Fan mode (device-specific)'),
285
- preset_mode: z.string().optional().describe('Preset mode (e.g., "home", "away", "eco")'),
286
- });
287
-
288
- // Control media player schema
289
- export const controlMediaPlayerSchema = z.object({
290
- entity_id: z.string().describe('The media player entity ID'),
291
- action: z.enum(['play', 'pause', 'stop', 'next', 'previous', 'volume_set', 'volume_mute']).describe('The action to perform'),
292
- volume_level: z.number().optional().describe('Volume level (0.0 to 1.0)'),
293
- is_volume_muted: z.boolean().optional().describe('Mute state'),
294
- });
295
-
296
- // Control cover schema
297
- export const controlCoverSchema = z.object({
298
- entity_id: z.string().describe('The cover entity ID (e.g., "cover.living_room_blinds")'),
299
- action: z.enum(['open', 'close', 'stop', 'set_position', 'set_tilt']).describe('The action to perform'),
300
- position: z.number().optional().describe('Cover position (0 = closed, 100 = open)'),
301
- tilt_position: z.number().optional().describe('Tilt position (0-100)'),
302
- });
303
-
304
- // Control fan schema
305
- export const controlFanSchema = z.object({
306
- entity_id: z.string().describe('The fan entity ID'),
307
- action: z.enum(['turn_on', 'turn_off', 'toggle', 'set_speed', 'oscillate', 'set_direction']).describe('The action to perform'),
308
- percentage: z.number().optional().describe('Speed percentage (0-100)'),
309
- oscillating: z.boolean().optional().describe('Oscillation state'),
310
- direction: z.enum(['forward', 'reverse']).optional().describe('Fan direction'),
311
- });
312
-
313
- // Run script schema
314
- export const runScriptSchema = z.object({
315
- entity_id: z.string().describe('The script entity ID (e.g., "script.morning_routine")'),
316
- variables: z.record(z.string(), z.unknown()).optional().describe('Optional variables to pass to the script'),
317
- });
318
-
319
- // Notification action schema for actionable notifications
320
- export const notificationActionSchema = z.object({
321
- action: z.string().describe('Action identifier returned when tapped'),
322
- title: z.string().describe('Button text shown to user'),
323
- uri: z.string().optional().describe('URI to open when tapped (e.g., "https://example.com" or "app://settings")'),
324
- });
325
-
326
- // Send notification schema with mobile app support
327
- export const sendNotificationSchema = z.object({
328
- message: z.string().describe('The notification message. Use "clear_notification" to clear a notification by tag.'),
329
- title: z.string().optional().describe('Optional notification title'),
330
- target: z.string().optional().describe('Notification service target (e.g., "mobile_app_iphone", "mobile_app_pixel"). Defaults to "persistent_notification".'),
331
- // Mobile app specific options
332
- tag: z.string().optional().describe('Notification tag for grouping/replacing. Use same tag to update existing notification.'),
333
- group: z.string().optional().describe('Group notifications together (Android: group key, iOS: thread-id)'),
334
- priority: z.enum(['high', 'normal', 'low', 'min']).optional().describe('Notification priority (Android). "high" wakes device.'),
335
- importance: z.enum(['high', 'default', 'low', 'min']).optional().describe('Notification importance (Android channel importance)'),
336
- ttl: z.number().optional().describe('Time to live in seconds. Notification expires after this time.'),
337
- // Actions
338
- actions: z.array(notificationActionSchema).max(3).optional().describe('Up to 3 action buttons for actionable notifications'),
339
- // Media
340
- image: z.string().optional().describe('URL to image to display in notification'),
341
- video: z.string().optional().describe('URL to video (iOS only)'),
342
- icon_url: z.string().optional().describe('URL to custom notification icon (Android only)'),
343
- // Behavior
344
- sticky: z.boolean().optional().describe('If true, notification cannot be dismissed by swiping (Android)'),
345
- persistent: z.boolean().optional().describe('If true, notification persists across reboots (Android)'),
346
- clickAction: z.string().optional().describe('URI to open when notification is tapped'),
347
- // Sound
348
- channel: z.string().optional().describe('Android notification channel ID'),
349
- vibrationPattern: z.array(z.number()).optional().describe('Custom vibration pattern (Android) e.g., [100, 200, 100]'),
350
- ledColor: z.string().optional().describe('LED color as hex (Android) e.g., "#FF0000"'),
351
- // iOS specific
352
- sound: z.string().optional().describe('Sound file name (iOS) or "default"'),
353
- badge: z.number().optional().describe('App badge count (iOS)'),
354
- interruptionLevel: z.enum(['passive', 'active', 'time-sensitive', 'critical']).optional().describe('iOS interruption level'),
355
- // Raw data passthrough for advanced use
356
- data: z.record(z.string(), z.unknown()).optional().describe('Additional raw data to pass to notification service'),
357
- });
358
-
359
- // List notification targets schema (empty - no params needed)
360
- export const listNotificationTargetsSchema = z.object({});
361
-
362
- // List calendars schema (empty - no params needed)
363
- export const listCalendarsSchema = z.object({});
364
-
365
- // Get calendar events schema
366
- export const getCalendarEventsSchema = z.object({
367
- entity_id: z.string().optional().describe('Calendar entity ID (e.g., "calendar.my_calendar"). If not provided, gets events from all calendars.'),
368
- start_date: z.string().optional().describe('Start date in ISO format (e.g., "2026-01-01"). Defaults to today.'),
369
- end_date: z.string().optional().describe('End date in ISO format (e.g., "2026-12-31"). Defaults to 30 days from start.'),
370
- days: z.number().optional().describe('Alternative: number of days from start_date (overrides end_date if provided)'),
371
- });
372
-
373
- // =============================================================================
374
- // Omada-specific schemas
375
- // =============================================================================
376
-
377
- // Site ID input schema (optional site selector)
378
- export const siteInputSchema = z.object({
379
- siteId: z.string().min(1).optional().describe('Site ID (optional, uses default if not provided)'),
380
- });
381
-
382
- // Client ID schema (extends site input)
383
- export const clientIdSchema = siteInputSchema.extend({
384
- clientId: z.string().min(1, 'clientId (MAC or client identifier) is required').describe('Client MAC address or identifier'),
385
- });
386
-
387
- // Device ID schema (extends site input)
388
- export const deviceIdSchema = siteInputSchema.extend({
389
- deviceId: z.string().min(1, 'deviceId (MAC or device identifier) is required').describe('Device MAC address or identifier'),
390
- });
391
-
392
- // Stack ID schema (extends site input)
393
- export const stackIdSchema = siteInputSchema.extend({
394
- stackId: z.string().min(1, 'stackId is required').describe('Switch stack identifier'),
395
- });
396
-
397
- // =============================================================================
398
- // Home Assistant pagination schemas
399
- // =============================================================================
400
-
401
- /**
402
- * Pagination schema for Home Assistant entity queries.
403
- * Reduces response size by returning paginated, lightweight entities by default.
404
- */
405
- export const haPaginationSchema = z.object({
406
- page: z.number().int().min(1).default(1).describe('Page number (starts at 1)'),
407
- pageSize: z.number().int().min(1).max(200).default(50).describe('Entities per page (1-200, default 50)'),
408
- includeAttributes: z.boolean().default(false).describe('Include full attributes (increases response size significantly)'),
409
- });
410
-
411
- /**
412
- * Pagination parameters interface for Home Assistant queries.
413
- */
414
- export interface HAPaginationParams {
415
- page: number;
416
- pageSize: number;
417
- includeAttributes: boolean;
418
- }
419
-
420
- /**
421
- * Apply pagination to an array and return paginated result metadata.
422
- */
423
- export function paginateArray<T>(items: T[], page: number, pageSize: number): {
424
- page: number;
425
- pageSize: number;
426
- totalCount: number;
427
- totalPages: number;
428
- items: T[];
429
- } {
430
- const totalCount = items.length;
431
- const totalPages = Math.ceil(totalCount / pageSize);
432
- const start = (page - 1) * pageSize;
433
- const paginatedItems = items.slice(start, start + pageSize);
434
-
435
- return {
436
- page,
437
- pageSize,
438
- totalCount,
439
- totalPages,
440
- items: paginatedItems,
441
- };
442
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Owner question tools: askOwner / getOwnerAnswer / cancelOwnerQuestion.
3
- * See src/ownerQuestions/service.ts for the design.
4
- */
5
-
6
- import type { McpServer } from '@modelcontextprotocol/server';
7
- import { z } from 'zod';
8
-
9
- import { LIMITS, type OwnerQuestionService } from '../../ownerQuestions/service.js';
10
- import { validatePlainText } from '../../utils/plainText.js';
11
- import { toToolResult, wrapToolHandler, Permission, type ToolExtra } from '../common.js';
12
-
13
- const plainText = (field: string, maxLength: number, allowNewlines = false) =>
14
- z.string().transform((value, ctx) => {
15
- try {
16
- return validatePlainText(value, { field, maxLength, allowNewlines });
17
- } catch (error) {
18
- ctx.addIssue({ code: 'custom', message: error instanceof Error ? error.message : String(error) });
19
- return z.NEVER;
20
- }
21
- });
22
-
23
- export const askOwnerSchema = z.object({
24
- question: plainText('question', LIMITS.questionChars, true).describe(
25
- `The question, plain text only (no emoji), at most ${LIMITS.questionChars} characters; longer text is refused, not cut`
26
- ),
27
- title: plainText('title', LIMITS.titleChars).optional().describe(`Notification title, at most ${LIMITS.titleChars} characters (default "Approval needed")`),
28
- buttons: z
29
- .array(plainText('button label', LIMITS.buttonChars))
30
- .min(1)
31
- .max(LIMITS.maxButtons)
32
- .refine((labels) => new Set(labels.map((l) => l.toLowerCase())).size === labels.length, 'Button labels must be unique')
33
- .optional()
34
- .describe('1-3 button labels; default ["Approve", "Deny"]. The answer reports the tapped index (0 = first).'),
35
- target: z
36
- .string()
37
- .regex(/^mobile_app_[a-z0-9_]{1,64}$/, 'target must be a mobile app notify service, e.g. "mobile_app_pixel_7"')
38
- .describe('The phone to ask: its notify service name, e.g. "mobile_app_pixel_7"'),
39
- timeoutSeconds: z
40
- .number()
41
- .int()
42
- .min(LIMITS.minTimeoutSeconds)
43
- .max(LIMITS.maxTimeoutSeconds)
44
- .optional()
45
- .describe(`How long the question stays open (default ${LIMITS.defaultTimeoutSeconds} s, max 7 days). No answer by then = timeout, treat as NO.`),
46
- channel: plainText('channel', 40).optional().describe('Android notification channel name'),
47
- importance: z.enum(['high', 'default', 'low', 'min']).optional().describe('Android notification channel importance'),
48
- });
49
-
50
- const requestIdSchema = z.string().min(1).max(4096).describe('The requestId returned by askOwner');
51
-
52
- export const getOwnerAnswerSchema = z.object({
53
- requestId: requestIdSchema,
54
- waitSeconds: z
55
- .number()
56
- .int()
57
- .min(0)
58
- .max(LIMITS.maxWaitSeconds)
59
- .optional()
60
- .describe(`Wait up to this many seconds for an answer before returning "pending" (default 25, max ${LIMITS.maxWaitSeconds})`),
61
- });
62
-
63
- export const cancelOwnerQuestionSchema = z.object({ requestId: requestIdSchema });
64
-
65
- /** The token `sub` of the caller; stdio (local trust) has none. */
66
- function callerOf(extra: ToolExtra): string {
67
- return extra.http?.authInfo?.clientId ?? 'local';
68
- }
69
-
70
- export function registerOwnerQuestionTools(server: McpServer, service: OwnerQuestionService): number {
71
- server.registerTool(
72
- 'askOwner',
73
- {
74
- description:
75
- "Ask the owner a question on their phone with up to 3 buttons (default Approve / Deny) and return immediately with a requestId. " +
76
- 'Then call getOwnerAnswer(requestId) until the status is not "pending". ' +
77
- 'Anything except an explicit approving answer must be treated as NO: deny, timeout, cancelled and unknown. ' +
78
- 'The owner may be away from the phone for hours; use a timeout that fits.',
79
- inputSchema: askOwnerSchema,
80
- },
81
- wrapToolHandler(
82
- 'askOwner',
83
- async (args: z.infer<typeof askOwnerSchema>, extra: ToolExtra) =>
84
- toToolResult(
85
- await service.ask(
86
- {
87
- question: args.question,
88
- title: args.title ?? 'Approval needed',
89
- buttons: args.buttons ?? ['Approve', 'Deny'],
90
- target: args.target,
91
- timeoutSeconds: args.timeoutSeconds ?? LIMITS.defaultTimeoutSeconds,
92
- channel: args.channel,
93
- importance: args.importance,
94
- },
95
- callerOf(extra)
96
- )
97
- ),
98
- Permission.NOTIFY
99
- )
100
- );
101
-
102
- server.registerTool(
103
- 'getOwnerAnswer',
104
- {
105
- description:
106
- 'Get the answer to an askOwner question, waiting up to waitSeconds. Status: "answered" (with index, button, answeredAt, answeredBy), ' +
107
- '"pending" (ask again), "timeout", "cancelled" or "unknown". Only the client that asked can read the answer.',
108
- inputSchema: getOwnerAnswerSchema,
109
- },
110
- wrapToolHandler(
111
- 'getOwnerAnswer',
112
- async (args: z.infer<typeof getOwnerAnswerSchema>, extra: ToolExtra) =>
113
- toToolResult(await service.getAnswer(args.requestId, callerOf(extra), args.waitSeconds ?? 25)),
114
- Permission.NOTIFY
115
- )
116
- );
117
-
118
- server.registerTool(
119
- 'cancelOwnerQuestion',
120
- {
121
- description: 'Withdraw an open askOwner question; the phone notification changes to "Cancelled". Returns the final status.',
122
- inputSchema: cancelOwnerQuestionSchema,
123
- },
124
- wrapToolHandler(
125
- 'cancelOwnerQuestion',
126
- async (args: z.infer<typeof cancelOwnerQuestionSchema>, extra: ToolExtra) =>
127
- toToolResult(await service.cancel(args.requestId, callerOf(extra))),
128
- Permission.NOTIFY
129
- )
130
- );
131
-
132
- return 3;
133
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Automation control tools - Enable, disable, toggle, reload automations
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { HaClient } from '../../haClient/index.js';
7
- import { entityIdSchema, emptySchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
- import type { z } from 'zod';
9
-
10
- type EntityIdArgs = z.infer<typeof entityIdSchema>;
11
-
12
- export function registerEnableAutomationTool(server: McpServer, client: HaClient): void {
13
- server.registerTool(
14
- 'enableAutomation',
15
- {
16
- description: 'Enable a disabled Home Assistant automation.',
17
- inputSchema: entityIdSchema,
18
- },
19
- wrapToolHandler('enableAutomation', async ({ entity_id }: EntityIdArgs) => {
20
- const result = await client.enableAutomation(entity_id);
21
- return toToolResult({ success: true, entity_id, action: 'enabled', context: result.context });
22
- }, Permission.CONFIGURE)
23
- );
24
- }
25
-
26
- export function registerDisableAutomationTool(server: McpServer, client: HaClient): void {
27
- server.registerTool(
28
- 'disableAutomation',
29
- {
30
- description: 'Disable a Home Assistant automation.',
31
- inputSchema: entityIdSchema,
32
- },
33
- wrapToolHandler('disableAutomation', async ({ entity_id }: EntityIdArgs) => {
34
- const result = await client.disableAutomation(entity_id);
35
- return toToolResult({ success: true, entity_id, action: 'disabled', context: result.context });
36
- }, Permission.CONFIGURE)
37
- );
38
- }
39
-
40
- export function registerToggleAutomationTool(server: McpServer, client: HaClient): void {
41
- server.registerTool(
42
- 'toggleAutomation',
43
- {
44
- description: 'Toggle a Home Assistant automation (enable if disabled, disable if enabled).',
45
- inputSchema: entityIdSchema,
46
- },
47
- wrapToolHandler('toggleAutomation', async ({ entity_id }: EntityIdArgs) => {
48
- const result = await client.toggleAutomation(entity_id);
49
- return toToolResult({ success: true, entity_id, action: 'toggled', context: result.context });
50
- }, Permission.CONFIGURE)
51
- );
52
- }
53
-
54
- export function registerReloadAutomationsTool(server: McpServer, client: HaClient): void {
55
- server.registerTool(
56
- 'reloadAutomations',
57
- {
58
- description: 'Reload all automations from the Home Assistant configuration. Useful after editing automation YAML files.',
59
- inputSchema: emptySchema,
60
- },
61
- wrapToolHandler('reloadAutomations', async () => {
62
- await client.reloadAutomations();
63
- return toToolResult({ success: true, message: 'Automations reloaded from configuration' });
64
- }, Permission.CONFIGURE)
65
- );
66
- }