@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,52 +0,0 @@
1
- /**
2
- * System tools - Restart, logs, updates
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { HaClient } from '../../haClient/index.js';
7
- import { emptySchema, systemLogSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
- import type { z } from 'zod';
9
-
10
- type SystemLogArgs = z.infer<typeof systemLogSchema>;
11
-
12
- export function registerRestartHomeAssistantTool(server: McpServer, client: HaClient): void {
13
- server.registerTool(
14
- 'restartHomeAssistant',
15
- {
16
- description: 'Restart the Home Assistant server. Use with caution.',
17
- inputSchema: emptySchema,
18
- },
19
- wrapToolHandler('restartHomeAssistant', async () => {
20
- await client.restartServer();
21
- return toToolResult({ success: true, message: 'Home Assistant restart initiated' });
22
- }, Permission.ADMIN)
23
- );
24
- }
25
-
26
- export function registerGetSystemLogTool(server: McpServer, client: HaClient): void {
27
- server.registerTool(
28
- 'getSystemLog',
29
- {
30
- description: 'Get system log entries from the Home Assistant logbook. Shows recent events and state changes. Default: last 24h, 100 entries.',
31
- inputSchema: systemLogSchema,
32
- },
33
- wrapToolHandler('getSystemLog', async ({ hours = 24, entity_id, limit = 100 }: SystemLogArgs) => {
34
- const log = await client.getSystemLog({ hours, entity_id, limit });
35
- return toToolResult({ hours, limit, entity_id: entity_id || null, entries: log });
36
- }, Permission.ADMIN)
37
- );
38
- }
39
-
40
- export function registerCheckUpdatesTool(server: McpServer, client: HaClient): void {
41
- server.registerTool(
42
- 'checkUpdates',
43
- {
44
- description: 'Check for available updates for Home Assistant Core, Supervisor, and OS. Requires Home Assistant OS or Supervised installation.',
45
- inputSchema: emptySchema,
46
- },
47
- wrapToolHandler('checkUpdates', async () => {
48
- const updates = await client.getAvailableUpdates();
49
- return toToolResult(updates);
50
- }, Permission.ADMIN)
51
- );
52
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * triggerAutomation tool - Manually trigger an automation
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { HaClient } from '../../haClient/index.js';
7
- import { triggerAutomationSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
- import type { z } from 'zod';
9
-
10
- type TriggerAutomationArgs = z.infer<typeof triggerAutomationSchema>;
11
-
12
- export function registerTriggerAutomationTool(server: McpServer, client: HaClient): void {
13
- server.registerTool(
14
- 'triggerAutomation',
15
- {
16
- description: 'Manually trigger a Home Assistant automation. Can optionally pass variables to the automation.',
17
- inputSchema: triggerAutomationSchema,
18
- },
19
- wrapToolHandler('triggerAutomation', async ({ entity_id, variables }: TriggerAutomationArgs) => {
20
- const result = await client.triggerAutomation(entity_id, variables as Record<string, unknown> | undefined);
21
- return toToolResult({ success: true, entity_id, message: 'Automation triggered successfully', context: result.context });
22
- }, Permission.CONFIGURE)
23
- );
24
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * Tools index - registers all MCP tools organized by domain
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { HaClient } from '../haClient/index.js';
7
- import type { LocalAIClient } from '../localAI/index.js';
8
- import type { OmadaClient } from '../omadaClient/index.js';
9
- import { logger } from '../utils/logger.js';
10
-
11
- // Tool registration functions by domain
12
- import { registerHomeAssistantTools } from './homeassistant/index.js';
13
- import { registerOmadaTools, type OmadaRegistrationMode } from './omada/index.js';
14
- import { registerAITools } from './ai/index.js';
15
- import { registerInfraTools, type RestAction } from './infra/index.js';
16
- import { registerOwnerQuestionTools } from './homeassistant/askOwner.js';
17
- import type { OwnerQuestionService } from '../ownerQuestions/service.js';
18
-
19
- export interface RegisterToolsOptions {
20
- server: McpServer;
21
- haClient?: HaClient;
22
- omadaClient?: OmadaClient;
23
- aiClient?: LocalAIClient;
24
- /** Pre-registered REST actions for invokeAction (empty/undefined = tool not registered). */
25
- restActions?: Record<string, RestAction>;
26
- /** Tool registration strategy for the Omada plugin (default 'eager'). */
27
- toolRegistrationMode?: OmadaRegistrationMode;
28
- /** Owner questions (askOwner & co.); registered only when the service is running. */
29
- ownerQuestions?: OwnerQuestionService;
30
- }
31
-
32
- /**
33
- * Register all available tools based on configured clients
34
- */
35
- export function registerAllTools(options: RegisterToolsOptions): void {
36
- const { server, haClient, omadaClient, aiClient, restActions, toolRegistrationMode, ownerQuestions } = options;
37
- logger.debug('Registering all tools');
38
-
39
- let totalTools = 0;
40
-
41
- // Home Assistant tools (if client provided)
42
- if (haClient) {
43
- const haToolCount = registerHomeAssistantTools(server, haClient);
44
- totalTools += haToolCount;
45
- if (ownerQuestions) {
46
- totalTools += registerOwnerQuestionTools(server, ownerQuestions);
47
- }
48
- }
49
-
50
- // Omada tools (if client provided)
51
- if (omadaClient) {
52
- const omadaToolCount = registerOmadaTools(server, omadaClient, toolRegistrationMode);
53
- totalTools += omadaToolCount;
54
- }
55
-
56
- // AI tools (always register, but may be disabled if no aiClient)
57
- const aiToolCount = registerAITools(server, aiClient);
58
- totalTools += aiToolCount;
59
-
60
- // Infra tools (only when REST actions are configured)
61
- totalTools += registerInfraTools(server, restActions ?? {});
62
-
63
- logger.info('All tools registered successfully', {
64
- totalTools,
65
- haEnabled: !!haClient,
66
- omadaEnabled: !!omadaClient,
67
- aiEnabled: !!aiClient,
68
- infraEnabled: Object.keys(restActions ?? {}).length > 0,
69
- });
70
- }
71
-
72
- // Re-export common utilities
73
- export { toToolResult, wrapToolHandler, Permission } from './common.js';
74
-
75
- // Re-export domain-specific registration functions for selective use
76
- export { registerHomeAssistantTools } from './homeassistant/index.js';
77
- export { registerOmadaTools } from './omada/index.js';
78
- export { registerAITools } from './ai/index.js';
@@ -1,95 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /**
4
- * A pre-registered REST action the `invokeAction` tool may fire.
5
- *
6
- * SECURITY MODEL: the LLM caller supplies ONE thing — an action NAME — which is
7
- * exact-match looked up here. Method, URL, auth, and timeout all come from this
8
- * operator-controlled config; no model-supplied string is ever interpolated into
9
- * any part of the request. A malformed/hostile name can only fail the lookup.
10
- */
11
- export interface RestAction {
12
- /** Human hint surfaced in the tool description so the agent picks well. */
13
- description?: string;
14
- method: 'GET' | 'POST';
15
- url: string;
16
- /** Optional Authorization: Bearer <token> (e.g. watchtower's HTTP-API token). */
17
- bearerToken?: string;
18
- /** Per-action timeout; some actions (watchtower update = image pulls) run long. */
19
- timeoutMs?: number;
20
- /**
21
- * Minimum interval between firings of THIS action (rate limit). Defaults to
22
- * 60s when unset; 0 disables. Caps the blast radius of a prompt-injected
23
- * model looping a legitimate action (deploy storms / endpoint DoS) — the
24
- * name-only lookup already stops forged actions; this stops repeated real ones.
25
- */
26
- cooldownMs?: number;
27
- }
28
-
29
- /**
30
- * Action names are deliberately boring: lowercase alnum with -/_ separators,
31
- * 1–64 chars, must start alphanumeric. This rejects prototype-pollution shaped
32
- * keys (`__proto__`), path/URL fragments, and whitespace tricks at CONFIG time,
33
- * so the runtime lookup only ever sees tame identifiers.
34
- */
35
- const ACTION_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
36
-
37
- const actionSchema = z.object({
38
- description: z.string().max(200).optional(),
39
- method: z.enum(['GET', 'POST']).default('POST'),
40
- url: z.string().url({ message: 'each action needs a valid http(s) url' }),
41
- bearerToken: z.string().min(1).optional(),
42
- timeoutMs: z.number().int().min(1000).max(300_000).optional(),
43
- // 0 = explicitly disabled; capped at 24h so a config typo can't brick an action.
44
- cooldownMs: z.number().int().min(0).max(86_400_000).optional(),
45
- });
46
-
47
- /**
48
- * Parse MCP_REST_ACTIONS (JSON):
49
- * {"update-node-u2":{"method":"POST","url":"http://192.168.0.9:8425/v1/update","bearerToken":"…","description":"deploy latest images on node-u2"}}
50
- *
51
- * Returns an empty map when unset; throws on malformed input so a config typo
52
- * fails loudly at startup instead of silently disabling the tool.
53
- */
54
- export function parseRestActions(raw: string | undefined): Record<string, RestAction> {
55
- if (!raw || raw.trim() === '') {
56
- return {};
57
- }
58
-
59
- let json: unknown;
60
- try {
61
- json = JSON.parse(raw);
62
- } catch {
63
- throw new Error(
64
- 'MCP_REST_ACTIONS must be valid JSON: {"name":{"method":"POST","url":"http://host/path","bearerToken":"..."}}'
65
- );
66
- }
67
-
68
- if (typeof json !== 'object' || json === null || Array.isArray(json)) {
69
- throw new Error('MCP_REST_ACTIONS must be a JSON object keyed by action name');
70
- }
71
-
72
- // Null-prototype output: even a hostile config key can never shadow or reach
73
- // Object.prototype members, and runtime lookups stay own-property-only.
74
- const actions: Record<string, RestAction> = Object.create(null) as Record<string, RestAction>;
75
-
76
- for (const [name, value] of Object.entries(json)) {
77
- if (!ACTION_NAME_PATTERN.test(name)) {
78
- throw new Error(
79
- `Invalid MCP_REST_ACTIONS action name '${name}': must match ${ACTION_NAME_PATTERN.source}`
80
- );
81
- }
82
- const parsed = actionSchema.safeParse(value);
83
- if (!parsed.success) {
84
- const messages = parsed.error.issues.map((issue) => issue.message);
85
- throw new Error(`Invalid MCP_REST_ACTIONS entry '${name}':\n${messages.join('\n')}`);
86
- }
87
- const url = new URL(parsed.data.url);
88
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
89
- throw new Error(`Invalid MCP_REST_ACTIONS entry '${name}': only http(s) urls are allowed`);
90
- }
91
- actions[name] = parsed.data;
92
- }
93
-
94
- return actions;
95
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Infra tools — pre-registered REST actions ("actuator rails for HTTP").
3
- *
4
- * Design rules:
5
- * - The healer lives OUTSIDE the patient (2026-07-10 v1.5.5 outage lesson):
6
- * this server never holds a docker socket or update logic; it only pokes
7
- * separately-running endpoints (watchtower HTTP API, webhooks) that keep
8
- * their own privileges.
9
- * - The model can only NAME an action, never shape a request: exact-match,
10
- * own-property lookup into operator config. No model string reaches the
11
- * URL, method, headers, or body. Redirects are refused; responses are
12
- * truncated before they re-enter the context window.
13
- */
14
-
15
- import type { McpServer } from '@modelcontextprotocol/server';
16
- import { z } from 'zod';
17
-
18
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
19
- import { logger } from '../../utils/logger.js';
20
-
21
- import type { RestAction } from './actions.js';
22
-
23
- export type { RestAction } from './actions.js';
24
- export { parseRestActions } from './actions.js';
25
-
26
- /** Default per-action timeout; watchtower update cycles pull images. */
27
- const DEFAULT_TIMEOUT_MS = 120_000;
28
- /** Cap on response bytes surfaced to the caller (anti context-flooding). */
29
- const MAX_RESPONSE_CHARS = 500;
30
- /** Default per-action cooldown (override per action via `cooldownMs`; 0 disables). */
31
- const DEFAULT_COOLDOWN_MS = 60_000;
32
-
33
- /**
34
- * Last-fired timestamps per action name — the rate limiter's whole state.
35
- *
36
- * MODULE-level on purpose: the HTTP server registers tools per SESSION, so a
37
- * closure-scoped map would give every new session a fresh cooldown (trivially
38
- * bypassed by reconnecting). One process = one store. In-memory is correct for
39
- * this single-process server; the check-and-claim below runs synchronously
40
- * before any await, so two racing calls cannot both pass (no TOCTOU on the
41
- * event loop). Resets on restart — acceptable: a restart itself takes longer
42
- * than the default cooldown.
43
- */
44
- const lastFired = new Map<string, number>();
45
-
46
- /** Test hook: clear the process-wide cooldown state between test cases. */
47
- export function resetActionCooldowns(): void {
48
- lastFired.clear();
49
- }
50
-
51
- export function registerInfraTools(server: McpServer, actions: Record<string, RestAction>): number {
52
- const names = Object.keys(actions);
53
- if (names.length === 0) {
54
- return 0;
55
- }
56
-
57
- const catalogue = names
58
- .map((n) => {
59
- const description = actions[n].description;
60
- return description ? `${n} (${description})` : n;
61
- })
62
- .join('; ');
63
-
64
- const inputSchema = z.object({
65
- action: z
66
- .string()
67
- .min(1)
68
- .max(64, 'action names are at most 64 chars')
69
- .describe(`Name of the pre-registered action to invoke — one of: ${names.join(', ')}`),
70
- });
71
-
72
- server.registerTool(
73
- 'invokeAction',
74
- {
75
- description:
76
- 'Invoke a pre-registered REST action by name. Actions are fixed, operator-configured HTTP calls ' +
77
- '(method, URL, and credentials come from server config — only the name is chosen here). ' +
78
- `Available actions: ${catalogue}. ` +
79
- 'Actions have real-world effects (deploys, restarts, webhooks) - confirm with the user before applying.',
80
- inputSchema: inputSchema,
81
- },
82
- wrapToolHandler('invokeAction', async ({ action }) => {
83
- // Own-property, exact-match lookup. The registry has a null prototype,
84
- // so names like 'constructor' or '__proto__' can never resolve to
85
- // anything but a genuinely configured action.
86
- if (!Object.hasOwn(actions, action)) {
87
- throw new Error(`Unknown action '${action}'. Available actions: ${names.join(', ')}`);
88
- }
89
- const spec = actions[action];
90
-
91
- // Rate limit: check-and-CLAIM synchronously before the fetch, so the
92
- // slot is consumed even when the call fails (no hammering a dying
93
- // endpoint), and two racing calls can't both pass.
94
- const cooldownMs = spec.cooldownMs ?? DEFAULT_COOLDOWN_MS;
95
- if (cooldownMs > 0) {
96
- const now = Date.now();
97
- const elapsed = now - (lastFired.get(action) ?? Number.NEGATIVE_INFINITY);
98
- if (elapsed < cooldownMs) {
99
- const retryInS = Math.ceil((cooldownMs - elapsed) / 1000);
100
- throw new Error(
101
- `Action '${action}' is cooling down — try again in ~${String(retryInS)}s. ` +
102
- 'Repeated firing of infrastructure actions is rate-limited by design.'
103
- );
104
- }
105
- lastFired.set(action, now);
106
- }
107
-
108
- logger.info('Invoking registered REST action', { action, method: spec.method, url: spec.url });
109
-
110
- const response = await fetch(spec.url, {
111
- method: spec.method,
112
- headers: spec.bearerToken ? { Authorization: `Bearer ${spec.bearerToken}` } : undefined,
113
- // A registered endpoint that suddenly redirects is a surprise, not
114
- // a convenience — fail closed rather than follow.
115
- redirect: 'error',
116
- signal: AbortSignal.timeout(spec.timeoutMs ?? DEFAULT_TIMEOUT_MS),
117
- });
118
-
119
- const body = (await response.text().catch(() => '')).slice(0, MAX_RESPONSE_CHARS);
120
-
121
- if (!response.ok) {
122
- throw new Error(`Action '${action}' returned HTTP ${String(response.status)}${body ? `: ${body}` : ''}`);
123
- }
124
-
125
- return toToolResult({
126
- action,
127
- status: 'completed',
128
- httpStatus: response.status,
129
- response: body || undefined,
130
- });
131
- },
132
- Permission.ADMIN
133
- )
134
- );
135
-
136
- logger.info('Infra tools registered', { toolCount: 1, actions: names });
137
- return 1;
138
- }
@@ -1,28 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import { z } from 'zod';
3
-
4
- import type { OmadaClient } from '../../omadaClient/index.js';
5
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
6
-
7
- export function registerBlockClientTool(server: McpServer, client: OmadaClient): void {
8
- const inputSchema = z.object({
9
- clientMac: z.string().min(1, 'clientMac (MAC address) is required'),
10
- siteId: z.string().min(1).optional(),
11
- });
12
-
13
- server.registerTool(
14
- 'omada_blockClient',
15
- {
16
- description:
17
- 'Block a client from the network by MAC address. The client is denied all network access until unblocked. ' +
18
- 'Use omada_listClients first to find the exact MAC. This affects real network connectivity - confirm with the user before applying. ' +
19
- "Caution: a blocked device cannot reconnect, and once the controller stops treating it as current, Omada's API can no longer unblock it. " +
20
- 'For recurring schedules (e.g. a curfew) prefer an Omada time-range access rule on the controller over daily block/unblock calls.',
21
- inputSchema: inputSchema,
22
- },
23
- wrapToolHandler('omada_blockClient', async ({ clientMac, siteId }) =>
24
- toToolResult(await client.blockClient(clientMac, siteId)),
25
- Permission.CONTROL
26
- )
27
- );
28
- }
@@ -1,32 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import { z } from 'zod';
3
-
4
- import type { OmadaClient } from '../../omadaClient/index.js';
5
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
6
-
7
- export function registerCyclePoePortTool(server: McpServer, client: OmadaClient): void {
8
- const inputSchema = z.object({
9
- switchMac: z.string().min(1, 'switchMac (MAC address of the Omada switch) is required'),
10
- ports: z.array(z.number().int().positive()).min(1, 'at least one port number is required'),
11
- siteId: z.string().min(1).optional(),
12
- });
13
-
14
- server.registerTool(
15
- 'omada_cyclePoePort',
16
- {
17
- description:
18
- 'Power-cycle PoE on one or more switch ports — a remote hard-reboot for whatever the ports power ' +
19
- '(access points, cameras, PoE sensors). The switch briefly cuts and restores power on ONLY the given ' +
20
- 'ports; the switch itself and other ports are unaffected. Use omada_listDevices to find the switch MAC ' +
21
- 'and identify which port powers which device first. The powered device goes down for the cycle - ' +
22
- 'confirm with the user before applying.',
23
- inputSchema: inputSchema,
24
- },
25
- wrapToolHandler('omada_cyclePoePort', async ({ switchMac, ports, siteId }) => {
26
- await client.cyclePoePorts(switchMac, ports, siteId);
27
- return toToolResult({ switchMac, ports, status: 'poe-recovery triggered' });
28
- },
29
- Permission.CONTROL
30
- )
31
- );
32
- }
@@ -1,27 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import { z } from 'zod';
3
-
4
- import type { OmadaClient } from '../../omadaClient/index.js';
5
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
6
-
7
- export function registerDeleteClientTool(server: McpServer, client: OmadaClient): void {
8
- const inputSchema = z.object({
9
- clientMac: z.string().min(1, 'clientMac (MAC address) is required'),
10
- siteId: z.string().min(1).optional(),
11
- });
12
-
13
- server.registerTool(
14
- 'omada_deleteClient',
15
- {
16
- description:
17
- "Delete the controller's record of a client by MAC: its name, history and block state. " +
18
- 'Use it to clear a block that omada_unblockClient can no longer reach (the device stays refused by the access points although Omada lists nothing). ' +
19
- 'The device is treated as new when it reconnects. Irreversible for that history - confirm with the user before applying.',
20
- inputSchema,
21
- },
22
- wrapToolHandler('omada_deleteClient', async ({ clientMac, siteId }: z.infer<typeof inputSchema>) =>
23
- toToolResult(await client.deleteClient(clientMac, siteId)),
24
- Permission.CONFIGURE
25
- )
26
- );
27
- }
@@ -1,23 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import { z } from 'zod';
3
- import type { OmadaClient } from '../../omadaClient/index.js';
4
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
5
-
6
- export function registerDisableClientRateLimitTool(server: McpServer, client: OmadaClient): void {
7
- const inputSchema = z.object({
8
- clientMac: z.string().min(1, 'clientMac (MAC address) is required'),
9
- siteId: z.string().min(1).optional(),
10
- });
11
-
12
- server.registerTool(
13
- 'omada_disableClientRateLimit',
14
- {
15
- description: 'Remove bandwidth limits from a client',
16
- inputSchema: inputSchema,
17
- },
18
- wrapToolHandler('omada_disableClientRateLimit', async ({ clientMac, siteId }) =>
19
- toToolResult(await client.disableClientRateLimit(clientMac, siteId)),
20
- Permission.CONTROL
21
- )
22
- );
23
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { clientIdSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetClientTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getClient',
8
- {
9
- description: 'Get detailed information about a specific connected client',
10
- inputSchema: clientIdSchema,
11
- },
12
- wrapToolHandler('omada_getClient', async ({ clientId, siteId }) => toToolResult(await client.getClient(clientId, siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { deviceIdSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetDeviceTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getDevice',
8
- {
9
- description: 'Get detailed information about a specific network device',
10
- inputSchema: deviceIdSchema,
11
- },
12
- wrapToolHandler('omada_getDevice', async ({ deviceId, siteId }) => toToolResult(await client.getDevice(deviceId, siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetFirewallSettingTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getFirewallSetting',
8
- {
9
- description: 'Get firewall configuration for a site',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getFirewallSetting', async ({ siteId }) => toToolResult(await client.getFirewallSetting(siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetInternetInfoTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getInternetInfo',
8
- {
9
- description: 'Get internet connection configuration for a site',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getInternetInfo', async ({ siteId }) => toToolResult(await client.getInternetInfo(siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetLanNetworkListTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getLanNetworkList',
8
- {
9
- description: 'Get LAN network configuration list',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getLanNetworkList', async ({ siteId }) => toToolResult(await client.getLanNetworkList(siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetLanProfileListTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getLanProfileList',
8
- {
9
- description: 'Get LAN profile configuration list',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getLanProfileList', async ({ siteId }) => toToolResult(await client.getLanProfileList(siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,26 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import { z } from 'zod';
3
-
4
- import type { OmadaClient } from '../../omadaClient/index.js';
5
- import { toToolResult, wrapToolHandler, Permission } from '../common.js';
6
- import { createPaginationSchema } from '../utils/pagination-schema.js';
7
-
8
- const portForwardingSchema = z.object({
9
- type: z.enum(['User', 'UPnP']).describe('Port forwarding type: User (manually configured) or UPnP (automatically configured)'),
10
- siteId: z.string().min(1).describe('Site ID (required)'),
11
- ...createPaginationSchema(10),
12
- });
13
-
14
- export function registerGetPortForwardingStatusTool(server: McpServer, client: OmadaClient): void {
15
- server.registerTool(
16
- 'omada_getPortForwardingStatus',
17
- {
18
- description: 'Get port forwarding rules (User or UPnP)',
19
- inputSchema: portForwardingSchema,
20
- },
21
- wrapToolHandler('omada_getPortForwardingStatus', async ({ type, siteId, page = 1, pageSize = 10 }) =>
22
- toToolResult(await client.getPortForwardingStatus(type, siteId, page, pageSize)),
23
- Permission.QUERY
24
- )
25
- );
26
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetRateLimitProfilesTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getRateLimitProfiles',
8
- {
9
- description: 'Get available rate limit profiles for a site',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getRateLimitProfiles', async ({ siteId }) => toToolResult(await client.getRateLimitProfiles(siteId)), Permission.QUERY)
13
- );
14
- }
@@ -1,14 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import type { OmadaClient } from '../../omadaClient/index.js';
3
- import { siteInputSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
4
-
5
- export function registerGetSiteNtpStatusTool(server: McpServer, client: OmadaClient): void {
6
- server.registerTool(
7
- 'omada_getSiteNtpStatus',
8
- {
9
- description: 'Get the NTP (network time) server configuration and status for a site',
10
- inputSchema: siteInputSchema,
11
- },
12
- wrapToolHandler('omada_getSiteNtpStatus', async ({ siteId }) => toToolResult(await client.getSiteNtpStatus(siteId)), Permission.QUERY)
13
- );
14
- }