@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.4.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.
Files changed (293) hide show
  1. package/.env.example +1 -1
  2. package/README.md +24 -2
  3. package/dist/omadaClient/accessControl.d.ts +290 -0
  4. package/dist/omadaClient/accessControl.js +509 -0
  5. package/dist/omadaClient/accessControl.js.map +1 -0
  6. package/dist/omadaClient/client.d.ts +6 -1
  7. package/dist/omadaClient/client.js +12 -1
  8. package/dist/omadaClient/client.js.map +1 -1
  9. package/dist/omadaClient/diagnose.d.ts +68 -0
  10. package/dist/omadaClient/diagnose.js +164 -0
  11. package/dist/omadaClient/diagnose.js.map +1 -0
  12. package/dist/omadaClient/index.d.ts +9 -0
  13. package/dist/omadaClient/index.js +19 -0
  14. package/dist/omadaClient/index.js.map +1 -1
  15. package/dist/omadaClient/macGroups.d.ts +58 -0
  16. package/dist/omadaClient/macGroups.js +110 -0
  17. package/dist/omadaClient/macGroups.js.map +1 -0
  18. package/dist/omadaClient/serialize.d.ts +13 -0
  19. package/dist/omadaClient/serialize.js +29 -0
  20. package/dist/omadaClient/serialize.js.map +1 -0
  21. package/dist/omadaClient/timeRange.d.ts +41 -0
  22. package/dist/omadaClient/timeRange.js +109 -0
  23. package/dist/omadaClient/timeRange.js.map +1 -0
  24. package/dist/tools/infra/actions.d.ts +1 -1
  25. package/dist/tools/infra/actions.js +1 -1
  26. package/dist/tools/omada/accessControl.d.ts +151 -0
  27. package/dist/tools/omada/accessControl.js +158 -0
  28. package/dist/tools/omada/accessControl.js.map +1 -0
  29. package/dist/tools/omada/index.js +9 -0
  30. package/dist/tools/omada/index.js.map +1 -1
  31. package/dist/tools/omada/macGroups.d.ts +19 -0
  32. package/dist/tools/omada/macGroups.js +77 -0
  33. package/dist/tools/omada/macGroups.js.map +1 -0
  34. package/dist/tools/omada/namespace.js +16 -0
  35. package/dist/tools/omada/namespace.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +5 -1
  39. package/.codex/review-rules.md +0 -16
  40. package/.dockerignore +0 -8
  41. package/.gitattributes +0 -30
  42. package/.github/workflows/ci.yml +0 -193
  43. package/AGENTS.md +0 -389
  44. package/Dockerfile +0 -50
  45. package/docker-compose.yml +0 -23
  46. package/docs/CHANGELOG.md +0 -341
  47. package/docs/COVERAGE.md +0 -219
  48. package/docs/QUICK_START.md +0 -240
  49. package/eslint.config.js +0 -33
  50. package/n8n/ai-agent-safety-monitor.json +0 -355
  51. package/openapi.json +0 -1259
  52. package/scripts/check-version-sync.mjs +0 -69
  53. package/scripts/generate-jwt.ts +0 -121
  54. package/src/config.ts +0 -440
  55. package/src/haClient/automations.ts +0 -261
  56. package/src/haClient/calendars.ts +0 -94
  57. package/src/haClient/config.ts +0 -52
  58. package/src/haClient/devices.ts +0 -653
  59. package/src/haClient/entities.ts +0 -128
  60. package/src/haClient/events.ts +0 -689
  61. package/src/haClient/history.ts +0 -73
  62. package/src/haClient/index.ts +0 -336
  63. package/src/haClient/request.ts +0 -142
  64. package/src/haClient/services.ts +0 -45
  65. package/src/haClient/states.ts +0 -60
  66. package/src/haClient/updates.ts +0 -58
  67. package/src/haClient/wsCommand.ts +0 -44
  68. package/src/index.ts +0 -164
  69. package/src/localAI/index.ts +0 -88
  70. package/src/localAI/prompts.ts +0 -50
  71. package/src/localAI/providers/base.ts +0 -120
  72. package/src/localAI/providers/ollama.ts +0 -78
  73. package/src/localAI/providers/openai.ts +0 -86
  74. package/src/localAI/types.ts +0 -67
  75. package/src/omadaClient/auth.ts +0 -178
  76. package/src/omadaClient/client.ts +0 -337
  77. package/src/omadaClient/device.ts +0 -118
  78. package/src/omadaClient/index.ts +0 -310
  79. package/src/omadaClient/network.ts +0 -284
  80. package/src/omadaClient/request.ts +0 -365
  81. package/src/omadaClient/security.ts +0 -51
  82. package/src/omadaClient/site.ts +0 -344
  83. package/src/ownerQuestions/service.ts +0 -536
  84. package/src/permissions/index.ts +0 -143
  85. package/src/resources/index.ts +0 -213
  86. package/src/server/auth.ts +0 -175
  87. package/src/server/chatFace.ts +0 -789
  88. package/src/server/chatSlice.ts +0 -57
  89. package/src/server/common.ts +0 -170
  90. package/src/server/eventSubscription.ts +0 -343
  91. package/src/server/http.ts +0 -626
  92. package/src/server/instructions.ts +0 -97
  93. package/src/server/rateLimiter.ts +0 -227
  94. package/src/server/stdio.ts +0 -41
  95. package/src/server/stream.ts +0 -205
  96. package/src/tools/ai/analyzeSensors.ts +0 -33
  97. package/src/tools/ai/index.ts +0 -20
  98. package/src/tools/common.ts +0 -442
  99. package/src/tools/homeassistant/askOwner.ts +0 -133
  100. package/src/tools/homeassistant/automationControls.ts +0 -66
  101. package/src/tools/homeassistant/calendar.ts +0 -97
  102. package/src/tools/homeassistant/callService.ts +0 -25
  103. package/src/tools/homeassistant/controlClimate.ts +0 -68
  104. package/src/tools/homeassistant/controlCover.ts +0 -48
  105. package/src/tools/homeassistant/controlFan.ts +0 -44
  106. package/src/tools/homeassistant/controlLight.ts +0 -38
  107. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  108. package/src/tools/homeassistant/createAutomation.ts +0 -35
  109. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  110. package/src/tools/homeassistant/entityAction.ts +0 -35
  111. package/src/tools/homeassistant/entityGuard.ts +0 -28
  112. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  113. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  114. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  115. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  116. package/src/tools/homeassistant/getHistory.ts +0 -24
  117. package/src/tools/homeassistant/getState.ts +0 -24
  118. package/src/tools/homeassistant/getStates.ts +0 -46
  119. package/src/tools/homeassistant/getVersion.ts +0 -21
  120. package/src/tools/homeassistant/index.ts +0 -121
  121. package/src/tools/homeassistant/listAutomations.ts +0 -35
  122. package/src/tools/homeassistant/listEntities.ts +0 -44
  123. package/src/tools/homeassistant/listPersons.ts +0 -46
  124. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  125. package/src/tools/homeassistant/searchEntities.ts +0 -58
  126. package/src/tools/homeassistant/sendNotification.ts +0 -179
  127. package/src/tools/homeassistant/systemTools.ts +0 -52
  128. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  129. package/src/tools/index.ts +0 -78
  130. package/src/tools/infra/actions.ts +0 -95
  131. package/src/tools/infra/index.ts +0 -138
  132. package/src/tools/omada/blockClient.ts +0 -28
  133. package/src/tools/omada/cyclePoePort.ts +0 -32
  134. package/src/tools/omada/deleteClient.ts +0 -27
  135. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  136. package/src/tools/omada/getClient.ts +0 -14
  137. package/src/tools/omada/getDevice.ts +0 -14
  138. package/src/tools/omada/getFirewallSetting.ts +0 -14
  139. package/src/tools/omada/getInternetInfo.ts +0 -14
  140. package/src/tools/omada/getLanNetworkList.ts +0 -14
  141. package/src/tools/omada/getLanProfileList.ts +0 -14
  142. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  143. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  144. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  145. package/src/tools/omada/getSsidDetail.ts +0 -22
  146. package/src/tools/omada/getSsidList.ts +0 -21
  147. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  148. package/src/tools/omada/getThreatList.ts +0 -42
  149. package/src/tools/omada/getWlanGroupList.ts +0 -14
  150. package/src/tools/omada/graph.ts +0 -187
  151. package/src/tools/omada/index.ts +0 -153
  152. package/src/tools/omada/listBlockedClients.ts +0 -17
  153. package/src/tools/omada/listClients.ts +0 -14
  154. package/src/tools/omada/listClientsActivity.ts +0 -25
  155. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  156. package/src/tools/omada/listDevices.ts +0 -14
  157. package/src/tools/omada/listDevicesStats.ts +0 -41
  158. package/src/tools/omada/listMostActiveClients.ts +0 -14
  159. package/src/tools/omada/listSites.ts +0 -18
  160. package/src/tools/omada/namespace.ts +0 -1164
  161. package/src/tools/omada/searchDevices.ts +0 -20
  162. package/src/tools/omada/setClientRateLimit.ts +0 -26
  163. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  164. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  165. package/src/tools/omada/setSsidEnabled.ts +0 -38
  166. package/src/tools/omada/unblockClient.ts +0 -26
  167. package/src/tools/types.ts +0 -11
  168. package/src/tools/utils/pagination-schema.ts +0 -27
  169. package/src/types/ai/index.ts +0 -11
  170. package/src/types/ai/types.ts +0 -67
  171. package/src/types/homeassistant/automations.ts +0 -43
  172. package/src/types/homeassistant/calendars.ts +0 -26
  173. package/src/types/homeassistant/config.ts +0 -14
  174. package/src/types/homeassistant/entities.ts +0 -14
  175. package/src/types/homeassistant/errors.ts +0 -33
  176. package/src/types/homeassistant/haConfig.ts +0 -7
  177. package/src/types/homeassistant/history.ts +0 -13
  178. package/src/types/homeassistant/index.ts +0 -46
  179. package/src/types/homeassistant/request.ts +0 -6
  180. package/src/types/homeassistant/serviceCall.ts +0 -18
  181. package/src/types/homeassistant/states.ts +0 -58
  182. package/src/types/homeassistant/updates.ts +0 -28
  183. package/src/types/index.ts +0 -12
  184. package/src/types/omada/activeClientInfo.ts +0 -17
  185. package/src/types/omada/clientActivity.ts +0 -32
  186. package/src/types/omada/clientBlockStatus.ts +0 -11
  187. package/src/types/omada/clientPastConnection.ts +0 -82
  188. package/src/types/omada/index.ts +0 -14
  189. package/src/types/omada/omadaApiResponse.ts +0 -5
  190. package/src/types/omada/omadaClientInfo.ts +0 -11
  191. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  192. package/src/types/omada/omadaDeviceStats.ts +0 -31
  193. package/src/types/omada/omadaSiteSummary.ts +0 -5
  194. package/src/types/omada/oswStackDetail.ts +0 -11
  195. package/src/types/omada/paginatedResult.ts +0 -6
  196. package/src/types/omada/rateLimitProfile.ts +0 -58
  197. package/src/types/omada/threatInfo.ts +0 -38
  198. package/src/types/omada/tokenResult.ts +0 -6
  199. package/src/types.ts +0 -2
  200. package/src/utils/config-validations.ts +0 -68
  201. package/src/utils/jwt.ts +0 -112
  202. package/src/utils/logger.ts +0 -121
  203. package/src/utils/plainText.ts +0 -48
  204. package/src/utils/redact.ts +0 -59
  205. package/src/utils/sanitizeError.ts +0 -18
  206. package/src/utils/tlsDispatcher.ts +0 -69
  207. package/src/version.ts +0 -5
  208. package/tests/bdd/automation-creation.spec.ts +0 -214
  209. package/tests/bdd/automation-management.spec.ts +0 -267
  210. package/tests/bdd/calendar.spec.ts +0 -190
  211. package/tests/bdd/configuration.spec.ts +0 -283
  212. package/tests/bdd/device-control.spec.ts +0 -344
  213. package/tests/bdd/entity-listing.spec.ts +0 -208
  214. package/tests/bdd/entity-search.spec.ts +0 -183
  215. package/tests/bdd/history-retrieval.spec.ts +0 -195
  216. package/tests/bdd/notifications.spec.ts +0 -238
  217. package/tests/bdd/service-calls.spec.ts +0 -144
  218. package/tests/bdd/state-retrieval.spec.ts +0 -204
  219. package/tests/bench/hotpaths.bench.ts +0 -68
  220. package/tests/config.test.ts +0 -355
  221. package/tests/features/automation-creation.feature +0 -36
  222. package/tests/features/automation-management.feature +0 -54
  223. package/tests/features/calendar.feature +0 -37
  224. package/tests/features/configuration.feature +0 -70
  225. package/tests/features/device-control.feature +0 -100
  226. package/tests/features/entity-listing.feature +0 -51
  227. package/tests/features/entity-search.feature +0 -44
  228. package/tests/features/history-retrieval.feature +0 -38
  229. package/tests/features/notifications.feature +0 -37
  230. package/tests/features/service-calls.feature +0 -35
  231. package/tests/features/state-retrieval.feature +0 -37
  232. package/tests/fixtures/openapi-default-slice.json +0 -290
  233. package/tests/haClient/automationTrace.test.ts +0 -59
  234. package/tests/haClient/automations.test.ts +0 -149
  235. package/tests/haClient/config.test.ts +0 -117
  236. package/tests/haClient/entities.test.ts +0 -188
  237. package/tests/haClient/events.test.ts +0 -455
  238. package/tests/haClient/eventsTls.test.ts +0 -52
  239. package/tests/haClient/history.test.ts +0 -228
  240. package/tests/haClient/index.test.ts +0 -243
  241. package/tests/haClient/request.test.ts +0 -217
  242. package/tests/haClient/services.test.ts +0 -120
  243. package/tests/haClient/states.test.ts +0 -119
  244. package/tests/haClient/updates.test.ts +0 -193
  245. package/tests/index.test.ts +0 -160
  246. package/tests/integration/homeassistant.test.ts +0 -144
  247. package/tests/integration/omada.test.ts +0 -167
  248. package/tests/integration/server.test.ts +0 -151
  249. package/tests/localAI/index.test.ts +0 -177
  250. package/tests/localAI/ollama.test.ts +0 -187
  251. package/tests/localAI/openai.test.ts +0 -257
  252. package/tests/omadaClient/actions.test.ts +0 -161
  253. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  254. package/tests/omadaClient/ntp.test.ts +0 -381
  255. package/tests/omadaClient/site.test.ts +0 -37
  256. package/tests/ownerQuestions/service.test.ts +0 -499
  257. package/tests/ownerQuestions/tools.test.ts +0 -186
  258. package/tests/permissions/index.test.ts +0 -217
  259. package/tests/resources/index.test.ts +0 -288
  260. package/tests/scripts/generateJwt.test.ts +0 -44
  261. package/tests/security/accessControl.test.ts +0 -223
  262. package/tests/security/authentication.test.ts +0 -117
  263. package/tests/security/configuration.test.ts +0 -170
  264. package/tests/security/cryptoTransport.test.ts +0 -172
  265. package/tests/security/dependencies.test.ts +0 -152
  266. package/tests/security/injection.test.ts +0 -117
  267. package/tests/security/insecureDesign.test.ts +0 -121
  268. package/tests/security/logging.test.ts +0 -131
  269. package/tests/security/resourceConsumption.test.ts +0 -142
  270. package/tests/server/auth.test.ts +0 -249
  271. package/tests/server/chatFace.test.ts +0 -320
  272. package/tests/server/common.test.ts +0 -84
  273. package/tests/server/http.test.ts +0 -561
  274. package/tests/server/instructions.test.ts +0 -152
  275. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  276. package/tests/server/stdio.test.ts +0 -99
  277. package/tests/server/stream.test.ts +0 -285
  278. package/tests/server/toolVisibility.http.test.ts +0 -111
  279. package/tests/server/toolVisibility.test.ts +0 -129
  280. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  281. package/tests/tools/common.test.ts +0 -123
  282. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  283. package/tests/tools/infra/invokeAction.test.ts +0 -280
  284. package/tests/tools/omada/graph.test.ts +0 -385
  285. package/tests/tools/omada/handlers.test.ts +0 -897
  286. package/tests/tools/omada/namespace.test.ts +0 -483
  287. package/tests/tools/registry.test.ts +0 -168
  288. package/tests/utils/config-validations.test.ts +0 -352
  289. package/tests/utils/jwt.test.ts +0 -115
  290. package/tests/utils/logger.test.ts +0 -327
  291. package/tests/utils/redact.test.ts +0 -105
  292. package/tsconfig.json +0 -24
  293. 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
- }