@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1

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 (281) hide show
  1. package/.env.example +1 -1
  2. package/README.md +11 -2
  3. package/dist/omadaClient/client.d.ts +6 -1
  4. package/dist/omadaClient/client.js +12 -1
  5. package/dist/omadaClient/client.js.map +1 -1
  6. package/dist/omadaClient/diagnose.d.ts +68 -0
  7. package/dist/omadaClient/diagnose.js +164 -0
  8. package/dist/omadaClient/diagnose.js.map +1 -0
  9. package/dist/omadaClient/index.d.ts +6 -0
  10. package/dist/omadaClient/index.js +15 -0
  11. package/dist/omadaClient/index.js.map +1 -1
  12. package/dist/omadaClient/macGroups.d.ts +58 -0
  13. package/dist/omadaClient/macGroups.js +124 -0
  14. package/dist/omadaClient/macGroups.js.map +1 -0
  15. package/dist/tools/infra/actions.d.ts +1 -1
  16. package/dist/tools/infra/actions.js +1 -1
  17. package/dist/tools/omada/index.js +6 -0
  18. package/dist/tools/omada/index.js.map +1 -1
  19. package/dist/tools/omada/macGroups.d.ts +19 -0
  20. package/dist/tools/omada/macGroups.js +77 -0
  21. package/dist/tools/omada/macGroups.js.map +1 -0
  22. package/dist/tools/omada/namespace.js +8 -0
  23. package/dist/tools/omada/namespace.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +5 -1
  27. package/.codex/review-rules.md +0 -16
  28. package/.dockerignore +0 -8
  29. package/.gitattributes +0 -30
  30. package/.github/workflows/ci.yml +0 -193
  31. package/AGENTS.md +0 -389
  32. package/Dockerfile +0 -50
  33. package/docker-compose.yml +0 -23
  34. package/docs/CHANGELOG.md +0 -341
  35. package/docs/COVERAGE.md +0 -219
  36. package/docs/QUICK_START.md +0 -240
  37. package/eslint.config.js +0 -33
  38. package/n8n/ai-agent-safety-monitor.json +0 -355
  39. package/openapi.json +0 -1259
  40. package/scripts/check-version-sync.mjs +0 -69
  41. package/scripts/generate-jwt.ts +0 -121
  42. package/src/config.ts +0 -440
  43. package/src/haClient/automations.ts +0 -261
  44. package/src/haClient/calendars.ts +0 -94
  45. package/src/haClient/config.ts +0 -52
  46. package/src/haClient/devices.ts +0 -653
  47. package/src/haClient/entities.ts +0 -128
  48. package/src/haClient/events.ts +0 -689
  49. package/src/haClient/history.ts +0 -73
  50. package/src/haClient/index.ts +0 -336
  51. package/src/haClient/request.ts +0 -142
  52. package/src/haClient/services.ts +0 -45
  53. package/src/haClient/states.ts +0 -60
  54. package/src/haClient/updates.ts +0 -58
  55. package/src/haClient/wsCommand.ts +0 -44
  56. package/src/index.ts +0 -164
  57. package/src/localAI/index.ts +0 -88
  58. package/src/localAI/prompts.ts +0 -50
  59. package/src/localAI/providers/base.ts +0 -120
  60. package/src/localAI/providers/ollama.ts +0 -78
  61. package/src/localAI/providers/openai.ts +0 -86
  62. package/src/localAI/types.ts +0 -67
  63. package/src/omadaClient/auth.ts +0 -178
  64. package/src/omadaClient/client.ts +0 -337
  65. package/src/omadaClient/device.ts +0 -118
  66. package/src/omadaClient/index.ts +0 -310
  67. package/src/omadaClient/network.ts +0 -284
  68. package/src/omadaClient/request.ts +0 -365
  69. package/src/omadaClient/security.ts +0 -51
  70. package/src/omadaClient/site.ts +0 -344
  71. package/src/ownerQuestions/service.ts +0 -536
  72. package/src/permissions/index.ts +0 -143
  73. package/src/resources/index.ts +0 -213
  74. package/src/server/auth.ts +0 -175
  75. package/src/server/chatFace.ts +0 -789
  76. package/src/server/chatSlice.ts +0 -57
  77. package/src/server/common.ts +0 -170
  78. package/src/server/eventSubscription.ts +0 -343
  79. package/src/server/http.ts +0 -626
  80. package/src/server/instructions.ts +0 -97
  81. package/src/server/rateLimiter.ts +0 -227
  82. package/src/server/stdio.ts +0 -41
  83. package/src/server/stream.ts +0 -205
  84. package/src/tools/ai/analyzeSensors.ts +0 -33
  85. package/src/tools/ai/index.ts +0 -20
  86. package/src/tools/common.ts +0 -442
  87. package/src/tools/homeassistant/askOwner.ts +0 -133
  88. package/src/tools/homeassistant/automationControls.ts +0 -66
  89. package/src/tools/homeassistant/calendar.ts +0 -97
  90. package/src/tools/homeassistant/callService.ts +0 -25
  91. package/src/tools/homeassistant/controlClimate.ts +0 -68
  92. package/src/tools/homeassistant/controlCover.ts +0 -48
  93. package/src/tools/homeassistant/controlFan.ts +0 -44
  94. package/src/tools/homeassistant/controlLight.ts +0 -38
  95. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  96. package/src/tools/homeassistant/createAutomation.ts +0 -35
  97. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  98. package/src/tools/homeassistant/entityAction.ts +0 -35
  99. package/src/tools/homeassistant/entityGuard.ts +0 -28
  100. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  101. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  102. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  103. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  104. package/src/tools/homeassistant/getHistory.ts +0 -24
  105. package/src/tools/homeassistant/getState.ts +0 -24
  106. package/src/tools/homeassistant/getStates.ts +0 -46
  107. package/src/tools/homeassistant/getVersion.ts +0 -21
  108. package/src/tools/homeassistant/index.ts +0 -121
  109. package/src/tools/homeassistant/listAutomations.ts +0 -35
  110. package/src/tools/homeassistant/listEntities.ts +0 -44
  111. package/src/tools/homeassistant/listPersons.ts +0 -46
  112. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  113. package/src/tools/homeassistant/searchEntities.ts +0 -58
  114. package/src/tools/homeassistant/sendNotification.ts +0 -179
  115. package/src/tools/homeassistant/systemTools.ts +0 -52
  116. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  117. package/src/tools/index.ts +0 -78
  118. package/src/tools/infra/actions.ts +0 -95
  119. package/src/tools/infra/index.ts +0 -138
  120. package/src/tools/omada/blockClient.ts +0 -28
  121. package/src/tools/omada/cyclePoePort.ts +0 -32
  122. package/src/tools/omada/deleteClient.ts +0 -27
  123. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  124. package/src/tools/omada/getClient.ts +0 -14
  125. package/src/tools/omada/getDevice.ts +0 -14
  126. package/src/tools/omada/getFirewallSetting.ts +0 -14
  127. package/src/tools/omada/getInternetInfo.ts +0 -14
  128. package/src/tools/omada/getLanNetworkList.ts +0 -14
  129. package/src/tools/omada/getLanProfileList.ts +0 -14
  130. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  131. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  132. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  133. package/src/tools/omada/getSsidDetail.ts +0 -22
  134. package/src/tools/omada/getSsidList.ts +0 -21
  135. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  136. package/src/tools/omada/getThreatList.ts +0 -42
  137. package/src/tools/omada/getWlanGroupList.ts +0 -14
  138. package/src/tools/omada/graph.ts +0 -187
  139. package/src/tools/omada/index.ts +0 -153
  140. package/src/tools/omada/listBlockedClients.ts +0 -17
  141. package/src/tools/omada/listClients.ts +0 -14
  142. package/src/tools/omada/listClientsActivity.ts +0 -25
  143. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  144. package/src/tools/omada/listDevices.ts +0 -14
  145. package/src/tools/omada/listDevicesStats.ts +0 -41
  146. package/src/tools/omada/listMostActiveClients.ts +0 -14
  147. package/src/tools/omada/listSites.ts +0 -18
  148. package/src/tools/omada/namespace.ts +0 -1164
  149. package/src/tools/omada/searchDevices.ts +0 -20
  150. package/src/tools/omada/setClientRateLimit.ts +0 -26
  151. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  152. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  153. package/src/tools/omada/setSsidEnabled.ts +0 -38
  154. package/src/tools/omada/unblockClient.ts +0 -26
  155. package/src/tools/types.ts +0 -11
  156. package/src/tools/utils/pagination-schema.ts +0 -27
  157. package/src/types/ai/index.ts +0 -11
  158. package/src/types/ai/types.ts +0 -67
  159. package/src/types/homeassistant/automations.ts +0 -43
  160. package/src/types/homeassistant/calendars.ts +0 -26
  161. package/src/types/homeassistant/config.ts +0 -14
  162. package/src/types/homeassistant/entities.ts +0 -14
  163. package/src/types/homeassistant/errors.ts +0 -33
  164. package/src/types/homeassistant/haConfig.ts +0 -7
  165. package/src/types/homeassistant/history.ts +0 -13
  166. package/src/types/homeassistant/index.ts +0 -46
  167. package/src/types/homeassistant/request.ts +0 -6
  168. package/src/types/homeassistant/serviceCall.ts +0 -18
  169. package/src/types/homeassistant/states.ts +0 -58
  170. package/src/types/homeassistant/updates.ts +0 -28
  171. package/src/types/index.ts +0 -12
  172. package/src/types/omada/activeClientInfo.ts +0 -17
  173. package/src/types/omada/clientActivity.ts +0 -32
  174. package/src/types/omada/clientBlockStatus.ts +0 -11
  175. package/src/types/omada/clientPastConnection.ts +0 -82
  176. package/src/types/omada/index.ts +0 -14
  177. package/src/types/omada/omadaApiResponse.ts +0 -5
  178. package/src/types/omada/omadaClientInfo.ts +0 -11
  179. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  180. package/src/types/omada/omadaDeviceStats.ts +0 -31
  181. package/src/types/omada/omadaSiteSummary.ts +0 -5
  182. package/src/types/omada/oswStackDetail.ts +0 -11
  183. package/src/types/omada/paginatedResult.ts +0 -6
  184. package/src/types/omada/rateLimitProfile.ts +0 -58
  185. package/src/types/omada/threatInfo.ts +0 -38
  186. package/src/types/omada/tokenResult.ts +0 -6
  187. package/src/types.ts +0 -2
  188. package/src/utils/config-validations.ts +0 -68
  189. package/src/utils/jwt.ts +0 -112
  190. package/src/utils/logger.ts +0 -121
  191. package/src/utils/plainText.ts +0 -48
  192. package/src/utils/redact.ts +0 -59
  193. package/src/utils/sanitizeError.ts +0 -18
  194. package/src/utils/tlsDispatcher.ts +0 -69
  195. package/src/version.ts +0 -5
  196. package/tests/bdd/automation-creation.spec.ts +0 -214
  197. package/tests/bdd/automation-management.spec.ts +0 -267
  198. package/tests/bdd/calendar.spec.ts +0 -190
  199. package/tests/bdd/configuration.spec.ts +0 -283
  200. package/tests/bdd/device-control.spec.ts +0 -344
  201. package/tests/bdd/entity-listing.spec.ts +0 -208
  202. package/tests/bdd/entity-search.spec.ts +0 -183
  203. package/tests/bdd/history-retrieval.spec.ts +0 -195
  204. package/tests/bdd/notifications.spec.ts +0 -238
  205. package/tests/bdd/service-calls.spec.ts +0 -144
  206. package/tests/bdd/state-retrieval.spec.ts +0 -204
  207. package/tests/bench/hotpaths.bench.ts +0 -68
  208. package/tests/config.test.ts +0 -355
  209. package/tests/features/automation-creation.feature +0 -36
  210. package/tests/features/automation-management.feature +0 -54
  211. package/tests/features/calendar.feature +0 -37
  212. package/tests/features/configuration.feature +0 -70
  213. package/tests/features/device-control.feature +0 -100
  214. package/tests/features/entity-listing.feature +0 -51
  215. package/tests/features/entity-search.feature +0 -44
  216. package/tests/features/history-retrieval.feature +0 -38
  217. package/tests/features/notifications.feature +0 -37
  218. package/tests/features/service-calls.feature +0 -35
  219. package/tests/features/state-retrieval.feature +0 -37
  220. package/tests/fixtures/openapi-default-slice.json +0 -290
  221. package/tests/haClient/automationTrace.test.ts +0 -59
  222. package/tests/haClient/automations.test.ts +0 -149
  223. package/tests/haClient/config.test.ts +0 -117
  224. package/tests/haClient/entities.test.ts +0 -188
  225. package/tests/haClient/events.test.ts +0 -455
  226. package/tests/haClient/eventsTls.test.ts +0 -52
  227. package/tests/haClient/history.test.ts +0 -228
  228. package/tests/haClient/index.test.ts +0 -243
  229. package/tests/haClient/request.test.ts +0 -217
  230. package/tests/haClient/services.test.ts +0 -120
  231. package/tests/haClient/states.test.ts +0 -119
  232. package/tests/haClient/updates.test.ts +0 -193
  233. package/tests/index.test.ts +0 -160
  234. package/tests/integration/homeassistant.test.ts +0 -144
  235. package/tests/integration/omada.test.ts +0 -167
  236. package/tests/integration/server.test.ts +0 -151
  237. package/tests/localAI/index.test.ts +0 -177
  238. package/tests/localAI/ollama.test.ts +0 -187
  239. package/tests/localAI/openai.test.ts +0 -257
  240. package/tests/omadaClient/actions.test.ts +0 -161
  241. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  242. package/tests/omadaClient/ntp.test.ts +0 -381
  243. package/tests/omadaClient/site.test.ts +0 -37
  244. package/tests/ownerQuestions/service.test.ts +0 -499
  245. package/tests/ownerQuestions/tools.test.ts +0 -186
  246. package/tests/permissions/index.test.ts +0 -217
  247. package/tests/resources/index.test.ts +0 -288
  248. package/tests/scripts/generateJwt.test.ts +0 -44
  249. package/tests/security/accessControl.test.ts +0 -223
  250. package/tests/security/authentication.test.ts +0 -117
  251. package/tests/security/configuration.test.ts +0 -170
  252. package/tests/security/cryptoTransport.test.ts +0 -172
  253. package/tests/security/dependencies.test.ts +0 -152
  254. package/tests/security/injection.test.ts +0 -117
  255. package/tests/security/insecureDesign.test.ts +0 -121
  256. package/tests/security/logging.test.ts +0 -131
  257. package/tests/security/resourceConsumption.test.ts +0 -142
  258. package/tests/server/auth.test.ts +0 -249
  259. package/tests/server/chatFace.test.ts +0 -320
  260. package/tests/server/common.test.ts +0 -84
  261. package/tests/server/http.test.ts +0 -561
  262. package/tests/server/instructions.test.ts +0 -152
  263. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  264. package/tests/server/stdio.test.ts +0 -99
  265. package/tests/server/stream.test.ts +0 -285
  266. package/tests/server/toolVisibility.http.test.ts +0 -111
  267. package/tests/server/toolVisibility.test.ts +0 -129
  268. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  269. package/tests/tools/common.test.ts +0 -123
  270. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  271. package/tests/tools/infra/invokeAction.test.ts +0 -280
  272. package/tests/tools/omada/graph.test.ts +0 -385
  273. package/tests/tools/omada/handlers.test.ts +0 -897
  274. package/tests/tools/omada/namespace.test.ts +0 -483
  275. package/tests/tools/registry.test.ts +0 -168
  276. package/tests/utils/config-validations.test.ts +0 -352
  277. package/tests/utils/jwt.test.ts +0 -115
  278. package/tests/utils/logger.test.ts +0 -327
  279. package/tests/utils/redact.test.ts +0 -105
  280. package/tsconfig.json +0 -24
  281. 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
- });