@coffeerunhobby/mcp-ha-connect 0.5.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 (294) hide show
  1. package/.dockerignore +8 -0
  2. package/.env.example +149 -0
  3. package/.gitattributes +30 -0
  4. package/Dockerfile +33 -0
  5. package/LICENSE +21 -0
  6. package/README.md +394 -0
  7. package/dist/config.d.ts +33 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +183 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/haClient/automations.d.ts +67 -0
  12. package/dist/haClient/automations.d.ts.map +1 -0
  13. package/dist/haClient/automations.js +181 -0
  14. package/dist/haClient/automations.js.map +1 -0
  15. package/dist/haClient/config.d.ts +28 -0
  16. package/dist/haClient/config.d.ts.map +1 -0
  17. package/dist/haClient/config.js +38 -0
  18. package/dist/haClient/config.js.map +1 -0
  19. package/dist/haClient/devices.d.ts +219 -0
  20. package/dist/haClient/devices.d.ts.map +1 -0
  21. package/dist/haClient/devices.js +506 -0
  22. package/dist/haClient/devices.js.map +1 -0
  23. package/dist/haClient/entities.d.ts +35 -0
  24. package/dist/haClient/entities.d.ts.map +1 -0
  25. package/dist/haClient/entities.js +97 -0
  26. package/dist/haClient/entities.js.map +1 -0
  27. package/dist/haClient/events.d.ts +138 -0
  28. package/dist/haClient/events.d.ts.map +1 -0
  29. package/dist/haClient/events.js +344 -0
  30. package/dist/haClient/events.js.map +1 -0
  31. package/dist/haClient/history.d.ts +26 -0
  32. package/dist/haClient/history.d.ts.map +1 -0
  33. package/dist/haClient/history.js +54 -0
  34. package/dist/haClient/history.js.map +1 -0
  35. package/dist/haClient/index.d.ts +153 -0
  36. package/dist/haClient/index.d.ts.map +1 -0
  37. package/dist/haClient/index.js +223 -0
  38. package/dist/haClient/index.js.map +1 -0
  39. package/dist/haClient/request.d.ts +34 -0
  40. package/dist/haClient/request.d.ts.map +1 -0
  41. package/dist/haClient/request.js +93 -0
  42. package/dist/haClient/request.js.map +1 -0
  43. package/dist/haClient/services.d.ts +22 -0
  44. package/dist/haClient/services.d.ts.map +1 -0
  45. package/dist/haClient/services.js +39 -0
  46. package/dist/haClient/services.js.map +1 -0
  47. package/dist/haClient/states.d.ts +26 -0
  48. package/dist/haClient/states.d.ts.map +1 -0
  49. package/dist/haClient/states.js +53 -0
  50. package/dist/haClient/states.js.map +1 -0
  51. package/dist/haClient/updates.d.ts +22 -0
  52. package/dist/haClient/updates.d.ts.map +1 -0
  53. package/dist/haClient/updates.js +48 -0
  54. package/dist/haClient/updates.js.map +1 -0
  55. package/dist/haClient.d.ts +97 -0
  56. package/dist/haClient.d.ts.map +1 -0
  57. package/dist/haClient.js +344 -0
  58. package/dist/haClient.js.map +1 -0
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +92 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/localAI/index.d.ts +37 -0
  64. package/dist/localAI/index.d.ts.map +1 -0
  65. package/dist/localAI/index.js +73 -0
  66. package/dist/localAI/index.js.map +1 -0
  67. package/dist/localAI/prompts.d.ts +20 -0
  68. package/dist/localAI/prompts.d.ts.map +1 -0
  69. package/dist/localAI/prompts.js +48 -0
  70. package/dist/localAI/prompts.js.map +1 -0
  71. package/dist/localAI/providers/base.d.ts +51 -0
  72. package/dist/localAI/providers/base.d.ts.map +1 -0
  73. package/dist/localAI/providers/base.js +80 -0
  74. package/dist/localAI/providers/base.js.map +1 -0
  75. package/dist/localAI/providers/ollama.d.ts +22 -0
  76. package/dist/localAI/providers/ollama.d.ts.map +1 -0
  77. package/dist/localAI/providers/ollama.js +63 -0
  78. package/dist/localAI/providers/ollama.js.map +1 -0
  79. package/dist/localAI/providers/openai.d.ts +23 -0
  80. package/dist/localAI/providers/openai.d.ts.map +1 -0
  81. package/dist/localAI/providers/openai.js +63 -0
  82. package/dist/localAI/providers/openai.js.map +1 -0
  83. package/dist/localAI/types.d.ts +61 -0
  84. package/dist/localAI/types.d.ts.map +1 -0
  85. package/dist/localAI/types.js +6 -0
  86. package/dist/localAI/types.js.map +1 -0
  87. package/dist/ollamaClient.d.ts +37 -0
  88. package/dist/ollamaClient.d.ts.map +1 -0
  89. package/dist/ollamaClient.js +110 -0
  90. package/dist/ollamaClient.js.map +1 -0
  91. package/dist/resources/index.d.ts +11 -0
  92. package/dist/resources/index.d.ts.map +1 -0
  93. package/dist/resources/index.js +238 -0
  94. package/dist/resources/index.js.map +1 -0
  95. package/dist/server/common.d.ts +9 -0
  96. package/dist/server/common.d.ts.map +1 -0
  97. package/dist/server/common.js +27 -0
  98. package/dist/server/common.js.map +1 -0
  99. package/dist/server/eventSubscription.d.ts +53 -0
  100. package/dist/server/eventSubscription.d.ts.map +1 -0
  101. package/dist/server/eventSubscription.js +257 -0
  102. package/dist/server/eventSubscription.js.map +1 -0
  103. package/dist/server/http.d.ts +12 -0
  104. package/dist/server/http.d.ts.map +1 -0
  105. package/dist/server/http.js +491 -0
  106. package/dist/server/http.js.map +1 -0
  107. package/dist/server/rateLimiter.d.ts +70 -0
  108. package/dist/server/rateLimiter.d.ts.map +1 -0
  109. package/dist/server/rateLimiter.js +156 -0
  110. package/dist/server/rateLimiter.js.map +1 -0
  111. package/dist/server/sse.d.ts +32 -0
  112. package/dist/server/sse.d.ts.map +1 -0
  113. package/dist/server/sse.js +76 -0
  114. package/dist/server/sse.js.map +1 -0
  115. package/dist/server/stdio.d.ts +8 -0
  116. package/dist/server/stdio.d.ts.map +1 -0
  117. package/dist/server/stdio.js +16 -0
  118. package/dist/server/stdio.js.map +1 -0
  119. package/dist/server/stream.d.ts +26 -0
  120. package/dist/server/stream.d.ts.map +1 -0
  121. package/dist/server/stream.js +81 -0
  122. package/dist/server/stream.js.map +1 -0
  123. package/dist/server.d.ts +8 -0
  124. package/dist/server.d.ts.map +1 -0
  125. package/dist/server.js +30 -0
  126. package/dist/server.js.map +1 -0
  127. package/dist/tools/callService.d.ts +8 -0
  128. package/dist/tools/callService.d.ts.map +1 -0
  129. package/dist/tools/callService.js +100 -0
  130. package/dist/tools/callService.js.map +1 -0
  131. package/dist/tools/getClient.d.ts +8 -0
  132. package/dist/tools/getClient.d.ts.map +1 -0
  133. package/dist/tools/getClient.js +94 -0
  134. package/dist/tools/getClient.js.map +1 -0
  135. package/dist/tools/getDevice.d.ts +8 -0
  136. package/dist/tools/getDevice.d.ts.map +1 -0
  137. package/dist/tools/getDevice.js +94 -0
  138. package/dist/tools/getDevice.js.map +1 -0
  139. package/dist/tools/getEntitiesByDomain.d.ts +8 -0
  140. package/dist/tools/getEntitiesByDomain.d.ts.map +1 -0
  141. package/dist/tools/getEntitiesByDomain.js +80 -0
  142. package/dist/tools/getEntitiesByDomain.js.map +1 -0
  143. package/dist/tools/getState.d.ts +8 -0
  144. package/dist/tools/getState.d.ts.map +1 -0
  145. package/dist/tools/getState.js +90 -0
  146. package/dist/tools/getState.js.map +1 -0
  147. package/dist/tools/getStates.d.ts +8 -0
  148. package/dist/tools/getStates.d.ts.map +1 -0
  149. package/dist/tools/getStates.js +70 -0
  150. package/dist/tools/getStates.js.map +1 -0
  151. package/dist/tools/index.d.ts +9 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +9 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/listClients.d.ts +8 -0
  156. package/dist/tools/listClients.d.ts.map +1 -0
  157. package/dist/tools/listClients.js +73 -0
  158. package/dist/tools/listClients.js.map +1 -0
  159. package/dist/tools/listDevices.d.ts +8 -0
  160. package/dist/tools/listDevices.d.ts.map +1 -0
  161. package/dist/tools/listDevices.js +73 -0
  162. package/dist/tools/listDevices.js.map +1 -0
  163. package/dist/tools/listSites.d.ts +8 -0
  164. package/dist/tools/listSites.d.ts.map +1 -0
  165. package/dist/tools/listSites.js +67 -0
  166. package/dist/tools/listSites.js.map +1 -0
  167. package/dist/tools/registry.d.ts +9 -0
  168. package/dist/tools/registry.d.ts.map +1 -0
  169. package/dist/tools/registry.js +1382 -0
  170. package/dist/tools/registry.js.map +1 -0
  171. package/dist/tools/searchDevices.d.ts +8 -0
  172. package/dist/tools/searchDevices.d.ts.map +1 -0
  173. package/dist/tools/searchDevices.js +84 -0
  174. package/dist/tools/searchDevices.js.map +1 -0
  175. package/dist/tools/searchEntities.d.ts +8 -0
  176. package/dist/tools/searchEntities.d.ts.map +1 -0
  177. package/dist/tools/searchEntities.js +80 -0
  178. package/dist/tools/searchEntities.js.map +1 -0
  179. package/dist/types/entity.d.ts +98 -0
  180. package/dist/types/entity.d.ts.map +1 -0
  181. package/dist/types/entity.js +2 -0
  182. package/dist/types/entity.js.map +1 -0
  183. package/dist/types/errors.d.ts +16 -0
  184. package/dist/types/errors.d.ts.map +1 -0
  185. package/dist/types/errors.js +32 -0
  186. package/dist/types/errors.js.map +1 -0
  187. package/dist/types/haConfig.d.ts +8 -0
  188. package/dist/types/haConfig.d.ts.map +1 -0
  189. package/dist/types/haConfig.js +2 -0
  190. package/dist/types/haConfig.js.map +1 -0
  191. package/dist/types/index.d.ts +6 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +2 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/dist/types/request.d.ts +7 -0
  196. package/dist/types/request.d.ts.map +1 -0
  197. package/dist/types/request.js +2 -0
  198. package/dist/types/request.js.map +1 -0
  199. package/dist/types/serviceCall.d.ts +18 -0
  200. package/dist/types/serviceCall.d.ts.map +1 -0
  201. package/dist/types/serviceCall.js +2 -0
  202. package/dist/types/serviceCall.js.map +1 -0
  203. package/dist/types.d.ts +2 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/types.js +3 -0
  206. package/dist/types.js.map +1 -0
  207. package/dist/utils/config-validations.d.ts +12 -0
  208. package/dist/utils/config-validations.d.ts.map +1 -0
  209. package/dist/utils/config-validations.js +43 -0
  210. package/dist/utils/config-validations.js.map +1 -0
  211. package/dist/utils/logger.d.ts +24 -0
  212. package/dist/utils/logger.d.ts.map +1 -0
  213. package/dist/utils/logger.js +94 -0
  214. package/dist/utils/logger.js.map +1 -0
  215. package/dist/wrapper.d.ts +8 -0
  216. package/dist/wrapper.d.ts.map +1 -0
  217. package/dist/wrapper.js +301 -0
  218. package/dist/wrapper.js.map +1 -0
  219. package/docker-compose.yml +17 -0
  220. package/docs/CHANGELOG.md +86 -0
  221. package/docs/LOCAL_CLIENT.md +268 -0
  222. package/docs/SSE_API.md +341 -0
  223. package/eslint.config.js +33 -0
  224. package/n8n/ai-agent-safety-monitor.json +355 -0
  225. package/openapi.json +166 -0
  226. package/package.json +51 -0
  227. package/src/config.ts +260 -0
  228. package/src/haClient/automations.ts +217 -0
  229. package/src/haClient/config.ts +41 -0
  230. package/src/haClient/devices.ts +653 -0
  231. package/src/haClient/entities.ts +128 -0
  232. package/src/haClient/events.ts +456 -0
  233. package/src/haClient/history.ts +68 -0
  234. package/src/haClient/index.ts +287 -0
  235. package/src/haClient/request.ts +127 -0
  236. package/src/haClient/services.ts +45 -0
  237. package/src/haClient/states.ts +60 -0
  238. package/src/haClient/updates.ts +58 -0
  239. package/src/index.ts +99 -0
  240. package/src/localAI/index.ts +88 -0
  241. package/src/localAI/prompts.ts +50 -0
  242. package/src/localAI/providers/base.ts +120 -0
  243. package/src/localAI/providers/ollama.ts +78 -0
  244. package/src/localAI/providers/openai.ts +86 -0
  245. package/src/localAI/types.ts +67 -0
  246. package/src/resources/index.ts +266 -0
  247. package/src/server/common.ts +38 -0
  248. package/src/server/eventSubscription.ts +318 -0
  249. package/src/server/http.ts +546 -0
  250. package/src/server/rateLimiter.ts +195 -0
  251. package/src/server/sse.ts +120 -0
  252. package/src/server/stdio.ts +22 -0
  253. package/src/server/stream.ts +113 -0
  254. package/src/tools/callService.ts +114 -0
  255. package/src/tools/getEntitiesByDomain.ts +92 -0
  256. package/src/tools/getState.ts +103 -0
  257. package/src/tools/getStates.ts +80 -0
  258. package/src/tools/index.ts +9 -0
  259. package/src/tools/registry.ts +1521 -0
  260. package/src/tools/searchEntities.ts +92 -0
  261. package/src/types/entity.ts +110 -0
  262. package/src/types/errors.ts +33 -0
  263. package/src/types/haConfig.ts +7 -0
  264. package/src/types/index.ts +5 -0
  265. package/src/types/request.ts +6 -0
  266. package/src/types/serviceCall.ts +18 -0
  267. package/src/types.ts +2 -0
  268. package/src/utils/config-validations.ts +46 -0
  269. package/src/utils/logger.ts +114 -0
  270. package/src/wrapper.ts +340 -0
  271. package/tests/config.test.ts +332 -0
  272. package/tests/haClient/automations.test.ts +133 -0
  273. package/tests/haClient/config.test.ts +96 -0
  274. package/tests/haClient/entities.test.ts +188 -0
  275. package/tests/haClient/history.test.ts +170 -0
  276. package/tests/haClient/index.test.ts +221 -0
  277. package/tests/haClient/request.test.ts +217 -0
  278. package/tests/haClient/services.test.ts +120 -0
  279. package/tests/haClient/states.test.ts +119 -0
  280. package/tests/haClient/updates.test.ts +193 -0
  281. package/tests/index.test.ts +164 -0
  282. package/tests/localAI/index.test.ts +168 -0
  283. package/tests/localAI/ollama.test.ts +187 -0
  284. package/tests/localAI/openai.test.ts +257 -0
  285. package/tests/server/common.test.ts +64 -0
  286. package/tests/server/http.test.ts +326 -0
  287. package/tests/server/sse.test.ts +240 -0
  288. package/tests/server/stdio.test.ts +102 -0
  289. package/tests/server/stream.test.ts +287 -0
  290. package/tests/tools/registry.test.ts +355 -0
  291. package/tests/utils/config-validations.test.ts +289 -0
  292. package/tests/utils/logger.test.ts +327 -0
  293. package/tsconfig.json +25 -0
  294. package/vitest.config.ts +26 -0
