@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
package/src/config.ts ADDED
@@ -0,0 +1,260 @@
1
+ import { z } from 'zod';
2
+ import { isValidBindAddress, isValidOrigin } from './utils/config-validations.js';
3
+ import { logger } from './utils/logger.js';
4
+ import type { AIProviderType } from './localAI/types.js';
5
+
6
+ const createBooleanStringSchema = (
7
+ defaultValue: boolean
8
+ ): z.ZodEffects<z.ZodOptional<z.ZodUnion<[z.ZodLiteral<'true'>, z.ZodLiteral<'false'>]>>, boolean, 'true' | 'false' | undefined> =>
9
+ z
10
+ .union([z.literal('true'), z.literal('false')])
11
+ .optional()
12
+ .transform((value: 'true' | 'false' | undefined) => {
13
+ if (value === undefined) return defaultValue;
14
+ return value === 'true';
15
+ });
16
+
17
+ const numericStringSchema = z
18
+ .string()
19
+ .optional()
20
+ .transform((value: string | undefined) => (value ? Number.parseInt(value, 10) : undefined))
21
+ .pipe(z.number().positive().optional());
22
+
23
+ const listStringSchema = z
24
+ .string()
25
+ .optional()
26
+ .transform((value: string | undefined) =>
27
+ value
28
+ ? value
29
+ .split(',')
30
+ .map((s: string) => s.trim())
31
+ .filter(Boolean)
32
+ : undefined
33
+ );
34
+
35
+ const envSchema = z
36
+ .object({
37
+ // Home Assistant Client Configuration
38
+ haUrl: z.string().url({ message: 'HA_URL must be a valid URL' }),
39
+ haToken: z.string().min(1, 'HA_TOKEN is required'),
40
+ haStrictSsl: createBooleanStringSchema(true),
41
+ haTimeout: numericStringSchema,
42
+
43
+ // AI Provider Configuration (use 'none' to disable AI features)
44
+ aiProvider: z.enum(['ollama', 'openai', 'none']).optional(),
45
+ aiUrl: z.string().url().optional(),
46
+ aiModel: z.string().optional(),
47
+ aiTimeout: numericStringSchema,
48
+ aiApiKey: z.string().optional(),
49
+
50
+ // MCP Generic Server Configuration
51
+ logLevel: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
52
+ logFormat: z.enum(['plain', 'json', 'gcp-json']).optional().default('plain'),
53
+ useHttp: createBooleanStringSchema(false),
54
+ stateful: createBooleanStringSchema(false),
55
+
56
+ // MCP Server HTTP Configuration
57
+ httpPort: numericStringSchema,
58
+ httpTransport: z.enum(['stream', 'sse']).optional().default('stream'),
59
+ httpBindAddr: z.string().optional(),
60
+ httpPath: z.string().optional(),
61
+ httpEnableHealthcheck: createBooleanStringSchema(true),
62
+ httpHealthcheckPath: z.string().optional(),
63
+ httpAllowCors: createBooleanStringSchema(true),
64
+ httpAllowedOrigins: listStringSchema,
65
+
66
+ // SSE Event Subscription Configuration
67
+ sseEventsEnabled: createBooleanStringSchema(true),
68
+ sseEventsPath: z.string().optional(),
69
+
70
+ // Rate Limiting Configuration
71
+ rateLimitEnabled: createBooleanStringSchema(true),
72
+ rateLimitWindowMs: numericStringSchema,
73
+ rateLimitMaxRequests: numericStringSchema,
74
+ })
75
+ .refine(
76
+ (data) => {
77
+ // Validate httpBindAddr if provided
78
+ if (data.httpBindAddr && !isValidBindAddress(data.httpBindAddr)) {
79
+ return false;
80
+ }
81
+ return true;
82
+ },
83
+ {
84
+ message: 'MCP_HTTP_BIND_ADDR must be a valid IPv4 or IPv6 address',
85
+ path: ['httpBindAddr'],
86
+ }
87
+ )
88
+ .refine(
89
+ (data) => {
90
+ // Validate httpAllowedOrigins if provided
91
+ if (data.httpAllowedOrigins) {
92
+ for (const origin of data.httpAllowedOrigins) {
93
+ if (!isValidOrigin(origin)) {
94
+ return false;
95
+ }
96
+ }
97
+ }
98
+ return true;
99
+ },
100
+ (data) => {
101
+ const invalidOrigin = data.httpAllowedOrigins?.find((origin) => !isValidOrigin(origin));
102
+ return {
103
+ message: `MCP_HTTP_ALLOWED_ORIGINS contains invalid origin: ${invalidOrigin}`,
104
+ path: ['httpAllowedOrigins'],
105
+ };
106
+ }
107
+ );
108
+
109
+ export interface EnvironmentConfig {
110
+ // Home Assistant Client Configuration
111
+ baseUrl: string;
112
+ token: string;
113
+ strictSsl: boolean;
114
+ timeout: number;
115
+
116
+ // AI Provider Configuration
117
+ aiProvider: AIProviderType;
118
+ aiUrl: string;
119
+ aiModel: string;
120
+ aiTimeout: number;
121
+ aiApiKey?: string;
122
+
123
+ // MCP Generic Server Configuration
124
+ logLevel: 'debug' | 'info' | 'warn' | 'error';
125
+ logFormat: 'plain' | 'json' | 'gcp-json';
126
+ useHttp: boolean;
127
+ stateful: boolean;
128
+
129
+ // MCP Server HTTP Configuration
130
+ httpPort?: number;
131
+ httpTransport: 'stream' | 'sse';
132
+ httpBindAddr?: string;
133
+ httpPath?: string;
134
+ httpEnableHealthcheck: boolean;
135
+ httpHealthcheckPath?: string;
136
+ httpAllowCors: boolean;
137
+ httpAllowedOrigins?: string[];
138
+
139
+ // SSE Event Subscription Configuration
140
+ sseEventsEnabled: boolean;
141
+ sseEventsPath: string;
142
+
143
+ // Rate Limiting Configuration
144
+ rateLimitEnabled: boolean;
145
+ rateLimitWindowMs: number;
146
+ rateLimitMaxRequests: number;
147
+ }
148
+
149
+ export function loadConfig(env: NodeJS.ProcessEnv = process.env): EnvironmentConfig {
150
+ const parsed = envSchema.safeParse({
151
+ // Home Assistant Client Configuration
152
+ haUrl: env.HA_URL,
153
+ haToken: env.HA_TOKEN,
154
+ haStrictSsl: env.HA_STRICT_SSL,
155
+ haTimeout: env.HA_TIMEOUT,
156
+
157
+ // AI Provider Configuration
158
+ aiProvider: env.AI_PROVIDER,
159
+ aiUrl: env.AI_URL,
160
+ aiModel: env.AI_MODEL,
161
+ aiTimeout: env.AI_TIMEOUT,
162
+ aiApiKey: env.AI_API_KEY,
163
+
164
+ // MCP Generic Server Configuration
165
+ logLevel: env.MCP_SERVER_LOG_LEVEL,
166
+ logFormat: env.MCP_SERVER_LOG_FORMAT,
167
+ useHttp: env.MCP_SERVER_USE_HTTP,
168
+ stateful: env.MCP_SERVER_STATEFUL,
169
+
170
+ // MCP Server HTTP Configuration
171
+ httpPort: env.MCP_HTTP_PORT,
172
+ httpTransport: env.MCP_HTTP_TRANSPORT,
173
+ httpBindAddr: env.MCP_HTTP_BIND_ADDR,
174
+ httpPath: env.MCP_HTTP_PATH,
175
+ httpEnableHealthcheck: env.MCP_HTTP_ENABLE_HEALTHCHECK,
176
+ httpHealthcheckPath: env.MCP_HTTP_HEALTHCHECK_PATH,
177
+ httpAllowCors: env.MCP_HTTP_ALLOW_CORS,
178
+ httpAllowedOrigins: env.MCP_HTTP_ALLOWED_ORIGINS,
179
+
180
+ // SSE Event Subscription Configuration
181
+ sseEventsEnabled: env.MCP_SSE_EVENTS_ENABLED,
182
+ sseEventsPath: env.MCP_SSE_EVENTS_PATH,
183
+
184
+ // Rate Limiting Configuration
185
+ rateLimitEnabled: env.MCP_RATE_LIMIT_ENABLED,
186
+ rateLimitWindowMs: env.MCP_RATE_LIMIT_WINDOW_MS,
187
+ rateLimitMaxRequests: env.MCP_RATE_LIMIT_MAX_REQUESTS,
188
+ });
189
+
190
+ if (!parsed.success) {
191
+ const messages = parsed.error.issues.map((issue: z.ZodIssue) => issue.message);
192
+ throw new Error(`Invalid environment configuration:\n${messages.join('\n')}`);
193
+ }
194
+
195
+ // Determine default httpPath based on transport if not explicitly set
196
+ const defaultPath = parsed.data.httpTransport === 'sse' ? '/sse' : '/mcp';
197
+ const httpPath = parsed.data.httpPath ?? defaultPath;
198
+
199
+ // Set default bind address and allowed origins for security
200
+ const httpBindAddr = parsed.data.httpBindAddr ?? '127.0.0.1';
201
+ let httpAllowedOrigins = parsed.data.httpAllowedOrigins ?? ['127.0.0.1', 'localhost'];
202
+
203
+ // If wildcard is present, use empty array to disable SDK origin validation
204
+ if (httpAllowedOrigins.includes('*')) {
205
+ logger.warn('Wildcard (*) origin allowed - origin validation is disabled. This should only be used in development.');
206
+ httpAllowedOrigins = [];
207
+ }
208
+
209
+ return {
210
+ // Home Assistant Client Configuration
211
+ baseUrl: parsed.data.haUrl.replace(/\/$/, ''),
212
+ token: parsed.data.haToken,
213
+ strictSsl: parsed.data.haStrictSsl,
214
+ timeout: parsed.data.haTimeout ?? 30000,
215
+
216
+ // AI Provider Configuration
217
+ aiProvider: (parsed.data.aiProvider ?? 'ollama') as AIProviderType,
218
+ aiUrl: parsed.data.aiUrl ?? 'http://localhost:11434',
219
+ aiModel: parsed.data.aiModel ?? 'qwen3:14b',
220
+ aiTimeout: parsed.data.aiTimeout ?? 60000,
221
+ aiApiKey: parsed.data.aiApiKey,
222
+
223
+ // MCP Generic Server Configuration
224
+ logLevel: parsed.data.logLevel,
225
+ logFormat: parsed.data.logFormat,
226
+ useHttp: parsed.data.useHttp,
227
+ stateful: parsed.data.stateful,
228
+
229
+ // MCP Server HTTP Configuration
230
+ httpPort: parsed.data.httpPort,
231
+ httpTransport: parsed.data.httpTransport,
232
+ httpBindAddr,
233
+ httpPath,
234
+ httpEnableHealthcheck: parsed.data.httpEnableHealthcheck,
235
+ httpHealthcheckPath: parsed.data.httpHealthcheckPath,
236
+ httpAllowCors: parsed.data.httpAllowCors,
237
+ httpAllowedOrigins,
238
+
239
+ // SSE Event Subscription Configuration
240
+ sseEventsEnabled: parsed.data.sseEventsEnabled,
241
+ sseEventsPath: parsed.data.sseEventsPath ?? '/subscribe_events',
242
+
243
+ // Rate Limiting Configuration
244
+ rateLimitEnabled: parsed.data.rateLimitEnabled,
245
+ rateLimitWindowMs: parsed.data.rateLimitWindowMs ?? 60000,
246
+ rateLimitMaxRequests: parsed.data.rateLimitMaxRequests ?? 100,
247
+ };
248
+ }
249
+
250
+ // Legacy compatibility functions
251
+ export function validateConfig(config: EnvironmentConfig): void {
252
+ // Config is already validated by Zod, this is for compatibility
253
+ if (!config.baseUrl || !config.token) {
254
+ throw new Error('Invalid configuration: missing baseUrl or token');
255
+ }
256
+ }
257
+
258
+ export function loadConfigFromEnv(): EnvironmentConfig {
259
+ return loadConfig();
260
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Automation Operations for Home Assistant API
3
+ * Handles automation listing and management
4
+ */
5
+
6
+ import type { Automation, AutomationConfig, ServiceCallResponse } from '../types/index.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import type { StateOperations } from './states.js';
9
+ import type { ServiceOperations } from './services.js';
10
+ import type { RequestHandler } from './request.js';
11
+
12
+ /**
13
+ * Automation-related operations for the Home Assistant API.
14
+ */
15
+ export class AutomationOperations {
16
+ constructor(
17
+ private readonly stateOps: StateOperations,
18
+ private readonly serviceOps?: ServiceOperations,
19
+ private readonly request?: RequestHandler
20
+ ) {}
21
+
22
+ /**
23
+ * Get all automations
24
+ */
25
+ async getAutomations(): Promise<Automation[]> {
26
+ logger.debug('Fetching automations');
27
+
28
+ const allStates = await this.stateOps.getStates();
29
+ const automations = allStates
30
+ .filter(entity => entity.entity_id.startsWith('automation.'))
31
+ .map(entity => ({
32
+ id: entity.entity_id,
33
+ alias: String(entity.attributes.friendly_name || entity.entity_id.replace('automation.', '')),
34
+ state: entity.state as 'on' | 'off',
35
+ last_triggered: entity.attributes.last_triggered as string | null,
36
+ description: entity.attributes.description as string | undefined,
37
+ mode: entity.attributes.mode as Automation['mode'],
38
+ }));
39
+
40
+ logger.info('Fetched automations', { count: automations.length });
41
+ return automations;
42
+ }
43
+
44
+ /**
45
+ * Get a single automation by entity_id
46
+ */
47
+ async getAutomation(entityId: string): Promise<Automation | null> {
48
+ logger.debug('Fetching automation', { entityId });
49
+
50
+ const state = await this.stateOps.getState(entityId);
51
+ if (!state || !entityId.startsWith('automation.')) {
52
+ return null;
53
+ }
54
+
55
+ return {
56
+ id: state.entity_id,
57
+ alias: String(state.attributes.friendly_name || state.entity_id.replace('automation.', '')),
58
+ state: state.state as 'on' | 'off',
59
+ last_triggered: state.attributes.last_triggered as string | null,
60
+ description: state.attributes.description as string | undefined,
61
+ mode: state.attributes.mode as Automation['mode'],
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Trigger an automation manually
67
+ */
68
+ async triggerAutomation(entityId: string, variables?: Record<string, unknown>): Promise<ServiceCallResponse> {
69
+ if (!this.serviceOps) {
70
+ throw new Error('ServiceOperations not available');
71
+ }
72
+
73
+ logger.info('Triggering automation', { entityId, variables });
74
+
75
+ return this.serviceOps.callService({
76
+ domain: 'automation',
77
+ service: 'trigger',
78
+ target: { entity_id: entityId },
79
+ service_data: variables ? { variables } : undefined,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Enable an automation
85
+ */
86
+ async enableAutomation(entityId: string): Promise<ServiceCallResponse> {
87
+ if (!this.serviceOps) {
88
+ throw new Error('ServiceOperations not available');
89
+ }
90
+
91
+ logger.info('Enabling automation', { entityId });
92
+
93
+ return this.serviceOps.callService({
94
+ domain: 'automation',
95
+ service: 'turn_on',
96
+ target: { entity_id: entityId },
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Disable an automation
102
+ */
103
+ async disableAutomation(entityId: string): Promise<ServiceCallResponse> {
104
+ if (!this.serviceOps) {
105
+ throw new Error('ServiceOperations not available');
106
+ }
107
+
108
+ logger.info('Disabling automation', { entityId });
109
+
110
+ return this.serviceOps.callService({
111
+ domain: 'automation',
112
+ service: 'turn_off',
113
+ target: { entity_id: entityId },
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Toggle an automation (enable if disabled, disable if enabled)
119
+ */
120
+ async toggleAutomation(entityId: string): Promise<ServiceCallResponse> {
121
+ if (!this.serviceOps) {
122
+ throw new Error('ServiceOperations not available');
123
+ }
124
+
125
+ logger.info('Toggling automation', { entityId });
126
+
127
+ return this.serviceOps.callService({
128
+ domain: 'automation',
129
+ service: 'toggle',
130
+ target: { entity_id: entityId },
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Reload all automations from configuration
136
+ */
137
+ async reloadAutomations(): Promise<void> {
138
+ if (!this.serviceOps) {
139
+ throw new Error('ServiceOperations not available');
140
+ }
141
+
142
+ logger.info('Reloading automations');
143
+
144
+ await this.serviceOps.callService({
145
+ domain: 'automation',
146
+ service: 'reload',
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Create a new automation via the config API
152
+ * Note: Requires Home Assistant 2023.7+ with config flow support
153
+ */
154
+ async createAutomation(config: AutomationConfig): Promise<{ id: string }> {
155
+ if (!this.request) {
156
+ throw new Error('RequestHandler not available');
157
+ }
158
+
159
+ logger.info('Creating automation', { alias: config.alias });
160
+
161
+ // Use the config/automation/config endpoint to create automations
162
+ const result = await this.request.post<{ result: string }>('/config/automation/config', {
163
+ alias: config.alias,
164
+ description: config.description,
165
+ mode: config.mode ?? 'single',
166
+ trigger: config.trigger,
167
+ condition: config.condition,
168
+ action: config.action,
169
+ });
170
+
171
+ return { id: result.result };
172
+ }
173
+
174
+ /**
175
+ * Update an existing automation
176
+ * Note: Requires the automation's unique_id from the automation registry
177
+ */
178
+ async updateAutomation(automationId: string, config: Partial<AutomationConfig>): Promise<void> {
179
+ if (!this.request) {
180
+ throw new Error('RequestHandler not available');
181
+ }
182
+
183
+ logger.info('Updating automation', { automationId, alias: config.alias });
184
+
185
+ await this.request.post(`/config/automation/config/${automationId}`, config);
186
+ }
187
+
188
+ /**
189
+ * Delete an automation
190
+ * Note: Only works for automations created via the UI/API
191
+ */
192
+ async deleteAutomation(automationId: string): Promise<void> {
193
+ if (!this.request) {
194
+ throw new Error('RequestHandler not available');
195
+ }
196
+
197
+ logger.info('Deleting automation', { automationId });
198
+
199
+ await this.request.request(`/config/automation/config/${automationId}`, {
200
+ method: 'DELETE',
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Get automation trace (execution history)
206
+ */
207
+ async getAutomationTrace(entityId: string): Promise<unknown[]> {
208
+ if (!this.request) {
209
+ throw new Error('RequestHandler not available');
210
+ }
211
+
212
+ const automationId = entityId.replace('automation.', '');
213
+ logger.debug('Fetching automation trace', { automationId });
214
+
215
+ return this.request.get<unknown[]>(`/trace/automation/${automationId}`);
216
+ }
217
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Config Operations for Home Assistant API
3
+ * Handles configuration and version information
4
+ */
5
+
6
+ import type { HaVersion } from '../types/index.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import type { RequestHandler } from './request.js';
9
+
10
+ /**
11
+ * Config-related operations for the Home Assistant API.
12
+ */
13
+ export class ConfigOperations {
14
+ constructor(private readonly request: RequestHandler) {}
15
+
16
+ /**
17
+ * Get config
18
+ */
19
+ async getConfig(): Promise<Record<string, unknown>> {
20
+ logger.debug('Fetching config');
21
+ return await this.request.get<Record<string, unknown>>('/config');
22
+ }
23
+
24
+ /**
25
+ * Check API (health check)
26
+ */
27
+ async checkApi(): Promise<{ message: string }> {
28
+ logger.debug('Checking API health');
29
+ return await this.request.get<{ message: string }>('/');
30
+ }
31
+
32
+ /**
33
+ * Get Home Assistant version and configuration info
34
+ */
35
+ async getVersion(): Promise<HaVersion> {
36
+ logger.debug('Fetching Home Assistant version');
37
+ const config = await this.request.get<HaVersion>('/config');
38
+ logger.info('Fetched version', { version: config.version });
39
+ return config;
40
+ }
41
+ }