@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,483 +0,0 @@
1
- /**
2
- * Unit tests for the Omada resource-graph namespace manifest.
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
- import {
7
- OMADA_RESOURCES,
8
- auditFilters,
9
- childrenOf,
10
- getResourceNode,
11
- normalizePath,
12
- resolveLogWindowMs,
13
- resolveUsageWindowSec,
14
- type ReadArgs,
15
- } from '../../../src/tools/omada/namespace.js';
16
- import { Permission } from '../../../src/permissions/index.js';
17
-
18
- /** A minimal OmadaClient stand-in that records the last readResource() call. */
19
- function makeRecordingClient(): { client: any; calls: Array<Record<string, unknown>> } {
20
- const calls: Array<Record<string, unknown>> = [];
21
- const client = {
22
- readResource: (opts: Record<string, unknown>) => {
23
- calls.push(opts);
24
- return Promise.resolve({ ok: true });
25
- },
26
- // Typed client methods backing a few graph nodes (recorded like readResource).
27
- searchDevices: (searchKey: string) => {
28
- calls.push({ __method: 'searchDevices', searchKey });
29
- return Promise.resolve([]);
30
- },
31
- };
32
- return { client, calls };
33
- }
34
-
35
- async function fetchOpts(path: string, args: ReadArgs = {}): Promise<Record<string, unknown>> {
36
- const node = getResourceNode(path);
37
- if (!node?.fetch) {
38
- throw new Error(`no fetch for ${path}`);
39
- }
40
- const { client, calls } = makeRecordingClient();
41
- await node.fetch(client, args);
42
- return calls[0];
43
- }
44
-
45
- describe('Omada namespace - normalizePath', () => {
46
- it('maps empty / root to "/"', () => {
47
- expect(normalizePath('')).toBe('/');
48
- expect(normalizePath('/')).toBe('/');
49
- });
50
-
51
- it('adds a leading slash', () => {
52
- expect(normalizePath('gateway')).toBe('/gateway');
53
- expect(normalizePath('gateway/wan')).toBe('/gateway/wan');
54
- });
55
-
56
- it('strips a trailing slash', () => {
57
- expect(normalizePath('/gateway/')).toBe('/gateway');
58
- expect(normalizePath('/clients/active/')).toBe('/clients/active');
59
- });
60
-
61
- it('trims whitespace', () => {
62
- expect(normalizePath(' /gateway ')).toBe('/gateway');
63
- });
64
- });
65
-
66
- describe('Omada namespace - getResourceNode', () => {
67
- it('resolves a known node and normalizes input', () => {
68
- expect(getResourceNode('/gateway/wan')?.path).toBe('/gateway/wan');
69
- expect(getResourceNode('gateway/wan/')?.path).toBe('/gateway/wan');
70
- });
71
-
72
- it('returns undefined for unknown paths', () => {
73
- expect(getResourceNode('/does/not/exist')).toBeUndefined();
74
- });
75
-
76
- it('resolves the root container', () => {
77
- const root = getResourceNode('/');
78
- expect(root?.kind).toBe('container');
79
- expect(root?.fetch).toBeUndefined();
80
- });
81
- });
82
-
83
- describe('Omada namespace - childrenOf', () => {
84
- it('lists top-level children at root', () => {
85
- const paths = childrenOf('/').map((n) => n.path);
86
- expect(paths).toContain('/sites');
87
- expect(paths).toContain('/devices');
88
- expect(paths).toContain('/clients');
89
- expect(paths).toContain('/gateway');
90
- expect(paths).toContain('/network');
91
- expect(paths).toContain('/wifi');
92
- expect(paths).toContain('/events');
93
- expect(paths).toContain('/dashboard');
94
- expect(paths).toContain('/firmware');
95
- });
96
-
97
- it('does not include grandchildren at root', () => {
98
- const paths = childrenOf('/').map((n) => n.path);
99
- expect(paths).not.toContain('/gateway/wan');
100
- expect(paths).not.toContain('/clients/active');
101
- });
102
-
103
- it('lists direct children of a container', () => {
104
- const paths = childrenOf('/gateway').map((n) => n.path);
105
- expect(paths).toEqual(expect.arrayContaining(['/gateway/wan', '/gateway/health']));
106
- });
107
-
108
- it('returns no children for a leaf', () => {
109
- expect(childrenOf('/network/firewall')).toEqual([]);
110
- });
111
- });
112
-
113
- describe('Omada namespace - manifest invariants', () => {
114
- it('every node has a unique, normalized path', () => {
115
- const seen = new Set<string>();
116
- for (const node of OMADA_RESOURCES) {
117
- expect(node.path).toBe(normalizePath(node.path));
118
- expect(seen.has(node.path)).toBe(false);
119
- seen.add(node.path);
120
- }
121
- });
122
-
123
- it('containers are browse-only (no fetch); collections/leaves are readable', () => {
124
- for (const node of OMADA_RESOURCES) {
125
- if (node.kind === 'container') {
126
- expect(node.fetch).toBeUndefined();
127
- } else {
128
- expect(typeof node.fetch).toBe('function');
129
- }
130
- }
131
- });
132
-
133
- it('every non-root node has an existing parent in the manifest', () => {
134
- const byPath = new Map(OMADA_RESOURCES.map((n) => [n.path, n]));
135
- for (const node of OMADA_RESOURCES) {
136
- if (node.path === '/') {
137
- continue;
138
- }
139
- const idx = node.path.lastIndexOf('/');
140
- const parent = idx <= 0 ? '/' : node.path.slice(0, idx);
141
- expect(byPath.has(parent)).toBe(true);
142
- }
143
- });
144
-
145
- it('every readable node declares a non-zero permission bit (fail-closed)', () => {
146
- for (const node of OMADA_RESOURCES) {
147
- if (node.fetch) {
148
- expect(node.permission).toBeGreaterThan(0);
149
- }
150
- }
151
- });
152
-
153
- it('reads are QUERY except the ADMIN-gated security and audit subtrees', () => {
154
- for (const node of OMADA_RESOURCES) {
155
- if (!node.fetch) {
156
- continue;
157
- }
158
- if (node.path.startsWith('/security') || node.path.startsWith('/audit')) {
159
- expect(node.permission).toBe(Permission.ADMIN);
160
- } else {
161
- expect((node.permission & Permission.QUERY) === Permission.QUERY).toBe(true);
162
- }
163
- }
164
- });
165
-
166
- it('the /security and /audit containers are ADMIN-gated', () => {
167
- expect(getResourceNode('/security')?.permission).toBe(Permission.ADMIN);
168
- expect(getResourceNode('/audit')?.permission).toBe(Permission.ADMIN);
169
- });
170
-
171
- it('paginated nodes declare a default page size', () => {
172
- for (const node of OMADA_RESOURCES) {
173
- if (node.paginated) {
174
- expect(typeof node.defaultPageSize).toBe('number');
175
- }
176
- }
177
- });
178
- });
179
-
180
- describe('Omada namespace - time-window helpers', () => {
181
- const NOW_MS = 1_700_000_000_000;
182
-
183
- describe('resolveLogWindowMs (epoch ms, default 7 days)', () => {
184
- it('defaults to the last 7 days when no params are given', () => {
185
- const { timeStart, timeEnd } = resolveLogWindowMs(undefined, NOW_MS);
186
- expect(timeEnd).toBe(NOW_MS);
187
- expect(timeStart).toBe(NOW_MS - 7 * 24 * 60 * 60 * 1000);
188
- });
189
-
190
- it('honours explicit startTime/endTime params', () => {
191
- const { timeStart, timeEnd } = resolveLogWindowMs({ startTime: '111', endTime: '222' }, NOW_MS);
192
- expect(timeStart).toBe(111);
193
- expect(timeEnd).toBe(222);
194
- });
195
-
196
- it('defaults only the missing bound', () => {
197
- const { timeStart, timeEnd } = resolveLogWindowMs({ endTime: '500' }, NOW_MS);
198
- expect(timeEnd).toBe(500);
199
- expect(timeStart).toBe(500 - 7 * 24 * 60 * 60 * 1000);
200
- });
201
- });
202
-
203
- describe('resolveUsageWindowSec (epoch seconds, default 24h)', () => {
204
- it('defaults to the last 24h in seconds when no params are given', () => {
205
- const { start, end } = resolveUsageWindowSec(undefined, NOW_MS);
206
- expect(end).toBe(Math.floor(NOW_MS / 1000));
207
- expect(start).toBe(Math.floor(NOW_MS / 1000) - 24 * 60 * 60);
208
- });
209
-
210
- it('honours explicit startTime/endTime params (seconds)', () => {
211
- const { start, end } = resolveUsageWindowSec({ startTime: '1000', endTime: '2000' }, NOW_MS);
212
- expect(start).toBe(1000);
213
- expect(end).toBe(2000);
214
- });
215
- });
216
- });
217
-
218
- describe('Omada namespace - fixed node wiring', () => {
219
- it('/events forwards a default millisecond window as filters.timeStart/timeEnd', async () => {
220
- const opts = await fetchOpts('/events');
221
- expect(opts.pathTemplate).toBe('/sites/{siteId}/logs/events');
222
- expect(opts.paginated).toBe(true);
223
- const query = opts.query as Record<string, number>;
224
- expect(query['filters.timeStart']).toBeTypeOf('number');
225
- expect(query['filters.timeEnd']).toBeTypeOf('number');
226
- expect(query['filters.timeEnd']).toBeGreaterThan(query['filters.timeStart']);
227
- // 13-digit epoch-ms, not seconds.
228
- expect(String(query['filters.timeEnd']).length).toBeGreaterThanOrEqual(13);
229
- });
230
-
231
- it('/events passes an optional module filter through', async () => {
232
- const opts = await fetchOpts('/events', { params: { module: 'Device' } });
233
- expect((opts.query as Record<string, unknown>)['filters.module']).toBe('Device');
234
- });
235
-
236
- it('/events/alerts forwards filters.resolved only for valid boolean strings', async () => {
237
- const resolved = await fetchOpts('/events/alerts', { params: { resolved: 'true' } });
238
- expect((resolved.query as Record<string, unknown>)['filters.resolved']).toBe('true');
239
- const ignored = await fetchOpts('/events/alerts', { params: { resolved: 'maybe' } });
240
- expect((ignored.query as Record<string, unknown>)['filters.resolved']).toBeUndefined();
241
- });
242
-
243
- it('/dashboard/cpu and /dashboard/memory forward a second-precision window', async () => {
244
- for (const path of ['/dashboard/cpu', '/dashboard/memory']) {
245
- const opts = await fetchOpts(path);
246
- const query = opts.query as Record<string, number>;
247
- expect(query.start).toBeTypeOf('number');
248
- expect(query.end).toBeTypeOf('number');
249
- expect(query.end).toBeGreaterThan(query.start);
250
- // 10-digit epoch-seconds, not ms.
251
- expect(String(query.end).length).toBeLessThanOrEqual(11);
252
- }
253
- });
254
-
255
- it('/devices/pending, /wifi/rogue, /wifi/wids are paginated single-page reads', async () => {
256
- for (const path of ['/devices/pending', '/wifi/rogue', '/wifi/wids']) {
257
- const opts = await fetchOpts(path, { page: 2, pageSize: 25 });
258
- expect(opts.paginated).toBe(true);
259
- expect(opts.page).toBe(2);
260
- expect(opts.pageSize).toBe(25);
261
- }
262
- });
263
- });
264
-
265
- describe('Omada namespace - Tier-2 / loose-end node wiring', () => {
266
- it('/devices/search forwards searchKey to the typed client method', async () => {
267
- const opts = await fetchOpts('/devices/search', { params: { searchKey: 'switch' } });
268
- expect(opts.__method).toBe('searchDevices');
269
- expect(opts.searchKey).toBe('switch');
270
- });
271
-
272
- it('/devices/cable-test is a browse-only container', () => {
273
- const node = getResourceNode('/devices/cable-test');
274
- expect(node?.kind).toBe('container');
275
- expect(node?.fetch).toBeUndefined();
276
- const children = childrenOf('/devices/cable-test').map((n) => n.path);
277
- expect(children).toEqual(
278
- expect.arrayContaining(['/devices/cable-test/ports', '/devices/cable-test/results', '/devices/cable-test/logs'])
279
- );
280
- });
281
-
282
- it('cable-test nodes substitute switchMac into the path template', async () => {
283
- const cases: Array<[string, string]> = [
284
- ['/devices/cable-test/ports', '/sites/{siteId}/cable-test/switches/{switchMac}/ports'],
285
- ['/devices/cable-test/results', '/sites/{siteId}/cable-test/switches/{switchMac}/full-results'],
286
- ['/devices/cable-test/logs', '/sites/{siteId}/cable-test/switches/{switchMac}/logs'],
287
- ];
288
- for (const [path, tmpl] of cases) {
289
- const opts = await fetchOpts(path, { params: { switchMac: 'AA-BB-CC-DD-EE-FF' } });
290
- expect(opts.pathTemplate).toBe(tmpl);
291
- expect((opts.pathParams as Record<string, string>).switchMac).toBe('AA-BB-CC-DD-EE-FF');
292
- }
293
- });
294
-
295
- it('/network/port-forwarding/rules reads the NAT rule list, paginated', async () => {
296
- const opts = await fetchOpts('/network/port-forwarding/rules', { page: 3, pageSize: 20 });
297
- expect(opts.pathTemplate).toBe('/sites/{siteId}/nat/port-forwardings');
298
- expect(opts.paginated).toBe(true);
299
- expect(opts.page).toBe(3);
300
- expect(opts.pageSize).toBe(20);
301
- });
302
-
303
- it('/network/dhcp-leases reads the DHCP user-list, paginated', async () => {
304
- const opts = await fetchOpts('/network/dhcp-leases');
305
- expect(opts.pathTemplate).toBe('/sites/{siteId}/setting/service/dhcp/user-list');
306
- expect(opts.paginated).toBe(true);
307
- });
308
-
309
- it('/network/load-balance reads WAN load-balance status (no params)', async () => {
310
- const opts = await fetchOpts('/network/load-balance');
311
- expect(opts.pathTemplate).toBe('/sites/{siteId}/internet/load-balance/status');
312
- expect(opts.paginated).toBeFalsy();
313
- });
314
-
315
- it('/dashboard/client-distribution reads with no time window', async () => {
316
- const opts = await fetchOpts('/dashboard/client-distribution');
317
- expect(opts.pathTemplate).toBe('/sites/{siteId}/dashboard/client-distribution');
318
- expect(opts.query).toBeUndefined();
319
- });
320
-
321
- it('/dashboard/traffic-distribution and /dashboard/traffic-activities forward a second-precision window', async () => {
322
- for (const path of ['/dashboard/traffic-distribution', '/dashboard/traffic-activities']) {
323
- const opts = await fetchOpts(path);
324
- const query = opts.query as Record<string, number>;
325
- expect(query.start).toBeTypeOf('number');
326
- expect(query.end).toBeTypeOf('number');
327
- expect(query.end).toBeGreaterThan(query.start);
328
- // 10-digit epoch-seconds, not ms.
329
- expect(String(query.end).length).toBeLessThanOrEqual(11);
330
- }
331
- });
332
- });
333
-
334
- describe('Omada namespace - Tier-3 curated node wiring', () => {
335
- // Each entry: graph path → expected controller path template (all paginated unless noted).
336
- const PAGINATED: Array<[string, string]> = [
337
- ['/devices/poe', '/sites/{siteId}/switches/ports/poe-info'],
338
- ['/network/dhcp-reservations', '/sites/{siteId}/setting/service/dhcp'],
339
- ['/network/static-routes', '/sites/{siteId}/routing/static-routings'],
340
- ['/network/ip-mac-binding', '/sites/{siteId}/ip-mac-binds'],
341
- ['/network/acls/gateway', '/sites/{siteId}/acls/osg-acls'],
342
- ['/network/acls/switch', '/sites/{siteId}/acls/osw-acls'],
343
- ['/network/acls/eap', '/sites/{siteId}/acls/eap-acls'],
344
- ['/network/url-filters/gateway', '/sites/{siteId}/url-filters/gateway'],
345
- ['/network/url-filters/eap', '/sites/{siteId}/url-filters/eap'],
346
- ['/network/mac-filters/allow', '/sites/{siteId}/mac-filters/allow'],
347
- ['/network/mac-filters/deny', '/sites/{siteId}/mac-filters/deny'],
348
- ];
349
- const SIMPLE: Array<[string, string]> = [
350
- ['/devices/lldp', '/sites/{siteId}/lldp'],
351
- ['/network/attack-defense', '/sites/{siteId}/attack-defense'],
352
- ['/wifi/band-steering', '/sites/{siteId}/band-steering'],
353
- ];
354
-
355
- it('paginated Tier-3 nodes map to the correct controller path and forward page/pageSize', async () => {
356
- for (const [path, tmpl] of PAGINATED) {
357
- const opts = await fetchOpts(path, { page: 2, pageSize: 10 });
358
- expect(opts.pathTemplate).toBe(tmpl);
359
- expect(opts.paginated).toBe(true);
360
- expect(opts.page).toBe(2);
361
- expect(opts.pageSize).toBe(10);
362
- }
363
- });
364
-
365
- it('simple Tier-3 nodes map to the correct controller path with no pagination', async () => {
366
- for (const [path, tmpl] of SIMPLE) {
367
- const opts = await fetchOpts(path);
368
- expect(opts.pathTemplate).toBe(tmpl);
369
- expect(opts.paginated).toBeFalsy();
370
- }
371
- });
372
-
373
- it('Tier-3 grouping containers are browse-only with the expected children', () => {
374
- const groups: Array<[string, string[]]> = [
375
- ['/network/acls', ['/network/acls/gateway', '/network/acls/switch', '/network/acls/eap']],
376
- ['/network/url-filters', ['/network/url-filters/gateway', '/network/url-filters/eap']],
377
- ['/network/mac-filters', ['/network/mac-filters/allow', '/network/mac-filters/deny']],
378
- ];
379
- for (const [parent, kids] of groups) {
380
- expect(getResourceNode(parent)?.kind).toBe('container');
381
- expect(getResourceNode(parent)?.fetch).toBeUndefined();
382
- expect(childrenOf(parent).map((n) => n.path)).toEqual(expect.arrayContaining(kids));
383
- }
384
- });
385
- });
386
-
387
- describe('Omada namespace - Tier-4 node wiring (VPN / profiles / schedules / backup / audit)', () => {
388
- it('exposes the Tier-4 subtrees at the root', () => {
389
- const paths = childrenOf('/').map((n) => n.path);
390
- expect(paths).toEqual(expect.arrayContaining(['/vpn', '/profiles', '/schedules', '/backup', '/audit']));
391
- });
392
-
393
- it('Tier-4 grouping containers are browse-only with the expected children', () => {
394
- const groups: Array<[string, string[]]> = [
395
- ['/vpn', ['/vpn/site-to-site', '/vpn/client-to-site', '/vpn/wireguard', '/vpn/ipsec-stats']],
396
- ['/vpn/client-to-site', ['/vpn/client-to-site/servers', '/vpn/client-to-site/clients']],
397
- ['/profiles', ['/profiles/ppsk', '/profiles/time-range']],
398
- ['/schedules', ['/schedules/poe', '/schedules/port', '/schedules/upgrade']],
399
- ['/backup', ['/backup/files', '/backup/result']],
400
- ['/audit', ['/audit/site', '/audit/global']],
401
- ];
402
- for (const [parent, kids] of groups) {
403
- expect(getResourceNode(parent)?.kind).toBe('container');
404
- expect(getResourceNode(parent)?.fetch).toBeUndefined();
405
- expect(childrenOf(parent).map((n) => n.path)).toEqual(expect.arrayContaining(kids));
406
- }
407
- });
408
-
409
- it('simple Tier-4 reads map to the correct controller path with no pagination', async () => {
410
- const SIMPLE: Array<[string, string]> = [
411
- ['/vpn/site-to-site', '/sites/{siteId}/vpn/site-to-site-vpns'],
412
- ['/vpn/client-to-site/servers', '/sites/{siteId}/vpn/client-to-site-vpn-servers'],
413
- ['/vpn/client-to-site/clients', '/sites/{siteId}/vpn/client-to-site-vpn-clients'],
414
- ['/vpn/wireguard', '/sites/{siteId}/vpn/wireguards'],
415
- ['/profiles/time-range', '/sites/{siteId}/time-range-profiles'],
416
- ['/schedules/poe', '/sites/{siteId}/poe-schedules'],
417
- ['/schedules/port', '/sites/{siteId}/port-schedules'],
418
- ['/schedules/upgrade', '/sites/{siteId}/upgrade-schedules'],
419
- ['/backup/files', '/sites/{siteId}/maintenance/backup/files'],
420
- ['/backup/result', '/sites/{siteId}/backup/result'],
421
- ];
422
- for (const [path, tmpl] of SIMPLE) {
423
- const opts = await fetchOpts(path);
424
- expect(opts.pathTemplate).toBe(tmpl);
425
- expect(opts.paginated).toBeFalsy();
426
- }
427
- });
428
-
429
- it('/vpn/site-to-site fetches a single tunnel by id', async () => {
430
- const opts = await fetchOpts('/vpn/site-to-site', { id: 'vpn-123' });
431
- expect(opts.pathTemplate).toBe('/sites/{siteId}/vpn/site-to-site-vpns/{vpnId}');
432
- expect((opts.pathParams as Record<string, string>).vpnId).toBe('vpn-123');
433
- });
434
-
435
- it('/vpn/ipsec-stats is a paginated single-page read', async () => {
436
- const opts = await fetchOpts('/vpn/ipsec-stats', { page: 2, pageSize: 25 });
437
- expect(opts.pathTemplate).toBe('/sites/{siteId}/setting/vpn/stats/ipsec');
438
- expect(opts.paginated).toBe(true);
439
- expect(opts.page).toBe(2);
440
- expect(opts.pageSize).toBe(25);
441
- });
442
-
443
- it('/profiles/ppsk forwards the required type as a query param', async () => {
444
- const opts = await fetchOpts('/profiles/ppsk', { params: { type: '1' } });
445
- expect(opts.pathTemplate).toBe('/sites/{siteId}/ppsk-profiles');
446
- expect((opts.query as Record<string, unknown>).type).toBe(1);
447
- });
448
-
449
- it('/audit/site is a site-scoped paginated ADMIN read', async () => {
450
- const opts = await fetchOpts('/audit/site', { page: 3, pageSize: 20 });
451
- expect(opts.pathTemplate).toBe('/sites/{siteId}/audit-logs');
452
- expect(opts.paginated).toBe(true);
453
- expect(opts.page).toBe(3);
454
- expect(opts.pageSize).toBe(20);
455
- // No filters supplied → empty query object.
456
- expect(opts.query).toEqual({});
457
- });
458
-
459
- it('/audit/global is NOT site-scoped and reads the controller-wide log', async () => {
460
- const opts = await fetchOpts('/audit/global');
461
- expect(opts.pathTemplate).toBe('/audit-logs');
462
- expect(opts.siteScoped).toBe(false);
463
- expect(opts.paginated).toBe(true);
464
- });
465
- });
466
-
467
- describe('Omada namespace - auditFilters helper', () => {
468
- it('returns an empty object when no params are given', () => {
469
- expect(auditFilters(undefined)).toEqual({});
470
- expect(auditFilters({})).toEqual({});
471
- });
472
-
473
- it('maps startTime/endTime to numeric filters.* keys', () => {
474
- expect(auditFilters({ startTime: '1700000000000', endTime: '1700003600000' })).toEqual({
475
- 'filters.startTime': 1700000000000,
476
- 'filters.endTime': 1700003600000,
477
- });
478
- });
479
-
480
- it('passes searchKey through and omits absent bounds', () => {
481
- expect(auditFilters({ searchKey: 'login' })).toEqual({ searchKey: 'login' });
482
- });
483
- });
@@ -1,168 +0,0 @@
1
- /**
2
- * Tool registration tests
3
- * Tests the individual tool registration functions using McpServer
4
- */
5
-
6
- import { describe, it, expect, vi, beforeEach } from 'vitest';
7
- import { McpServer } from '@modelcontextprotocol/server';
8
- import { registerAllTools } from '../../src/tools/index.js';
9
-
10
- // Mock HaClient
11
- const mockClient = {
12
- getStates: vi.fn().mockResolvedValue([]),
13
- getState: vi.fn().mockResolvedValue({}),
14
- callService: vi.fn().mockResolvedValue({ context: {} }),
15
- getEntitiesByDomain: vi.fn().mockResolvedValue([]),
16
- searchEntities: vi.fn().mockResolvedValue([]),
17
- getAllSensors: vi.fn().mockResolvedValue([]),
18
- getDomainSummary: vi.fn().mockResolvedValue({}),
19
- getHistory: vi.fn().mockResolvedValue([]),
20
- getVersion: vi.fn().mockResolvedValue({ version: '2024.1.0' }),
21
- getLogbook: vi.fn().mockResolvedValue([]),
22
- listAutomations: vi.fn().mockResolvedValue([]),
23
- triggerAutomation: vi.fn().mockResolvedValue({}),
24
- createAutomation: vi.fn().mockResolvedValue({ id: 'test-id' }),
25
- deleteAutomation: vi.fn().mockResolvedValue({}),
26
- getAutomationTrace: vi.fn().mockResolvedValue({}),
27
- checkUpdates: vi.fn().mockResolvedValue({ available: [] }),
28
- devices: {
29
- triggerAutomation: vi.fn().mockResolvedValue({}),
30
- reloadAutomations: vi.fn().mockResolvedValue({}),
31
- },
32
- };
33
-
34
- describe('Tool Registration', () => {
35
- let server: McpServer;
36
-
37
- beforeEach(() => {
38
- vi.clearAllMocks();
39
-
40
- server = new McpServer({
41
- name: 'test-server',
42
- version: '1.0.0',
43
- });
44
- });
45
-
46
- describe('registerAllTools', () => {
47
- it('should register all tools without errors', () => {
48
- expect(() => registerAllTools({ server, haClient: mockClient as any })).not.toThrow();
49
- });
50
-
51
- it('should register tools on McpServer', () => {
52
- registerAllTools({ server, haClient: mockClient as any });
53
-
54
- // The server should have handlers registered
55
- expect(server).toBeDefined();
56
- });
57
-
58
- it('should work with optional aiClient', () => {
59
- expect(() => registerAllTools({ server, haClient: mockClient as any, aiClient: undefined })).not.toThrow();
60
- });
61
-
62
- it('should work without haClient (Omada-only mode)', () => {
63
- expect(() => registerAllTools({ server })).not.toThrow();
64
- });
65
- });
66
-
67
- describe('Tool Categories', () => {
68
- it('should include core entity tools', () => {
69
- const coreTools = [
70
- 'getStates',
71
- 'getState',
72
- 'callService',
73
- 'getEntitiesByDomain',
74
- 'searchEntities',
75
- 'getAllSensors',
76
- 'listEntities',
77
- 'getDomainSummary',
78
- ];
79
-
80
- expect(coreTools.length).toBeGreaterThan(5);
81
- });
82
-
83
- it('should include automation tools', () => {
84
- const automationTools = [
85
- 'listAutomations',
86
- 'triggerAutomation',
87
- 'enableAutomation',
88
- 'disableAutomation',
89
- 'toggleAutomation',
90
- 'reloadAutomations',
91
- 'createAutomation',
92
- 'deleteAutomation',
93
- 'getAutomationTrace',
94
- ];
95
-
96
- expect(automationTools).toContain('listAutomations');
97
- expect(automationTools).toContain('createAutomation');
98
- });
99
-
100
- it('should include device control tools', () => {
101
- const deviceTools = [
102
- 'controlLight',
103
- 'controlClimate',
104
- 'controlMediaPlayer',
105
- 'controlCover',
106
- 'controlFan',
107
- ];
108
-
109
- expect(deviceTools).toContain('controlLight');
110
- expect(deviceTools).toContain('controlClimate');
111
- });
112
-
113
- it('should include utility tools', () => {
114
- const utilityTools = [
115
- 'getHistory',
116
- 'getVersion',
117
- 'entityAction',
118
- 'activateScene',
119
- 'runScript',
120
- 'sendNotification',
121
- ];
122
-
123
- expect(utilityTools).toContain('getHistory');
124
- expect(utilityTools).toContain('getVersion');
125
- });
126
-
127
- it('should include system tools', () => {
128
- const systemTools = [
129
- 'restartHomeAssistant',
130
- 'getSystemLog',
131
- 'checkUpdates',
132
- ];
133
-
134
- expect(systemTools).toContain('restartHomeAssistant');
135
- expect(systemTools).toContain('checkUpdates');
136
- });
137
- });
138
-
139
- describe('Tool Response Format', () => {
140
- it('should return text content type', () => {
141
- const response = {
142
- content: [
143
- {
144
- type: 'text',
145
- text: JSON.stringify({ success: true }),
146
- },
147
- ],
148
- };
149
-
150
- expect(response.content[0].type).toBe('text');
151
- });
152
-
153
- it('should return valid JSON in text field', () => {
154
- const response = {
155
- content: [
156
- {
157
- type: 'text',
158
- text: JSON.stringify({ entity_id: 'light.living_room', state: 'on' }),
159
- },
160
- ],
161
- };
162
-
163
- expect(() => JSON.parse(response.content[0].text)).not.toThrow();
164
- const parsed = JSON.parse(response.content[0].text);
165
- expect(parsed.entity_id).toBe('light.living_room');
166
- });
167
- });
168
- });