@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,97 +0,0 @@
1
- /**
2
- * MCP Server Instructions
3
- *
4
- * Provides guidance to LLMs on how to optimally use the server's tools.
5
- * These instructions are sent during initialization and help LLMs understand
6
- * tool relationships, best practices, and constraints.
7
- *
8
- * @see https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/
9
- */
10
-
11
- export interface InstructionsConfig {
12
- haEnabled: boolean;
13
- omadaEnabled: boolean;
14
- aiEnabled: boolean;
15
- }
16
-
17
- /**
18
- * Generates server instructions based on enabled plugins.
19
- * Instructions are kept concise and actionable per MCP best practices.
20
- */
21
- export function generateInstructions(config: InstructionsConfig): string {
22
- const sections: string[] = [];
23
-
24
- // Base instructions
25
- sections.push(
26
- 'MCP-HA-Connect: Smart home control via Home Assistant with network management via TP-Link Omada.'
27
- );
28
-
29
- if (config.haEnabled) {
30
- sections.push(getHomeAssistantInstructions());
31
- }
32
-
33
- if (config.omadaEnabled) {
34
- sections.push(getOmadaInstructions());
35
- }
36
-
37
- if (config.aiEnabled) {
38
- sections.push(getAIInstructions());
39
- }
40
-
41
- // Cross-plugin relationships
42
- if (config.haEnabled && config.omadaEnabled) {
43
- sections.push(getCrossPluginInstructions());
44
- }
45
-
46
- return sections.join(' ');
47
- }
48
-
49
- function getHomeAssistantInstructions(): string {
50
- return [
51
- // Entity queries
52
- 'ENTITY QUERIES: Entity tools return paginated results (50/page default). Use includeAttributes=true only when full attribute data is needed. For people/family queries, use listPersons instead of searchEntities.',
53
-
54
- // State management
55
- 'STATE: getState returns single entity; getStates returns all entities paginated. Use getDomainSummary for quick counts by domain.',
56
-
57
- // Device control
58
- 'CONTROL: Use specialized control tools (controlLight, controlClimate, controlFan, controlCover, controlMediaPlayer) instead of generic callService when available - they provide better validation and defaults.',
59
-
60
- // Automations
61
- 'AUTOMATIONS: After createAutomation, automations reload automatically. Use listAutomations to verify. triggerAutomation can pass variables to test automations safely.',
62
-
63
- // Notifications
64
- 'NOTIFICATIONS: Use listNotificationTargets to discover available mobile devices before sendNotification. Target format is usually mobile_app_<device_name>.',
65
- ].join(' ');
66
- }
67
-
68
- function getOmadaInstructions(): string {
69
- return [
70
- // Site context
71
- 'OMADA: Most Omada tools require siteId. Use omada_listSites first to get available sites if siteId is unknown.',
72
-
73
- // Client vs Device
74
- 'CLIENTS vs DEVICES: Clients are connected users/devices (phones, laptops). Devices are network infrastructure (APs, switches, gateways). Use omada_listClients for user devices, omada_listDevices for infrastructure.',
75
-
76
- // Rate limiting
77
- 'RATE LIMITS: Use omada_getRateLimitProfiles before setting limits. omada_setClientRateLimitProfile applies predefined profiles; omada_setClientRateLimit sets custom Kbps values.',
78
-
79
- // Remote-hands actions (v1.6)
80
- 'ACTIONS: omada_cyclePoePort power-cycles PoE ports (remote-reboots APs/cameras; switch MAC via omada_listDevices). omada_setSsidEnabled toggles an SSID by name (guest WiFi on/off). omada_setSiteNtpServers sets site NTP.',
81
-
82
- // Read-only preference
83
- 'SAFETY: Prefer read-only Omada tools. Rate limits, PoE cycling, and SSID toggles affect real availability - confirm with user before applying.',
84
- ].join(' ');
85
- }
86
-
87
- function getAIInstructions(): string {
88
- return [
89
- 'AI ANALYSIS: analyzeSensors uses local Ollama for privacy-preserving sensor analysis. Pass sensor data as key-value pairs. Useful for anomaly detection and recommendations.',
90
- ].join(' ');
91
- }
92
-
93
- function getCrossPluginInstructions(): string {
94
- return [
95
- 'INTEGRATION: Network clients from Omada can correlate with HA device_trackers for presence detection. HA person entities link to device_trackers which may correspond to Omada clients.',
96
- ].join(' ');
97
- }
@@ -1,227 +0,0 @@
1
- /**
2
- * Rate Limiter Middleware
3
- * Simple token bucket rate limiter for HTTP endpoints
4
- */
5
-
6
- import type { IncomingMessage, ServerResponse } from 'node:http';
7
- import { logger } from '../utils/logger.js';
8
-
9
- export interface RateLimitConfig {
10
- windowMs?: number; // Time window in milliseconds (default: 60000 = 1 minute)
11
- maxRequests?: number; // Maximum requests per window (default: 100)
12
- skipPaths?: string[]; // Paths to skip rate limiting (e.g., health checks)
13
- keyGenerator?: (req: IncomingMessage) => string; // Custom key generator
14
- /**
15
- * Immediate-peer IPs whose forwarding headers (CF-Connecting-IP / X-Forwarded-For /
16
- * X-Real-IP) may be trusted to identify the real client (M4). Empty by default —
17
- * with no trusted proxy, client-supplied headers are IGNORED and the socket
18
- * address is used, so a forged header cannot evade limits or grow the bucket map.
19
- */
20
- trustedProxies?: string[];
21
- }
22
-
23
- interface RateLimitEntry {
24
- count: number;
25
- windowStart: number;
26
- }
27
-
28
- /**
29
- * Rate Limiter class
30
- */
31
- export class RateLimiter {
32
- private readonly windowMs: number;
33
- private readonly maxRequests: number;
34
- private readonly skipPaths: Set<string>;
35
- private readonly keyGenerator: (req: IncomingMessage) => string;
36
- private readonly trustedProxies: Set<string>;
37
- private readonly entries = new Map<string, RateLimitEntry>();
38
- private cleanupInterval: ReturnType<typeof setInterval> | null = null;
39
-
40
- constructor(config: RateLimitConfig = {}) {
41
- this.windowMs = config.windowMs ?? 60000; // 1 minute default
42
- this.maxRequests = config.maxRequests ?? 100; // 100 requests per minute
43
- this.skipPaths = new Set(config.skipPaths ?? ['/health', '/healthcheck']);
44
- this.trustedProxies = new Set(config.trustedProxies ?? []);
45
- // Bind so a custom generator OR the default both see `this`.
46
- this.keyGenerator = config.keyGenerator ?? ((req) => this.defaultKeyGenerator(req));
47
-
48
- // Start cleanup interval to prevent memory leaks
49
- this.startCleanup();
50
- }
51
-
52
- /** First value of a possibly-array header, trimmed; undefined if absent/empty. */
53
- private firstHeader(value: string | string[] | undefined): string | undefined {
54
- const v = Array.isArray(value) ? value[0] : value;
55
- const trimmed = v?.trim();
56
- return trimmed ? trimmed : undefined;
57
- }
58
-
59
- /**
60
- * Default key generator (M4 / OWASP API4:2023).
61
- *
62
- * Client-supplied forwarding headers are honored ONLY when the immediate peer
63
- * (`socket.remoteAddress`) is a configured trusted proxy. Otherwise we key on the
64
- * socket address — an attacker cannot forge that, so they can neither evade the
65
- * limit by rotating X-Forwarded-For nor exhaust memory with unbounded forged keys.
66
- */
67
- private defaultKeyGenerator(req: IncomingMessage): string {
68
- const socketAddr = req.socket?.remoteAddress ?? 'unknown';
69
-
70
- if (!this.trustedProxies.has(socketAddr)) {
71
- return socketAddr;
72
- }
73
-
74
- // Behind a trusted proxy: prefer Cloudflare's single-client CF-Connecting-IP,
75
- // then the left-most X-Forwarded-For hop, then X-Real-IP.
76
- const cf = this.firstHeader(req.headers['cf-connecting-ip']);
77
- if (cf) {
78
- return cf;
79
- }
80
-
81
- const xff = this.firstHeader(req.headers['x-forwarded-for']);
82
- if (xff) {
83
- return xff.split(',')[0].trim();
84
- }
85
-
86
- const realIp = this.firstHeader(req.headers['x-real-ip']);
87
- if (realIp) {
88
- return realIp;
89
- }
90
-
91
- return socketAddr;
92
- }
93
-
94
- /**
95
- * Start periodic cleanup of expired entries
96
- */
97
- private startCleanup(): void {
98
- // Cleanup every window period
99
- this.cleanupInterval = setInterval(() => {
100
- const now = Date.now();
101
- let cleaned = 0;
102
-
103
- for (const [key, entry] of this.entries) {
104
- if (now - entry.windowStart > this.windowMs * 2) {
105
- this.entries.delete(key);
106
- cleaned++;
107
- }
108
- }
109
-
110
- if (cleaned > 0) {
111
- logger.debug('Rate limiter cleanup', { entriesRemoved: cleaned, remaining: this.entries.size });
112
- }
113
- }, this.windowMs);
114
- }
115
-
116
- /**
117
- * Stop cleanup interval
118
- */
119
- stop(): void {
120
- if (this.cleanupInterval) {
121
- clearInterval(this.cleanupInterval);
122
- this.cleanupInterval = null;
123
- }
124
- }
125
-
126
- /**
127
- * Check if request should be allowed
128
- */
129
- check(req: IncomingMessage): { allowed: boolean; remaining: number; resetTime: number } {
130
- const url = req.url ?? '/';
131
- const path = url.split('?')[0];
132
-
133
- // Skip rate limiting for certain paths
134
- if (this.skipPaths.has(path)) {
135
- return { allowed: true, remaining: this.maxRequests, resetTime: 0 };
136
- }
137
-
138
- const key = this.keyGenerator(req);
139
- const now = Date.now();
140
- let entry = this.entries.get(key);
141
-
142
- // Create new entry or reset if window expired
143
- if (!entry || now - entry.windowStart > this.windowMs) {
144
- entry = { count: 0, windowStart: now };
145
- this.entries.set(key, entry);
146
- }
147
-
148
- // Increment count
149
- entry.count++;
150
-
151
- const remaining = Math.max(0, this.maxRequests - entry.count);
152
- const resetTime = entry.windowStart + this.windowMs;
153
-
154
- if (entry.count > this.maxRequests) {
155
- logger.warn('Rate limit exceeded', {
156
- key,
157
- count: entry.count,
158
- limit: this.maxRequests,
159
- path,
160
- });
161
- return { allowed: false, remaining: 0, resetTime };
162
- }
163
-
164
- return { allowed: true, remaining, resetTime };
165
- }
166
-
167
- /**
168
- * Middleware function for HTTP server
169
- */
170
- middleware(): (req: IncomingMessage, res: ServerResponse) => boolean {
171
- return (req: IncomingMessage, res: ServerResponse): boolean => {
172
- const result = this.check(req);
173
-
174
- // Add rate limit headers
175
- res.setHeader('X-RateLimit-Limit', this.maxRequests.toString());
176
- res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
177
- res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000).toString());
178
-
179
- if (!result.allowed) {
180
- res.setHeader('Retry-After', Math.ceil((result.resetTime - Date.now()) / 1000).toString());
181
- res.writeHead(429, { 'Content-Type': 'application/json' });
182
- res.end(JSON.stringify({
183
- error: 'Too Many Requests',
184
- message: 'Rate limit exceeded. Please try again later.',
185
- retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000),
186
- }));
187
- return false;
188
- }
189
-
190
- return true;
191
- };
192
- }
193
-
194
- /**
195
- * Get current stats
196
- */
197
- getStats(): { totalEntries: number; config: { windowMs: number; maxRequests: number } } {
198
- return {
199
- totalEntries: this.entries.size,
200
- config: {
201
- windowMs: this.windowMs,
202
- maxRequests: this.maxRequests,
203
- },
204
- };
205
- }
206
-
207
- /**
208
- * Reset rate limit for a specific key
209
- */
210
- reset(key: string): boolean {
211
- return this.entries.delete(key);
212
- }
213
-
214
- /**
215
- * Clear all rate limit entries
216
- */
217
- clear(): void {
218
- this.entries.clear();
219
- }
220
- }
221
-
222
- /**
223
- * Create a rate limiter instance with default config
224
- */
225
- export function createRateLimiter(config?: RateLimitConfig): RateLimiter {
226
- return new RateLimiter(config);
227
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Stdio transport for MCP server
3
- * Used for Claude Desktop and other stdio-based clients
4
- */
5
-
6
- import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
7
- import type { HaClient } from '../haClient/index.js';
8
- import type { LocalAIClient } from '../localAI/index.js';
9
- import type { OmadaClient } from '../omadaClient/index.js';
10
- import { logger } from '../utils/logger.js';
11
- import { setLocalFullTrust } from '../tools/common.js';
12
- import type { OmadaRegistrationMode } from '../tools/omada/index.js';
13
- import type { RestAction } from '../tools/infra/index.js';
14
- import { createServer } from './common.js';
15
-
16
- export interface StdioServerOptions {
17
- haClient?: HaClient;
18
- omadaClient?: OmadaClient;
19
- aiClient?: LocalAIClient;
20
- /** Pre-registered REST actions for invokeAction (empty/undefined = tool not registered). */
21
- restActions?: Record<string, RestAction>;
22
- /** Tool registration strategy for the Omada plugin (default 'eager'). */
23
- toolRegistrationMode?: OmadaRegistrationMode;
24
- }
25
-
26
- export async function startStdioServer(options: StdioServerOptions): Promise<void> {
27
- logger.info('Starting stdio server');
28
-
29
- // L3: stdio has no per-request auth; the local process owner is trusted. Grant
30
- // full permissions to tool calls that arrive without an explicit mask. HTTP
31
- // transports never enable this — they fail closed on a missing mask.
32
- setLocalFullTrust(true);
33
-
34
- const server = createServer(options);
35
- const transport = new StdioServerTransport();
36
-
37
- logger.info('Connecting stdio server');
38
- await server.connect(transport);
39
-
40
- logger.info('Stdio server connected and ready');
41
- }
@@ -1,205 +0,0 @@
1
- /**
2
- * Streamable HTTP transport for MCP server
3
- * Implements the MCP protocol version 2025-03-26 (current/recommended)
4
- */
5
-
6
- import { randomUUID } from 'node:crypto';
7
- import type { IncomingMessage, ServerResponse } from 'node:http';
8
- import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
9
- import type { EnvironmentConfig } from '../config.js';
10
- import type { HaClient } from '../haClient/index.js';
11
- import type { LocalAIClient } from '../localAI/index.js';
12
- import type { OmadaClient } from '../omadaClient/index.js';
13
- import { logger } from '../utils/logger.js';
14
- import { createServer } from './common.js';
15
- import type { OwnerQuestionService } from '../ownerQuestions/service.js';
16
- import type { AuthenticatedRequest } from './auth.js';
17
-
18
- interface StreamTransportState {
19
- transport: NodeStreamableHTTPServerTransport;
20
- server: ReturnType<typeof createServer>;
21
- }
22
-
23
- // Export the type for use in http.ts
24
- export type { StreamTransportState };
25
-
26
- export interface StreamTransportOptions {
27
- haClient?: HaClient;
28
- omadaClient?: OmadaClient;
29
- aiClient?: LocalAIClient;
30
- config: EnvironmentConfig;
31
- ownerQuestions?: OwnerQuestionService;
32
- }
33
-
34
- export interface TransportSecurityOptions {
35
- allowedOrigins: string[];
36
- allowedHosts: string[];
37
- enableDnsRebindingProtection: boolean;
38
- }
39
-
40
- /**
41
- * Build the DNS-rebinding protection options for the Streamable HTTP transport
42
- * (M3 / OWASP A05:2021).
43
- *
44
- * Host-header validation is **opt-in**. The SDK only validates the `Host` header
45
- * when `allowedHosts` is non-empty (EXACT string match), and only validates
46
- * `Origin` when `allowedOrigins` is non-empty. CORS/Origin enforcement already
47
- * lives in the HTTP layer, so this helper governs ONLY the Host check and leaves
48
- * `allowedOrigins` empty to avoid double-enforcing origins at the transport.
49
- *
50
- * - `MCP_HTTP_ALLOWED_HOSTS` UNSET → Host validation OFF (protection disabled).
51
- * This preserves pre-1.3.0 behavior: an upgrade never silently 403s clients
52
- * that reach the server by a host not listed in the (CORS) origins. The
53
- * server's own reachable host and its allowed CORS origins are frequently
54
- * different addresses, so deriving hosts from origins would break real setups.
55
- * - `MCP_HTTP_ALLOWED_HOSTS` SET → enforce exactly those host[:port] values, plus
56
- * loopback/bind-address conveniences so on-box health checks and local tools
57
- * keep working. Any other `Host` header is rejected with a 403.
58
- */
59
- export function buildTransportSecurityOptions(config: EnvironmentConfig): TransportSecurityOptions {
60
- const explicitHosts = config.httpAllowedHosts ?? [];
61
-
62
- // Opt-out by default: no explicit hosts ⇒ no Host-header validation.
63
- if (explicitHosts.length === 0) {
64
- return { allowedOrigins: [], allowedHosts: [], enableDnsRebindingProtection: false };
65
- }
66
-
67
- const port = config.httpPort ?? 3000;
68
- const bindAddr = config.httpBindAddr ?? '127.0.0.1';
69
- const hosts = new Set<string>();
70
-
71
- for (const entry of explicitHosts) {
72
- try {
73
- // Accept a full URL form (e.g. https://example.com) → host header form.
74
- hosts.add(new URL(entry).host);
75
- } catch {
76
- // Bare hostname / IP[:port] already in host-header form.
77
- hosts.add(entry);
78
- }
79
- }
80
-
81
- // Always permit direct loopback / bind-address access on the listen port.
82
- for (const host of [bindAddr, 'localhost', '127.0.0.1']) {
83
- hosts.add(host);
84
- hosts.add(`${host}:${port}`);
85
- }
86
-
87
- return {
88
- allowedOrigins: [],
89
- allowedHosts: [...hosts],
90
- enableDnsRebindingProtection: true,
91
- };
92
- }
93
-
94
- /**
95
- * Creates a Streamable HTTP transport
96
- * This implements the MCP protocol version 2025-03-26
97
- */
98
- export function createStreamTransport(options: StreamTransportOptions, callerPermissions?: number): StreamTransportState {
99
- const { haClient, omadaClient, aiClient, config, ownerQuestions } = options;
100
- const mcpServer = createServer({
101
- haClient,
102
- omadaClient,
103
- aiClient,
104
- ownerQuestions,
105
- restActions: config.restActions,
106
- toolRegistrationMode: config.toolRegistrationMode,
107
- // Hide tools this caller can never use from tools/list (RBAC still enforced per call).
108
- callerPermissions,
109
- });
110
-
111
- const enableStatefulSessions = config.stateful;
112
- const sessionIdGenerator = enableStatefulSessions ? () => randomUUID() : undefined;
113
-
114
- if (!enableStatefulSessions) {
115
- logger.info('Starting Streamable HTTP transport in stateless mode; Mcp-Session-Id headers are optional');
116
- }
117
-
118
- const security = buildTransportSecurityOptions(config);
119
-
120
- const transport = new NodeStreamableHTTPServerTransport({
121
- sessionIdGenerator,
122
- allowedOrigins: security.allowedOrigins,
123
- allowedHosts: security.allowedHosts,
124
- enableDnsRebindingProtection: security.enableDnsRebindingProtection,
125
- onsessioninitialized: (sessionId: string) => {
126
- logger.info('Session initialized', { sessionId });
127
- },
128
- onsessionclosed: (sessionId: string) => {
129
- logger.info('Session closed', { sessionId });
130
- },
131
- });
132
-
133
- transport.onerror = (error: Error) => {
134
- logger.error('Streamable HTTP transport error', {
135
- error,
136
- message: error.message,
137
- });
138
- };
139
-
140
- return { transport, server: mcpServer };
141
- }
142
-
143
- /**
144
- * Handles incoming Streamable HTTP requests (GET, POST, DELETE)
145
- * For stateful mode, transports should be stored and reused by the caller
146
- */
147
- export async function handleStreamRequest(
148
- options: StreamTransportOptions,
149
- req: IncomingMessage,
150
- res: ServerResponse,
151
- parsedBody?: unknown,
152
- existingTransport?: StreamTransportState
153
- ): Promise<StreamTransportState | void> {
154
- const { config } = options;
155
- const originHeader = req.headers.origin;
156
- const hostHeader = req.headers.host;
157
-
158
- logger.info('Streamable HTTP request received', {
159
- method: req.method,
160
- url: req.url,
161
- sessionId: req.headers['mcp-session-id'] ?? undefined,
162
- origin: originHeader ?? '(not set)',
163
- host: hostHeader ?? '(not set)',
164
- });
165
-
166
- // Reuse existing transport if provided, otherwise create new one
167
- // The auth middleware attached the caller's mask; a new server lists only the tools it permits.
168
- const callerPermissions = (req as AuthenticatedRequest).auth?.extra?.permissions as number | undefined;
169
- const state = existingTransport ?? createStreamTransport(options, callerPermissions);
170
-
171
- if (!existingTransport) {
172
- await state.server.connect(state.transport);
173
- }
174
-
175
- // Ensure Accept header includes required MIME types for SDK compatibility
176
- // Some MCP clients (like n8n) don't send the full Accept header
177
- const acceptHeader = req.headers.accept ?? '';
178
- if (!acceptHeader.includes('text/event-stream') || !acceptHeader.includes('application/json')) {
179
- req.headers.accept = 'application/json, text/event-stream';
180
- }
181
-
182
- try {
183
- await state.transport.handleRequest(req, res, parsedBody);
184
-
185
- logger.debug('Streamable HTTP request handled', {
186
- method: req.method,
187
- sessionId: req.headers['mcp-session-id'] ?? undefined,
188
- });
189
- } catch (error) {
190
- logger.error('Failed to handle Streamable HTTP request', {
191
- error,
192
- method: req.method,
193
- url: req.url,
194
- origin: originHeader ?? '(not set)',
195
- host: hostHeader ?? '(not set)',
196
- allowedOrigins: config.httpAllowedOrigins,
197
- });
198
- throw error;
199
- }
200
-
201
- // Return state for session management if stateful
202
- if (config.stateful) {
203
- return state;
204
- }
205
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * analyzeSensors tool - AI-powered sensor analysis
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { LocalAIClient, SensorData } from '../../localAI/index.js';
7
- import { analyzeSensorsSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
- import type { z } from 'zod';
9
-
10
- type AnalyzeSensorsArgs = z.infer<typeof analyzeSensorsSchema>;
11
-
12
- export function registerAnalyzeSensorsTool(server: McpServer, aiClient?: LocalAIClient): void {
13
- server.registerTool(
14
- 'analyzeSensors',
15
- {
16
- description: 'Analyze sensor data using AI (Ollama) to detect issues and provide recommendations.',
17
- inputSchema: analyzeSensorsSchema,
18
- },
19
- wrapToolHandler('analyzeSensors', async ({ sensors }: AnalyzeSensorsArgs) => {
20
- if (!aiClient) {
21
- return toToolResult({
22
- error: 'AI client not configured',
23
- message: 'AI analysis requires AI_URL to be set (or legacy OLLAMA_URL)',
24
- }, true);
25
- }
26
- if (!sensors || typeof sensors !== 'object') {
27
- return toToolResult({ error: 'sensors object is required' }, true);
28
- }
29
- const analysis = await aiClient.analyzeSensors(sensors as SensorData);
30
- return toToolResult(analysis);
31
- }, Permission.AI)
32
- );
33
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * AI tools index - registers all AI-powered MCP tools
3
- */
4
-
5
- import type { McpServer } from '@modelcontextprotocol/server';
6
- import type { LocalAIClient } from '../../localAI/index.js';
7
- import { logger } from '../../utils/logger.js';
8
-
9
- import { registerAnalyzeSensorsTool } from './analyzeSensors.js';
10
-
11
- export function registerAITools(server: McpServer, aiClient?: LocalAIClient): number {
12
- logger.debug('Registering AI tools');
13
- let toolCount = 0;
14
-
15
- registerAnalyzeSensorsTool(server, aiClient);
16
- toolCount += 1;
17
-
18
- logger.info('AI tools registered', { toolCount, aiEnabled: !!aiClient });
19
- return toolCount;
20
- }