@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.env.example +1 -1
  2. package/README.md +24 -2
  3. package/dist/omadaClient/accessControl.d.ts +290 -0
  4. package/dist/omadaClient/accessControl.js +509 -0
  5. package/dist/omadaClient/accessControl.js.map +1 -0
  6. package/dist/omadaClient/client.d.ts +6 -1
  7. package/dist/omadaClient/client.js +12 -1
  8. package/dist/omadaClient/client.js.map +1 -1
  9. package/dist/omadaClient/diagnose.d.ts +68 -0
  10. package/dist/omadaClient/diagnose.js +164 -0
  11. package/dist/omadaClient/diagnose.js.map +1 -0
  12. package/dist/omadaClient/index.d.ts +9 -0
  13. package/dist/omadaClient/index.js +19 -0
  14. package/dist/omadaClient/index.js.map +1 -1
  15. package/dist/omadaClient/macGroups.d.ts +58 -0
  16. package/dist/omadaClient/macGroups.js +110 -0
  17. package/dist/omadaClient/macGroups.js.map +1 -0
  18. package/dist/omadaClient/serialize.d.ts +13 -0
  19. package/dist/omadaClient/serialize.js +29 -0
  20. package/dist/omadaClient/serialize.js.map +1 -0
  21. package/dist/omadaClient/timeRange.d.ts +41 -0
  22. package/dist/omadaClient/timeRange.js +109 -0
  23. package/dist/omadaClient/timeRange.js.map +1 -0
  24. package/dist/tools/infra/actions.d.ts +1 -1
  25. package/dist/tools/infra/actions.js +1 -1
  26. package/dist/tools/omada/accessControl.d.ts +151 -0
  27. package/dist/tools/omada/accessControl.js +158 -0
  28. package/dist/tools/omada/accessControl.js.map +1 -0
  29. package/dist/tools/omada/index.js +9 -0
  30. package/dist/tools/omada/index.js.map +1 -1
  31. package/dist/tools/omada/macGroups.d.ts +19 -0
  32. package/dist/tools/omada/macGroups.js +77 -0
  33. package/dist/tools/omada/macGroups.js.map +1 -0
  34. package/dist/tools/omada/namespace.js +16 -0
  35. package/dist/tools/omada/namespace.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +5 -1
  39. package/.codex/review-rules.md +0 -16
  40. package/.dockerignore +0 -8
  41. package/.gitattributes +0 -30
  42. package/.github/workflows/ci.yml +0 -193
  43. package/AGENTS.md +0 -389
  44. package/Dockerfile +0 -50
  45. package/docker-compose.yml +0 -23
  46. package/docs/CHANGELOG.md +0 -341
  47. package/docs/COVERAGE.md +0 -219
  48. package/docs/QUICK_START.md +0 -240
  49. package/eslint.config.js +0 -33
  50. package/n8n/ai-agent-safety-monitor.json +0 -355
  51. package/openapi.json +0 -1259
  52. package/scripts/check-version-sync.mjs +0 -69
  53. package/scripts/generate-jwt.ts +0 -121
  54. package/src/config.ts +0 -440
  55. package/src/haClient/automations.ts +0 -261
  56. package/src/haClient/calendars.ts +0 -94
  57. package/src/haClient/config.ts +0 -52
  58. package/src/haClient/devices.ts +0 -653
  59. package/src/haClient/entities.ts +0 -128
  60. package/src/haClient/events.ts +0 -689
  61. package/src/haClient/history.ts +0 -73
  62. package/src/haClient/index.ts +0 -336
  63. package/src/haClient/request.ts +0 -142
  64. package/src/haClient/services.ts +0 -45
  65. package/src/haClient/states.ts +0 -60
  66. package/src/haClient/updates.ts +0 -58
  67. package/src/haClient/wsCommand.ts +0 -44
  68. package/src/index.ts +0 -164
  69. package/src/localAI/index.ts +0 -88
  70. package/src/localAI/prompts.ts +0 -50
  71. package/src/localAI/providers/base.ts +0 -120
  72. package/src/localAI/providers/ollama.ts +0 -78
  73. package/src/localAI/providers/openai.ts +0 -86
  74. package/src/localAI/types.ts +0 -67
  75. package/src/omadaClient/auth.ts +0 -178
  76. package/src/omadaClient/client.ts +0 -337
  77. package/src/omadaClient/device.ts +0 -118
  78. package/src/omadaClient/index.ts +0 -310
  79. package/src/omadaClient/network.ts +0 -284
  80. package/src/omadaClient/request.ts +0 -365
  81. package/src/omadaClient/security.ts +0 -51
  82. package/src/omadaClient/site.ts +0 -344
  83. package/src/ownerQuestions/service.ts +0 -536
  84. package/src/permissions/index.ts +0 -143
  85. package/src/resources/index.ts +0 -213
  86. package/src/server/auth.ts +0 -175
  87. package/src/server/chatFace.ts +0 -789
  88. package/src/server/chatSlice.ts +0 -57
  89. package/src/server/common.ts +0 -170
  90. package/src/server/eventSubscription.ts +0 -343
  91. package/src/server/http.ts +0 -626
  92. package/src/server/instructions.ts +0 -97
  93. package/src/server/rateLimiter.ts +0 -227
  94. package/src/server/stdio.ts +0 -41
  95. package/src/server/stream.ts +0 -205
  96. package/src/tools/ai/analyzeSensors.ts +0 -33
  97. package/src/tools/ai/index.ts +0 -20
  98. package/src/tools/common.ts +0 -442
  99. package/src/tools/homeassistant/askOwner.ts +0 -133
  100. package/src/tools/homeassistant/automationControls.ts +0 -66
  101. package/src/tools/homeassistant/calendar.ts +0 -97
  102. package/src/tools/homeassistant/callService.ts +0 -25
  103. package/src/tools/homeassistant/controlClimate.ts +0 -68
  104. package/src/tools/homeassistant/controlCover.ts +0 -48
  105. package/src/tools/homeassistant/controlFan.ts +0 -44
  106. package/src/tools/homeassistant/controlLight.ts +0 -38
  107. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  108. package/src/tools/homeassistant/createAutomation.ts +0 -35
  109. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  110. package/src/tools/homeassistant/entityAction.ts +0 -35
  111. package/src/tools/homeassistant/entityGuard.ts +0 -28
  112. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  113. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  114. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  115. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  116. package/src/tools/homeassistant/getHistory.ts +0 -24
  117. package/src/tools/homeassistant/getState.ts +0 -24
  118. package/src/tools/homeassistant/getStates.ts +0 -46
  119. package/src/tools/homeassistant/getVersion.ts +0 -21
  120. package/src/tools/homeassistant/index.ts +0 -121
  121. package/src/tools/homeassistant/listAutomations.ts +0 -35
  122. package/src/tools/homeassistant/listEntities.ts +0 -44
  123. package/src/tools/homeassistant/listPersons.ts +0 -46
  124. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  125. package/src/tools/homeassistant/searchEntities.ts +0 -58
  126. package/src/tools/homeassistant/sendNotification.ts +0 -179
  127. package/src/tools/homeassistant/systemTools.ts +0 -52
  128. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  129. package/src/tools/index.ts +0 -78
  130. package/src/tools/infra/actions.ts +0 -95
  131. package/src/tools/infra/index.ts +0 -138
  132. package/src/tools/omada/blockClient.ts +0 -28
  133. package/src/tools/omada/cyclePoePort.ts +0 -32
  134. package/src/tools/omada/deleteClient.ts +0 -27
  135. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  136. package/src/tools/omada/getClient.ts +0 -14
  137. package/src/tools/omada/getDevice.ts +0 -14
  138. package/src/tools/omada/getFirewallSetting.ts +0 -14
  139. package/src/tools/omada/getInternetInfo.ts +0 -14
  140. package/src/tools/omada/getLanNetworkList.ts +0 -14
  141. package/src/tools/omada/getLanProfileList.ts +0 -14
  142. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  143. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  144. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  145. package/src/tools/omada/getSsidDetail.ts +0 -22
  146. package/src/tools/omada/getSsidList.ts +0 -21
  147. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  148. package/src/tools/omada/getThreatList.ts +0 -42
  149. package/src/tools/omada/getWlanGroupList.ts +0 -14
  150. package/src/tools/omada/graph.ts +0 -187
  151. package/src/tools/omada/index.ts +0 -153
  152. package/src/tools/omada/listBlockedClients.ts +0 -17
  153. package/src/tools/omada/listClients.ts +0 -14
  154. package/src/tools/omada/listClientsActivity.ts +0 -25
  155. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  156. package/src/tools/omada/listDevices.ts +0 -14
  157. package/src/tools/omada/listDevicesStats.ts +0 -41
  158. package/src/tools/omada/listMostActiveClients.ts +0 -14
  159. package/src/tools/omada/listSites.ts +0 -18
  160. package/src/tools/omada/namespace.ts +0 -1164
  161. package/src/tools/omada/searchDevices.ts +0 -20
  162. package/src/tools/omada/setClientRateLimit.ts +0 -26
  163. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  164. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  165. package/src/tools/omada/setSsidEnabled.ts +0 -38
  166. package/src/tools/omada/unblockClient.ts +0 -26
  167. package/src/tools/types.ts +0 -11
  168. package/src/tools/utils/pagination-schema.ts +0 -27
  169. package/src/types/ai/index.ts +0 -11
  170. package/src/types/ai/types.ts +0 -67
  171. package/src/types/homeassistant/automations.ts +0 -43
  172. package/src/types/homeassistant/calendars.ts +0 -26
  173. package/src/types/homeassistant/config.ts +0 -14
  174. package/src/types/homeassistant/entities.ts +0 -14
  175. package/src/types/homeassistant/errors.ts +0 -33
  176. package/src/types/homeassistant/haConfig.ts +0 -7
  177. package/src/types/homeassistant/history.ts +0 -13
  178. package/src/types/homeassistant/index.ts +0 -46
  179. package/src/types/homeassistant/request.ts +0 -6
  180. package/src/types/homeassistant/serviceCall.ts +0 -18
  181. package/src/types/homeassistant/states.ts +0 -58
  182. package/src/types/homeassistant/updates.ts +0 -28
  183. package/src/types/index.ts +0 -12
  184. package/src/types/omada/activeClientInfo.ts +0 -17
  185. package/src/types/omada/clientActivity.ts +0 -32
  186. package/src/types/omada/clientBlockStatus.ts +0 -11
  187. package/src/types/omada/clientPastConnection.ts +0 -82
  188. package/src/types/omada/index.ts +0 -14
  189. package/src/types/omada/omadaApiResponse.ts +0 -5
  190. package/src/types/omada/omadaClientInfo.ts +0 -11
  191. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  192. package/src/types/omada/omadaDeviceStats.ts +0 -31
  193. package/src/types/omada/omadaSiteSummary.ts +0 -5
  194. package/src/types/omada/oswStackDetail.ts +0 -11
  195. package/src/types/omada/paginatedResult.ts +0 -6
  196. package/src/types/omada/rateLimitProfile.ts +0 -58
  197. package/src/types/omada/threatInfo.ts +0 -38
  198. package/src/types/omada/tokenResult.ts +0 -6
  199. package/src/types.ts +0 -2
  200. package/src/utils/config-validations.ts +0 -68
  201. package/src/utils/jwt.ts +0 -112
  202. package/src/utils/logger.ts +0 -121
  203. package/src/utils/plainText.ts +0 -48
  204. package/src/utils/redact.ts +0 -59
  205. package/src/utils/sanitizeError.ts +0 -18
  206. package/src/utils/tlsDispatcher.ts +0 -69
  207. package/src/version.ts +0 -5
  208. package/tests/bdd/automation-creation.spec.ts +0 -214
  209. package/tests/bdd/automation-management.spec.ts +0 -267
  210. package/tests/bdd/calendar.spec.ts +0 -190
  211. package/tests/bdd/configuration.spec.ts +0 -283
  212. package/tests/bdd/device-control.spec.ts +0 -344
  213. package/tests/bdd/entity-listing.spec.ts +0 -208
  214. package/tests/bdd/entity-search.spec.ts +0 -183
  215. package/tests/bdd/history-retrieval.spec.ts +0 -195
  216. package/tests/bdd/notifications.spec.ts +0 -238
  217. package/tests/bdd/service-calls.spec.ts +0 -144
  218. package/tests/bdd/state-retrieval.spec.ts +0 -204
  219. package/tests/bench/hotpaths.bench.ts +0 -68
  220. package/tests/config.test.ts +0 -355
  221. package/tests/features/automation-creation.feature +0 -36
  222. package/tests/features/automation-management.feature +0 -54
  223. package/tests/features/calendar.feature +0 -37
  224. package/tests/features/configuration.feature +0 -70
  225. package/tests/features/device-control.feature +0 -100
  226. package/tests/features/entity-listing.feature +0 -51
  227. package/tests/features/entity-search.feature +0 -44
  228. package/tests/features/history-retrieval.feature +0 -38
  229. package/tests/features/notifications.feature +0 -37
  230. package/tests/features/service-calls.feature +0 -35
  231. package/tests/features/state-retrieval.feature +0 -37
  232. package/tests/fixtures/openapi-default-slice.json +0 -290
  233. package/tests/haClient/automationTrace.test.ts +0 -59
  234. package/tests/haClient/automations.test.ts +0 -149
  235. package/tests/haClient/config.test.ts +0 -117
  236. package/tests/haClient/entities.test.ts +0 -188
  237. package/tests/haClient/events.test.ts +0 -455
  238. package/tests/haClient/eventsTls.test.ts +0 -52
  239. package/tests/haClient/history.test.ts +0 -228
  240. package/tests/haClient/index.test.ts +0 -243
  241. package/tests/haClient/request.test.ts +0 -217
  242. package/tests/haClient/services.test.ts +0 -120
  243. package/tests/haClient/states.test.ts +0 -119
  244. package/tests/haClient/updates.test.ts +0 -193
  245. package/tests/index.test.ts +0 -160
  246. package/tests/integration/homeassistant.test.ts +0 -144
  247. package/tests/integration/omada.test.ts +0 -167
  248. package/tests/integration/server.test.ts +0 -151
  249. package/tests/localAI/index.test.ts +0 -177
  250. package/tests/localAI/ollama.test.ts +0 -187
  251. package/tests/localAI/openai.test.ts +0 -257
  252. package/tests/omadaClient/actions.test.ts +0 -161
  253. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  254. package/tests/omadaClient/ntp.test.ts +0 -381
  255. package/tests/omadaClient/site.test.ts +0 -37
  256. package/tests/ownerQuestions/service.test.ts +0 -499
  257. package/tests/ownerQuestions/tools.test.ts +0 -186
  258. package/tests/permissions/index.test.ts +0 -217
  259. package/tests/resources/index.test.ts +0 -288
  260. package/tests/scripts/generateJwt.test.ts +0 -44
  261. package/tests/security/accessControl.test.ts +0 -223
  262. package/tests/security/authentication.test.ts +0 -117
  263. package/tests/security/configuration.test.ts +0 -170
  264. package/tests/security/cryptoTransport.test.ts +0 -172
  265. package/tests/security/dependencies.test.ts +0 -152
  266. package/tests/security/injection.test.ts +0 -117
  267. package/tests/security/insecureDesign.test.ts +0 -121
  268. package/tests/security/logging.test.ts +0 -131
  269. package/tests/security/resourceConsumption.test.ts +0 -142
  270. package/tests/server/auth.test.ts +0 -249
  271. package/tests/server/chatFace.test.ts +0 -320
  272. package/tests/server/common.test.ts +0 -84
  273. package/tests/server/http.test.ts +0 -561
  274. package/tests/server/instructions.test.ts +0 -152
  275. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  276. package/tests/server/stdio.test.ts +0 -99
  277. package/tests/server/stream.test.ts +0 -285
  278. package/tests/server/toolVisibility.http.test.ts +0 -111
  279. package/tests/server/toolVisibility.test.ts +0 -129
  280. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  281. package/tests/tools/common.test.ts +0 -123
  282. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  283. package/tests/tools/infra/invokeAction.test.ts +0 -280
  284. package/tests/tools/omada/graph.test.ts +0 -385
  285. package/tests/tools/omada/handlers.test.ts +0 -897
  286. package/tests/tools/omada/namespace.test.ts +0 -483
  287. package/tests/tools/registry.test.ts +0 -168
  288. package/tests/utils/config-validations.test.ts +0 -352
  289. package/tests/utils/jwt.test.ts +0 -115
  290. package/tests/utils/logger.test.ts +0 -327
  291. package/tests/utils/redact.test.ts +0 -105
  292. package/tsconfig.json +0 -24
  293. package/vitest.config.ts +0 -38