@@ -0,0 +1,355 @@
1
+ {
2
+ "name": "AI Agent Safety Monitor",
3
+ "nodes": [
4
+ {
5
+ "parameters": {
6
+ "rule": {
7
+ "interval": [
8
+ {
9
+ "field": "minutes"
10
+ }
11
+ ]
12
+ }
13
+ },
14
+ "type": "n8n-nodes-base.scheduleTrigger",
15
+ "typeVersion": 1.2,
16
+ "position": [
17
+ 256,
18
+ 304
19
+ ],
20
+ "id": "schedule",
21
+ "name": "Every 5 Minutes"
22
+ },
23
+ {
24
+ "parameters": {
25
+ "method": "POST",
26
+ "url": "http://mcpserver.10.0.0.18.nip.io:3000/mcp",
27
+ "sendHeaders": true,
28
+ "headerParameters": {
29
+ "parameters": [
30
+ {
31
+ "name": "Accept",
32
+ "value": "application/json, text/event-stream"
33
+ }
34
+ ]
35
+ },
36
+ "sendBody": true,
37
+ "specifyBody": "json",
38
+ "jsonBody": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"getAllSensors\",\"arguments\":{}}}",
39
+ "options": {}
40
+ },
41
+ "type": "n8n-nodes-base.httpRequest",
42
+ "typeVersion": 4.2,
43
+ "position": [
44
+ 464,
45
+ 304
46
+ ],
47
+ "id": "get-sensors",
48
+ "name": "Get Sensors"
49
+ },
50
+ {
51
+ "parameters": {
52
+ "jsCode": "// Parse SSE response from MCP server\nlet rawResponse = $input.first().json;\nlet mcpResponse;\n\n// Handle SSE format (event: message\\ndata: {...})\nif (typeof rawResponse === 'string' || rawResponse.data) {\n const text = typeof rawResponse === 'string' ? rawResponse : rawResponse.data;\n const dataMatch = text.match(/^data:\\s*(.+)$/m);\n if (dataMatch) {\n mcpResponse = JSON.parse(dataMatch[1]);\n } else {\n throw new Error('No data line in SSE response');\n }\n} else {\n mcpResponse = rawResponse;\n}\n\nconst content = mcpResponse.result?.content?.[0]?.text;\nif (!content) throw new Error('No data from MCP');\nconst allSensors = JSON.parse(content);\n\nconst issues = [];\n\nfor (const sensor of allSensors) {\n const id = sensor.entity_id?.toLowerCase() || '';\n const state = String(sensor.state || '').toLowerCase();\n const stateClass = sensor.attributes?.state_class?.toLowerCase() || '';\n const deviceClass = sensor.attributes?.device_class?.toLowerCase() || '';\n const name = sensor.attributes?.friendly_name?.toLowerCase() || '';\n \n if (stateClass === 'total' || stateClass === 'total_increasing') continue;\n if (id.includes('cloud_connection') || id.includes('connectivity') || id.includes('_update') || id.includes('_connection')) continue;\n \n if (id.startsWith('binary_sensor.')) {\n if ((deviceClass === 'gas' || id.match(/\\bgas\\b/)) && state === 'on') {\n issues.push({sensor: sensor.entity_id, issue: 'GAS DETECTED', severity: 'critical', state, name: sensor.attributes?.friendly_name});\n }\n else if ((deviceClass === 'smoke' || id.includes('smoke')) && state === 'on') {\n issues.push({sensor: sensor.entity_id, issue: 'SMOKE DETECTED', severity: 'critical', state, name: sensor.attributes?.friendly_name});\n }\n else if ((deviceClass === 'carbon_monoxide' || id.match(/\\bco\\b/) || id.includes('carbon')) && state === 'on') {\n issues.push({sensor: sensor.entity_id, issue: 'CO DETECTED', severity: 'critical', state, name: sensor.attributes?.friendly_name});\n }\n else if ((deviceClass === 'moisture' || id.includes('leak') || id.includes('flood') || id.includes('water_') || name.includes('leak') || name.includes('moisture')) && state === 'on') {\n issues.push({sensor: sensor.entity_id, issue: 'Water detected', severity: 'critical', state, name: sensor.attributes?.friendly_name});\n }\n }\n else if (id.startsWith('sensor.')) {\n if ((id.includes('status') || id.includes('health')) && (state === 'attention' || state === 'warning' || state === 'error' || state === 'critical' || state === 'problem')) {\n issues.push({sensor: sensor.entity_id, issue: `Status: ${state}`, severity: 'high', state, name: sensor.attributes?.friendly_name});\n }\n else if (deviceClass === 'battery') {\n const batteryLevel = parseFloat(state);\n if (!isNaN(batteryLevel) && batteryLevel < 15) {\n issues.push({sensor: sensor.entity_id, issue: `Battery low: ${batteryLevel}%`, severity: batteryLevel < 5 ? 'high' : 'medium', state, name: sensor.attributes?.friendly_name});\n }\n }\n else if (deviceClass === 'temperature') {\n const temp = parseFloat(state);\n const unit = sensor.attributes?.unit_of_measurement;\n if (!isNaN(temp) && (unit === '°C' || unit === 'C')) {\n if (temp > 40) {\n issues.push({sensor: sensor.entity_id, issue: `Very high temp: ${temp}C`, severity: 'high', state, name: sensor.attributes?.friendly_name});\n } else if (temp < 0 && !name.includes('outdoor') && !name.includes('outside')) {\n issues.push({sensor: sensor.entity_id, issue: `Freezing temp: ${temp}C`, severity: 'medium', state, name: sensor.attributes?.friendly_name});\n }\n }\n }\n }\n}\n\nreturn [{json: {total_sensors: allSensors.length, issues_found: issues.length, issues: issues}}];"
53
+ },
54
+ "type": "n8n-nodes-base.code",
55
+ "typeVersion": 2,
56
+ "position": [
57
+ 656,
58
+ 304
59
+ ],
60
+ "id": "filter",
61
+ "name": "Filter Issues"
62
+ },
63
+ {
64
+ "parameters": {
65
+ "jsCode": "const data = $input.first().json;\n\nif (data.issues_found === 0) {\n return [{json: {should_notify: false, notification: {title: 'All OK', message: `Monitored ${data.total_sensors} sensors - all normal`}, reasoning: 'No issues'}}];\n}\n\nconst criticalCount = data.issues.filter(i => i.severity === 'critical').length;\n\nconst prompt = `Home safety AI monitoring ${data.total_sensors} sensors.\n\nISSUES (${data.issues_found}):\n${JSON.stringify(data.issues, null, 2)}\n\nCritical: ${criticalCount}\n\nIMPORTANT: Create notification WITHOUT emojis or special characters. Plain text only.\n\nJSON:\n{\n \"should_notify\": true,\n \"notification\": {\n \"title\": \"${criticalCount > 0 ? 'CRITICAL ALERT' : 'System Notice'}\",\n \"message\": \"Plain text summary and actions needed\"\n },\n \"reasoning\": \"brief\"\n}`;\n\nreturn [{json: {model: 'phi4:14b', prompt: prompt, stream: false, issues: data.issues}}];\n"
66
+ },
67
+ "type": "n8n-nodes-base.code",
68
+ "typeVersion": 2,
69
+ "position": [
70
+ 864,
71
+ 304
72
+ ],
73
+ "id": "build",
74
+ "name": "Build Prompt"
75
+ },
76
+ {
77
+ "parameters": {
78
+ "method": "POST",
79
+ "url": "http://ollama.10.0.0.17.nip.io:11434/api/generate",
80
+ "sendBody": true,
81
+ "specifyBody": "json",
82
+ "jsonBody": "={\"model\":\"ministral-3:14b\",\"prompt\":\"Home safety AI monitoring 278 sensors.\\n\\nISSUES (3):\\n[\\n {\\n \\\"sensor\\\": \\\"sensor.nas_station_s5_drive_1_temperature\\\",\\n \\\"issue\\\": \\\"Very high temp: 42C\\\",\\n \\\"severity\\\": \\\"high\\\",\\n \\\"state\\\": \\\"42\\\",\\n \\\"name\\\": \\\"nas-station-s5 (Drive 1) Temperature\\\"\\n },\\n {\\n \\\"sensor\\\": \\\"sensor.nas_station_s5_m_2_drive_2_temperature\\\",\\n \\\"issue\\\": \\\"Very high temp: 41C\\\",\\n \\\"severity\\\": \\\"high\\\",\\n \\\"state\\\": \\\"41\\\",\\n \\\"name\\\": \\\"nas-station-s5 (M.2 Drive 2) Temperature\\\"\\n },\\n {\\n \\\"sensor\\\": \\\"sensor.nas_station_s5_temperature\\\",\\n \\\"issue\\\": \\\"Very high temp: 60C\\\",\\n \\\"severity\\\": \\\"high\\\",\\n \\\"state\\\": \\\"60\\\",\\n \\\"name\\\": \\\"nas-station-s5 Temperature\\\"\\n }\\n]\\n\\nCritical: 0\\n\\nIMPORTANT: Create notification WITHOUT emojis or special characters. Plain text only.\\n\\nJSON:\\n{\\n \\\"should_notify\\\": true,\\n \\\"notification\\\": {\\n \\\"title\\\": \\\"System Notice\\\",\\n \\\"message\\\": \\\"Plain text summary and actions needed\\\"\\n },\\n \\\"reasoning\\\": \\\"brief\\\"\\n}\",\"stream\":false}",
83
+ "options": {}
84
+ },
85
+ "type": "n8n-nodes-base.httpRequest",
86
+ "typeVersion": 4.2,
87
+ "position": [
88
+ 1056,
89
+ 304
90
+ ],
91
+ "id": "ollama",
92
+ "name": "Ollama"
93
+ },
94
+ {
95
+ "parameters": {
96
+ "jsCode": "const ollamaResponse = $input.first().json;\nif (!ollamaResponse.response) {\n throw new Error('No response from Ollama');\n}\n\nlet responseText = ollamaResponse.response;\n\n// Remove markdown code fences\nresponseText = responseText.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\n\n// Extract JSON (find first { to last })\nfunction extractJSON(text) {\n const firstBrace = text.indexOf('{');\n const lastBrace = text.lastIndexOf('}');\n \n if (firstBrace === -1 || lastBrace === -1 || firstBrace >= lastBrace) {\n throw new Error('No valid JSON object found in response');\n }\n \n return text.substring(firstBrace, lastBrace + 1);\n}\n\nlet jsonText = extractJSON(responseText);\n\n// DON'T escape newlines - JSON.parse() handles them fine\n// Just parse directly\nlet decision;\ntry {\n decision = JSON.parse(jsonText);\n} catch (e) {\n // If parse fails, try removing problematic characters\n jsonText = jsonText\n .replace(/[\\u0000-\\u001F]/g, '') // Remove control characters except newline/tab\n .replace(/,(\\s*[}\\]])/g, '$1'); // Remove trailing commas\n \n try {\n decision = JSON.parse(jsonText);\n } catch (e2) {\n throw new Error('JSON parse failed: ' + e2.message + '\\n\\nJSON text: ' + jsonText.substring(0, 500));\n }\n}\n\n// Clean up text helper\nfunction cleanText(text) {\n if (!text) return text;\n \n return text\n // Remove emojis\n .replace(/[\\u{1F600}-\\u{1F64F}]/gu, '')\n .replace(/[\\u{1F300}-\\u{1F5FF}]/gu, '')\n .replace(/[\\u{1F680}-\\u{1F6FF}]/gu, '')\n .replace(/[\\u{2600}-\\u{26FF}]/gu, '')\n .replace(/[\\u{2700}-\\u{27BF}]/gu, '')\n // Clean excessive whitespace\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n// Clean notification fields\nif (decision.notification?.message) {\n decision.notification.message = cleanText(decision.notification.message);\n}\n\nif (decision.notification?.title) {\n decision.notification.title = cleanText(decision.notification.title);\n}\n\nreturn [{json: decision}];"
97
+ },
98
+ "type": "n8n-nodes-base.code",
99
+ "typeVersion": 2,
100
+ "position": [
101
+ 1264,
102
+ 304
103
+ ],
104
+ "id": "parse",
105
+ "name": "Parse & Strip"
106
+ },
107
+ {
108
+ "parameters": {
109
+ "conditions": {
110
+ "options": {
111
+ "caseSensitive": true,
112
+ "leftValue": "",
113
+ "typeValidation": "strict",
114
+ "version": 1
115
+ },
116
+ "conditions": [
117
+ {
118
+ "id": "notify",
119
+ "leftValue": "={{ $json.should_notify }}",
120
+ "rightValue": true,
121
+ "operator": {
122
+ "type": "boolean",
123
+ "operation": "equals"
124
+ }
125
+ }
126
+ ],
127
+ "combinator": "or"
128
+ },
129
+ "options": {}
130
+ },
131
+ "type": "n8n-nodes-base.if",
132
+ "typeVersion": 2,
133
+ "position": [
134
+ 1664,
135
+ 304
136
+ ],
137
+ "id": "check",
138
+ "name": "Notify?"
139
+ },
140
+ {
141
+ "parameters": {
142
+ "jsCode": "const message = $input.first().json.notification.message || 'Issue detected';\nconst title = $input.first().json.notification.title || 'Alert';\n\nconst mcpRequest = {\n jsonrpc: '2.0',\n id: 2,\n method: 'tools/call',\n params: {\n name: 'callService',\n arguments: {\n domain: 'notify',\n service: 'persistent_notification',\n service_data: {message, title}\n }\n }\n};\n\nreturn [{json: {mcpRequest}}];"
143
+ },
144
+ "type": "n8n-nodes-base.code",
145
+ "typeVersion": 2,
146
+ "position": [
147
+ 1872,
148
+ 208
149
+ ],
150
+ "id": "build-req",
151
+ "name": "Build Request"
152
+ },
153
+ {
154
+ "parameters": {
155
+ "method": "POST",
156
+ "url": "http://mcpserver.10.0.0.18.nip.io:3000/mcp",
157
+ "sendHeaders": true,
158
+ "headerParameters": {
159
+ "parameters": [
160
+ {
161
+ "name": "Accept",
162
+ "value": "application/json, text/event-stream"
163
+ }
164
+ ]
165
+ },
166
+ "sendBody": true,
167
+ "specifyBody": "json",
168
+ "jsonBody": "={{ JSON.stringify($json.mcpRequest) }}",
169
+ "options": {}
170
+ },
171
+ "type": "n8n-nodes-base.httpRequest",
172
+ "typeVersion": 4.2,
173
+ "position": [
174
+ 2064,
175
+ 208
176
+ ],
177
+ "id": "notify",
178
+ "name": "Send Alert"
179
+ },
180
+ {
181
+ "parameters": {
182
+ "jsCode": "return [{json: {result: 'Alert sent', issues: $('Build Prompt').item.json.issues}}];"
183
+ },
184
+ "type": "n8n-nodes-base.code",
185
+ "typeVersion": 2,
186
+ "position": [
187
+ 2272,
188
+ 208
189
+ ],
190
+ "id": "done-alert",
191
+ "name": "Done Alert"
192
+ },
193
+ {
194
+ "parameters": {
195
+ "jsCode": "return [{json: {result: 'All OK'}}];"
196
+ },
197
+ "type": "n8n-nodes-base.code",
198
+ "typeVersion": 2,
199
+ "position": [
200
+ 1872,
201
+ 400
202
+ ],
203
+ "id": "done-ok",
204
+ "name": "Done OK"
205
+ },
206
+ {
207
+ "parameters": {
208
+ "jsCode": "const notification = $input.first().json.notification;\nconst buildPromptData = $('Build Prompt').first().json;\nconst issues = buildPromptData.issues || [];\n\n// Create detailed alert log\nconst alertLog = {\n 'ALERT': notification.title,\n 'Message': notification.message,\n 'Issue Count': issues.length,\n 'Issues': issues.map(i => `${i.severity.toUpperCase()}: ${i.name} - ${i.issue}`),\n 'Time': new Date().toLocaleString()\n};\n\nconsole.log(JSON.stringify(alertLog, null, 2));\n\n// Pass data through unchanged\nreturn $input.all();"
209
+ },
210
+ "type": "n8n-nodes-base.code",
211
+ "typeVersion": 2,
212
+ "position": [
213
+ 1472,
214
+ 304
215
+ ],
216
+ "id": "code-in-javascript",
217
+ "name": "Code in JavaScript"
218
+ }
219
+ ],
220
+ "pinData": {},
221
+ "connections": {
222
+ "Every 5 Minutes": {
223
+ "main": [
224
+ [
225
+ {
226
+ "node": "Get Sensors",
227
+ "type": "main",
228
+ "index": 0
229
+ }
230
+ ]
231
+ ]
232
+ },
233
+ "Get Sensors": {
234
+ "main": [
235
+ [
236
+ {
237
+ "node": "Filter Issues",
238
+ "type": "main",
239
+ "index": 0
240
+ }
241
+ ]
242
+ ]
243
+ },
244
+ "Filter Issues": {
245
+ "main": [
246
+ [
247
+ {
248
+ "node": "Build Prompt",
249
+ "type": "main",
250
+ "index": 0
251
+ }
252
+ ]
253
+ ]
254
+ },
255
+ "Build Prompt": {
256
+ "main": [
257
+ [
258
+ {
259
+ "node": "Ollama",
260
+ "type": "main",
261
+ "index": 0
262
+ }
263
+ ]
264
+ ]
265
+ },
266
+ "Ollama": {
267
+ "main": [
268
+ [
269
+ {
270
+ "node": "Parse & Strip",
271
+ "type": "main",
272
+ "index": 0
273
+ }
274
+ ]
275
+ ]
276
+ },
277
+ "Parse & Strip": {
278
+ "main": [
279
+ [
280
+ {
281
+ "node": "Code in JavaScript",
282
+ "type": "main",
283
+ "index": 0
284
+ }
285
+ ]
286
+ ]
287
+ },
288
+ "Notify?": {
289
+ "main": [
290
+ [
291
+ {
292
+ "node": "Build Request",
293
+ "type": "main",
294
+ "index": 0
295
+ }
296
+ ],
297
+ [
298
+ {
299
+ "node": "Done OK",
300
+ "type": "main",
301
+ "index": 0
302
+ }
303
+ ]
304
+ ]
305
+ },
306
+ "Build Request": {
307
+ "main": [
308
+ [
309
+ {
310
+ "node": "Send Alert",
311
+ "type": "main",
312
+ "index": 0
313
+ }
314
+ ]
315
+ ]
316
+ },
317
+ "Send Alert": {
318
+ "main": [
319
+ [
320
+ {
321
+ "node": "Done Alert",
322
+ "type": "main",
323
+ "index": 0
324
+ }
325
+ ]
326
+ ]
327
+ },
328
+ "Done OK": {
329
+ "main": [
330
+ []
331
+ ]
332
+ },
333
+ "Code in JavaScript": {
334
+ "main": [
335
+ [
336
+ {
337
+ "node": "Notify?",
338
+ "type": "main",
339
+ "index": 0
340
+ }
341
+ ]
342
+ ]
343
+ }
344
+ },
345
+ "active": false,
346
+ "settings": {
347
+ "executionOrder": "v1"
348
+ },
349
+ "versionId": "00000000-0000-0000-0000-000000000000",
350
+ "meta": {
351
+ "templateCredsSetupCompleted": true
352
+ },
353
+ "id": "1",
354
+ "tags": []
355
+ }
package/openapi.json ADDED
@@ -0,0 +1,166 @@
1
+ {
2
+ "openapi": "3.1.0",
3
+ "info": {
4
+ "title": "Home Assistant MCP Wrapper",
5
+ "version": "1.2",
6
+ "description": "Pretty REST endpoints that call MCP tools under the hood. Raw JSON-RPC passthrough remains available at POST / for n8n compatibility."
7
+ },
8
+ "servers": [
9
+ { "url": "http://mcpserver.10.0.0.18.nip.io:3000", "description": "MCP Server (nip.io)" },
10
+ { "url": "http://127.0.0.1:3000", "description": "Local development" }
11
+ ],
12
+ "paths": {
13
+ "/health": {
14
+ "get": {
15
+ "operationId": "health",
16
+ "summary": "Health check",
17
+ "responses": {
18
+ "200": {
19
+ "description": "OK",
20
+ "content": {
21
+ "application/json": {
22
+ "schema": {
23
+ "type": "object",
24
+ "properties": {
25
+ "status": { "type": "string", "example": "ok" },
26
+ "mcp_path": { "type": "string" },
27
+ "ha_url": { "type": "string" },
28
+ "token_configured": { "type": "boolean" }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ },
37
+
38
+ "/get_all_sensors": {
39
+ "post": {
40
+ "operationId": "get_all_sensors",
41
+ "summary": "Get all sensors from Home Assistant",
42
+ "responses": {
43
+ "200": {
44
+ "description": "Sensors list",
45
+ "content": {
46
+ "application/json": {
47
+ "schema": {
48
+ "type": "object",
49
+ "required": ["count", "sensors"],
50
+ "properties": {
51
+ "count": { "type": "integer", "example": 271 },
52
+ "sensors": {
53
+ "type": "array",
54
+ "items": { "type": "object" }
55
+ },
56
+ "raw": { "type": "object", "description": "Original MCP response (debug)" }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ },
65
+
66
+ "/get_sensor_state": {
67
+ "post": {
68
+ "operationId": "get_sensor_state",
69
+ "summary": "Get state for a Home Assistant entity_id",
70
+ "requestBody": {
71
+ "required": true,
72
+ "content": {
73
+ "application/json": {
74
+ "schema": {
75
+ "type": "object",
76
+ "required": ["entity_id"],
77
+ "properties": {
78
+ "entity_id": { "type": "string", "example": "sensor.nas_station_s5_temperature" }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ },
84
+ "responses": {
85
+ "200": {
86
+ "description": "Entity state",
87
+ "content": {
88
+ "application/json": {
89
+ "schema": {
90
+ "type": "object",
91
+ "required": ["entity"],
92
+ "properties": {
93
+ "entity": { "type": "object" },
94
+ "raw": { "type": "object", "description": "Original MCP response (debug)" }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ },
103
+
104
+ "/control_device": {
105
+ "post": {
106
+ "operationId": "control_device",
107
+ "summary": "Call a Home Assistant service (domain/service) with data",
108
+ "requestBody": {
109
+ "required": true,
110
+ "content": {
111
+ "application/json": {
112
+ "schema": {
113
+ "type": "object",
114
+ "required": ["domain", "service"],
115
+ "properties": {
116
+ "domain": { "type": "string", "example": "notify" },
117
+ "service": { "type": "string", "example": "persistent_notification" },
118
+ "data": {
119
+ "type": "object",
120
+ "additionalProperties": true,
121
+ "example": { "title": "Alert", "message": "Hello" }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ },
128
+ "responses": {
129
+ "200": {
130
+ "description": "Service result",
131
+ "content": {
132
+ "application/json": {
133
+ "schema": {
134
+ "type": "object",
135
+ "required": ["ok"],
136
+ "properties": {
137
+ "ok": { "type": "boolean", "example": true },
138
+ "result": { "description": "Parsed HA/MCP result", "nullable": true },
139
+ "raw": { "type": "object", "description": "Original MCP response (debug)" }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ },
148
+
149
+ "/": {
150
+ "post": {
151
+ "operationId": "mcp_tools_call_raw",
152
+ "summary": "Raw JSON-RPC passthrough (kept for n8n compatibility)",
153
+ "requestBody": {
154
+ "required": true,
155
+ "content": { "application/json": { "schema": { "type": "object" } } }
156
+ },
157
+ "responses": {
158
+ "200": {
159
+ "description": "JSON-RPC response",
160
+ "content": { "application/json": { "schema": { "type": "object" } } }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@coffeerunhobby/mcp-ha-connect",
3
+ "version": "0.5.0",
4
+ "description": "Smart home MCP server for Home Assistant plus Local AI integration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-ha-connect": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx watch --env-file=.env src/index.ts",
12
+ "dev:http": "tsx watch --env-file=.env src/index.ts",
13
+ "build": "tsc",
14
+ "start": "node --env-file=.env dist/index.js",
15
+ "start:http": "node --env-file=.env dist/index.js",
16
+ "test": "vitest run --no-color",
17
+ "test:watch": "vitest --no-color",
18
+ "test:coverage": "vitest run --coverage --no-color",
19
+ "lint": "eslint src tests",
20
+ "type-check": "tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "home-assistant",
25
+ "home-automation",
26
+ "smart-home"
27
+ ],
28
+ "author": "Coffee Run Hobby",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.4",
32
+ "pino": "^9.5.0",
33
+ "ws": "^8.19.0",
34
+ "zod": "^3.24.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.10.5",
38
+ "@types/ws": "^8.18.1",
39
+ "@typescript-eslint/eslint-plugin": "^8.19.1",
40
+ "@typescript-eslint/parser": "^8.19.1",
41
+ "@vitest/coverage-v8": "^2.1.8",
42
+ "eslint": "^9.18.0",
43
+ "tsx": "^4.19.2",
44
+ "typescript": "^5.7.2",
45
+ "typescript-eslint": "^8.52.0",
46
+ "vitest": "^2.1.8"
47
+ },
48
+ "engines": {
49
+ "node": ">=20.0.0"
50
+ }
51
+ }