@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,561 +0,0 @@
1
- /**
2
- * HTTP server tests
3
- */
4
-
5
- import { describe, it, expect, vi, beforeEach } from 'vitest';
6
- import type { IncomingMessage, ServerResponse } from 'node:http';
7
- import type { HaClient } from '../../src/haClient/index.js';
8
- import type { EnvironmentConfig } from '../../src/config.js';
9
- import { VERSION } from '../../src/version.js';
10
- import {
11
- resolveForwardedProto,
12
- getOpenApiSpec,
13
- addRestApiCors,
14
- isRestApiCorsPath,
15
- isStatelessMcpGet,
16
- } from '../../src/server/http.js';
17
-
18
- // Mock dependencies
19
- vi.mock('node:http');
20
- vi.mock('../../src/server/stream.js');
21
- vi.mock('../../src/utils/logger.js');
22
-
23
- describe('HTTP Server', () => {
24
- let mockClient: HaClient;
25
- let mockConfig: EnvironmentConfig;
26
-
27
- beforeEach(() => {
28
- vi.clearAllMocks();
29
-
30
- mockClient = {
31
- checkApi: vi.fn().mockResolvedValue({ message: 'API running.' }),
32
- getStates: vi.fn(),
33
- getState: vi.fn(),
34
- callService: vi.fn(),
35
- getEntitiesByDomain: vi.fn(),
36
- searchEntities: vi.fn(),
37
- } as any;
38
-
39
- mockConfig = {
40
- baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
41
- token: 'test-token',
42
- strictSsl: false,
43
- timeout: 30000,
44
- aiProvider: 'ollama',
45
- aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
46
- aiModel: 'qwen3:14b',
47
- aiTimeout: 60000,
48
- logLevel: 'info',
49
- logFormat: 'plain',
50
- useHttp: true,
51
- stateful: false,
52
- httpPort: 3000,
53
- httpBindAddr: '127.0.0.1',
54
- httpPath: '/mcp',
55
- httpEnableHealthcheck: true,
56
- httpHealthcheckPath: '/health',
57
- httpAllowCors: true,
58
- httpAllowedOrigins: ['127.0.0.1', 'localhost'],
59
- sseEventsEnabled: true,
60
- sseEventsPath: '/subscribe_events',
61
- rateLimitEnabled: true,
62
- rateLimitWindowMs: 60000,
63
- rateLimitMaxRequests: 100,
64
- authMethod: 'none',
65
- };
66
- });
67
-
68
- describe('Health Check Handler', () => {
69
- it('should return health status for stream transport', () => {
70
- const expectedResponse = {
71
- status: 'healthy',
72
- version: VERSION,
73
- transport: 'stream',
74
- stateful: false,
75
- };
76
-
77
- expect(expectedResponse.status).toBe('healthy');
78
- expect(expectedResponse.transport).toBe('stream');
79
- expect(expectedResponse.stateful).toBe(false);
80
- });
81
-
82
- it('should include stateful flag when enabled', () => {
83
- const statefulConfig = { ...mockConfig, stateful: true };
84
-
85
- const expectedResponse = {
86
- status: 'healthy',
87
- version: VERSION,
88
- transport: 'stream',
89
- stateful: statefulConfig.stateful,
90
- };
91
-
92
- expect(expectedResponse.stateful).toBe(true);
93
- });
94
- });
95
-
96
- describe('CORS Handler', () => {
97
- it('should not set CORS headers when CORS is disabled', () => {
98
- const noCorsConfig = { ...mockConfig, httpAllowCors: false };
99
-
100
- expect(noCorsConfig.httpAllowCors).toBe(false);
101
- });
102
-
103
- it('should allow specific origins', () => {
104
- const allowedOrigins = ['http://mcpserver.10.0.0.18.nip.io:3000', 'http://192.168.0.10:8080'];
105
- const corsConfig = { ...mockConfig, httpAllowedOrigins: allowedOrigins };
106
-
107
- expect(corsConfig.httpAllowedOrigins).toContain('http://mcpserver.10.0.0.18.nip.io:3000');
108
- expect(corsConfig.httpAllowedOrigins).toContain('http://192.168.0.10:8080');
109
- });
110
-
111
- it('should allow all origins when list is empty (wildcard)', () => {
112
- const wildcardConfig = { ...mockConfig, httpAllowedOrigins: [] };
113
-
114
- // Empty array means wildcard was used
115
- expect(wildcardConfig.httpAllowedOrigins).toEqual([]);
116
- });
117
-
118
- it('should set correct CORS headers', () => {
119
- const expectedHeaders = {
120
- 'Access-Control-Allow-Origin': 'http://mcpserver.10.0.0.18.nip.io:3000',
121
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
122
- 'Access-Control-Allow-Headers': 'Content-Type, Mcp-Session-Id',
123
- 'Access-Control-Max-Age': '86400',
124
- };
125
-
126
- expect(expectedHeaders['Access-Control-Allow-Methods']).toContain('GET');
127
- expect(expectedHeaders['Access-Control-Allow-Methods']).toContain('POST');
128
- expect(expectedHeaders['Access-Control-Allow-Headers']).toContain('Mcp-Session-Id');
129
- });
130
- });
131
-
132
- describe('Server Configuration', () => {
133
- it('should use default port if not specified', () => {
134
- const configWithoutPort = { ...mockConfig };
135
- delete configWithoutPort.httpPort;
136
-
137
- const port = configWithoutPort.httpPort ?? 3000;
138
- expect(port).toBe(3000);
139
- });
140
-
141
- it('should use default bind address if not specified', () => {
142
- const configWithoutBind = { ...mockConfig };
143
- delete configWithoutBind.httpBindAddr;
144
-
145
- const bindAddr = configWithoutBind.httpBindAddr ?? '127.0.0.1';
146
- expect(bindAddr).toBe('127.0.0.1');
147
- });
148
-
149
- it('should use default health check path if not specified', () => {
150
- const configWithoutHealthPath = { ...mockConfig };
151
- delete configWithoutHealthPath.httpHealthcheckPath;
152
-
153
- const healthPath = configWithoutHealthPath.httpHealthcheckPath ?? '/health';
154
- expect(healthPath).toBe('/health');
155
- });
156
-
157
- it('should support custom configuration', () => {
158
- const customConfig = {
159
- ...mockConfig,
160
- httpPort: 8080,
161
- httpBindAddr: '0.0.0.0',
162
- httpPath: '/custom-mcp',
163
- httpHealthcheckPath: '/status',
164
- };
165
-
166
- expect(customConfig.httpPort).toBe(8080);
167
- expect(customConfig.httpBindAddr).toBe('0.0.0.0');
168
- expect(customConfig.httpPath).toBe('/custom-mcp');
169
- expect(customConfig.httpHealthcheckPath).toBe('/status');
170
- });
171
- });
172
-
173
- describe('Request Routing', () => {
174
- it('should route to health check for /health', () => {
175
- const healthPath = mockConfig.httpHealthcheckPath;
176
- const requestUrl = '/health';
177
-
178
- expect(requestUrl).toBe(healthPath);
179
- });
180
-
181
- it('should route to MCP endpoint', () => {
182
- const mcpPath = mockConfig.httpPath;
183
- const requestUrl = '/mcp';
184
-
185
- expect(requestUrl).toBe(mcpPath);
186
- });
187
-
188
- it('should handle OPTIONS requests for CORS preflight', () => {
189
- const method = 'OPTIONS';
190
-
191
- expect(method).toBe('OPTIONS');
192
- // Should return 204 No Content
193
- });
194
-
195
- it('should return 404 for unknown paths', () => {
196
- const unknownPath = '/unknown-endpoint';
197
- const validPaths = [mockConfig.httpPath, mockConfig.httpHealthcheckPath];
198
-
199
- expect(validPaths).not.toContain(unknownPath);
200
- });
201
- });
202
-
203
- describe('Session Management', () => {
204
- it('should create new session when stateful mode is disabled', () => {
205
- expect(mockConfig.stateful).toBe(false);
206
- // Each request should create a new transport
207
- });
208
-
209
- it('should reuse session when stateful mode is enabled', () => {
210
- const statefulConfig = { ...mockConfig, stateful: true };
211
-
212
- expect(statefulConfig.stateful).toBe(true);
213
- // Should maintain sessions in Map
214
- });
215
-
216
- it('should require Mcp-Session-Id header for stateful mode', () => {
217
- const statefulConfig = {
218
- ...mockConfig,
219
- stateful: true,
220
- };
221
-
222
- expect(statefulConfig.stateful).toBe(true);
223
- // Should validate session ID from header
224
- });
225
-
226
- it('should return error for invalid session ID', () => {
227
- const statefulConfig = { ...mockConfig, stateful: true };
228
-
229
- expect(statefulConfig.stateful).toBe(true);
230
- // Should return 400 error for missing/invalid session
231
- });
232
- });
233
-
234
- describe('Error Handling', () => {
235
- it('should handle request parsing errors', () => {
236
- const invalidJson = 'not-valid-json';
237
-
238
- expect(() => JSON.parse(invalidJson)).toThrow();
239
- // Should return 500 with error message
240
- });
241
-
242
- it('should not send headers if already sent', () => {
243
- let headersSent = true;
244
-
245
- if (!headersSent) {
246
- // Would send error response
247
- }
248
-
249
- expect(headersSent).toBe(true);
250
- });
251
-
252
- it('should log errors with request details', () => {
253
- const error = new Error('Test error');
254
- const requestMethod = 'POST';
255
- const requestUrl = '/mcp';
256
-
257
- const logData = {
258
- error,
259
- method: requestMethod,
260
- url: requestUrl,
261
- };
262
-
263
- expect(logData.method).toBe('POST');
264
- expect(logData.url).toBe('/mcp');
265
- expect(logData.error).toBe(error);
266
- });
267
- });
268
-
269
- describe('JSON Response Helper', () => {
270
- it('should format JSON response correctly', () => {
271
- const statusCode = 200;
272
- const data = { status: 'healthy', version: VERSION };
273
-
274
- const expectedContentType = 'application/json';
275
- const expectedBody = JSON.stringify(data);
276
-
277
- expect(expectedContentType).toBe('application/json');
278
- expect(expectedBody).toBe(`{"status":"healthy","version":"${VERSION}"}`);
279
- });
280
-
281
- it('should handle error responses', () => {
282
- const statusCode = 500;
283
- const errorData = {
284
- error: 'Internal server error',
285
- message: 'Something went wrong',
286
- };
287
-
288
- const expectedBody = JSON.stringify(errorData);
289
-
290
- expect(statusCode).toBe(500);
291
- expect(JSON.parse(expectedBody).error).toBe('Internal server error');
292
- });
293
- });
294
-
295
- describe('Body Parsing', () => {
296
- it('should parse valid JSON body', () => {
297
- const jsonBody = '{"key":"value"}';
298
- const parsed = JSON.parse(jsonBody);
299
-
300
- expect(parsed).toEqual({ key: 'value' });
301
- });
302
-
303
- it('should return undefined for empty body', () => {
304
- const emptyBody = '';
305
-
306
- expect(emptyBody || undefined).toBeUndefined();
307
- });
308
-
309
- it('should throw error for invalid JSON', () => {
310
- const invalidJson = '{invalid}';
311
-
312
- expect(() => JSON.parse(invalidJson)).toThrow();
313
- });
314
- });
315
-
316
- describe('Response Timing', () => {
317
- it('should add Server-Timing header format', () => {
318
- // Server-Timing header format: total;dur=X (integer ms)
319
- const durationMs = 42.5;
320
- const expectedHeader = `total;dur=${Math.round(durationMs)}`;
321
-
322
- expect(expectedHeader).toBe('total;dur=43');
323
- expect(expectedHeader).toMatch(/^total;dur=\d+$/);
324
- });
325
-
326
- it('should wrap response end method', () => {
327
- const headers: Record<string, string> = {};
328
-
329
- const mockRes = {
330
- headersSent: false,
331
- setHeader: vi.fn((key: string, value: string) => {
332
- headers[key] = value;
333
- }),
334
- end: vi.fn(),
335
- } as unknown as ServerResponse;
336
-
337
- // Simulate the timing wrapper behavior
338
- const startTime = performance.now();
339
- const originalEnd = mockRes.end.bind(mockRes);
340
-
341
- mockRes.end = function () {
342
- if (!mockRes.headersSent) {
343
- const durationMs = Math.round(performance.now() - startTime);
344
- mockRes.setHeader('Server-Timing', `total;dur=${durationMs}`);
345
- }
346
- return originalEnd();
347
- } as typeof mockRes.end;
348
-
349
- // Call end
350
- mockRes.end();
351
-
352
- expect(headers['Server-Timing']).toMatch(/^total;dur=\d+$/);
353
- });
354
-
355
- it('should not set headers if already sent', () => {
356
- const headers: Record<string, string> = {};
357
-
358
- const mockRes = {
359
- headersSent: true, // Headers already sent
360
- setHeader: vi.fn((key: string, value: string) => {
361
- headers[key] = value;
362
- }),
363
- end: vi.fn(),
364
- } as unknown as ServerResponse;
365
-
366
- // Simulate the timing wrapper behavior
367
- const originalEnd = mockRes.end.bind(mockRes);
368
-
369
- mockRes.end = function () {
370
- if (!mockRes.headersSent) {
371
- mockRes.setHeader('Server-Timing', 'total;dur=0');
372
- }
373
- return originalEnd();
374
- } as typeof mockRes.end;
375
-
376
- mockRes.end();
377
-
378
- // Headers should not be set because headersSent was true
379
- expect(headers['Server-Timing']).toBeUndefined();
380
- });
381
-
382
- it('should measure actual elapsed time', async () => {
383
- const headers: Record<string, string> = {};
384
-
385
- const mockRes = {
386
- headersSent: false,
387
- setHeader: vi.fn((key: string, value: string) => {
388
- headers[key] = value;
389
- }),
390
- end: vi.fn(),
391
- } as unknown as ServerResponse;
392
-
393
- const startTime = performance.now();
394
- const originalEnd = mockRes.end.bind(mockRes);
395
-
396
- mockRes.end = function () {
397
- if (!mockRes.headersSent) {
398
- const durationMs = Math.round(performance.now() - startTime);
399
- mockRes.setHeader('Server-Timing', `total;dur=${durationMs}`);
400
- }
401
- return originalEnd();
402
- } as typeof mockRes.end;
403
-
404
- // Simulate some work
405
- await new Promise((resolve) => setTimeout(resolve, 10));
406
-
407
- mockRes.end();
408
-
409
- // Extract duration from header
410
- const timing = headers['Server-Timing'];
411
- const match = timing.match(/total;dur=(\d+)/);
412
- const duration = match ? parseInt(match[1], 10) : 0;
413
-
414
- // Real elapsed time was measured (not 0/instant). NOT asserted >= 10:
415
- // setTimeout(10) may fire a hair under 10ms and line 391 Math.round()s the
416
- // result, so a legitimate ~9.4ms run rounds to 9. That jitter is env-specific
417
- // (it tripped Node 22 in CI but not Node 20). A 5ms floor proves the timer was
418
- // captured without being brittle to sub-millisecond timer undershoot + rounding.
419
- expect(duration).toBeGreaterThanOrEqual(5);
420
- // But not too long (sanity check)
421
- expect(duration).toBeLessThan(1000);
422
- });
423
- });
424
-
425
- describe('OpenAPI spec scheme (X-Forwarded-Proto)', () => {
426
- it('returns https only when the header is exactly https', () => {
427
- expect(resolveForwardedProto('https')).toBe('https');
428
- expect(resolveForwardedProto('HTTPS')).toBe('https');
429
- expect(resolveForwardedProto(' https ')).toBe('https');
430
- });
431
-
432
- it('takes the first hop from a comma-separated proxy chain', () => {
433
- expect(resolveForwardedProto('https,http')).toBe('https');
434
- expect(resolveForwardedProto('http, https')).toBe('http');
435
- });
436
-
437
- it('falls back to http for missing, empty, or unknown values', () => {
438
- expect(resolveForwardedProto(undefined)).toBe('http');
439
- expect(resolveForwardedProto('')).toBe('http');
440
- expect(resolveForwardedProto('http')).toBe('http');
441
- // A spoofed/garbage scheme can never be injected into the advertised URL.
442
- expect(resolveForwardedProto('javascript')).toBe('http');
443
- expect(resolveForwardedProto('ftp')).toBe('http');
444
- });
445
-
446
- it('handles array-valued headers by using the first entry', () => {
447
- expect(resolveForwardedProto(['https', 'http'])).toBe('https');
448
- expect(resolveForwardedProto(['http'])).toBe('http');
449
- });
450
-
451
- it('advertises the resolved scheme + host as the OpenAPI server url', () => {
452
- // The regression: behind Cloudflare the spec must say https, not http, or the
453
- // browser blocks tool calls from the https Open WebUI page as mixed content.
454
- const scheme = resolveForwardedProto('https');
455
- const spec = getOpenApiSpec(`${scheme}://mcp.example.com`) as {
456
- servers: { url: string }[];
457
- info: { version: string };
458
- };
459
- expect(spec.servers[0].url).toBe('https://mcp.example.com');
460
- expect(spec.info.version).toBe(VERSION);
461
- });
462
- });
463
-
464
- describe('REST API CORS on pre-handler rejections', () => {
465
- const SSE_PATH = '/subscribe_events';
466
-
467
- it('identifies the browser-facing REST surface', () => {
468
- expect(isRestApiCorsPath('/openapi.json', '/openapi.json', SSE_PATH)).toBe(true);
469
- expect(isRestApiCorsPath('/api/version', '/api/version', SSE_PATH)).toBe(true);
470
- expect(isRestApiCorsPath('/api/states/light.x', '/api/states/light.x', SSE_PATH)).toBe(true);
471
- expect(isRestApiCorsPath('/subscribe_events', '/subscribe_events', SSE_PATH)).toBe(true);
472
- // Query string must not defeat the SSE path match (urlPath is path-only).
473
- expect(isRestApiCorsPath('/subscribe_events?token=x', '/subscribe_events', SSE_PATH)).toBe(true);
474
- });
475
-
476
- it('excludes MCP / health / unknown paths from the REST CORS surface', () => {
477
- expect(isRestApiCorsPath('/mcp', '/mcp', SSE_PATH)).toBe(false);
478
- expect(isRestApiCorsPath('/health', '/health', SSE_PATH)).toBe(false);
479
- expect(isRestApiCorsPath('/', '/', SSE_PATH)).toBe(false);
480
- // Must not be fooled by a path that merely contains "/api/".
481
- expect(isRestApiCorsPath('/not/api/x', '/not/api/x', SSE_PATH)).toBe(false);
482
- });
483
-
484
- it('reflects the request Origin in the CORS headers', () => {
485
- const set: Record<string, string> = {};
486
- const res = { setHeader: (k: string, v: string) => { set[k] = v; } } as unknown as ServerResponse;
487
- const req = { headers: { origin: 'https://owui.example.com' } } as unknown as IncomingMessage;
488
- addRestApiCors(req, res);
489
- expect(set['Access-Control-Allow-Origin']).toBe('https://owui.example.com');
490
- expect(set['Access-Control-Allow-Methods']).toContain('GET');
491
- expect(set['Access-Control-Allow-Headers']).toContain('Authorization');
492
- // Open WebUI sends x-session-id on every tool call; it must be allowed or the
493
- // browser blocks the request as a CORS preflight failure ("NetworkError").
494
- expect(set['Access-Control-Allow-Headers']).toContain('x-session-id');
495
- expect(set['Access-Control-Max-Age']).toBe('86400');
496
- });
497
-
498
- it('falls back to * when the request has no Origin', () => {
499
- const set: Record<string, string> = {};
500
- const res = { setHeader: (k: string, v: string) => { set[k] = v; } } as unknown as ServerResponse;
501
- const req = { headers: {} } as unknown as IncomingMessage;
502
- addRestApiCors(req, res);
503
- expect(set['Access-Control-Allow-Origin']).toBe('*');
504
- });
505
-
506
- it('REGRESSION: a 401 on /api/* still carries Access-Control-Allow-Origin', () => {
507
- // Reproduces the request pipeline ordering: REST CORS is applied for the path
508
- // BEFORE the auth middleware rejects, so the browser sees the real 401 instead
509
- // of an opaque "NetworkError when attempting to fetch resource".
510
- const set: Record<string, string> = {};
511
- let status = 0;
512
- const res = {
513
- setHeader: (k: string, v: string) => { set[k] = v; },
514
- writeHead: (code: number) => { status = code; },
515
- end: () => {},
516
- } as unknown as ServerResponse;
517
- const req = { headers: { origin: 'https://owui.example.com' } } as unknown as IncomingMessage;
518
-
519
- const url = '/api/version';
520
- // 1) dispatcher applies REST CORS for the path...
521
- if (isRestApiCorsPath(url, url, SSE_PATH)) {
522
- addRestApiCors(req, res);
523
- }
524
- // 2) ...then auth fails and writes a 401.
525
- const authOk = false;
526
- if (!authOk) {
527
- res.writeHead(401);
528
- res.end();
529
- }
530
-
531
- expect(status).toBe(401);
532
- expect(set['Access-Control-Allow-Origin']).toBe('https://owui.example.com');
533
- });
534
- });
535
-
536
- describe('stateless SSE GET rejection (isStatelessMcpGet)', () => {
537
- // REGRESSION: in stateless mode a GET /mcp opened the SDK's standalone SSE
538
- // stream on a fresh per-request transport that never emits and never ends —
539
- // an eternal zero-byte response that pinned the Cloudflare→origin keep-alive
540
- // socket and stalled every request queued behind it (mcp-remote's probe GET
541
- // hung its own subsequent initialize POST → "Request timed out"). The
542
- // dispatcher must refuse the GET with 405 before any transport work.
543
- it('rejects GET in stateless mode', () => {
544
- expect(isStatelessMcpGet(false, 'GET')).toBe(true);
545
- });
546
-
547
- it('allows GET in stateful mode (real server→client SSE stream exists)', () => {
548
- expect(isStatelessMcpGet(true, 'GET')).toBe(false);
549
- });
550
-
551
- it('never rejects POST or DELETE in either mode', () => {
552
- expect(isStatelessMcpGet(false, 'POST')).toBe(false);
553
- expect(isStatelessMcpGet(false, 'DELETE')).toBe(false);
554
- expect(isStatelessMcpGet(true, 'POST')).toBe(false);
555
- });
556
-
557
- it('does not reject an undefined method (let the transport answer it)', () => {
558
- expect(isStatelessMcpGet(false, undefined)).toBe(false);
559
- });
560
- });
561
- });