@@ -1,320 +0,0 @@
1
- /**
2
- * Chat face (v1.7) — the unified binding registry that drives REST dispatch,
3
- * per-route RBAC, and the generated OpenAPI spec.
4
- *
5
- * The two load-bearing properties under test:
6
- * 1. BYTE-COMPATIBILITY: with no MCP_CHAT_TOOLS slice, the generated spec is
7
- * identical to the pre-v1.7 hand-written one (golden fixture) — the NAS
8
- * deployment must see zero difference from this refactor.
9
- * 2. ADMIN-NEVER-CHAT: no chat binding may carry the ADMIN bit or expose
10
- * invokeAction — the deploy trigger stays off the prompt-injectable
11
- * surface structurally.
12
- */
13
-
14
- import { describe, it, expect, vi } from 'vitest';
15
- import { readFileSync } from 'node:fs';
16
- import { fileURLToPath } from 'node:url';
17
- import { dirname, join } from 'node:path';
18
- import type { IncomingMessage, ServerResponse } from 'node:http';
19
-
20
- import {
21
- LEGACY_BINDINGS,
22
- CHAT_TOOL_BINDINGS,
23
- resolveChatBindings,
24
- buildOpenApiSpec,
25
- dispatchChatRequest,
26
- requiredPermissionFor,
27
- zodToOpenApiSchema,
28
- } from '../../src/server/chatFace.js';
29
- import { parseChatTools, CHAT_CATEGORIES } from '../../src/server/chatSlice.js';
30
- import { Permission } from '../../src/permissions/index.js';
31
- import { VERSION } from '../../src/version.js';
32
- import type { HaClient } from '../../src/haClient/index.js';
33
- import type { OmadaClient } from '../../src/omadaClient/index.js';
34
- import { z } from 'zod';
35
-
36
- const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'fixtures');
37
-
38
- const LEGACY_OPERATION_IDS = [
39
- 'getStates',
40
- 'getState',
41
- 'callService',
42
- 'getAllSensors',
43
- 'getEntitiesByDomain',
44
- 'searchEntities',
45
- 'getHistory',
46
- 'getVersion',
47
- ];
48
-
49
- describe('chat face — spec byte-compatibility (golden file)', () => {
50
- it('default slice renders the pre-v1.7 spec exactly (modulo version)', () => {
51
- const golden = JSON.parse(
52
- readFileSync(join(fixtureDir, 'openapi-default-slice.json'), 'utf8')
53
- ) as { info: { version: string } };
54
- // The fixture was captured at its release version; only the version string
55
- // may differ across releases — everything else must match to the byte.
56
- golden.info.version = VERSION;
57
-
58
- const generated = JSON.parse(
59
- JSON.stringify(buildOpenApiSpec('http://__BASE_URL__', LEGACY_BINDINGS))
60
- ) as unknown;
61
-
62
- expect(generated).toEqual(golden);
63
- // Belt-and-braces: key ORDER matters for byte-compat, so compare strings too.
64
- expect(JSON.stringify(generated)).toBe(JSON.stringify(golden));
65
- });
66
- });
67
-
68
- describe('chat face — security invariants', () => {
69
- const allBindings = [...LEGACY_BINDINGS, ...CHAT_TOOL_BINDINGS];
70
-
71
- it('no chat binding carries the ADMIN bit (invokeAction can never be chat-faced)', () => {
72
- for (const binding of allBindings) {
73
- expect(
74
- binding.permission & Permission.ADMIN,
75
- `${binding.operationId} must not require (= be granted via) ADMIN on the chat face`
76
- ).toBe(0);
77
- expect(binding.operationId).not.toBe('invokeAction');
78
- }
79
- });
80
-
81
- it('omada_cyclePoePort is deliberately not chat-eligible', () => {
82
- expect(allBindings.map((b) => b.operationId)).not.toContain('omada_cyclePoePort');
83
- });
84
-
85
- it('every binding declares a known category and a non-zero permission', () => {
86
- for (const binding of allBindings) {
87
- expect(CHAT_CATEGORIES).toContain(binding.category);
88
- expect(binding.permission).toBeGreaterThan(0);
89
- }
90
- });
91
- });
92
-
93
- describe('parseChatTools', () => {
94
- it('returns undefined for unset/blank (legacy default)', () => {
95
- expect(parseChatTools(undefined)).toBeUndefined();
96
- expect(parseChatTools('')).toBeUndefined();
97
- expect(parseChatTools(' ')).toBeUndefined();
98
- });
99
-
100
- it('parses categories with r/w/rw access', () => {
101
- const slice = parseChatTools('ha-core:rw, ha-history:r,omada-read:r');
102
- expect(slice?.get('ha-core')).toEqual(new Set(['read', 'write']));
103
- expect(slice?.get('ha-history')).toEqual(new Set(['read']));
104
- expect(slice?.get('omada-read')).toEqual(new Set(['read']));
105
- expect(slice?.has('omada-write')).toBe(false);
106
- });
107
-
108
- it('fails loudly on unknown categories and malformed access', () => {
109
- expect(() => parseChatTools('infra:rw')).toThrow(/Invalid MCP_CHAT_TOOLS category/);
110
- expect(() => parseChatTools('ha-core')).toThrow(/Invalid MCP_CHAT_TOOLS access/);
111
- expect(() => parseChatTools('ha-core:x')).toThrow(/Invalid MCP_CHAT_TOOLS access/);
112
- });
113
- });
114
-
115
- describe('resolveChatBindings', () => {
116
- const haClient = {} as HaClient;
117
- const omadaClient = {} as OmadaClient;
118
-
119
- it('unset slice -> exactly the legacy 8 (zero-surprise upgrade)', () => {
120
- const bindings = resolveChatBindings(undefined, { haClient, omadaClient });
121
- expect(bindings.map((b) => b.operationId)).toEqual(LEGACY_OPERATION_IDS);
122
- });
123
-
124
- it('slice governs both legacy and tool bindings by category:access', () => {
125
- const bindings = resolveChatBindings(parseChatTools('ha-core:r'), { haClient, omadaClient });
126
- const ids = bindings.map((b) => b.operationId);
127
- // ha-core reads, but NOT callService (write), NOT getHistory (ha-history)
128
- expect(ids).toContain('getStates');
129
- expect(ids).toContain('getDomainSummary'); // tool binding, ha-core:read
130
- expect(ids).not.toContain('callService');
131
- expect(ids).not.toContain('getHistory');
132
- expect(ids).not.toContain('omada_browse');
133
- });
134
-
135
- it('omada categories activate the graph + SSID tools', () => {
136
- const bindings = resolveChatBindings(parseChatTools('omada-read:r,omada-write:w'), {
137
- haClient,
138
- omadaClient,
139
- });
140
- const ids = bindings.map((b) => b.operationId);
141
- expect(ids).toEqual(['omada_browse', 'omada_read', 'omada_setSsidEnabled']);
142
- });
143
-
144
- it('omada bindings are dropped from the face when no Omada client is configured', () => {
145
- const bindings = resolveChatBindings(parseChatTools('omada-read:r'), { haClient });
146
- expect(bindings).toEqual([]);
147
- });
148
-
149
- it('active omada tools appear in the generated spec as /api/tools/* paths', () => {
150
- const bindings = resolveChatBindings(parseChatTools('ha-core:rw,ha-history:r,omada-read:r'), {
151
- haClient,
152
- omadaClient,
153
- });
154
- const spec = buildOpenApiSpec('http://x', bindings) as { paths: Record<string, unknown> };
155
- expect(Object.keys(spec.paths)).toContain('/api/tools/omada_browse');
156
- expect(Object.keys(spec.paths)).toContain('/api/tools/omada_read');
157
- expect(Object.keys(spec.paths)).toContain('/api/states');
158
- expect(Object.keys(spec.paths)).not.toContain('/api/tools/omada_setSsidEnabled');
159
- });
160
- });
161
-
162
- describe('requiredPermissionFor', () => {
163
- it('mirrors the MCP twin permissions on legacy routes', () => {
164
- expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/states')).toBe(Permission.QUERY);
165
- expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/states/light.x')).toBe(Permission.QUERY);
166
- expect(requiredPermissionFor(LEGACY_BINDINGS, 'POST', '/api/services/light/turn_on')).toBe(
167
- Permission.CONTROL
168
- );
169
- expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/nope')).toBeNull();
170
- });
171
- });
172
-
173
- // ---------------------------------------------------------------------------
174
- // Dispatch behavior
175
- // ---------------------------------------------------------------------------
176
-
177
- function mockReq(method: string, permissions: number | undefined): IncomingMessage {
178
- return {
179
- method,
180
- headers: { host: 'test' },
181
- ...(permissions !== undefined ? { auth: { extra: { permissions } } } : {}),
182
- } as unknown as IncomingMessage;
183
- }
184
-
185
- function makeDispatch(overrides: {
186
- bindings: Parameters<typeof dispatchChatRequest>[3]['bindings'];
187
- deps?: Parameters<typeof dispatchChatRequest>[3]['deps'];
188
- body?: unknown;
189
- }) {
190
- const sent: { status?: number; body?: unknown } = {};
191
- const sendJson = vi.fn((_res: ServerResponse, status: number, body: unknown) => {
192
- sent.status = status;
193
- sent.body = body;
194
- });
195
- const run = (req: IncomingMessage, url: string) =>
196
- dispatchChatRequest(req, {} as ServerResponse, url, {
197
- bindings: overrides.bindings,
198
- deps: overrides.deps ?? {},
199
- parseBody: () => Promise.resolve(overrides.body),
200
- sendJson,
201
- });
202
- return { run, sent };
203
- }
204
-
205
- describe('dispatchChatRequest', () => {
206
- it('404s unknown routes without touching a client', async () => {
207
- const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS });
208
- await run(mockReq('GET', 0xff), '/api/definitely-not-a-route');
209
- expect(sent.status).toBe(404);
210
- });
211
-
212
- it('fails closed: missing permission mask -> 403', async () => {
213
- const { run, sent } = makeDispatch({
214
- bindings: LEGACY_BINDINGS,
215
- deps: { haClient: {} as HaClient },
216
- });
217
- await run(mockReq('GET', undefined), '/api/states');
218
- expect(sent.status).toBe(403);
219
- expect((sent.body as { error: string }).error).toBe('Forbidden');
220
- });
221
-
222
- it('503s when a required client is not configured', async () => {
223
- const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS, deps: {} });
224
- await run(mockReq('GET', 0xff), '/api/states');
225
- expect(sent.status).toBe(503);
226
- });
227
-
228
- it('runs a legacy handler with decoded path params', async () => {
229
- const haClient = {
230
- getState: vi.fn().mockResolvedValue({ entity_id: 'light.living room', state: 'on' }),
231
- } as unknown as HaClient;
232
- const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS, deps: { haClient } });
233
- await run(mockReq('GET', Permission.QUERY), '/api/states/light.living%20room');
234
- expect(sent.status).toBe(200);
235
- expect((haClient as unknown as { getState: ReturnType<typeof vi.fn> }).getState).toHaveBeenCalledWith(
236
- 'light.living room'
237
- );
238
- });
239
-
240
- it('dispatches a chat tool THROUGH the shared MCP handler (single source of truth)', async () => {
241
- const omadaClient = {
242
- setSsidEnabled: vi.fn().mockResolvedValue({ ssid: 'guest', enabled: false, applied: true }),
243
- } as unknown as OmadaClient;
244
- const bindings = resolveChatBindings(parseChatTools('omada-write:w'), { omadaClient });
245
-
246
- const { run, sent } = makeDispatch({
247
- bindings,
248
- deps: { omadaClient },
249
- body: { ssid: 'guest', enabled: false },
250
- });
251
- await run(mockReq('POST', Permission.CONTROL), '/api/tools/omada_setSsidEnabled');
252
-
253
- expect(sent.status).toBe(200);
254
- expect(sent.body).toMatchObject({ ssid: 'guest', enabled: false, applied: true });
255
- });
256
-
257
- it('rejects invalid tool arguments with 400 (zod validation, same as MCP face)', async () => {
258
- const omadaClient = { setSsidEnabled: vi.fn() } as unknown as OmadaClient;
259
- const bindings = resolveChatBindings(parseChatTools('omada-write:w'), { omadaClient });
260
-
261
- const { run, sent } = makeDispatch({
262
- bindings,
263
- deps: { omadaClient },
264
- body: { enabled: 'yes' }, // missing ssid; enabled wrong type
265
- });
266
- await run(mockReq('POST', Permission.CONTROL), '/api/tools/omada_setSsidEnabled');
267
-
268
- expect(sent.status).toBe(400);
269
- expect((sent.body as { error: string }).error).toBe('Invalid arguments');
270
- expect(
271
- (omadaClient as unknown as { setSsidEnabled: ReturnType<typeof vi.fn> }).setSsidEnabled
272
- ).not.toHaveBeenCalled();
273
- });
274
-
275
- it("maps the shared handler's own permission denial to 403 (defense in depth)", async () => {
276
- const omadaClient = { setSsidEnabled: vi.fn() } as unknown as OmadaClient;
277
- // Force the binding past route-level RBAC by dispatching with CONTROL,
278
- // then strip the bit the inner wrapToolHandler checks — impossible via the
279
- // real dispatcher (same bit), so simulate a QUERY-only caller on a route
280
- // whose binding requires QUERY but whose inner handler needs CONTROL:
281
- // omada_read's per-path permissions provide exactly this split, but the
282
- // simplest deterministic probe is a caller with route bit only.
283
- const bindings = resolveChatBindings(parseChatTools('omada-read:r'), { omadaClient });
284
- const { run, sent } = makeDispatch({
285
- bindings,
286
- deps: { omadaClient },
287
- body: { path: '/sites' }, // resource path requiring more than QUERY (per manifest)
288
- });
289
- await run(mockReq('POST', Permission.QUERY), '/api/tools/omada_read');
290
- // Either the manifest grants /sites to QUERY (200) or denies (403) — the
291
- // invariant under test is that a denial surfaces as 403, never 200.
292
- expect([200, 403, 500]).toContain(sent.status);
293
- if (sent.status === 403) {
294
- expect(JSON.stringify(sent.body)).toContain('Permission denied');
295
- }
296
- });
297
- });
298
-
299
- describe('zodToOpenApiSchema', () => {
300
- it('converts the schema subset used by chat tools', () => {
301
- const schema = z.object({
302
- name: z.string().describe('The name'),
303
- enabled: z.boolean(),
304
- mode: z.enum(['a', 'b']).optional(),
305
- count: z.number().int().min(1).optional(),
306
- path: z.string().default('/'),
307
- });
308
- const out = zodToOpenApiSchema(schema) as {
309
- type: string;
310
- properties: Record<string, { type?: string; enum?: string[]; default?: unknown }>;
311
- required?: string[];
312
- };
313
- expect(out.type).toBe('object');
314
- expect(out.properties.name).toEqual({ type: 'string', description: 'The name' });
315
- expect(out.properties.enabled.type).toBe('boolean');
316
- expect(out.properties.mode.enum).toEqual(['a', 'b']);
317
- expect(out.properties.path.default).toBe('/');
318
- expect(out.required).toEqual(['name', 'enabled']);
319
- });
320
- });
@@ -1,84 +0,0 @@
1
- /**
2
- * Server common functionality tests
3
- */
4
-
5
- import { describe, it, expect, vi, beforeEach } from 'vitest';
6
- import { createServer } from '../../src/server/common.js';
7
-
8
- // Mock HaClient
9
- const mockHaClient = {
10
- getStates: vi.fn(),
11
- getState: vi.fn(),
12
- searchEntities: vi.fn(),
13
- getEntitiesByDomain: vi.fn(),
14
- };
15
-
16
- // Mock the tools index
17
- vi.mock('../../src/tools/index.js', () => ({
18
- registerAllTools: vi.fn(),
19
- }));
20
-
21
- // Mock the resources index
22
- vi.mock('../../src/resources/index.js', () => ({
23
- registerAllResources: vi.fn(),
24
- }));
25
-
26
- describe('createServer', () => {
27
- beforeEach(() => {
28
- vi.clearAllMocks();
29
- });
30
-
31
- it('should create MCP server with correct name and version', () => {
32
- const server = createServer({ haClient: mockHaClient as any });
33
-
34
- expect(server).toBeDefined();
35
- // Server should have the correct configuration
36
- // Note: We can't easily test internal properties, but we can verify it was created
37
- });
38
-
39
- it('should register all tools on server creation', async () => {
40
- const { registerAllTools } = await import('../../src/tools/index.js');
41
- const server = createServer({ haClient: mockHaClient as any });
42
-
43
- expect(registerAllTools).toHaveBeenCalledWith({
44
- server,
45
- haClient: mockHaClient,
46
- omadaClient: undefined,
47
- aiClient: undefined,
48
- });
49
- });
50
-
51
- it('should create server with tools capability', () => {
52
- const server = createServer({ haClient: mockHaClient as any });
53
-
54
- // The server should be created successfully
55
- expect(server).toBeDefined();
56
- expect(typeof server.connect).toBe('function');
57
- });
58
-
59
- it('should work without haClient (Omada-only mode)', () => {
60
- const server = createServer({});
61
-
62
- expect(server).toBeDefined();
63
- expect(typeof server.connect).toBe('function');
64
- });
65
-
66
- it('should accept omadaClient and aiClient', async () => {
67
- const { registerAllTools } = await import('../../src/tools/index.js');
68
- const mockOmadaClient = { listSites: vi.fn() };
69
- const mockAiClient = { analyze: vi.fn() };
70
-
71
- const server = createServer({
72
- haClient: mockHaClient as any,
73
- omadaClient: mockOmadaClient as any,
74
- aiClient: mockAiClient as any,
75
- });
76
-
77
- expect(registerAllTools).toHaveBeenCalledWith({
78
- server,
79
- haClient: mockHaClient,
80
- omadaClient: mockOmadaClient,
81
- aiClient: mockAiClient,
82
- });
83
- });
84
- });