@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.
- package/.dockerignore +8 -0
- package/.env.example +149 -0
- package/.gitattributes +30 -0
- package/Dockerfile +33 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/haClient/automations.d.ts +67 -0
- package/dist/haClient/automations.d.ts.map +1 -0
- package/dist/haClient/automations.js +181 -0
- package/dist/haClient/automations.js.map +1 -0
- package/dist/haClient/config.d.ts +28 -0
- package/dist/haClient/config.d.ts.map +1 -0
- package/dist/haClient/config.js +38 -0
- package/dist/haClient/config.js.map +1 -0
- package/dist/haClient/devices.d.ts +219 -0
- package/dist/haClient/devices.d.ts.map +1 -0
- package/dist/haClient/devices.js +506 -0
- package/dist/haClient/devices.js.map +1 -0
- package/dist/haClient/entities.d.ts +35 -0
- package/dist/haClient/entities.d.ts.map +1 -0
- package/dist/haClient/entities.js +97 -0
- package/dist/haClient/entities.js.map +1 -0
- package/dist/haClient/events.d.ts +138 -0
- package/dist/haClient/events.d.ts.map +1 -0
- package/dist/haClient/events.js +344 -0
- package/dist/haClient/events.js.map +1 -0
- package/dist/haClient/history.d.ts +26 -0
- package/dist/haClient/history.d.ts.map +1 -0
- package/dist/haClient/history.js +54 -0
- package/dist/haClient/history.js.map +1 -0
- package/dist/haClient/index.d.ts +153 -0
- package/dist/haClient/index.d.ts.map +1 -0
- package/dist/haClient/index.js +223 -0
- package/dist/haClient/index.js.map +1 -0
- package/dist/haClient/request.d.ts +34 -0
- package/dist/haClient/request.d.ts.map +1 -0
- package/dist/haClient/request.js +93 -0
- package/dist/haClient/request.js.map +1 -0
- package/dist/haClient/services.d.ts +22 -0
- package/dist/haClient/services.d.ts.map +1 -0
- package/dist/haClient/services.js +39 -0
- package/dist/haClient/services.js.map +1 -0
- package/dist/haClient/states.d.ts +26 -0
- package/dist/haClient/states.d.ts.map +1 -0
- package/dist/haClient/states.js +53 -0
- package/dist/haClient/states.js.map +1 -0
- package/dist/haClient/updates.d.ts +22 -0
- package/dist/haClient/updates.d.ts.map +1 -0
- package/dist/haClient/updates.js +48 -0
- package/dist/haClient/updates.js.map +1 -0
- package/dist/haClient.d.ts +97 -0
- package/dist/haClient.d.ts.map +1 -0
- package/dist/haClient.js +344 -0
- package/dist/haClient.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/localAI/index.d.ts +37 -0
- package/dist/localAI/index.d.ts.map +1 -0
- package/dist/localAI/index.js +73 -0
- package/dist/localAI/index.js.map +1 -0
- package/dist/localAI/prompts.d.ts +20 -0
- package/dist/localAI/prompts.d.ts.map +1 -0
- package/dist/localAI/prompts.js +48 -0
- package/dist/localAI/prompts.js.map +1 -0
- package/dist/localAI/providers/base.d.ts +51 -0
- package/dist/localAI/providers/base.d.ts.map +1 -0
- package/dist/localAI/providers/base.js +80 -0
- package/dist/localAI/providers/base.js.map +1 -0
- package/dist/localAI/providers/ollama.d.ts +22 -0
- package/dist/localAI/providers/ollama.d.ts.map +1 -0
- package/dist/localAI/providers/ollama.js +63 -0
- package/dist/localAI/providers/ollama.js.map +1 -0
- package/dist/localAI/providers/openai.d.ts +23 -0
- package/dist/localAI/providers/openai.d.ts.map +1 -0
- package/dist/localAI/providers/openai.js +63 -0
- package/dist/localAI/providers/openai.js.map +1 -0
- package/dist/localAI/types.d.ts +61 -0
- package/dist/localAI/types.d.ts.map +1 -0
- package/dist/localAI/types.js +6 -0
- package/dist/localAI/types.js.map +1 -0
- package/dist/ollamaClient.d.ts +37 -0
- package/dist/ollamaClient.d.ts.map +1 -0
- package/dist/ollamaClient.js +110 -0
- package/dist/ollamaClient.js.map +1 -0
- package/dist/resources/index.d.ts +11 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +238 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/common.d.ts +9 -0
- package/dist/server/common.d.ts.map +1 -0
- package/dist/server/common.js +27 -0
- package/dist/server/common.js.map +1 -0
- package/dist/server/eventSubscription.d.ts +53 -0
- package/dist/server/eventSubscription.d.ts.map +1 -0
- package/dist/server/eventSubscription.js +257 -0
- package/dist/server/eventSubscription.js.map +1 -0
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +491 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/rateLimiter.d.ts +70 -0
- package/dist/server/rateLimiter.d.ts.map +1 -0
- package/dist/server/rateLimiter.js +156 -0
- package/dist/server/rateLimiter.js.map +1 -0
- package/dist/server/sse.d.ts +32 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +76 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/stdio.d.ts +8 -0
- package/dist/server/stdio.d.ts.map +1 -0
- package/dist/server/stdio.js +16 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/server/stream.d.ts +26 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/stream.js +81 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/callService.d.ts +8 -0
- package/dist/tools/callService.d.ts.map +1 -0
- package/dist/tools/callService.js +100 -0
- package/dist/tools/callService.js.map +1 -0
- package/dist/tools/getClient.d.ts +8 -0
- package/dist/tools/getClient.d.ts.map +1 -0
- package/dist/tools/getClient.js +94 -0
- package/dist/tools/getClient.js.map +1 -0
- package/dist/tools/getDevice.d.ts +8 -0
- package/dist/tools/getDevice.d.ts.map +1 -0
- package/dist/tools/getDevice.js +94 -0
- package/dist/tools/getDevice.js.map +1 -0
- package/dist/tools/getEntitiesByDomain.d.ts +8 -0
- package/dist/tools/getEntitiesByDomain.d.ts.map +1 -0
- package/dist/tools/getEntitiesByDomain.js +80 -0
- package/dist/tools/getEntitiesByDomain.js.map +1 -0
- package/dist/tools/getState.d.ts +8 -0
- package/dist/tools/getState.d.ts.map +1 -0
- package/dist/tools/getState.js +90 -0
- package/dist/tools/getState.js.map +1 -0
- package/dist/tools/getStates.d.ts +8 -0
- package/dist/tools/getStates.d.ts.map +1 -0
- package/dist/tools/getStates.js +70 -0
- package/dist/tools/getStates.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listClients.d.ts +8 -0
- package/dist/tools/listClients.d.ts.map +1 -0
- package/dist/tools/listClients.js +73 -0
- package/dist/tools/listClients.js.map +1 -0
- package/dist/tools/listDevices.d.ts +8 -0
- package/dist/tools/listDevices.d.ts.map +1 -0
- package/dist/tools/listDevices.js +73 -0
- package/dist/tools/listDevices.js.map +1 -0
- package/dist/tools/listSites.d.ts +8 -0
- package/dist/tools/listSites.d.ts.map +1 -0
- package/dist/tools/listSites.js +67 -0
- package/dist/tools/listSites.js.map +1 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +1382 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/searchDevices.d.ts +8 -0
- package/dist/tools/searchDevices.d.ts.map +1 -0
- package/dist/tools/searchDevices.js +84 -0
- package/dist/tools/searchDevices.js.map +1 -0
- package/dist/tools/searchEntities.d.ts +8 -0
- package/dist/tools/searchEntities.d.ts.map +1 -0
- package/dist/tools/searchEntities.js +80 -0
- package/dist/tools/searchEntities.js.map +1 -0
- package/dist/types/entity.d.ts +98 -0
- package/dist/types/entity.d.ts.map +1 -0
- package/dist/types/entity.js +2 -0
- package/dist/types/entity.js.map +1 -0
- package/dist/types/errors.d.ts +16 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +32 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/haConfig.d.ts +8 -0
- package/dist/types/haConfig.d.ts.map +1 -0
- package/dist/types/haConfig.js +2 -0
- package/dist/types/haConfig.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/request.d.ts +7 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/serviceCall.d.ts +18 -0
- package/dist/types/serviceCall.d.ts.map +1 -0
- package/dist/types/serviceCall.js +2 -0
- package/dist/types/serviceCall.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config-validations.d.ts +12 -0
- package/dist/utils/config-validations.d.ts.map +1 -0
- package/dist/utils/config-validations.js +43 -0
- package/dist/utils/config-validations.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +94 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/wrapper.d.ts +8 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +301 -0
- package/dist/wrapper.js.map +1 -0
- package/docker-compose.yml +17 -0
- package/docs/CHANGELOG.md +86 -0
- package/docs/LOCAL_CLIENT.md +268 -0
- package/docs/SSE_API.md +341 -0
- package/eslint.config.js +33 -0
- package/n8n/ai-agent-safety-monitor.json +355 -0
- package/openapi.json +166 -0
- package/package.json +51 -0
- package/src/config.ts +260 -0
- package/src/haClient/automations.ts +217 -0
- package/src/haClient/config.ts +41 -0
- package/src/haClient/devices.ts +653 -0
- package/src/haClient/entities.ts +128 -0
- package/src/haClient/events.ts +456 -0
- package/src/haClient/history.ts +68 -0
- package/src/haClient/index.ts +287 -0
- package/src/haClient/request.ts +127 -0
- package/src/haClient/services.ts +45 -0
- package/src/haClient/states.ts +60 -0
- package/src/haClient/updates.ts +58 -0
- package/src/index.ts +99 -0
- package/src/localAI/index.ts +88 -0
- package/src/localAI/prompts.ts +50 -0
- package/src/localAI/providers/base.ts +120 -0
- package/src/localAI/providers/ollama.ts +78 -0
- package/src/localAI/providers/openai.ts +86 -0
- package/src/localAI/types.ts +67 -0
- package/src/resources/index.ts +266 -0
- package/src/server/common.ts +38 -0
- package/src/server/eventSubscription.ts +318 -0
- package/src/server/http.ts +546 -0
- package/src/server/rateLimiter.ts +195 -0
- package/src/server/sse.ts +120 -0
- package/src/server/stdio.ts +22 -0
- package/src/server/stream.ts +113 -0
- package/src/tools/callService.ts +114 -0
- package/src/tools/getEntitiesByDomain.ts +92 -0
- package/src/tools/getState.ts +103 -0
- package/src/tools/getStates.ts +80 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/registry.ts +1521 -0
- package/src/tools/searchEntities.ts +92 -0
- package/src/types/entity.ts +110 -0
- package/src/types/errors.ts +33 -0
- package/src/types/haConfig.ts +7 -0
- package/src/types/index.ts +5 -0
- package/src/types/request.ts +6 -0
- package/src/types/serviceCall.ts +18 -0
- package/src/types.ts +2 -0
- package/src/utils/config-validations.ts +46 -0
- package/src/utils/logger.ts +114 -0
- package/src/wrapper.ts +340 -0
- package/tests/config.test.ts +332 -0
- package/tests/haClient/automations.test.ts +133 -0
- package/tests/haClient/config.test.ts +96 -0
- package/tests/haClient/entities.test.ts +188 -0
- package/tests/haClient/history.test.ts +170 -0
- package/tests/haClient/index.test.ts +221 -0
- package/tests/haClient/request.test.ts +217 -0
- package/tests/haClient/services.test.ts +120 -0
- package/tests/haClient/states.test.ts +119 -0
- package/tests/haClient/updates.test.ts +193 -0
- package/tests/index.test.ts +164 -0
- package/tests/localAI/index.test.ts +168 -0
- package/tests/localAI/ollama.test.ts +187 -0
- package/tests/localAI/openai.test.ts +257 -0
- package/tests/server/common.test.ts +64 -0
- package/tests/server/http.test.ts +326 -0
- package/tests/server/sse.test.ts +240 -0
- package/tests/server/stdio.test.ts +102 -0
- package/tests/server/stream.test.ts +287 -0
- package/tests/tools/registry.test.ts +355 -0
- package/tests/utils/config-validations.test.ts +289 -0
- package/tests/utils/logger.test.ts +327 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +26 -0
package/src/wrapper.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HTTP Wrapper for Home Assistant MCP Server
|
|
4
|
+
* Provides REST API endpoints that call MCP tools under the hood
|
|
5
|
+
* Compatible with legacy mcp-wrapper.js behavior
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Use relative path from wrapper location
|
|
18
|
+
const MCP_PATH = path.join(__dirname, 'index.js');
|
|
19
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
20
|
+
|
|
21
|
+
// ====== FAIL EARLY VALIDATION ======
|
|
22
|
+
function validateConfig(): string[] {
|
|
23
|
+
const errors: string[] = [];
|
|
24
|
+
|
|
25
|
+
if (!process.env.HA_URL) errors.push('ERROR: HA_URL environment variable not set');
|
|
26
|
+
if (!process.env.HA_TOKEN) errors.push('ERROR: HA_TOKEN environment variable not set');
|
|
27
|
+
|
|
28
|
+
if (process.env.HA_URL && !process.env.HA_URL.startsWith('http')) {
|
|
29
|
+
errors.push('ERROR: HA_URL must start with http:// or https://');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!existsSync(MCP_PATH)) errors.push(`ERROR: MCP script not found at: ${MCP_PATH}`);
|
|
33
|
+
|
|
34
|
+
const nodeVersion = process.versions.node.split('.')[0];
|
|
35
|
+
if (parseInt(nodeVersion) < 18) {
|
|
36
|
+
errors.push(`ERROR: Node.js 18+ required, found: ${process.versions.node}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return errors;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configErrors = validateConfig();
|
|
43
|
+
if (configErrors.length > 0) {
|
|
44
|
+
console.error('\nConfiguration Errors:\n');
|
|
45
|
+
configErrors.forEach(err => console.error(err));
|
|
46
|
+
console.error('\nFix these issues before starting:');
|
|
47
|
+
console.error('export HA_URL=http://homeassistant.10.0.0.19.nip.io:8123');
|
|
48
|
+
console.error('export HA_TOKEN=your_long_lived_access_token\n');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ====== CORS ======
|
|
53
|
+
const ALLOWED_ORIGINS = new Set([
|
|
54
|
+
"http://127.0.0.1:8282",
|
|
55
|
+
"http://localhost:8282"
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function setCors(req: IncomingMessage, res: ServerResponse): void {
|
|
59
|
+
const origin = req.headers.origin;
|
|
60
|
+
|
|
61
|
+
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
62
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
63
|
+
res.setHeader("Vary", "Origin");
|
|
64
|
+
} else {
|
|
65
|
+
// LAN/dev friendly. Tighten if you expose this publicly.
|
|
66
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
70
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
71
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ====== HELPERS ======
|
|
75
|
+
interface McpResult {
|
|
76
|
+
result?: {
|
|
77
|
+
content?: Array<{ text?: string }>;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractText(result: McpResult): string | undefined {
|
|
82
|
+
return result?.result?.content?.[0]?.text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function tryParseJson(text: string | undefined): any {
|
|
86
|
+
if (typeof text !== 'string') return null;
|
|
87
|
+
const t = text.trim();
|
|
88
|
+
if (!t) return null;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(t);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sendJson(res: ServerResponse, statusCode: number, obj: any): void {
|
|
97
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify(obj));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readBody(req: IncomingMessage): Promise<any> {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
let body = '';
|
|
104
|
+
req.on('data', chunk => body += chunk);
|
|
105
|
+
req.on('end', () => {
|
|
106
|
+
if (!body.trim()) return resolve(null);
|
|
107
|
+
try {
|
|
108
|
+
resolve(JSON.parse(body));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
reject(new Error(`Invalid JSON body: ${(e as Error).message}`));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// This runs the MCP stdio script once per request
|
|
117
|
+
function runMcpWithJsonInput(jsonObj: any): Promise<any> {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const mcp = spawn('node', [MCP_PATH], {
|
|
120
|
+
env: {
|
|
121
|
+
...process.env,
|
|
122
|
+
HA_URL: process.env.HA_URL!,
|
|
123
|
+
HA_TOKEN: process.env.HA_TOKEN!,
|
|
124
|
+
},
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let mcpOutput = '';
|
|
129
|
+
let mcpError = '';
|
|
130
|
+
let headerSeen = false;
|
|
131
|
+
|
|
132
|
+
mcp.stdout.on('data', (data) => {
|
|
133
|
+
const str = data.toString();
|
|
134
|
+
if (str.includes('Home Assistant MCP server running') || str.includes('MCP server connected and ready')) {
|
|
135
|
+
headerSeen = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (headerSeen) mcpOutput += str;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
mcp.stderr.on('data', (data) => {
|
|
142
|
+
const str = data.toString();
|
|
143
|
+
if (!str.includes('Home Assistant MCP server running') && !str.includes('MCP server connected and ready')) {
|
|
144
|
+
mcpError += str;
|
|
145
|
+
} else {
|
|
146
|
+
headerSeen = true;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
mcp.stdin.write(JSON.stringify(jsonObj) + '\n');
|
|
151
|
+
mcp.stdin.end();
|
|
152
|
+
|
|
153
|
+
const timeout = setTimeout(() => {
|
|
154
|
+
mcp.kill();
|
|
155
|
+
reject(new Error('MCP timeout after 30s'));
|
|
156
|
+
}, 30000);
|
|
157
|
+
|
|
158
|
+
mcp.on('close', (code) => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
|
|
161
|
+
if (!mcpOutput.trim()) {
|
|
162
|
+
reject(new Error(`No output from MCP (exit=${code}). stderr=${mcpError}`));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(mcpOutput.trim());
|
|
168
|
+
resolve(parsed);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
reject(new Error(`Invalid JSON from MCP: ${(e as Error).message}. raw=${mcpOutput}`));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
mcp.on('error', (err) => {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
reject(err);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function callMcpTool(name: string, argumentsObj?: any): Promise<any> {
|
|
182
|
+
const request = {
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
id: 1,
|
|
185
|
+
method: "tools/call",
|
|
186
|
+
params: { name, arguments: argumentsObj ?? {} }
|
|
187
|
+
};
|
|
188
|
+
return runMcpWithJsonInput(request);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ====== SERVER ======
|
|
192
|
+
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
193
|
+
setCors(req, res);
|
|
194
|
+
|
|
195
|
+
// Preflight
|
|
196
|
+
if (req.method === "OPTIONS") {
|
|
197
|
+
res.writeHead(204);
|
|
198
|
+
res.end();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Health
|
|
203
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
204
|
+
sendJson(res, 200, {
|
|
205
|
+
status: 'ok',
|
|
206
|
+
mcp_path: MCP_PATH,
|
|
207
|
+
ha_url: process.env.HA_URL,
|
|
208
|
+
token_configured: !!process.env.HA_TOKEN,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// OpenAPI served from disk
|
|
214
|
+
if (req.method === 'GET' && (req.url === '/openapi.json' || req.url === '/openapi')) {
|
|
215
|
+
try {
|
|
216
|
+
const openApiPath = path.join(__dirname, '..', 'openapi.json');
|
|
217
|
+
const { readFileSync } = await import('fs');
|
|
218
|
+
const content = readFileSync(openApiPath, 'utf8');
|
|
219
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
220
|
+
res.end(content);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
sendJson(res, 500, {
|
|
223
|
+
error: "Failed to read openapi.json",
|
|
224
|
+
details: (e as Error).message
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ===== Pretty REST endpoints =====
|
|
231
|
+
if (req.method === 'POST' && req.url === '/get_all_sensors') {
|
|
232
|
+
try {
|
|
233
|
+
const raw = await callMcpTool('getStates', {});
|
|
234
|
+
const text = extractText(raw);
|
|
235
|
+
const parsed = tryParseJson(text);
|
|
236
|
+
|
|
237
|
+
if (!parsed || !Array.isArray(parsed.entities)) {
|
|
238
|
+
sendJson(res, 502, { error: "Unexpected MCP response format", raw });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Filter to sensors only to match legacy behavior
|
|
243
|
+
const sensors = parsed.entities.filter((e: any) =>
|
|
244
|
+
e.entity_id?.startsWith('sensor.')
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
sendJson(res, 200, { count: sensors.length, sensors, raw });
|
|
248
|
+
} catch (e) {
|
|
249
|
+
sendJson(res, 500, { error: (e as Error).message });
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (req.method === 'POST' && req.url === '/get_sensor_state') {
|
|
255
|
+
try {
|
|
256
|
+
const body = await readBody(req);
|
|
257
|
+
const entity_id = body?.entity_id;
|
|
258
|
+
if (!entity_id || typeof entity_id !== 'string') {
|
|
259
|
+
sendJson(res, 400, { error: "Missing required field: entity_id (string)" });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const raw = await callMcpTool('getState', { entity_id });
|
|
264
|
+
const text = extractText(raw);
|
|
265
|
+
const entity = tryParseJson(text);
|
|
266
|
+
|
|
267
|
+
if (!entity || typeof entity !== 'object') {
|
|
268
|
+
sendJson(res, 502, { error: "Unexpected MCP response format", raw });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
sendJson(res, 200, { entity, raw });
|
|
273
|
+
} catch (e) {
|
|
274
|
+
sendJson(res, 500, { error: (e as Error).message });
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (req.method === 'POST' && req.url === '/control_device') {
|
|
280
|
+
try {
|
|
281
|
+
const body = await readBody(req);
|
|
282
|
+
const domain = body?.domain;
|
|
283
|
+
const service = body?.service;
|
|
284
|
+
const data = body?.data ?? {};
|
|
285
|
+
|
|
286
|
+
if (!domain || typeof domain !== 'string') {
|
|
287
|
+
sendJson(res, 400, { error: "Missing required field: domain (string)" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!service || typeof service !== 'string') {
|
|
291
|
+
sendJson(res, 400, { error: "Missing required field: service (string)" });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
|
295
|
+
sendJson(res, 400, { error: "Field 'data' must be an object" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const raw = await callMcpTool('callService', {
|
|
300
|
+
domain,
|
|
301
|
+
service,
|
|
302
|
+
service_data: data,
|
|
303
|
+
});
|
|
304
|
+
const text = extractText(raw);
|
|
305
|
+
const parsed = tryParseJson(text);
|
|
306
|
+
|
|
307
|
+
sendJson(res, 200, { ok: true, result: parsed ?? text ?? null, raw });
|
|
308
|
+
} catch (e) {
|
|
309
|
+
sendJson(res, 500, { error: (e as Error).message });
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ===== Raw passthrough endpoint (for n8n compatibility) =====
|
|
315
|
+
if (req.method === 'POST' && req.url === '/') {
|
|
316
|
+
try {
|
|
317
|
+
const body = await readBody(req);
|
|
318
|
+
if (!body) {
|
|
319
|
+
sendJson(res, 400, { error: "Empty JSON body" });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const result = await runMcpWithJsonInput(body);
|
|
323
|
+
sendJson(res, 200, result);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
sendJson(res, 500, { error: (e as Error).message });
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
334
|
+
console.log('MCP Wrapper started successfully!');
|
|
335
|
+
console.log(`Listening on 0.0.0.0:${PORT}`);
|
|
336
|
+
console.log(`MCP script: ${MCP_PATH}`);
|
|
337
|
+
console.log(`Home Assistant: ${process.env.HA_URL}`);
|
|
338
|
+
console.log(`Token: ${process.env.HA_TOKEN ? '***configured***' : 'MISSING'}`);
|
|
339
|
+
console.log(`\nTest with: curl http://localhost:${PORT}/health\n`);
|
|
340
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { loadConfig, validateConfig } from '../src/config.js';
|
|
7
|
+
import type { EnvironmentConfig } from '../src/config.js';
|
|
8
|
+
|
|
9
|
+
describe('loadConfig', () => {
|
|
10
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
originalEnv = { ...process.env };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should load valid configuration from environment', () => {
|
|
17
|
+
const env = {
|
|
18
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
19
|
+
HA_TOKEN: 'test-token-12345',
|
|
20
|
+
HA_STRICT_SSL: 'false',
|
|
21
|
+
HA_TIMEOUT: '30000',
|
|
22
|
+
MCP_SERVER_LOG_LEVEL: 'info',
|
|
23
|
+
MCP_SERVER_LOG_FORMAT: 'plain',
|
|
24
|
+
MCP_SERVER_USE_HTTP: 'false',
|
|
25
|
+
MCP_SERVER_STATEFUL: 'false',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const config = loadConfig(env);
|
|
29
|
+
|
|
30
|
+
expect(config.baseUrl).toBe('http://homeassistant.10.0.0.19.nip.io:8123');
|
|
31
|
+
expect(config.token).toBe('test-token-12345');
|
|
32
|
+
expect(config.strictSsl).toBe(false);
|
|
33
|
+
expect(config.timeout).toBe(30000);
|
|
34
|
+
expect(config.logLevel).toBe('info');
|
|
35
|
+
expect(config.logFormat).toBe('plain');
|
|
36
|
+
expect(config.useHttp).toBe(false);
|
|
37
|
+
expect(config.stateful).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should apply default values for optional fields', () => {
|
|
41
|
+
const env = {
|
|
42
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
43
|
+
HA_TOKEN: 'test-token-12345',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const config = loadConfig(env);
|
|
47
|
+
|
|
48
|
+
expect(config.strictSsl).toBe(true); // default
|
|
49
|
+
expect(config.timeout).toBe(30000); // default
|
|
50
|
+
expect(config.logLevel).toBe('info'); // default
|
|
51
|
+
expect(config.logFormat).toBe('plain'); // default
|
|
52
|
+
expect(config.useHttp).toBe(false); // default
|
|
53
|
+
expect(config.stateful).toBe(false); // default
|
|
54
|
+
expect(config.httpEnableHealthcheck).toBe(true); // default
|
|
55
|
+
expect(config.httpAllowCors).toBe(true); // default
|
|
56
|
+
expect(config.httpTransport).toBe('stream'); // default
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should remove trailing slash from baseUrl', () => {
|
|
60
|
+
const env = {
|
|
61
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123/',
|
|
62
|
+
HA_TOKEN: 'test-token-12345',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const config = loadConfig(env);
|
|
66
|
+
|
|
67
|
+
expect(config.baseUrl).toBe('http://homeassistant.10.0.0.19.nip.io:8123');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should throw error for missing HA_URL', () => {
|
|
71
|
+
const env = {
|
|
72
|
+
HA_TOKEN: 'test-token-12345',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
expect(() => loadConfig(env)).toThrow('Invalid environment configuration');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should throw error for missing HA_TOKEN', () => {
|
|
79
|
+
const env = {
|
|
80
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(() => loadConfig(env)).toThrow('Invalid environment configuration');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should throw error for invalid HA_URL', () => {
|
|
87
|
+
const env = {
|
|
88
|
+
HA_URL: 'not-a-url',
|
|
89
|
+
HA_TOKEN: 'test-token-12345',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
expect(() => loadConfig(env)).toThrow('Invalid environment configuration');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should parse HTTP configuration when USE_HTTP is true', () => {
|
|
96
|
+
const env = {
|
|
97
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
98
|
+
HA_TOKEN: 'test-token-12345',
|
|
99
|
+
MCP_SERVER_USE_HTTP: 'true',
|
|
100
|
+
MCP_HTTP_PORT: '3000',
|
|
101
|
+
MCP_HTTP_TRANSPORT: 'stream',
|
|
102
|
+
MCP_HTTP_BIND_ADDR: '127.0.0.1',
|
|
103
|
+
MCP_HTTP_PATH: '/mcp',
|
|
104
|
+
MCP_HTTP_ENABLE_HEALTHCHECK: 'true',
|
|
105
|
+
MCP_HTTP_HEALTHCHECK_PATH: '/health',
|
|
106
|
+
MCP_HTTP_ALLOW_CORS: 'true',
|
|
107
|
+
MCP_HTTP_ALLOWED_ORIGINS: '127.0.0.1,localhost',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const config = loadConfig(env);
|
|
111
|
+
|
|
112
|
+
expect(config.useHttp).toBe(true);
|
|
113
|
+
expect(config.httpPort).toBe(3000);
|
|
114
|
+
expect(config.httpTransport).toBe('stream');
|
|
115
|
+
expect(config.httpBindAddr).toBe('127.0.0.1');
|
|
116
|
+
expect(config.httpPath).toBe('/mcp');
|
|
117
|
+
expect(config.httpEnableHealthcheck).toBe(true);
|
|
118
|
+
expect(config.httpHealthcheckPath).toBe('/health');
|
|
119
|
+
expect(config.httpAllowCors).toBe(true);
|
|
120
|
+
expect(config.httpAllowedOrigins).toEqual(['127.0.0.1', 'localhost']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should use default httpPath based on transport', () => {
|
|
124
|
+
const envStream = {
|
|
125
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
126
|
+
HA_TOKEN: 'test-token-12345',
|
|
127
|
+
MCP_HTTP_TRANSPORT: 'stream',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const configStream = loadConfig(envStream);
|
|
131
|
+
expect(configStream.httpPath).toBe('/mcp');
|
|
132
|
+
|
|
133
|
+
const envSse = {
|
|
134
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
135
|
+
HA_TOKEN: 'test-token-12345',
|
|
136
|
+
MCP_HTTP_TRANSPORT: 'sse',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const configSse = loadConfig(envSse);
|
|
140
|
+
expect(configSse.httpPath).toBe('/sse');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle wildcard in allowed origins', () => {
|
|
144
|
+
const env = {
|
|
145
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
146
|
+
HA_TOKEN: 'test-token-12345',
|
|
147
|
+
MCP_HTTP_ALLOWED_ORIGINS: '*',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const config = loadConfig(env);
|
|
151
|
+
|
|
152
|
+
// Wildcard should result in empty array to disable SDK validation
|
|
153
|
+
expect(config.httpAllowedOrigins).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should parse comma-separated origins', () => {
|
|
157
|
+
const env = {
|
|
158
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
159
|
+
HA_TOKEN: 'test-token-12345',
|
|
160
|
+
MCP_HTTP_ALLOWED_ORIGINS: 'http://mcpserver.10.0.0.18.nip.io:3000,http://192.168.0.10:8080,localhost',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const config = loadConfig(env);
|
|
164
|
+
|
|
165
|
+
expect(config.httpAllowedOrigins).toEqual([
|
|
166
|
+
'http://mcpserver.10.0.0.18.nip.io:3000',
|
|
167
|
+
'http://192.168.0.10:8080',
|
|
168
|
+
'localhost',
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should throw error for invalid bind address', () => {
|
|
173
|
+
const env = {
|
|
174
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
175
|
+
HA_TOKEN: 'test-token-12345',
|
|
176
|
+
MCP_HTTP_BIND_ADDR: 'invalid-ip',
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(() => loadConfig(env)).toThrow('MCP_HTTP_BIND_ADDR must be a valid IPv4 or IPv6 address');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should accept valid IPv4 bind address', () => {
|
|
183
|
+
const env = {
|
|
184
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
185
|
+
HA_TOKEN: 'test-token-12345',
|
|
186
|
+
MCP_HTTP_BIND_ADDR: '192.168.1.100',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const config = loadConfig(env);
|
|
190
|
+
expect(config.httpBindAddr).toBe('192.168.1.100');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should accept valid IPv6 bind address', () => {
|
|
194
|
+
const env = {
|
|
195
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
196
|
+
HA_TOKEN: 'test-token-12345',
|
|
197
|
+
MCP_HTTP_BIND_ADDR: '::1',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const config = loadConfig(env);
|
|
201
|
+
expect(config.httpBindAddr).toBe('::1');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should parse numeric timeout', () => {
|
|
205
|
+
const env = {
|
|
206
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
207
|
+
HA_TOKEN: 'test-token-12345',
|
|
208
|
+
HA_TIMEOUT: '45000',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const config = loadConfig(env);
|
|
212
|
+
expect(config.timeout).toBe(45000);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should support all log levels', () => {
|
|
216
|
+
const levels = ['debug', 'info', 'warn', 'error'] as const;
|
|
217
|
+
|
|
218
|
+
for (const level of levels) {
|
|
219
|
+
const env = {
|
|
220
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
221
|
+
HA_TOKEN: 'test-token-12345',
|
|
222
|
+
MCP_SERVER_LOG_LEVEL: level,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const config = loadConfig(env);
|
|
226
|
+
expect(config.logLevel).toBe(level);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should support all log formats', () => {
|
|
231
|
+
const formats = ['plain', 'json', 'gcp-json'] as const;
|
|
232
|
+
|
|
233
|
+
for (const format of formats) {
|
|
234
|
+
const env = {
|
|
235
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
236
|
+
HA_TOKEN: 'test-token-12345',
|
|
237
|
+
MCP_SERVER_LOG_FORMAT: format,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const config = loadConfig(env);
|
|
241
|
+
expect(config.logFormat).toBe(format);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should support both HTTP transports', () => {
|
|
246
|
+
const transports = ['stream', 'sse'] as const;
|
|
247
|
+
|
|
248
|
+
for (const transport of transports) {
|
|
249
|
+
const env = {
|
|
250
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
251
|
+
HA_TOKEN: 'test-token-12345',
|
|
252
|
+
MCP_HTTP_TRANSPORT: transport,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const config = loadConfig(env);
|
|
256
|
+
expect(config.httpTransport).toBe(transport);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should parse boolean strings correctly', () => {
|
|
261
|
+
const envTrue = {
|
|
262
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
263
|
+
HA_TOKEN: 'test-token-12345',
|
|
264
|
+
HA_STRICT_SSL: 'true',
|
|
265
|
+
MCP_SERVER_USE_HTTP: 'true',
|
|
266
|
+
MCP_SERVER_STATEFUL: 'true',
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const configTrue = loadConfig(envTrue);
|
|
270
|
+
expect(configTrue.strictSsl).toBe(true);
|
|
271
|
+
expect(configTrue.useHttp).toBe(true);
|
|
272
|
+
expect(configTrue.stateful).toBe(true);
|
|
273
|
+
|
|
274
|
+
const envFalse = {
|
|
275
|
+
HA_URL: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
276
|
+
HA_TOKEN: 'test-token-12345',
|
|
277
|
+
HA_STRICT_SSL: 'false',
|
|
278
|
+
MCP_SERVER_USE_HTTP: 'false',
|
|
279
|
+
MCP_SERVER_STATEFUL: 'false',
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const configFalse = loadConfig(envFalse);
|
|
283
|
+
expect(configFalse.strictSsl).toBe(false);
|
|
284
|
+
expect(configFalse.useHttp).toBe(false);
|
|
285
|
+
expect(configFalse.stateful).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('validateConfig', () => {
|
|
290
|
+
it('should validate valid configuration', () => {
|
|
291
|
+
const config: EnvironmentConfig = {
|
|
292
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
293
|
+
token: 'test-token-12345',
|
|
294
|
+
strictSsl: false,
|
|
295
|
+
timeout: 30000,
|
|
296
|
+
aiProvider: 'ollama',
|
|
297
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
298
|
+
aiModel: 'qwen3:14b',
|
|
299
|
+
aiTimeout: 60000,
|
|
300
|
+
logLevel: 'info',
|
|
301
|
+
logFormat: 'plain',
|
|
302
|
+
useHttp: false,
|
|
303
|
+
stateful: false,
|
|
304
|
+
httpTransport: 'stream',
|
|
305
|
+
httpEnableHealthcheck: true,
|
|
306
|
+
httpAllowCors: true,
|
|
307
|
+
sseEventsEnabled: true,
|
|
308
|
+
sseEventsPath: '/subscribe_events',
|
|
309
|
+
rateLimitEnabled: true,
|
|
310
|
+
rateLimitWindowMs: 60000,
|
|
311
|
+
rateLimitMaxRequests: 100,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
expect(() => validateConfig(config)).not.toThrow();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should throw error for missing baseUrl', () => {
|
|
318
|
+
const config = {
|
|
319
|
+
token: 'test-token-12345',
|
|
320
|
+
} as EnvironmentConfig;
|
|
321
|
+
|
|
322
|
+
expect(() => validateConfig(config)).toThrow('Invalid configuration');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should throw error for missing token', () => {
|
|
326
|
+
const config = {
|
|
327
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
328
|
+
} as EnvironmentConfig;
|
|
329
|
+
|
|
330
|
+
expect(() => validateConfig(config)).toThrow('Invalid configuration');
|
|
331
|
+
});
|
|
332
|
+
});
|