@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,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
- }