@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,287 @@
1
+ /**
2
+ * Home Assistant API Client
3
+ * Main client class that composes all operation modules
4
+ */
5
+
6
+ import type { EnvironmentConfig } from '../config.js';
7
+ import type {
8
+ Entity,
9
+ ServiceCallData,
10
+ ServiceCallResponse,
11
+ Automation,
12
+ AutomationConfig,
13
+ DomainSummary,
14
+ HaVersion,
15
+ LogbookEntry,
16
+ UpdateInfo,
17
+ } from '../types/index.js';
18
+ import { logger } from '../utils/logger.js';
19
+
20
+ import { AutomationOperations } from './automations.js';
21
+ import { ConfigOperations } from './config.js';
22
+ import { DeviceOperations } from './devices.js';
23
+ import { EntityOperations } from './entities.js';
24
+ import { HistoryOperations } from './history.js';
25
+ import { RequestHandler } from './request.js';
26
+ import { ServiceOperations } from './services.js';
27
+ import { StateOperations } from './states.js';
28
+ import { UpdateOperations } from './updates.js';
29
+
30
+ /**
31
+ * Main client for interacting with the Home Assistant API.
32
+ * Organized with dedicated operation classes for each domain.
33
+ */
34
+ export class HaClient {
35
+ private readonly request: RequestHandler;
36
+ private readonly stateOps: StateOperations;
37
+ private readonly serviceOps: ServiceOperations;
38
+ private readonly entityOps: EntityOperations;
39
+ private readonly automationOps: AutomationOperations;
40
+ private readonly historyOps: HistoryOperations;
41
+ private readonly updateOps: UpdateOperations;
42
+ private readonly configOps: ConfigOperations;
43
+ public readonly devices: DeviceOperations;
44
+
45
+ constructor(config: EnvironmentConfig) {
46
+ logger.debug('Initializing HaClient');
47
+
48
+ // Initialize request handler
49
+ this.request = new RequestHandler({
50
+ baseUrl: config.baseUrl,
51
+ token: config.token,
52
+ timeout: config.timeout,
53
+ strictSsl: config.strictSsl,
54
+ });
55
+
56
+ // Initialize operation modules with dependencies
57
+ this.stateOps = new StateOperations(this.request);
58
+ this.serviceOps = new ServiceOperations(this.request);
59
+ this.entityOps = new EntityOperations(this.stateOps);
60
+ this.automationOps = new AutomationOperations(this.stateOps, this.serviceOps, this.request);
61
+ this.historyOps = new HistoryOperations(this.request);
62
+ this.updateOps = new UpdateOperations(this.entityOps);
63
+ this.configOps = new ConfigOperations(this.request);
64
+ this.devices = new DeviceOperations(this.serviceOps, this.stateOps);
65
+
66
+ logger.info('HaClient initialized');
67
+ }
68
+
69
+ // ===== State Operations =====
70
+
71
+ /**
72
+ * Get all states (entities)
73
+ */
74
+ async getStates(): Promise<Entity[]> {
75
+ return this.stateOps.getStates();
76
+ }
77
+
78
+ /**
79
+ * Get state of a specific entity
80
+ */
81
+ async getState(entityId: string): Promise<Entity | null> {
82
+ return this.stateOps.getState(entityId);
83
+ }
84
+
85
+ /**
86
+ * Get all sensors (sensor.* and binary_sensor.* entities)
87
+ */
88
+ async getAllSensors(): Promise<Entity[]> {
89
+ return this.stateOps.getAllSensors();
90
+ }
91
+
92
+ // ===== Service Operations =====
93
+
94
+ /**
95
+ * Call a service
96
+ */
97
+ async callService(data: ServiceCallData): Promise<ServiceCallResponse> {
98
+ return this.serviceOps.callService(data);
99
+ }
100
+
101
+ /**
102
+ * Restart Home Assistant
103
+ */
104
+ async restartServer(): Promise<void> {
105
+ return this.serviceOps.restartServer();
106
+ }
107
+
108
+ // ===== Entity Operations =====
109
+
110
+ /**
111
+ * Get entities filtered by domain
112
+ */
113
+ async getEntitiesByDomain(domain: string): Promise<Entity[]> {
114
+ return this.entityOps.getEntitiesByDomain(domain);
115
+ }
116
+
117
+ /**
118
+ * Search entities by name or entity_id
119
+ */
120
+ async searchEntities(query: string): Promise<Entity[]> {
121
+ return this.entityOps.searchEntities(query);
122
+ }
123
+
124
+ /**
125
+ * Get domain summary with entity counts and state breakdown
126
+ */
127
+ async getDomainSummary(domain: string): Promise<DomainSummary> {
128
+ return this.entityOps.getDomainSummary(domain);
129
+ }
130
+
131
+ /**
132
+ * List entities with optional filtering
133
+ */
134
+ async listEntities(options: {
135
+ domain?: string;
136
+ search?: string;
137
+ limit?: number;
138
+ state?: string;
139
+ } = {}): Promise<Entity[]> {
140
+ return this.entityOps.listEntities(options);
141
+ }
142
+
143
+ // ===== Automation Operations =====
144
+
145
+ /**
146
+ * Get all automations
147
+ */
148
+ async getAutomations(): Promise<Automation[]> {
149
+ return this.automationOps.getAutomations();
150
+ }
151
+
152
+ /**
153
+ * Get a single automation by entity_id
154
+ */
155
+ async getAutomation(entityId: string): Promise<Automation | null> {
156
+ return this.automationOps.getAutomation(entityId);
157
+ }
158
+
159
+ /**
160
+ * Trigger an automation manually
161
+ */
162
+ async triggerAutomation(entityId: string, variables?: Record<string, unknown>): Promise<ServiceCallResponse> {
163
+ return this.automationOps.triggerAutomation(entityId, variables);
164
+ }
165
+
166
+ /**
167
+ * Enable an automation
168
+ */
169
+ async enableAutomation(entityId: string): Promise<ServiceCallResponse> {
170
+ return this.automationOps.enableAutomation(entityId);
171
+ }
172
+
173
+ /**
174
+ * Disable an automation
175
+ */
176
+ async disableAutomation(entityId: string): Promise<ServiceCallResponse> {
177
+ return this.automationOps.disableAutomation(entityId);
178
+ }
179
+
180
+ /**
181
+ * Toggle an automation
182
+ */
183
+ async toggleAutomation(entityId: string): Promise<ServiceCallResponse> {
184
+ return this.automationOps.toggleAutomation(entityId);
185
+ }
186
+
187
+ /**
188
+ * Reload all automations from configuration
189
+ */
190
+ async reloadAutomations(): Promise<void> {
191
+ return this.automationOps.reloadAutomations();
192
+ }
193
+
194
+ /**
195
+ * Create a new automation
196
+ */
197
+ async createAutomation(config: AutomationConfig): Promise<{ id: string }> {
198
+ return this.automationOps.createAutomation(config);
199
+ }
200
+
201
+ /**
202
+ * Update an existing automation
203
+ */
204
+ async updateAutomation(automationId: string, config: Partial<AutomationConfig>): Promise<void> {
205
+ return this.automationOps.updateAutomation(automationId, config);
206
+ }
207
+
208
+ /**
209
+ * Delete an automation
210
+ */
211
+ async deleteAutomation(automationId: string): Promise<void> {
212
+ return this.automationOps.deleteAutomation(automationId);
213
+ }
214
+
215
+ /**
216
+ * Get automation execution trace
217
+ */
218
+ async getAutomationTrace(entityId: string): Promise<unknown[]> {
219
+ return this.automationOps.getAutomationTrace(entityId);
220
+ }
221
+
222
+ // ===== History Operations =====
223
+
224
+ /**
225
+ * Get historical data for an entity
226
+ */
227
+ async getHistory(entityId: string, hours: number = 24): Promise<Entity[][]> {
228
+ return this.historyOps.getHistory(entityId, hours);
229
+ }
230
+
231
+ /**
232
+ * Get system log entries from the logbook
233
+ */
234
+ async getSystemLog(options: {
235
+ hours?: number;
236
+ entity_id?: string;
237
+ } = {}): Promise<LogbookEntry[]> {
238
+ return this.historyOps.getSystemLog(options);
239
+ }
240
+
241
+ // ===== Update Operations =====
242
+
243
+ /**
244
+ * Get available updates by checking update.* entities
245
+ */
246
+ async getAvailableUpdates(): Promise<{
247
+ updates: UpdateInfo[];
248
+ entities: Entity[];
249
+ }> {
250
+ return this.updateOps.getAvailableUpdates();
251
+ }
252
+
253
+ // ===== Config Operations =====
254
+
255
+ /**
256
+ * Get config
257
+ */
258
+ async getConfig(): Promise<Record<string, unknown>> {
259
+ return this.configOps.getConfig();
260
+ }
261
+
262
+ /**
263
+ * Check API (health check)
264
+ */
265
+ async checkApi(): Promise<{ message: string }> {
266
+ return this.configOps.checkApi();
267
+ }
268
+
269
+ /**
270
+ * Get Home Assistant version and configuration info
271
+ */
272
+ async getVersion(): Promise<HaVersion> {
273
+ return this.configOps.getVersion();
274
+ }
275
+ }
276
+
277
+ // Re-export operation classes for testing
278
+ export { RequestHandler } from './request.js';
279
+ export { StateOperations } from './states.js';
280
+ export { ServiceOperations } from './services.js';
281
+ export { EntityOperations } from './entities.js';
282
+ export { AutomationOperations } from './automations.js';
283
+ export { HistoryOperations } from './history.js';
284
+ export { UpdateOperations } from './updates.js';
285
+ export { ConfigOperations } from './config.js';
286
+ export { DeviceOperations } from './devices.js';
287
+ export type { LightControlOptions, ClimateControlOptions, MediaPlayerControlOptions, CoverControlOptions, FanControlOptions } from './devices.js';
@@ -0,0 +1,127 @@
1
+ /**
2
+ * HTTP Request Handler for Home Assistant API
3
+ * Handles authentication and HTTP requests
4
+ */
5
+
6
+ import https from 'https';
7
+ import type { RequestOptions } from '../types/index.js';
8
+ import { AuthenticationError, ApiError } from '../types/index.js';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ export interface RequestHandlerConfig {
12
+ baseUrl: string;
13
+ token: string;
14
+ timeout: number;
15
+ strictSsl: boolean;
16
+ }
17
+
18
+ /**
19
+ * HTTP request handler for Home Assistant API calls.
20
+ */
21
+ export class RequestHandler {
22
+ private httpsAgent?: https.Agent;
23
+ private baseUrl: string;
24
+ private token: string;
25
+ private timeout: number;
26
+
27
+ constructor(config: RequestHandlerConfig) {
28
+ this.baseUrl = config.baseUrl;
29
+ this.token = config.token;
30
+ this.timeout = config.timeout;
31
+
32
+ // Create HTTPS agent with custom SSL settings
33
+ if (!config.strictSsl) {
34
+ this.httpsAgent = new https.Agent({
35
+ rejectUnauthorized: false,
36
+ });
37
+ logger.warn('SSL certificate validation is disabled');
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Make authenticated request to Home Assistant API
43
+ */
44
+ async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
45
+ const url = new URL(`${this.baseUrl}/api${path}`);
46
+
47
+ // Add query parameters
48
+ if (options.params) {
49
+ Object.entries(options.params).forEach(([key, value]) => {
50
+ url.searchParams.append(key, String(value));
51
+ });
52
+ }
53
+
54
+ const headers: Record<string, string> = {
55
+ 'Authorization': `Bearer ${this.token}`,
56
+ 'Content-Type': 'application/json',
57
+ ...options.headers,
58
+ };
59
+
60
+ logger.debug('Making API request', {
61
+ method: options.method ?? 'GET',
62
+ path,
63
+ params: options.params,
64
+ });
65
+
66
+ try {
67
+ const response = await fetch(url.toString(), {
68
+ method: options.method ?? 'GET',
69
+ headers,
70
+ body: options.body ? JSON.stringify(options.body) : undefined,
71
+ // @ts-expect-error - agent is valid but not in types
72
+ agent: this.httpsAgent,
73
+ signal: AbortSignal.timeout(this.timeout),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const errorText = await response.text();
78
+
79
+ if (response.status === 401) {
80
+ throw new AuthenticationError(
81
+ 'Invalid Home Assistant token',
82
+ { status: response.status, body: errorText }
83
+ );
84
+ }
85
+
86
+ throw new ApiError(
87
+ `API request failed: ${response.statusText}`,
88
+ response.status,
89
+ { path, body: errorText }
90
+ );
91
+ }
92
+
93
+ // Handle empty responses
94
+ const contentType = response.headers.get('content-type');
95
+ if (!contentType?.includes('application/json')) {
96
+ return {} as T;
97
+ }
98
+
99
+ const data = await response.json();
100
+ return data as T;
101
+ } catch (error) {
102
+ if (error instanceof AuthenticationError || error instanceof ApiError) {
103
+ throw error;
104
+ }
105
+
106
+ throw new ApiError(
107
+ `Request failed: ${error instanceof Error ? error.message : String(error)}`,
108
+ 500,
109
+ { path }
110
+ );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Make a GET request
116
+ */
117
+ async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
118
+ return this.request<T>(path, { params });
119
+ }
120
+
121
+ /**
122
+ * Make a POST request
123
+ */
124
+ async post<T>(path: string, body?: unknown): Promise<T> {
125
+ return this.request<T>(path, { method: 'POST', body });
126
+ }
127
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Service Operations for Home Assistant API
3
+ * Handles service calls and server management
4
+ */
5
+
6
+ import type { ServiceCallData, ServiceCallResponse } from '../types/index.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import type { RequestHandler } from './request.js';
9
+
10
+ /**
11
+ * Service-related operations for the Home Assistant API.
12
+ */
13
+ export class ServiceOperations {
14
+ constructor(private readonly request: RequestHandler) {}
15
+
16
+ /**
17
+ * Call a service
18
+ */
19
+ async callService(data: ServiceCallData): Promise<ServiceCallResponse> {
20
+ logger.info('Calling service', {
21
+ domain: data.domain,
22
+ service: data.service,
23
+ hasTarget: !!data.target,
24
+ });
25
+
26
+ const path = `/services/${data.domain}/${data.service}`;
27
+ const body = {
28
+ ...data.service_data,
29
+ ...data.target,
30
+ };
31
+
32
+ return await this.request.post<ServiceCallResponse>(path, body);
33
+ }
34
+
35
+ /**
36
+ * Restart Home Assistant
37
+ */
38
+ async restartServer(): Promise<void> {
39
+ logger.warn('Restarting Home Assistant');
40
+
41
+ await this.request.post('/services/homeassistant/restart', {});
42
+
43
+ logger.info('Home Assistant restart initiated');
44
+ }
45
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * State Operations for Home Assistant API
3
+ * Handles entity state retrieval
4
+ */
5
+
6
+ import type { Entity } from '../types/index.js';
7
+ import { ApiError } from '../types/index.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import type { RequestHandler } from './request.js';
10
+
11
+ /**
12
+ * State-related operations for the Home Assistant API.
13
+ */
14
+ export class StateOperations {
15
+ constructor(private readonly request: RequestHandler) {}
16
+
17
+ /**
18
+ * Get all states (entities)
19
+ */
20
+ async getStates(): Promise<Entity[]> {
21
+ logger.debug('Fetching all states');
22
+ const states = await this.request.get<Entity[]>('/states');
23
+ logger.info('Fetched states', { count: states.length });
24
+ return states;
25
+ }
26
+
27
+ /**
28
+ * Get state of a specific entity
29
+ */
30
+ async getState(entityId: string): Promise<Entity | null> {
31
+ logger.debug('Fetching state', { entityId });
32
+
33
+ try {
34
+ const state = await this.request.get<Entity>(`/states/${entityId}`);
35
+ return state;
36
+ } catch (error) {
37
+ if (error instanceof ApiError && error.statusCode === 404) {
38
+ logger.warn('Entity not found', { entityId });
39
+ return null;
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get all sensors (sensor.* and binary_sensor.* entities)
47
+ */
48
+ async getAllSensors(): Promise<Entity[]> {
49
+ logger.debug('Fetching all sensors');
50
+
51
+ const allStates = await this.getStates();
52
+ const sensors = allStates.filter(entity =>
53
+ entity.entity_id.startsWith('sensor.') ||
54
+ entity.entity_id.startsWith('binary_sensor.')
55
+ );
56
+
57
+ logger.info('Fetched sensors', { count: sensors.length });
58
+ return sensors;
59
+ }
60
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Update Operations for Home Assistant API
3
+ * Handles checking for available updates
4
+ */
5
+
6
+ import type { Entity, UpdateInfo } from '../types/index.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import type { EntityOperations } from './entities.js';
9
+
10
+ /**
11
+ * Update-related operations for the Home Assistant API.
12
+ */
13
+ export class UpdateOperations {
14
+ constructor(private readonly entityOps: EntityOperations) {}
15
+
16
+ /**
17
+ * Get available updates by checking update.* entities
18
+ * Works with regular long-lived access tokens
19
+ */
20
+ async getAvailableUpdates(): Promise<{
21
+ updates: UpdateInfo[];
22
+ entities: Entity[];
23
+ }> {
24
+ logger.debug('Fetching available updates from update entities');
25
+
26
+ // Get all update entities
27
+ const updateEntities = await this.entityOps.getEntitiesByDomain('update');
28
+
29
+ // Filter to only those with updates available (state = 'on')
30
+ const updates: UpdateInfo[] = [];
31
+
32
+ for (const entity of updateEntities) {
33
+ const attrs = entity.attributes;
34
+ const installedVersion = attrs.installed_version as string | undefined;
35
+ const latestVersion = attrs.latest_version as string | undefined;
36
+ const title = attrs.title as string || attrs.friendly_name as string || entity.entity_id;
37
+
38
+ // state 'on' means update is available
39
+ if (entity.state === 'on' && installedVersion && latestVersion) {
40
+ updates.push({
41
+ update_type: entity.entity_id.includes('core') ? 'core' :
42
+ entity.entity_id.includes('supervisor') ? 'supervisor' :
43
+ entity.entity_id.includes('operating_system') ? 'os' : 'addon',
44
+ name: title,
45
+ version_current: installedVersion,
46
+ version_latest: latestVersion,
47
+ });
48
+ }
49
+ }
50
+
51
+ logger.info('Fetched available updates', { updateCount: updates.length, totalEntities: updateEntities.length });
52
+
53
+ return {
54
+ updates,
55
+ entities: updateEntities,
56
+ };
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Home Assistant MCP Server
4
+ * Entry point for the Model Context Protocol server
5
+ * Supports multiple transport modes: stdio, HTTP+SSE, HTTP+Stream
6
+ */
7
+
8
+ import { loadConfig } from './config.js';
9
+ import { HaClient } from './haClient/index.js';
10
+ import { LocalAIClient } from './localAI/index.js';
11
+ import { startHttpServer } from './server/http.js';
12
+ import { startStdioServer } from './server/stdio.js';
13
+ import { initLogger, logger } from './utils/logger.js';
14
+
15
+ async function main(): Promise<void> {
16
+ try {
17
+ // Load and validate configuration
18
+ const config = loadConfig();
19
+
20
+ // When running in stdio mode, logs must go to stderr to avoid interfering with MCP protocol on stdout
21
+ const useStderr = !config.useHttp;
22
+
23
+ // Initialize logger with configured level, format, and output stream
24
+ initLogger(config.logLevel, config.logFormat, useStderr);
25
+
26
+ logger.info('Loaded Home Assistant configuration', {
27
+ baseUrl: config.baseUrl,
28
+ hasToken: !!config.token,
29
+ strictSsl: config.strictSsl,
30
+ timeout: config.timeout,
31
+ useHttp: config.useHttp,
32
+ httpTransport: config.useHttp ? config.httpTransport : undefined,
33
+ aiProvider: config.aiProvider,
34
+ aiUrl: config.aiUrl,
35
+ aiModel: config.aiModel,
36
+ });
37
+
38
+ // Create Home Assistant client
39
+ const client = new HaClient(config);
40
+
41
+ // Test Home Assistant connection
42
+ const apiCheck = await client.checkApi();
43
+ logger.info('Connected to Home Assistant', { message: apiCheck.message });
44
+
45
+ // Create Local AI client for AI analysis (unless explicitly disabled)
46
+ let aiClient: LocalAIClient | undefined;
47
+ if (config.aiProvider === 'none') {
48
+ logger.info('AI features disabled (AI_PROVIDER=none)');
49
+ } else {
50
+ aiClient = new LocalAIClient(config);
51
+ const aiHealthy = await aiClient.checkHealth();
52
+ if (aiHealthy) {
53
+ logger.info('Connected to AI provider', {
54
+ provider: aiClient.getProviderName(),
55
+ url: config.aiUrl,
56
+ model: config.aiModel,
57
+ });
58
+ } else {
59
+ logger.warn('AI provider not available - AI analysis tool will be disabled', {
60
+ provider: config.aiProvider,
61
+ url: config.aiUrl,
62
+ });
63
+ aiClient = undefined;
64
+ }
65
+ }
66
+
67
+ // Start server in appropriate mode
68
+ if (config.useHttp) {
69
+ await startHttpServer(client, config, aiClient);
70
+ } else {
71
+ await startStdioServer(client, aiClient);
72
+ }
73
+ } catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ logger.error('Failed to start Home Assistant MCP server', {
76
+ error: message,
77
+ stack: error instanceof Error ? error.stack : undefined,
78
+ });
79
+ process.exitCode = 1;
80
+ }
81
+ }
82
+
83
+ // Handle graceful shutdown
84
+ process.on('SIGINT', () => {
85
+ logger.info('Received SIGINT, shutting down gracefully');
86
+ process.exit(0);
87
+ });
88
+
89
+ process.on('SIGTERM', () => {
90
+ logger.info('Received SIGTERM, shutting down gracefully');
91
+ process.exit(0);
92
+ });
93
+
94
+ // Start the server
95
+ main().catch((error) => {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ logger.error('Unhandled error in main', { error: message });
98
+ process.exitCode = 1;
99
+ });