mcp4openapi 0.2.7 → 0.3.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 (317) hide show
  1. package/README.md +147 -63
  2. package/dist/scripts/validate-profile.js +3 -3
  3. package/dist/scripts/validate-profile.js.map +1 -1
  4. package/dist/src/{oauth-provider.d.ts → auth/oauth-provider.d.ts} +7 -2
  5. package/dist/src/auth/oauth-provider.d.ts.map +1 -0
  6. package/dist/src/{oauth-provider.js → auth/oauth-provider.js} +30 -2
  7. package/dist/src/auth/oauth-provider.js.map +1 -0
  8. package/dist/src/core/cli-config.d.ts +9 -0
  9. package/dist/src/core/cli-config.d.ts.map +1 -0
  10. package/dist/src/core/cli-config.js +124 -0
  11. package/dist/src/core/cli-config.js.map +1 -0
  12. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -0
  13. package/dist/src/core/constants.d.ts.map +1 -0
  14. package/dist/src/{constants.js → core/constants.js} +1 -0
  15. package/dist/src/core/constants.js.map +1 -0
  16. package/dist/src/{errors.d.ts → core/errors.d.ts} +6 -0
  17. package/dist/src/core/errors.d.ts.map +1 -0
  18. package/dist/src/{errors.js → core/errors.js} +15 -6
  19. package/dist/src/core/errors.js.map +1 -0
  20. package/dist/src/core/filtering.d.ts +19 -0
  21. package/dist/src/core/filtering.d.ts.map +1 -0
  22. package/dist/src/core/filtering.js +292 -0
  23. package/dist/src/core/filtering.js.map +1 -0
  24. package/dist/src/core/index.d.ts +26 -0
  25. package/dist/src/core/index.d.ts.map +1 -0
  26. package/dist/src/core/index.js +275 -0
  27. package/dist/src/core/index.js.map +1 -0
  28. package/dist/src/core/lib.d.ts +8 -0
  29. package/dist/src/core/lib.d.ts.map +1 -0
  30. package/dist/src/core/lib.js +7 -0
  31. package/dist/src/core/lib.js.map +1 -0
  32. package/dist/src/{logger.d.ts → core/logger.d.ts} +6 -13
  33. package/dist/src/core/logger.d.ts.map +1 -0
  34. package/dist/src/core/logger.js +197 -0
  35. package/dist/src/core/logger.js.map +1 -0
  36. package/dist/src/{metrics.d.ts → core/metrics.d.ts} +11 -0
  37. package/dist/src/core/metrics.d.ts.map +1 -0
  38. package/dist/src/{metrics.js → core/metrics.js} +61 -0
  39. package/dist/src/core/metrics.js.map +1 -0
  40. package/dist/src/core/naming-warnings.d.ts.map +1 -0
  41. package/dist/src/{naming-warnings.js → core/naming-warnings.js} +6 -6
  42. package/dist/src/core/naming-warnings.js.map +1 -0
  43. package/dist/src/core/naming.d.ts.map +1 -0
  44. package/dist/src/core/naming.js.map +1 -0
  45. package/dist/src/generated-schemas.d.ts +281 -79
  46. package/dist/src/generated-schemas.d.ts.map +1 -1
  47. package/dist/src/generated-schemas.js +17 -3
  48. package/dist/src/generated-schemas.js.map +1 -1
  49. package/dist/src/index.d.ts +1 -6
  50. package/dist/src/index.d.ts.map +1 -1
  51. package/dist/src/index.js +1 -156
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/lib.d.ts +1 -7
  54. package/dist/src/lib.d.ts.map +1 -1
  55. package/dist/src/lib.js +1 -6
  56. package/dist/src/lib.js.map +1 -1
  57. package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
  58. package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
  59. package/dist/src/mcp/mcp-server-manager.js +38 -0
  60. package/dist/src/mcp/mcp-server-manager.js.map +1 -0
  61. package/dist/src/{mcp-server.d.ts → mcp/mcp-server.d.ts} +43 -3
  62. package/dist/src/mcp/mcp-server.d.ts.map +1 -0
  63. package/dist/src/{mcp-server.js → mcp/mcp-server.js} +639 -123
  64. package/dist/src/mcp/mcp-server.js.map +1 -0
  65. package/dist/src/{openapi-parser.d.ts → openapi/openapi-parser.d.ts} +1 -1
  66. package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
  67. package/dist/src/{openapi-parser.js → openapi/openapi-parser.js} +2 -2
  68. package/dist/src/openapi/openapi-parser.js.map +1 -0
  69. package/dist/src/{profile-loader.d.ts → profile/profile-loader.d.ts} +3 -2
  70. package/dist/src/profile/profile-loader.d.ts.map +1 -0
  71. package/dist/src/{profile-loader.js → profile/profile-loader.js} +17 -6
  72. package/dist/src/profile/profile-loader.js.map +1 -0
  73. package/dist/src/profile/profile-registry.d.ts +18 -0
  74. package/dist/src/profile/profile-registry.d.ts.map +1 -0
  75. package/dist/src/profile/profile-registry.js +26 -0
  76. package/dist/src/profile/profile-registry.js.map +1 -0
  77. package/dist/src/profile/profile-resolver.d.ts +25 -0
  78. package/dist/src/profile/profile-resolver.d.ts.map +1 -0
  79. package/dist/src/profile/profile-resolver.js +204 -0
  80. package/dist/src/profile/profile-resolver.js.map +1 -0
  81. package/dist/src/profile/startup-profile.d.ts +17 -0
  82. package/dist/src/profile/startup-profile.d.ts.map +1 -0
  83. package/dist/src/profile/startup-profile.js +30 -0
  84. package/dist/src/profile/startup-profile.js.map +1 -0
  85. package/dist/src/profile/startup-validation.d.ts +11 -0
  86. package/dist/src/profile/startup-validation.d.ts.map +1 -0
  87. package/dist/src/profile/startup-validation.js +21 -0
  88. package/dist/src/profile/startup-validation.js.map +1 -0
  89. package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
  90. package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
  91. package/dist/src/testing/dynamic-mock-server.js +138 -0
  92. package/dist/src/testing/dynamic-mock-server.js.map +1 -0
  93. package/dist/src/testing/listen-support.d.ts +3 -0
  94. package/dist/src/testing/listen-support.d.ts.map +1 -0
  95. package/dist/src/testing/listen-support.js +50 -0
  96. package/dist/src/testing/listen-support.js.map +1 -0
  97. package/dist/src/testing/request-assertions.d.ts +5 -0
  98. package/dist/src/testing/request-assertions.d.ts.map +1 -0
  99. package/dist/src/testing/request-assertions.js +165 -0
  100. package/dist/src/testing/request-assertions.js.map +1 -0
  101. package/dist/src/testing/template-utils.d.ts +10 -0
  102. package/dist/src/testing/template-utils.d.ts.map +1 -0
  103. package/dist/src/testing/template-utils.js +72 -0
  104. package/dist/src/testing/template-utils.js.map +1 -0
  105. package/dist/src/testing/test-http-utils.d.ts +1 -1
  106. package/dist/src/testing/test-http-utils.d.ts.map +1 -1
  107. package/dist/src/testing/test-http-utils.js +1 -1
  108. package/dist/src/testing/test-http-utils.js.map +1 -1
  109. package/dist/src/testing/test-loader.d.ts +6 -0
  110. package/dist/src/testing/test-loader.d.ts.map +1 -0
  111. package/dist/src/testing/test-loader.js +212 -0
  112. package/dist/src/testing/test-loader.js.map +1 -0
  113. package/dist/src/testing/test-schema.d.ts +1270 -0
  114. package/dist/src/testing/test-schema.d.ts.map +1 -0
  115. package/dist/src/testing/test-schema.js +76 -0
  116. package/dist/src/testing/test-schema.js.map +1 -0
  117. package/dist/src/tool-filter/compat.d.ts +49 -0
  118. package/dist/src/tool-filter/compat.d.ts.map +1 -0
  119. package/dist/src/tool-filter/compat.js +72 -0
  120. package/dist/src/tool-filter/compat.js.map +1 -0
  121. package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
  122. package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
  123. package/dist/src/tool-filter/config/env-config-parser.js +103 -0
  124. package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
  125. package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
  126. package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
  127. package/dist/src/tool-filter/config/header-config-parser.js +118 -0
  128. package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
  129. package/dist/src/tool-filter/errors.d.ts +18 -0
  130. package/dist/src/tool-filter/errors.d.ts.map +1 -0
  131. package/dist/src/tool-filter/errors.js +21 -0
  132. package/dist/src/tool-filter/errors.js.map +1 -0
  133. package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
  134. package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
  135. package/dist/src/tool-filter/filter/filter-engine.js +94 -0
  136. package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
  137. package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
  138. package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
  139. package/dist/src/tool-filter/filter/filter-rules.js +72 -0
  140. package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
  141. package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
  142. package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
  143. package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
  144. package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
  145. package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
  146. package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
  147. package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
  148. package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
  149. package/dist/src/tool-filter/index.d.ts +25 -0
  150. package/dist/src/tool-filter/index.d.ts.map +1 -0
  151. package/dist/src/tool-filter/index.js +30 -0
  152. package/dist/src/tool-filter/index.js.map +1 -0
  153. package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
  154. package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
  155. package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
  156. package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
  157. package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
  158. package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
  159. package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
  160. package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
  161. package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
  162. package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
  163. package/dist/src/tool-filter/operation/operation-detector.js +96 -0
  164. package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
  165. package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
  166. package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
  167. package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
  168. package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
  169. package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
  170. package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
  171. package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
  172. package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
  173. package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
  174. package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
  175. package/dist/src/tool-filter/regex/regex-validator.js +58 -0
  176. package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
  177. package/dist/src/tool-filter/types.d.ts +92 -0
  178. package/dist/src/tool-filter/types.d.ts.map +1 -0
  179. package/dist/src/tool-filter/types.js +5 -0
  180. package/dist/src/tool-filter/types.js.map +1 -0
  181. package/dist/src/tool-filter/utils.d.ts +11 -0
  182. package/dist/src/tool-filter/utils.d.ts.map +1 -0
  183. package/dist/src/tool-filter/utils.js +13 -0
  184. package/dist/src/tool-filter/utils.js.map +1 -0
  185. package/dist/src/{composite-executor.d.ts → tooling/composite-executor.d.ts} +3 -3
  186. package/dist/src/tooling/composite-executor.d.ts.map +1 -0
  187. package/dist/src/{composite-executor.js → tooling/composite-executor.js} +1 -1
  188. package/dist/src/tooling/composite-executor.js.map +1 -0
  189. package/dist/src/{dag-executor.d.ts → tooling/dag-executor.d.ts} +1 -1
  190. package/dist/src/tooling/dag-executor.d.ts.map +1 -0
  191. package/dist/src/tooling/dag-executor.js.map +1 -0
  192. package/dist/src/{proxy-executor.d.ts → tooling/proxy-executor.d.ts} +19 -4
  193. package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
  194. package/dist/src/tooling/proxy-executor.js +497 -0
  195. package/dist/src/tooling/proxy-executor.js.map +1 -0
  196. package/dist/src/{tool-generator.d.ts → tooling/tool-generator.d.ts} +4 -3
  197. package/dist/src/tooling/tool-generator.d.ts.map +1 -0
  198. package/dist/src/{tool-generator.js → tooling/tool-generator.js} +23 -7
  199. package/dist/src/tooling/tool-generator.js.map +1 -0
  200. package/dist/src/{http-client-factory.d.ts → transport/http-client-factory.d.ts} +4 -1
  201. package/dist/src/transport/http-client-factory.d.ts.map +1 -0
  202. package/dist/src/{http-client-factory.js → transport/http-client-factory.js} +13 -3
  203. package/dist/src/transport/http-client-factory.js.map +1 -0
  204. package/dist/src/transport/http-transport-config.d.ts +6 -0
  205. package/dist/src/transport/http-transport-config.d.ts.map +1 -0
  206. package/dist/src/transport/http-transport-config.js +62 -0
  207. package/dist/src/transport/http-transport-config.js.map +1 -0
  208. package/dist/src/{http-transport.d.ts → transport/http-transport.d.ts} +72 -14
  209. package/dist/src/transport/http-transport.d.ts.map +1 -0
  210. package/dist/src/transport/http-transport.js +2522 -0
  211. package/dist/src/transport/http-transport.js.map +1 -0
  212. package/dist/src/{interceptors.d.ts → transport/interceptors.d.ts} +6 -2
  213. package/dist/src/transport/interceptors.d.ts.map +1 -0
  214. package/dist/src/{interceptors.js → transport/interceptors.js} +77 -46
  215. package/dist/src/transport/interceptors.js.map +1 -0
  216. package/dist/src/types/http-transport.d.ts +25 -0
  217. package/dist/src/types/http-transport.d.ts.map +1 -1
  218. package/dist/src/types/profile.d.ts +31 -1
  219. package/dist/src/types/profile.d.ts.map +1 -1
  220. package/dist/src/validation/argument-normalizer.d.ts +6 -0
  221. package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
  222. package/dist/src/validation/argument-normalizer.js +70 -0
  223. package/dist/src/validation/argument-normalizer.js.map +1 -0
  224. package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
  225. package/dist/src/validation/jsonrpc-validator.js.map +1 -0
  226. package/dist/src/{schema-validator.d.ts → validation/schema-validator.d.ts} +2 -2
  227. package/dist/src/validation/schema-validator.d.ts.map +1 -0
  228. package/dist/src/validation/schema-validator.js.map +1 -0
  229. package/dist/src/validation/validation-utils.d.ts.map +1 -0
  230. package/dist/src/validation/validation-utils.js.map +1 -0
  231. package/package.json +9 -3
  232. package/profile-schema.json +75 -3
  233. package/profiles/gitlab/developer-profile-oauth.json +1520 -0
  234. package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
  235. package/profiles/gitlab/openapi.yaml +6891 -0
  236. package/profiles/n8n/openapi.yaml +2441 -0
  237. package/profiles/n8n/profile-optimized.json +965 -0
  238. package/profiles/n8n/profile-optimized.test.json +1078 -0
  239. package/profiles/n8n/profile.json +1033 -0
  240. package/profiles/n8n/profile.test.json +983 -0
  241. package/profiles/n8n-nodes/openapi.yaml +24 -0
  242. package/profiles/n8n-nodes/profile-nodes.json +44 -0
  243. package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
  244. package/profiles/semgrep/openapi.yaml +4706 -0
  245. package/profiles/semgrep/profile.json +692 -0
  246. package/profiles/semgrep/profile.test.json +471 -0
  247. package/profiles/youtrack/openapi.json +16976 -0
  248. package/profiles/youtrack/profile.json +608 -0
  249. package/profiles/youtrack/profile.test.json +1926 -0
  250. package/dist/src/composite-executor.d.ts.map +0 -1
  251. package/dist/src/composite-executor.js.map +0 -1
  252. package/dist/src/constants.d.ts.map +0 -1
  253. package/dist/src/constants.js.map +0 -1
  254. package/dist/src/dag-executor.d.ts.map +0 -1
  255. package/dist/src/dag-executor.js.map +0 -1
  256. package/dist/src/errors.d.ts.map +0 -1
  257. package/dist/src/errors.js.map +0 -1
  258. package/dist/src/http-client-factory.d.ts.map +0 -1
  259. package/dist/src/http-client-factory.js.map +0 -1
  260. package/dist/src/http-transport.d.ts.map +0 -1
  261. package/dist/src/http-transport.js +0 -1826
  262. package/dist/src/http-transport.js.map +0 -1
  263. package/dist/src/interceptors.d.ts.map +0 -1
  264. package/dist/src/interceptors.js.map +0 -1
  265. package/dist/src/jsonrpc-validator.d.ts.map +0 -1
  266. package/dist/src/jsonrpc-validator.js.map +0 -1
  267. package/dist/src/logger.d.ts.map +0 -1
  268. package/dist/src/logger.js +0 -177
  269. package/dist/src/logger.js.map +0 -1
  270. package/dist/src/mcp-server.d.ts.map +0 -1
  271. package/dist/src/mcp-server.js.map +0 -1
  272. package/dist/src/metrics.d.ts.map +0 -1
  273. package/dist/src/metrics.js.map +0 -1
  274. package/dist/src/naming-warnings.d.ts.map +0 -1
  275. package/dist/src/naming-warnings.js.map +0 -1
  276. package/dist/src/naming.d.ts.map +0 -1
  277. package/dist/src/naming.js.map +0 -1
  278. package/dist/src/oauth-provider.d.ts.map +0 -1
  279. package/dist/src/oauth-provider.js.map +0 -1
  280. package/dist/src/openapi-parser.d.ts.map +0 -1
  281. package/dist/src/openapi-parser.js.map +0 -1
  282. package/dist/src/profile-loader.d.ts.map +0 -1
  283. package/dist/src/profile-loader.js.map +0 -1
  284. package/dist/src/proxy-executor.d.ts.map +0 -1
  285. package/dist/src/proxy-executor.js +0 -240
  286. package/dist/src/proxy-executor.js.map +0 -1
  287. package/dist/src/schema-validator.d.ts.map +0 -1
  288. package/dist/src/schema-validator.js.map +0 -1
  289. package/dist/src/testing/fixtures.d.ts +0 -684
  290. package/dist/src/testing/fixtures.d.ts.map +0 -1
  291. package/dist/src/testing/fixtures.js +0 -528
  292. package/dist/src/testing/fixtures.js.map +0 -1
  293. package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
  294. package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
  295. package/dist/src/testing/mock-gitlab-server.js +0 -1026
  296. package/dist/src/testing/mock-gitlab-server.js.map +0 -1
  297. package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
  298. package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
  299. package/dist/src/testing/mock-semgrep-server.js +0 -213
  300. package/dist/src/testing/mock-semgrep-server.js.map +0 -1
  301. package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
  302. package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
  303. package/dist/src/testing/mock-youtrack-server.js +0 -138
  304. package/dist/src/testing/mock-youtrack-server.js.map +0 -1
  305. package/dist/src/tool-generator.d.ts.map +0 -1
  306. package/dist/src/tool-generator.js.map +0 -1
  307. package/dist/src/validation-utils.d.ts.map +0 -1
  308. package/dist/src/validation-utils.js.map +0 -1
  309. /package/dist/src/{naming-warnings.d.ts → core/naming-warnings.d.ts} +0 -0
  310. /package/dist/src/{naming.d.ts → core/naming.d.ts} +0 -0
  311. /package/dist/src/{naming.js → core/naming.js} +0 -0
  312. /package/dist/src/{dag-executor.js → tooling/dag-executor.js} +0 -0
  313. /package/dist/src/{jsonrpc-validator.d.ts → validation/jsonrpc-validator.d.ts} +0 -0
  314. /package/dist/src/{jsonrpc-validator.js → validation/jsonrpc-validator.js} +0 -0
  315. /package/dist/src/{schema-validator.js → validation/schema-validator.js} +0 -0
  316. /package/dist/src/{validation-utils.d.ts → validation/validation-utils.d.ts} +0 -0
  317. /package/dist/src/{validation-utils.js → validation/validation-utils.js} +0 -0
@@ -1,1826 +0,0 @@
1
- /**
2
- * HTTP Streamable Transport for MCP
3
- *
4
- * Implements MCP Specification 2025-03-26
5
- * https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
6
- *
7
- * Why: Enables remote MCP server access with SSE streaming, session management,
8
- * and resumability for reliable communication over HTTP.
9
- */
10
- import express from 'express';
11
- import https from 'https';
12
- import fs from 'fs';
13
- import crypto from 'crypto';
14
- import { isIP } from 'node:net';
15
- import rateLimit from 'express-rate-limit';
16
- import { isInitializeRequest } from './jsonrpc-validator.js';
17
- import { MetricsCollector } from './metrics.js';
18
- import { ExternalOAuthProvider } from './oauth-provider.js';
19
- import { HTTP_STATUS, MIME_TYPES, OAUTH_PATHS, TIMEOUTS, OAUTH_RATE_LIMIT } from './constants.js';
20
- import { escapeHtmlSafe } from './validation-utils.js';
21
- // Default maximum token length (1000 characters)
22
- const DEFAULT_MAX_TOKEN_LENGTH = 1000;
23
- export class HttpTransport {
24
- constructor(config, logger) {
25
- this.server = null;
26
- this.sessions = new Map();
27
- this.metrics = null;
28
- this.cleanupInterval = null;
29
- this.messageHandler = null;
30
- this.oauthProvider = null;
31
- // Map access_token -> { refreshToken, expiresAt, clientId, scopes }
32
- // Used to bridge /oauth/token endpoint (where we see OAuthTokens) and session initialization (where we only see access token)
33
- this.oauthTokensByAccessToken = new Map();
34
- this.hasWarnedAboutBinding = false;
35
- /**
36
- * Session destruction listeners for cleanup in other components
37
- */
38
- this.sessionDestroyedListeners = [];
39
- // Freeze config to prevent runtime mutation of security-critical settings (allowedOrigins, rate limits, etc.)
40
- this.config = Object.freeze({ ...config });
41
- this.logger = logger;
42
- // Initialize metrics if enabled
43
- if (config.metricsEnabled) {
44
- this.metrics = new MetricsCollector({
45
- enabled: true,
46
- prefix: 'mcp_',
47
- });
48
- }
49
- // Initialize OAuth provider if configured
50
- if (config.oauthConfig) {
51
- this.logger.info('Initializing OAuth provider with config', { hasClientId: !!config.oauthConfig.client_id });
52
- this.oauthProvider = new ExternalOAuthProvider(config.oauthConfig, logger);
53
- // Note: authorizationEndpoint may be undefined at this point if config uses issuer-based discovery
54
- // It will be resolved lazily on first OAuth operation (authorize/token)
55
- this.logger.info('OAuth provider initialized', {
56
- endpoint: this.oauthProvider.authorizationEndpoint || '(to be derived from issuer)',
57
- hasIssuer: !!config.oauthConfig.issuer,
58
- });
59
- }
60
- else {
61
- this.logger.info('No OAuth config provided - OAuth provider not initialized');
62
- }
63
- this.app = express();
64
- this.setupMiddleware();
65
- this.setupRoutes();
66
- }
67
- /**
68
- * Setup Express middleware
69
- *
70
- * Why: Security (Origin validation, rate limiting), JSON parsing, session extraction, metrics
71
- */
72
- setupMiddleware() {
73
- // Request logging (before any middleware)
74
- this.app.use((req, res, next) => {
75
- this.logger.debug('Request received', {
76
- method: req.method,
77
- url: req.url,
78
- path: req.path,
79
- userAgent: req.get('user-agent'),
80
- ip: req.ip,
81
- });
82
- next();
83
- });
84
- // DNS rebinding protection when binding to localhost
85
- // Deny requests with mismatched Host headers to prevent DNS rebinding attacks
86
- // Applies when server host is localhost/127.0.0.1, regardless of auth configuration
87
- this.app.use((req, res, next) => {
88
- const hostCfg = this.config.host?.toLowerCase();
89
- if (hostCfg === 'localhost' || hostCfg === '127.0.0.1') {
90
- const hostHeader = (req.headers['host'] || '').toString().toLowerCase();
91
- const expectedHosts = new Set(['localhost', '127.0.0.1']);
92
- const headerHostOnly = hostHeader.split(':')[0];
93
- if (!expectedHosts.has(headerHostOnly)) {
94
- this.logger.warn('DNS rebinding protection: invalid Host header', {
95
- hostHeader,
96
- expected: Array.from(expectedHosts),
97
- });
98
- res.status(403).json({ error: 'Forbidden' });
99
- return;
100
- }
101
- }
102
- next();
103
- });
104
- // JSON body parser
105
- this.app.use(express.json());
106
- // Metrics: Track request start time
107
- this.app.use((req, res, next) => {
108
- req.startTime = Date.now();
109
- // Log response
110
- const originalSend = res.send;
111
- const originalJson = res.json;
112
- const logger = this.logger;
113
- res.send = function (body) {
114
- logger.debug('Outgoing response', {
115
- method: req.method,
116
- url: req.url,
117
- status: res.statusCode,
118
- contentType: res.get('content-type'),
119
- bodyLength: body ? body.length : 0,
120
- bodyPreview: typeof body === 'string' ? body.substring(0, 200) : '[object]'
121
- });
122
- return originalSend.call(this, body);
123
- };
124
- res.json = function (body) {
125
- logger.debug('Outgoing JSON response', {
126
- method: req.method,
127
- url: req.url,
128
- status: res.statusCode,
129
- body
130
- });
131
- return originalJson.call(this, body);
132
- };
133
- next();
134
- });
135
- // Debug: Log all requests
136
- this.app.use((req, res, next) => {
137
- this.logger.debug('Incoming request', {
138
- method: req.method,
139
- url: req.url,
140
- path: req.path,
141
- headers: {
142
- 'user-agent': req.headers['user-agent'],
143
- 'accept': req.headers.accept,
144
- 'content-type': req.headers['content-type'],
145
- 'authorization': req.headers.authorization ? '[REDACTED]' : undefined
146
- },
147
- ip: req.ip
148
- });
149
- next();
150
- });
151
- // Security: Origin validation (DNS rebinding protection)
152
- this.app.use((req, res, next) => {
153
- const origin = req.headers.origin;
154
- // Warn if binding to 0.0.0.0
155
- if (this.config.host === '0.0.0.0' && !this.hasWarnedAboutBinding) {
156
- this.logger.warn('HTTP transport bound to 0.0.0.0 - accessible from network. Ensure firewall protection.');
157
- this.hasWarnedAboutBinding = true;
158
- }
159
- // Skip Origin check for localhost
160
- if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') {
161
- return next();
162
- }
163
- // Validate Origin header for non-localhost
164
- if (origin && !this.isAllowedOrigin(origin)) {
165
- this.logger.warn('Rejected request from disallowed origin', { origin, ip: req.ip });
166
- return res.status(HTTP_STATUS.FORBIDDEN).json({
167
- error: 'Forbidden',
168
- message: 'Origin not allowed'
169
- });
170
- }
171
- next();
172
- });
173
- // Extract session ID from header
174
- this.app.use((req, res, next) => {
175
- const sessionId = req.headers['mcp-session-id'];
176
- if (sessionId) {
177
- req.sessionId = sessionId;
178
- }
179
- next();
180
- });
181
- }
182
- /**
183
- * Check if origin is allowed
184
- *
185
- * Why: Prevent DNS rebinding attacks
186
- *
187
- * Supports:
188
- * - Exact hostname: 'example.com', 'api.example.com'
189
- * - Wildcard subdomain: '*.example.com'
190
- * - IPv4 CIDR: '192.168.1.0/24', '10.0.0.0/8'
191
- * - IPv4 exact: '192.168.1.100'
192
- */
193
- isAllowedOrigin(origin) {
194
- try {
195
- const url = new URL(origin);
196
- const hostname = url.hostname;
197
- // Always allow localhost
198
- if (hostname === 'localhost' || hostname === '127.0.0.1') {
199
- return true;
200
- }
201
- // Allow configured host
202
- if (hostname === this.config.host) {
203
- return true;
204
- }
205
- // Allow OAuth redirect URI host if configured
206
- if (this.oauthProvider?.redirectUri) {
207
- try {
208
- const redirectUrl = new URL(this.oauthProvider.redirectUri);
209
- if (hostname === redirectUrl.hostname) {
210
- return true;
211
- }
212
- }
213
- catch {
214
- // Invalid URL, ignore
215
- }
216
- }
217
- // Check custom allowed origins
218
- if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {
219
- for (const allowed of this.config.allowedOrigins) {
220
- if (this.matchOrigin(hostname, allowed)) {
221
- return true;
222
- }
223
- }
224
- }
225
- return false;
226
- }
227
- catch {
228
- return false;
229
- }
230
- }
231
- /**
232
- * Match hostname against allowed origin pattern
233
- *
234
- * Supports:
235
- * - Exact match: 'example.com' === 'example.com'
236
- * - Wildcard: '*.example.com' matches 'api.example.com', 'web.example.com'
237
- * - CIDR: '192.168.1.0/24' matches '192.168.1.1' through '192.168.1.254'
238
- */
239
- matchOrigin(hostname, pattern) {
240
- const normalizedHost = this.stripIpv6Brackets(hostname);
241
- const normalizedPattern = this.stripIpv6Brackets(pattern);
242
- // Exact match
243
- if (normalizedHost === normalizedPattern) {
244
- return true;
245
- }
246
- // Wildcard subdomain match (*.example.com)
247
- if (normalizedPattern.startsWith('*.')) {
248
- const domain = normalizedPattern.substring(2); // Remove '*.'
249
- return normalizedHost.endsWith('.' + domain) || normalizedHost === domain;
250
- }
251
- // CIDR match (IPv4/IPv6)
252
- if (normalizedPattern.includes('/')) {
253
- return this.matchCIDR(normalizedHost, normalizedPattern);
254
- }
255
- return false;
256
- }
257
- /**
258
- * Check if IP address is within CIDR range (IPv4 or IPv6)
259
- *
260
- * Example: '192.168.1.50' matches '192.168.1.0/24'
261
- * '2001:db8::1' matches '2001:db8::/32'
262
- */
263
- matchCIDR(ip, cidr) {
264
- const [rawRange, bits] = cidr.split('/');
265
- const range = this.stripIpv6Brackets(rawRange);
266
- const maskBits = parseInt(bits, 10);
267
- if (isNaN(maskBits)) {
268
- return false;
269
- }
270
- const ipVersion = isIP(ip);
271
- const rangeVersion = isIP(range);
272
- if (ipVersion === 0 || rangeVersion === 0 || ipVersion !== rangeVersion) {
273
- return false;
274
- }
275
- if (ipVersion === 4) {
276
- if (maskBits < 0 || maskBits > 32) {
277
- this.logger.warn('Invalid CIDR mask bits', { cidr });
278
- return false;
279
- }
280
- const ipInt = this.ipv4ToInt(ip);
281
- const rangeInt = this.ipv4ToInt(range);
282
- if (ipInt === null || rangeInt === null) {
283
- return false;
284
- }
285
- const mask = (0xFFFFFFFF << (32 - maskBits)) >>> 0;
286
- return (ipInt & mask) === (rangeInt & mask);
287
- }
288
- if (maskBits < 0 || maskBits > 128) {
289
- this.logger.warn('Invalid IPv6 CIDR mask bits', { cidr });
290
- return false;
291
- }
292
- const ipInt = this.ipv6ToBigInt(ip);
293
- const rangeInt = this.ipv6ToBigInt(range);
294
- if (ipInt === null || rangeInt === null) {
295
- return false;
296
- }
297
- const mask = this.ipv6Mask(maskBits);
298
- return (ipInt & mask) === (rangeInt & mask);
299
- }
300
- /**
301
- * Convert IPv4 address to 32-bit integer
302
- *
303
- * Example: '192.168.1.1' -> 3232235777
304
- */
305
- ipv4ToInt(ip) {
306
- const parts = ip.split('.');
307
- if (parts.length !== 4) {
308
- return null;
309
- }
310
- let result = 0;
311
- for (let i = 0; i < 4; i++) {
312
- const octet = parseInt(parts[i], 10);
313
- if (isNaN(octet) || octet < 0 || octet > 255) {
314
- return null;
315
- }
316
- result = (result << 8) | octet;
317
- }
318
- return result >>> 0; // Unsigned
319
- }
320
- /**
321
- * Convert IPv6 address to 128-bit BigInt
322
- */
323
- ipv6ToBigInt(ip) {
324
- const cleaned = this.stripIpv6Brackets(ip);
325
- // Handle IPv4-mapped IPv6 (e.g., ::ffff:192.168.0.1)
326
- let ipv4Tail = null;
327
- let base = cleaned;
328
- if (cleaned.includes('.')) {
329
- const lastColon = cleaned.lastIndexOf(':');
330
- if (lastColon === -1)
331
- return null;
332
- const ipv4Part = cleaned.slice(lastColon + 1);
333
- ipv4Tail = this.ipv4ToInt(ipv4Part);
334
- if (ipv4Tail === null)
335
- return null;
336
- base = cleaned.slice(0, lastColon);
337
- }
338
- const parts = base.split('::');
339
- if (parts.length > 2) {
340
- return null;
341
- }
342
- const head = parts[0] ? parts[0].split(':') : [];
343
- const tail = parts.length === 2 && parts[1] ? parts[1].split(':') : [];
344
- if (head.some(p => p === '') || tail.some(p => p === '')) {
345
- return null;
346
- }
347
- const totalSegmentsNeeded = 8 - (ipv4Tail !== null ? 2 : 0);
348
- let segments = [];
349
- const parseHextets = (items) => {
350
- const result = [];
351
- for (const part of items) {
352
- const num = parseInt(part || '0', 16);
353
- if (isNaN(num) || num < 0 || num > 0xFFFF) {
354
- return null;
355
- }
356
- result.push(num);
357
- }
358
- return result;
359
- };
360
- const headVals = parseHextets(head);
361
- const tailVals = parseHextets(tail);
362
- if (!headVals || !tailVals) {
363
- return null;
364
- }
365
- const missing = totalSegmentsNeeded - (headVals.length + tailVals.length);
366
- if (missing < 0) {
367
- return null;
368
- }
369
- segments = [...headVals, ...Array(missing).fill(0), ...tailVals];
370
- if (segments.length !== totalSegmentsNeeded) {
371
- return null;
372
- }
373
- if (ipv4Tail !== null) {
374
- const high = (ipv4Tail >>> 16) & 0xFFFF;
375
- const low = ipv4Tail & 0xFFFF;
376
- segments.push(high, low);
377
- }
378
- if (segments.length !== 8) {
379
- return null;
380
- }
381
- let value = 0n;
382
- for (const part of segments) {
383
- value = (value << 16n) + BigInt(part);
384
- }
385
- return value;
386
- }
387
- ipv6Mask(maskBits) {
388
- if (maskBits === 0) {
389
- return 0n;
390
- }
391
- const ones = (1n << BigInt(maskBits)) - 1n;
392
- return BigInt.asUintN(128, ones << BigInt(128 - maskBits));
393
- }
394
- stripIpv6Brackets(value) {
395
- return value.replace(/^\[/, '').replace(/\]$/, '');
396
- }
397
- /**
398
- * Create configured rate limiter or a passthrough handler when disabled
399
- *
400
- * Why: Both MCP and metrics endpoints share the same rate limiting setup logic.
401
- * Centralizing it keeps behaviour consistent and avoids drifting configuration.
402
- */
403
- createRateLimiter(options) {
404
- if (!options.enabled) {
405
- return (_req, _res, next) => next();
406
- }
407
- const message = options.responseMessage ??
408
- `Rate limit exceeded. Max ${options.maxRequests} requests per ${options.windowMs / 1000} seconds.`;
409
- return rateLimit({
410
- windowMs: options.windowMs,
411
- max: options.maxRequests,
412
- standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
413
- legacyHeaders: false, // Disable deprecated `X-RateLimit-*` headers
414
- handler: (req, res) => {
415
- this.logger.warn(options.logMessage, {
416
- ip: req.ip,
417
- path: req.path,
418
- method: req.method,
419
- });
420
- res.status(HTTP_STATUS.TOO_MANY_REQUESTS).json({
421
- error: 'Too Many Requests',
422
- message,
423
- });
424
- },
425
- });
426
- }
427
- formatRateLimitMessage(scope, maxRequests, windowMs) {
428
- return `Rate limit exceeded for ${scope}. Max ${maxRequests} requests per ${windowMs / 1000} seconds.`;
429
- }
430
- /**
431
- * Setup MCP endpoint routes
432
- *
433
- * Why: Single endpoint for POST (client→server) and GET (SSE stream)
434
- */
435
- setupRoutes() {
436
- this.logger.info('Setting up HTTP routes');
437
- // Security: Rate limiting setup (needed for OAuth routes)
438
- const rateLimitEnabled = this.config.rateLimitEnabled !== false; // default: true
439
- // Rate limiter for OAuth endpoints (stricter limits for security)
440
- // OAuth endpoints are sensitive and should have lower limits than general API
441
- // Configuration priority: profile > env vars > defaults
442
- const oauthWindowMs = this.config.rateLimitOAuthWindowMs || OAUTH_RATE_LIMIT.WINDOW_MS;
443
- const oauthMaxRequests = this.config.rateLimitOAuthMax || OAUTH_RATE_LIMIT.MAX_REQUESTS;
444
- const oauthRateLimiter = this.createRateLimiter({
445
- enabled: rateLimitEnabled,
446
- windowMs: oauthWindowMs,
447
- maxRequests: oauthMaxRequests,
448
- logMessage: 'Rate limit exceeded for OAuth',
449
- responseMessage: `Too many OAuth requests. Limit: ${oauthMaxRequests} requests per ${Math.round(oauthWindowMs / 60000)} minutes. Please try again later.`,
450
- });
451
- // OAuth 2.0 routes (if configured)
452
- if (this.oauthProvider) {
453
- // Build redirect URI
454
- const redirectUri = this.oauthProvider.redirectUri ||
455
- `http://${this.config.host}:${this.config.port}/oauth/callback`;
456
- // Derive serverUrl from redirectUri
457
- const baseUrl = new URL(redirectUri).origin;
458
- const serverUrl = new URL(`${baseUrl}/mcp`);
459
- // issuerUrl should be the base URL of the authorization server (e.g. https://gitlab.com),
460
- // NOT the authorization endpoint (e.g. https://gitlab.com/oauth/authorize)
461
- // We try to derive it from authorizationEndpoint if not explicitly configured
462
- // Note: authorizationEndpoint might not be ready yet (async initialization),
463
- // so we use serverUrl.origin as fallback
464
- let issuerUrl;
465
- try {
466
- const authEndpoint = this.oauthProvider.authorizationEndpoint;
467
- if (authEndpoint) {
468
- // Try to extract base URL from auth endpoint
469
- const authUrl = new URL(authEndpoint);
470
- issuerUrl = new URL(authUrl.origin);
471
- }
472
- else {
473
- // Fallback: use server origin (will be updated after async init)
474
- issuerUrl = new URL(serverUrl.origin);
475
- }
476
- }
477
- catch (e) {
478
- // Fallback: use server origin
479
- issuerUrl = new URL(serverUrl.origin);
480
- }
481
- this.logger.info('Setting up OAuth routes', {
482
- serverUrl: serverUrl.toString(),
483
- issuerUrl: issuerUrl.toString(),
484
- redirectUri,
485
- });
486
- // Install MCP OAuth router
487
- // This adds standard OAuth endpoints:
488
- // - /.well-known/oauth-authorization-server
489
- // - /.well-known/oauth-protected-resource
490
- // - /oauth/authorize
491
- // - /oauth/token
492
- // - /oauth/register (dynamic client registration)
493
- // - /oauth/revoke (token revocation)
494
- // Only register resource server endpoints, not authorization server endpoints
495
- // since our MCP server is not an OAuth authorization server
496
- this.app.get(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, oauthRateLimiter, (req, res) => {
497
- // Build metadata object with only defined fields (RFC 8707)
498
- const metadata = {
499
- resource: serverUrl.href,
500
- authorization_servers: [serverUrl.origin], // We are the authorization server (proxy)
501
- bearer_methods_supported: ['header'],
502
- };
503
- // Optional: scopes_supported (only if scopes are defined)
504
- if (this.oauthProvider?.scopes && this.oauthProvider.scopes.length > 0) {
505
- metadata.scopes_supported = this.oauthProvider.scopes;
506
- }
507
- // Optional: resource_name (from config, already has fallback in mcp-server.ts)
508
- if (this.config.resourceName) {
509
- metadata.resource_name = this.config.resourceName;
510
- }
511
- // Optional: resource_documentation (from config, may be undefined)
512
- if (this.config.resourceDocumentation) {
513
- metadata.resource_documentation = this.config.resourceDocumentation;
514
- }
515
- res.json(metadata);
516
- });
517
- // Authorization endpoint
518
- // Initiates the OAuth flow by redirecting the user to the external provider
519
- this.app.get(OAUTH_PATHS.AUTHORIZE, oauthRateLimiter, async (req, res) => {
520
- try {
521
- const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query;
522
- if (!client_id || typeof client_id !== 'string') {
523
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing client_id');
524
- return;
525
- }
526
- if (!redirect_uri || typeof redirect_uri !== 'string') {
527
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing redirect_uri');
528
- return;
529
- }
530
- if (this.oauthProvider) {
531
- // Ensure provider is initialized before client validation
532
- // This registers configured client_id if present
533
- await this.oauthProvider.ensureEndpointsInitialized();
534
- // Find the client to validate configuration
535
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
536
- if (!client) {
537
- res.status(HTTP_STATUS.BAD_REQUEST).send('Invalid client_id');
538
- return;
539
- }
540
- // Prepare parameters for provider authorization
541
- const scopeStr = (scope || '').trim();
542
- const params = {
543
- responseType: response_type || 'code',
544
- clientId: client_id,
545
- redirectUri: redirect_uri,
546
- scope: scopeStr ? scopeStr.split(' ') : [],
547
- state: state,
548
- codeChallenge: code_challenge,
549
- codeChallengeMethod: code_challenge_method,
550
- scopes: scopeStr ? scopeStr.split(' ') : [],
551
- };
552
- // Call provider authorize method which handles the redirect logic
553
- await this.oauthProvider.authorize(client, params, res);
554
- }
555
- else {
556
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth provider not initialized');
557
- }
558
- }
559
- catch (error) {
560
- this.logger.error('OAuth authorize error', error instanceof Error ? error : new Error(String(error)));
561
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth authorization failed');
562
- }
563
- });
564
- // Token endpoint
565
- // Exchanges authorization code or refresh token for access token
566
- this.app.post(OAUTH_PATHS.TOKEN, oauthRateLimiter, express.urlencoded({ extended: false }), async (req, res) => {
567
- try {
568
- const { grant_type, code, redirect_uri, client_id, code_verifier, refresh_token } = req.body;
569
- this.logger.debug('OAuth token request', {
570
- grant_type,
571
- client_id,
572
- has_code: !!code,
573
- has_code_verifier: !!code_verifier,
574
- redirect_uri,
575
- });
576
- if (grant_type === 'authorization_code') {
577
- // Authorization Code Flow
578
- if (!code) {
579
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing code' });
580
- return;
581
- }
582
- if (this.oauthProvider) {
583
- // Ensure provider is initialized before client validation
584
- await this.oauthProvider.ensureEndpointsInitialized();
585
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
586
- if (!client) {
587
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
588
- return;
589
- }
590
- const tokens = await this.oauthProvider.exchangeAuthorizationCode(client, code, code_verifier, redirect_uri);
591
- // Store OAuth tokens for later session initialization
592
- this.storeOAuthTokens(tokens, client.client_id, client.scope?.split(' ') || []);
593
- res.json(tokens);
594
- }
595
- else {
596
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
597
- }
598
- }
599
- else if (grant_type === 'refresh_token') {
600
- // Refresh Token Flow
601
- if (!refresh_token) {
602
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing refresh_token' });
603
- return;
604
- }
605
- if (this.oauthProvider) {
606
- // Ensure provider is initialized before client validation
607
- await this.oauthProvider.ensureEndpointsInitialized();
608
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
609
- if (!client) {
610
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
611
- return;
612
- }
613
- const tokens = await this.oauthProvider.exchangeRefreshToken(client, refresh_token);
614
- // Store OAuth tokens for later session initialization
615
- // Note: When refreshing, the old access token should be invalidated
616
- // but we don't track it here - the new token replaces it in the map
617
- this.storeOAuthTokens(tokens, client.client_id, client.scope?.split(' ') || []);
618
- res.json(tokens);
619
- }
620
- else {
621
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
622
- }
623
- }
624
- else {
625
- this.logger.warn('Unsupported grant type', { grant_type, expected: 'authorization_code or refresh_token' });
626
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'unsupported_grant_type' });
627
- return;
628
- }
629
- }
630
- catch (error) {
631
- this.logger.error('OAuth token exchange error', error instanceof Error ? error : new Error(String(error)));
632
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_grant', error_description: String(error) });
633
- }
634
- });
635
- // OAuth callback endpoint to receive tokens from authorization server
636
- this.app.get(OAUTH_PATHS.CALLBACK, oauthRateLimiter, async (req, res) => {
637
- try {
638
- const { code, state, error, error_description } = req.query;
639
- this.logger.info('OAuth callback received', {
640
- hasCode: !!code,
641
- hasState: !!state,
642
- error: error,
643
- errorDescription: error_description
644
- });
645
- if (error) {
646
- // Sanitize error messages to prevent XSS
647
- const safeError = escapeHtmlSafe(error);
648
- const safeErrorDesc = escapeHtmlSafe(error_description);
649
- res.status(HTTP_STATUS.BAD_REQUEST).json({
650
- error: safeError,
651
- error_description: safeErrorDesc || safeError
652
- });
653
- return;
654
- }
655
- if (!code || typeof code !== 'string') {
656
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing authorization code');
657
- return;
658
- }
659
- // Delegate to OAuth provider to handle token exchange and redirect back to client (Cursor)
660
- if (this.oauthProvider) {
661
- await this.oauthProvider.handleCallback(req, res);
662
- }
663
- else {
664
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth provider not initialized');
665
- }
666
- }
667
- catch (error) {
668
- this.logger.error('OAuth callback error', error instanceof Error ? error : new Error(String(error)));
669
- if (!res.headersSent) {
670
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth callback failed');
671
- }
672
- }
673
- });
674
- // Provide authorization server metadata
675
- // We advertise the MCP server itself as the authorization server (Proxy Mode)
676
- // This allows us to handle the redirect dance between Cursor -> MCP -> GitLab -> MCP -> Cursor
677
- this.app.get(OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER, async (req, res) => {
678
- try {
679
- res.json({
680
- issuer: serverUrl.origin, // We are the issuer for the client
681
- authorization_endpoint: new URL(OAUTH_PATHS.AUTHORIZE, serverUrl.origin).href,
682
- token_endpoint: new URL(OAUTH_PATHS.TOKEN, serverUrl.origin).href,
683
- registration_endpoint: new URL(OAUTH_PATHS.REGISTER, serverUrl.origin).href,
684
- response_types_supported: ['code'],
685
- code_challenge_methods_supported: ['S256'],
686
- grant_types_supported: ['authorization_code', 'refresh_token'],
687
- scopes_supported: this.oauthProvider?.scopes || ['api'],
688
- });
689
- }
690
- catch (error) {
691
- this.logger.error('OAuth authorization server metadata error', error instanceof Error ? error : new Error(String(error)));
692
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth metadata failed');
693
- }
694
- });
695
- // Dynamic Client Registration endpoint
696
- // Cursor requires this to register itself with a redirect URI
697
- this.app.post(OAUTH_PATHS.REGISTER, express.json(), async (req, res) => {
698
- try {
699
- const { redirect_uris } = req.body;
700
- this.logger.info('Dynamic client registration request', { redirect_uris });
701
- // We don't actually strictly enforce registration in this proxy mode,
702
- // but we return a valid client configuration to satisfy the client.
703
- // We use a static client ID for the internal mapping.
704
- const clientId = 'mcp-proxy-client';
705
- const clientSecret = 'mcp-proxy-secret';
706
- // Register this client in our internal store so authorize requests pass validation
707
- if (this.oauthProvider) {
708
- const client = {
709
- client_id: clientId,
710
- client_secret: clientSecret,
711
- redirect_uris: redirect_uris || [],
712
- grant_types: ['authorization_code', 'refresh_token'],
713
- response_types: ['code'],
714
- scope: (this.oauthProvider.scopes || []).join(' '),
715
- };
716
- // We need to cast to any because registerClient might not be exposed on the interface
717
- // but we know ExternalOAuthProvider uses InMemoryClientsStore
718
- await this.oauthProvider.clientsStore.registerClient(client);
719
- }
720
- res.status(HTTP_STATUS.CREATED).json({
721
- client_id: clientId,
722
- client_secret: clientSecret,
723
- redirect_uris: redirect_uris,
724
- grant_types: ['authorization_code', 'refresh_token'],
725
- response_types: ['code'],
726
- scope: (this.oauthProvider?.scopes || []).join(' '),
727
- token_endpoint_auth_method: 'client_secret_post'
728
- });
729
- }
730
- catch (error) {
731
- this.logger.error('Client registration failed', error instanceof Error ? error : new Error(String(error)));
732
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'Registration failed' });
733
- }
734
- });
735
- this.logger.info('OAuth routes registered');
736
- }
737
- // Security: Rate limiting setup (for MCP endpoints)
738
- const windowMs = this.config.rateLimitWindowMs || TIMEOUTS.RATE_LIMIT_WINDOW_MS;
739
- const maxRequests = this.config.rateLimitMaxRequests || 100; // 100 req/min
740
- const metricsMaxRequests = this.config.rateLimitMetricsMax || 10; // 10 req/min for metrics
741
- if (rateLimitEnabled) {
742
- this.logger.info('Rate limiting enabled', {
743
- windowMs,
744
- maxRequests,
745
- metricsMaxRequests,
746
- });
747
- }
748
- // Rate limiter for MCP/SSE endpoints (100 req/min by default)
749
- const mcpRateLimiter = this.createRateLimiter({
750
- enabled: rateLimitEnabled,
751
- windowMs,
752
- maxRequests,
753
- logMessage: 'Rate limit exceeded',
754
- });
755
- // Rate limiter for metrics endpoint (10 req/min by default)
756
- const metricsRateLimiter = this.createRateLimiter({
757
- enabled: rateLimitEnabled,
758
- windowMs,
759
- maxRequests: metricsMaxRequests,
760
- logMessage: 'Rate limit exceeded for metrics',
761
- responseMessage: this.formatRateLimitMessage('metrics', metricsMaxRequests, windowMs),
762
- });
763
- // Main MCP endpoint - POST for sending messages
764
- this.app.post('/mcp', mcpRateLimiter, this.handlePost.bind(this));
765
- // CORS preflight handler
766
- this.app.options('/mcp', (req, res) => {
767
- const origin = req.headers.origin;
768
- // Only send CORS headers for explicitly allowed origins; otherwise reject
769
- if (origin && this.isAllowedOrigin(origin)) {
770
- // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration
771
- res.setHeader('Access-Control-Allow-Origin', origin);
772
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
773
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
774
- // We do not allow credentials; prevents cookie-based attacks by default
775
- res.setHeader('Access-Control-Allow-Credentials', 'false');
776
- res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache
777
- return res.status(HTTP_STATUS.OK).send();
778
- }
779
- // Disallowed origin: do not echo origin or emit permissive headers
780
- res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
781
- });
782
- this.logger.info('Registered POST /mcp route');
783
- // Main MCP endpoint - GET for SSE streaming
784
- this.app.get('/mcp', mcpRateLimiter, this.handleGet.bind(this));
785
- // Session termination
786
- this.app.delete('/mcp', mcpRateLimiter, this.handleDelete.bind(this));
787
- // Legacy alias endpoints - deprecated
788
- // Why: Backward compatibility for clients using /sse during migration
789
- this.app.post('/sse', mcpRateLimiter, (req, res, next) => {
790
- this.logger.warn('Deprecated endpoint used: POST /sse. Please migrate to POST /mcp');
791
- this.logger.info('Handling POST /sse request');
792
- return this.handlePost(req, res, next);
793
- });
794
- this.app.get('/sse', mcpRateLimiter, (req, res, next) => {
795
- console.log('=== SSE GET handler called for path:', req.path);
796
- this.logger.warn('Deprecated endpoint used: GET /sse. Please migrate to GET /mcp');
797
- this.logger.info(`Handling GET /sse request from: ${req.ip}`);
798
- return this.handleGet(req, res, next);
799
- });
800
- this.logger.info('Registered SSE routes: POST/GET/DELETE /sse');
801
- this.app.delete('/sse', mcpRateLimiter, (req, res, next) => {
802
- this.logger.warn('Deprecated endpoint used: DELETE /sse. Please migrate to DELETE /mcp');
803
- return this.handleDelete(req, res, next);
804
- });
805
- // Metrics endpoint (if enabled)
806
- if (this.config.metricsEnabled) {
807
- this.app.get(this.config.metricsPath, metricsRateLimiter, this.handleMetrics.bind(this));
808
- }
809
- // Health check (with rate limiting)
810
- this.app.get('/health', mcpRateLimiter, (req, res) => {
811
- const startTime = Date.now();
812
- res.json({ status: 'ok', sessions: this.sessions.size });
813
- if (this.metrics) {
814
- const duration = (Date.now() - startTime) / 1000;
815
- this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
816
- }
817
- });
818
- // Debug: SSE route registered
819
- this.logger.info('SSE routes registered successfully');
820
- // Default 404 handler - MUST be last route registered
821
- // This will catch all unmatched requests
822
- this.app.use((req, res) => {
823
- this.logger.warn('Unhandled request (404)', {
824
- method: req.method,
825
- url: req.url,
826
- path: req.path,
827
- headers: req.headers,
828
- ip: req.ip
829
- });
830
- res.status(HTTP_STATUS.NOT_FOUND).json({
831
- error: 'Not Found',
832
- message: `Endpoint ${req.method} ${req.path} not found`
833
- });
834
- });
835
- }
836
- /**
837
- * Handle metrics endpoint
838
- *
839
- * Why: Prometheus scraping endpoint
840
- */
841
- async handleMetrics(req, res) {
842
- const startTime = Date.now();
843
- try {
844
- if (!this.metrics) {
845
- res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Metrics disabled' });
846
- return;
847
- }
848
- const metrics = await this.metrics.getMetrics();
849
- res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
850
- res.send(metrics);
851
- // Don't record metrics call in metrics (avoid recursion)
852
- }
853
- catch (error) {
854
- this.logger.error('Metrics endpoint error', error);
855
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Internal Server Error', message: error.message });
856
- }
857
- }
858
- /**
859
- * Validate authentication token by making a probe request to the API
860
- *
861
- * Supports all auth types: bearer, query, custom-header
862
- * Returns true if token is valid, false otherwise
863
- */
864
- /**
865
- * Builds a URL by intelligently combining base URL and endpoint
866
- * Handles absolute URLs, absolute paths, and relative paths correctly
867
- */
868
- buildUrl(endpoint, baseUrl) {
869
- // If endpoint is already an absolute URL, use it as-is
870
- if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
871
- return new URL(endpoint);
872
- }
873
- // If endpoint is an absolute path (starts with /), combine with origin of baseUrl
874
- if (endpoint.startsWith('/')) {
875
- const baseUrlObj = new URL(baseUrl);
876
- return new URL(endpoint, baseUrlObj.origin);
877
- }
878
- // Otherwise, treat as relative path and append to baseUrl
879
- // Ensure baseUrl ends with '/' for proper URL construction
880
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
881
- return new URL(endpoint, normalizedBaseUrl);
882
- }
883
- async validateAuthToken(authConfig, token, baseUrl) {
884
- if (!authConfig.validation_endpoint) {
885
- return true; // Skip validation if not configured
886
- }
887
- const url = this.buildUrl(authConfig.validation_endpoint, baseUrl);
888
- const headers = {};
889
- const method = authConfig.validation_method || 'GET';
890
- const timeout = authConfig.validation_timeout_ms || 5000;
891
- const urlString = url.toString();
892
- // Apply auth based on type
893
- switch (authConfig.type) {
894
- case 'oauth':
895
- case 'bearer':
896
- headers['Authorization'] = `Bearer ${token}`;
897
- break;
898
- case 'custom-header':
899
- if (authConfig.header_name) {
900
- headers[authConfig.header_name] = token;
901
- }
902
- break;
903
- case 'query':
904
- if (authConfig.query_param) {
905
- url.searchParams.set(authConfig.query_param, token);
906
- }
907
- break;
908
- }
909
- try {
910
- this.logger.debug('Validating auth token', {
911
- endpoint: urlString,
912
- method,
913
- authType: authConfig.type,
914
- });
915
- const controller = new AbortController();
916
- const timeoutId = setTimeout(() => controller.abort(), timeout);
917
- const response = await fetch(url.toString(), {
918
- method,
919
- headers,
920
- signal: controller.signal,
921
- });
922
- clearTimeout(timeoutId);
923
- const isValid = response.status >= 200 && response.status < 300;
924
- this.logger.debug('Auth token validation result', {
925
- status: response.status,
926
- isValid,
927
- });
928
- return isValid;
929
- }
930
- catch (error) {
931
- this.logger.warn(`Auth token validation failed: ${error.message}`, {
932
- endpoint: authConfig.validation_endpoint,
933
- });
934
- return false;
935
- }
936
- }
937
- /**
938
- * Validate token format and length
939
- *
940
- * Why centralized: Single source of truth for token validation rules
941
- *
942
- * Relaxed validation: Allow common API token characters including colons,
943
- * to support various token formats (GitLab glpat-, YouTrack perm:, etc.)
944
- */
945
- validateToken(token, source) {
946
- const maxLength = this.config.maxTokenLength ?? DEFAULT_MAX_TOKEN_LENGTH;
947
- if (token.length > maxLength) {
948
- throw new Error(`${source} too long (max ${maxLength} characters)`);
949
- }
950
- if (token.length === 0) {
951
- throw new Error(`${source} is empty`);
952
- }
953
- // RFC 6750 Bearer token characters + common API token chars (including colons for YouTrack)
954
- // Allow: alphanumeric, dash, underscore, dot, tilde, plus, slash, equals, colon
955
- // Note: dash at end of character class to avoid being interpreted as range
956
- if (!/^[A-Za-z0-9._~+/:=-]+$/.test(token)) {
957
- throw new Error(`Invalid ${source} format: ...${(token || "").slice(-4)}`);
958
- }
959
- }
960
- /**
961
- * Extract and validate auth token from request headers
962
- *
963
- * Supports:
964
- * - Authorization: Bearer <token>
965
- * - X-API-Token: <token>
966
- * - OAuth session (via mcp-session-id header)
967
- *
968
- * Why strict validation: Prevents header injection attacks
969
- *
970
- * Returns: { type: 'bearer' | 'oauth' | 'api-token', token: string, sessionId?: string }
971
- */
972
- extractAuthToken(req) {
973
- // 1. Check for OAuth session first (highest priority for authenticated sessions)
974
- const sessionId = req.sessionId || req.headers['mcp-session-id'];
975
- if (sessionId && this.sessions.has(sessionId)) {
976
- const session = this.sessions.get(sessionId);
977
- if (session && session.authToken) {
978
- return { type: 'oauth', token: session.authToken, sessionId };
979
- }
980
- }
981
- // 2. Check Authorization: Bearer header
982
- const authHeader = req.headers.authorization;
983
- if (authHeader) {
984
- // Defense against ReDoS: Check length before regex
985
- const maxHeaderLength = (this.config.maxTokenLength ?? DEFAULT_MAX_TOKEN_LENGTH) + 10; // Bearer + spaces + margin
986
- if (authHeader.length > maxHeaderLength) {
987
- throw new Error(`Authorization header too long (max ${maxHeaderLength} characters)`);
988
- }
989
- // Relaxed Bearer token format validation - allow flexible whitespace
990
- // Trim whitespace to handle client variations (IntelliJ, VSCode, etc.)
991
- const trimmed = authHeader.trim();
992
- const match = trimmed.match(/^Bearer\s+(.+)$/);
993
- if (!match) {
994
- throw new Error('Invalid Authorization header format. Expected: Bearer <token>');
995
- }
996
- const token = match[1].trim();
997
- this.validateToken(token, 'Authorization token');
998
- return { type: 'bearer', token };
999
- }
1000
- // 3. Check X-API-Token header (for custom implementations)
1001
- const apiTokenHeader = req.headers['x-api-token'];
1002
- if (apiTokenHeader) {
1003
- if (typeof apiTokenHeader !== 'string') {
1004
- throw new Error('X-API-Token must be a string');
1005
- }
1006
- this.validateToken(apiTokenHeader, 'X-API-Token');
1007
- return { type: 'api-token', token: apiTokenHeader };
1008
- }
1009
- return { type: 'none' };
1010
- }
1011
- /**
1012
- * Handle POST requests - Client sending messages to server
1013
- *
1014
- * MCP Spec: POST can contain requests, notifications, or responses
1015
- */
1016
- async handlePost(req, res) {
1017
- const startTime = Date.now();
1018
- try {
1019
- this.logger.debug('handlePost called', { method: req.method, path: req.path, sessionId: req.sessionId, accept: req.headers.accept });
1020
- const sessionId = req.sessionId;
1021
- const body = req.body;
1022
- // Validate Accept header per MCP Streamable HTTP specification
1023
- const accept = req.headers.accept || '';
1024
- // POST requests can return either JSON or SSE, so must accept both if specified
1025
- // GET requests return SSE, so must accept text/event-stream
1026
- const acceptsJson = accept.includes(MIME_TYPES.JSON) || accept === '*/*' || accept === '';
1027
- const acceptsEventStream = accept.includes(MIME_TYPES.EVENT_STREAM) || accept === '*/*' || accept === '';
1028
- if (req.method === 'GET' && accept && !acceptsEventStream) {
1029
- this.logger.debug('Accept header validation failed for GET, returning 406');
1030
- res.status(HTTP_STATUS.NOT_ACCEPTABLE).json({
1031
- error: 'Not Acceptable',
1032
- message: `GET requests must accept ${MIME_TYPES.EVENT_STREAM}`
1033
- });
1034
- return;
1035
- }
1036
- // For POST, be more flexible - allow if client accepts either JSON or SSE
1037
- if (req.method === 'POST' && accept && !acceptsJson && !acceptsEventStream) {
1038
- this.logger.debug('Accept header validation failed for POST, returning 406');
1039
- res.status(HTTP_STATUS.NOT_ACCEPTABLE).json({
1040
- error: 'Not Acceptable',
1041
- message: `POST requests must accept ${MIME_TYPES.JSON} or ${MIME_TYPES.EVENT_STREAM}`
1042
- });
1043
- return;
1044
- }
1045
- // Check if this is initialization (no session ID yet)
1046
- const isInitialization = isInitializeRequest(body);
1047
- this.logger.debug('Session validation', { isInitialization, sessionId, bodyMethod: body?.method });
1048
- // Validate session (except for initialization)
1049
- if (!isInitialization && sessionId) {
1050
- const session = this.sessions.get(sessionId);
1051
- if (!session) {
1052
- res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1053
- return;
1054
- }
1055
- this.updateSessionActivity(sessionId);
1056
- }
1057
- else if (!isInitialization && !sessionId) {
1058
- this.logger.debug('Session validation failed: non-init request without sessionId');
1059
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required (except for initialization)' });
1060
- return;
1061
- }
1062
- // Determine message type
1063
- const messageType = this.getMessageType(body);
1064
- this.logger.debug('Message type determined', { messageType, hasMessageHandler: !!this.messageHandler });
1065
- // If only notifications/responses, return 202 Accepted
1066
- if (messageType === 'notification-only' || messageType === 'response-only') {
1067
- if (this.messageHandler) {
1068
- await this.messageHandler(body);
1069
- }
1070
- res.status(HTTP_STATUS.ACCEPTED).send();
1071
- return;
1072
- }
1073
- // If contains requests, process and return response
1074
- if (messageType === 'request') {
1075
- if (!this.messageHandler) {
1076
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Internal Server Error', message: 'Message handler not configured' });
1077
- return;
1078
- }
1079
- // Create session on initialization
1080
- let newSessionId;
1081
- if (isInitialization) {
1082
- // Extract and validate auth token from headers
1083
- const authInfo = this.extractAuthToken(req);
1084
- this.logger.debug('Auth token extracted', { authType: authInfo?.type, hasToken: !!authInfo?.token });
1085
- // If OAuth is configured, require authentication for initialization
1086
- // This ensures clients like Cursor properly handle OAuth flow
1087
- if (this.oauthProvider && !authInfo.token) {
1088
- this.logger.debug('OAuth configured but no token provided, triggering OAuth flow');
1089
- const resourceMetadataUrl = new URL(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, this.getServerUrl()).href;
1090
- res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="${this.oauthProvider.scopes.join(' ')}"`);
1091
- res.status(HTTP_STATUS.UNAUTHORIZED).json({
1092
- error: 'Unauthorized',
1093
- message: 'Authentication required for OAuth'
1094
- });
1095
- return;
1096
- }
1097
- // Allow initialization without token for non-OAuth scenarios
1098
- // Validate token if auth is configured and token is provided
1099
- if (authInfo && authInfo.token && this.config.authConfigs && this.config.baseUrl) {
1100
- // Find matching auth config based on priority (authConfigs is sorted)
1101
- // For 'bearer' token type, 'oauth' config is also a match
1102
- const authConfig = this.config.authConfigs.find(c => c.type === authInfo.type ||
1103
- (authInfo.type === 'bearer' && c.type === 'oauth'));
1104
- if (authConfig && authConfig.validation_endpoint) {
1105
- this.logger.info('Validating auth token during initialization', {
1106
- authType: authConfig.type, // Use config type for logging
1107
- endpoint: authConfig.validation_endpoint,
1108
- });
1109
- const isValid = await this.validateAuthToken(authConfig, authInfo.token, this.config.baseUrl);
1110
- if (!isValid) {
1111
- this.logger.warn('Auth token validation failed during initialization', {
1112
- authType: authInfo.type,
1113
- });
1114
- res.status(HTTP_STATUS.UNAUTHORIZED).json({
1115
- error: 'Unauthorized',
1116
- message: 'Invalid or expired authentication token'
1117
- });
1118
- return;
1119
- }
1120
- this.logger.info('Auth token validation successful');
1121
- }
1122
- }
1123
- // Look up OAuth tokens if this is an OAuth token
1124
- let refreshToken;
1125
- let accessTokenExpiresAt;
1126
- let scopes;
1127
- let oauthClientId;
1128
- if (authInfo.token && (authInfo.type === 'oauth' || authInfo.type === 'bearer')) {
1129
- const tokenData = this.oauthTokensByAccessToken.get(authInfo.token);
1130
- if (tokenData) {
1131
- refreshToken = tokenData.refreshToken;
1132
- accessTokenExpiresAt = tokenData.expiresAt;
1133
- scopes = tokenData.scopes;
1134
- oauthClientId = tokenData.clientId;
1135
- this.logger.debug('Found OAuth token data for session', {
1136
- hasRefreshToken: !!refreshToken,
1137
- hasExpiration: !!accessTokenExpiresAt,
1138
- scopesCount: scopes.length,
1139
- });
1140
- }
1141
- else {
1142
- this.logger.debug('No OAuth token data found in map (may be non-OAuth bearer token)', {
1143
- tokenPrefix: authInfo.token.substring(0, 10),
1144
- });
1145
- }
1146
- }
1147
- newSessionId = this.createSession(authInfo.token, refreshToken, accessTokenExpiresAt, scopes, oauthClientId);
1148
- }
1149
- this.logger.debug('Calling messageHandler', { body, sessionId: isInitialization ? newSessionId : sessionId });
1150
- const response = await this.messageHandler(body, isInitialization ? newSessionId : sessionId);
1151
- this.logger.debug('MessageHandler response', { response });
1152
- // Debug: Check OAuth conditions
1153
- this.logger.debug('Checking OAuth conditions', {
1154
- responseError: response.error,
1155
- hasOAuthProvider: !!this.oauthProvider,
1156
- oauthProviderType: typeof this.oauthProvider
1157
- });
1158
- // Check if response contains OAuth error and add WWW-Authenticate header
1159
- const responseObj = response;
1160
- if (responseObj.error && responseObj.error.data && responseObj.error.data.oauth_required) {
1161
- const resourceMetadataUrl = new URL(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, this.getServerUrl()).href;
1162
- res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="api"`);
1163
- res.status(HTTP_STATUS.UNAUTHORIZED); // Set 401 status for OAuth errors
1164
- }
1165
- // Decide response format based on Accept header
1166
- const accept = req.headers.accept || '';
1167
- const wantsOnlySSE = accept.trim() === MIME_TYPES.EVENT_STREAM;
1168
- if (wantsOnlySSE) {
1169
- // Return SSE response only when client explicitly wants text/event-stream only
1170
- this.logger.debug('Sending SSE response', { response, newSessionId });
1171
- this.startSSEResponse(res, response, newSessionId, sessionId);
1172
- }
1173
- else {
1174
- // Return JSON response (default for requests)
1175
- if (newSessionId) {
1176
- res.setHeader('Mcp-Session-Id', newSessionId);
1177
- }
1178
- this.logger.debug('Sending JSON response', { response, newSessionId });
1179
- res.json(response);
1180
- }
1181
- return;
1182
- }
1183
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'Bad Request', message: 'Invalid message type' });
1184
- }
1185
- catch (error) {
1186
- this.logger.error('POST request error', error);
1187
- const status = 500;
1188
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
1189
- // Record error metrics
1190
- if (this.metrics) {
1191
- const duration = (Date.now() - startTime) / 1000;
1192
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
1193
- }
1194
- }
1195
- finally {
1196
- // Record success metrics (if not already recorded in catch)
1197
- if (this.metrics && res.statusCode !== 500) {
1198
- const duration = (Date.now() - startTime) / 1000;
1199
- this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
1200
- }
1201
- }
1202
- }
1203
- /**
1204
- * Handle GET requests - Client opening SSE stream for server messages
1205
- *
1206
- * MCP Spec: GET opens SSE stream for server-initiated requests/notifications
1207
- */
1208
- async handleGet(req, res) {
1209
- const startTime = Date.now();
1210
- try {
1211
- const sessionId = req.sessionId;
1212
- const lastEventId = req.headers['last-event-id'];
1213
- // Validate Accept header
1214
- const accept = req.headers.accept || '';
1215
- if (!accept.includes(MIME_TYPES.EVENT_STREAM)) {
1216
- res.status(HTTP_STATUS.METHOD_NOT_ALLOWED).json({ error: 'Method Not Allowed', message: `Must accept ${MIME_TYPES.EVENT_STREAM}` });
1217
- return;
1218
- }
1219
- // Validate session
1220
- if (!sessionId) {
1221
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
1222
- return;
1223
- }
1224
- const session = this.sessions.get(sessionId);
1225
- if (!session) {
1226
- res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1227
- return;
1228
- }
1229
- this.updateSessionActivity(sessionId);
1230
- // Start SSE stream
1231
- this.startSSEStream(res, sessionId, lastEventId);
1232
- // Record metrics for successful SSE start
1233
- if (this.metrics) {
1234
- const duration = (Date.now() - startTime) / 1000;
1235
- this.metrics.recordHttpRequest(req.method, req.path, 200, duration);
1236
- }
1237
- }
1238
- catch (error) {
1239
- this.logger.error('GET request error', error);
1240
- const status = 500;
1241
- if (!res.headersSent) {
1242
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
1243
- }
1244
- // Record error metrics
1245
- if (this.metrics) {
1246
- const duration = (Date.now() - startTime) / 1000;
1247
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
1248
- }
1249
- }
1250
- }
1251
- /**
1252
- * Handle DELETE requests - Client terminating session
1253
- *
1254
- * MCP Spec: DELETE explicitly terminates session
1255
- */
1256
- handleDelete(req, res) {
1257
- const startTime = Date.now();
1258
- const sessionId = req.sessionId;
1259
- if (!sessionId) {
1260
- const status = 400;
1261
- res.status(status).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
1262
- if (this.metrics) {
1263
- const duration = (Date.now() - startTime) / 1000;
1264
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
1265
- }
1266
- return;
1267
- }
1268
- const session = this.sessions.get(sessionId);
1269
- if (!session) {
1270
- const status = 404;
1271
- res.status(status).json({ error: 'Not Found', message: 'Session not found' });
1272
- if (this.metrics) {
1273
- const duration = (Date.now() - startTime) / 1000;
1274
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
1275
- }
1276
- return;
1277
- }
1278
- this.destroySession(sessionId);
1279
- const status = 204;
1280
- res.status(status).send();
1281
- if (this.metrics) {
1282
- const duration = (Date.now() - startTime) / 1000;
1283
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
1284
- }
1285
- }
1286
- /**
1287
- * Start SSE response for a POST request
1288
- *
1289
- * Why: Returns response via SSE stream, allows server-initiated messages
1290
- */
1291
- startSSEResponse(res, response, newSessionId, sessionId) {
1292
- res.setHeader('Content-Type', MIME_TYPES.EVENT_STREAM);
1293
- res.setHeader('Cache-Control', 'no-cache');
1294
- res.setHeader('Connection', 'keep-alive');
1295
- if (newSessionId) {
1296
- res.setHeader('Mcp-Session-Id', newSessionId);
1297
- }
1298
- // Send response
1299
- const eventId = Date.now();
1300
- res.write(`id: ${eventId}\n`);
1301
- res.write(`data: ${JSON.stringify(response)}\n\n`);
1302
- // Close stream
1303
- res.end();
1304
- }
1305
- /**
1306
- * Start SSE stream for GET request
1307
- *
1308
- * Why: Allows server to send requests/notifications to client
1309
- */
1310
- startSSEStream(res, sessionId, lastEventId) {
1311
- res.setHeader('Content-Type', MIME_TYPES.EVENT_STREAM);
1312
- res.setHeader('Cache-Control', 'no-cache');
1313
- res.setHeader('Connection', 'keep-alive');
1314
- const streamId = crypto.randomBytes(16).toString('hex');
1315
- const session = this.sessions.get(sessionId);
1316
- const streamState = {
1317
- streamId,
1318
- lastEventId: lastEventId ? parseInt(lastEventId, 10) : 0,
1319
- messageQueue: [],
1320
- active: true,
1321
- response: res,
1322
- };
1323
- session.sseStreams.set(streamId, streamState);
1324
- // Replay missed messages if resuming
1325
- if (lastEventId) {
1326
- this.replayMessages(res, streamState);
1327
- }
1328
- // Setup heartbeat if enabled
1329
- let heartbeatInterval = null;
1330
- if (this.config.heartbeatEnabled) {
1331
- heartbeatInterval = setInterval(() => {
1332
- if (streamState.active) {
1333
- res.write(':ping\n\n');
1334
- }
1335
- }, this.config.heartbeatIntervalMs);
1336
- }
1337
- // Handle client disconnect
1338
- res.on('close', () => {
1339
- streamState.active = false;
1340
- if (heartbeatInterval) {
1341
- clearInterval(heartbeatInterval);
1342
- }
1343
- this.logger.info('SSE stream closed', { sessionId, streamId });
1344
- });
1345
- this.logger.info('SSE stream opened', { sessionId, streamId, resuming: !!lastEventId });
1346
- }
1347
- /**
1348
- * Replay messages after Last-Event-ID
1349
- *
1350
- * Why: Resumability - client can reconnect and receive missed messages
1351
- */
1352
- replayMessages(res, streamState) {
1353
- const missedMessages = streamState.messageQueue.filter(msg => msg.eventId > streamState.lastEventId);
1354
- for (const msg of missedMessages) {
1355
- res.write(`id: ${msg.eventId}\n`);
1356
- res.write(`data: ${JSON.stringify(msg.data)}\n\n`);
1357
- }
1358
- this.logger.info('Replayed messages', { count: missedMessages.length, streamId: streamState.streamId });
1359
- }
1360
- /**
1361
- * Send message to client via SSE
1362
- *
1363
- * Why: Server-initiated requests/notifications
1364
- */
1365
- sendToClient(sessionId, message) {
1366
- const session = this.sessions.get(sessionId);
1367
- if (!session) {
1368
- this.logger.warn('Cannot send to client: session not found', { sessionId });
1369
- return;
1370
- }
1371
- const eventId = Date.now();
1372
- const queuedMessage = {
1373
- eventId,
1374
- data: message,
1375
- timestamp: Date.now(),
1376
- };
1377
- // Send to all active streams for this session
1378
- for (const [streamId, streamState] of session.sseStreams) {
1379
- if (streamState.active) {
1380
- // Queue for resumability
1381
- streamState.messageQueue.push(queuedMessage);
1382
- // Keep only last 100 messages
1383
- if (streamState.messageQueue.length > 100) {
1384
- streamState.messageQueue.shift();
1385
- }
1386
- }
1387
- }
1388
- }
1389
- /**
1390
- * Determine message type (request, notification, response)
1391
- */
1392
- getMessageType(body) {
1393
- if (Array.isArray(body)) {
1394
- // Batch
1395
- const hasRequest = body.some((msg) => typeof msg === 'object' && msg !== null && 'method' in msg && 'id' in msg);
1396
- const hasNotification = body.some((msg) => typeof msg === 'object' && msg !== null && 'method' in msg && !('id' in msg));
1397
- const hasResponse = body.some((msg) => typeof msg === 'object' && msg !== null && ('result' in msg || 'error' in msg));
1398
- if (hasRequest)
1399
- return 'request';
1400
- if (hasNotification && !hasResponse)
1401
- return 'notification-only';
1402
- if (hasResponse && !hasNotification)
1403
- return 'response-only';
1404
- return 'mixed';
1405
- }
1406
- else if (typeof body === 'object' && body !== null) {
1407
- const msg = body;
1408
- if ('method' in msg) {
1409
- return 'id' in msg ? 'request' : 'notification-only';
1410
- }
1411
- if ('result' in msg || 'error' in msg) {
1412
- return 'response-only';
1413
- }
1414
- }
1415
- return 'unknown';
1416
- }
1417
- /**
1418
- * Create new session
1419
- *
1420
- * Why: Stateful sessions for MCP protocol
1421
- */
1422
- createSession(authToken, refreshToken, accessTokenExpiresAt, scopes, oauthClientId) {
1423
- // Validate token if provided (defense in depth)
1424
- if (authToken) {
1425
- this.validateToken(authToken, 'Session auth token');
1426
- }
1427
- const sessionId = crypto.randomUUID();
1428
- const session = {
1429
- id: sessionId,
1430
- createdAt: Date.now(),
1431
- lastActivityAt: Date.now(),
1432
- sseStreams: new Map(),
1433
- authToken,
1434
- refreshToken,
1435
- accessTokenExpiresAt,
1436
- scopes,
1437
- oauthClientId,
1438
- };
1439
- this.sessions.set(sessionId, session);
1440
- this.logger.info('Session created', {
1441
- sessionId,
1442
- hasAuthToken: !!authToken,
1443
- hasRefreshToken: !!refreshToken,
1444
- hasExpiration: !!accessTokenExpiresAt,
1445
- });
1446
- // Record metrics
1447
- if (this.metrics) {
1448
- this.metrics.recordSessionCreated();
1449
- }
1450
- return sessionId;
1451
- }
1452
- /**
1453
- * Update session activity timestamp
1454
- */
1455
- updateSessionActivity(sessionId) {
1456
- const session = this.sessions.get(sessionId);
1457
- if (session) {
1458
- session.lastActivityAt = Date.now();
1459
- }
1460
- }
1461
- /**
1462
- * Destroy session and cleanup resources
1463
- *
1464
- * Why: Free memory, close streams
1465
- */
1466
- destroySession(sessionId) {
1467
- const session = this.sessions.get(sessionId);
1468
- if (session) {
1469
- // Close all active SSE streams
1470
- for (const [, streamState] of session.sseStreams) {
1471
- streamState.active = false;
1472
- // Close the HTTP response to terminate the SSE connection
1473
- try {
1474
- if (!streamState.response.headersSent || !streamState.response.writableEnded) {
1475
- streamState.response.end();
1476
- }
1477
- }
1478
- catch (error) {
1479
- // Ignore errors if response is already closed
1480
- this.logger.debug('Failed to close SSE response', { error: error.message });
1481
- }
1482
- }
1483
- session.sseStreams.clear();
1484
- // Clean up OAuth token from map if present
1485
- if (session.authToken) {
1486
- this.oauthTokensByAccessToken.delete(session.authToken);
1487
- }
1488
- this.sessions.delete(sessionId);
1489
- this.logger.info('Session destroyed', { sessionId });
1490
- // Notify session destruction listeners (for cleanup in MCPServer)
1491
- this.notifySessionDestroyed(sessionId);
1492
- // Record metrics
1493
- if (this.metrics) {
1494
- this.metrics.recordSessionDestroyed();
1495
- }
1496
- }
1497
- }
1498
- /**
1499
- * Register listener for session destruction events
1500
- *
1501
- * Why: Allows MCPServer to cleanup per-session HTTP clients
1502
- */
1503
- onSessionDestroyed(listener) {
1504
- this.sessionDestroyedListeners.push(listener);
1505
- }
1506
- /**
1507
- * Notify all listeners about session destruction
1508
- */
1509
- notifySessionDestroyed(sessionId) {
1510
- for (const listener of this.sessionDestroyedListeners) {
1511
- try {
1512
- listener(sessionId);
1513
- }
1514
- catch (error) {
1515
- this.logger.error('Session destroyed listener error', error);
1516
- }
1517
- }
1518
- }
1519
- /**
1520
- * Store OAuth tokens in internal map for later session initialization
1521
- *
1522
- * Why: Bridge between /oauth/token endpoint (where we see OAuthTokens)
1523
- * and session initialization (where we only see access token in Authorization header)
1524
- */
1525
- storeOAuthTokens(tokens, clientId, scopes) {
1526
- if (!tokens.access_token) {
1527
- this.logger.warn('OAuth tokens missing access_token, skipping storage');
1528
- return;
1529
- }
1530
- const expiresAt = tokens.expires_in
1531
- ? Date.now() + tokens.expires_in * 1000
1532
- : undefined;
1533
- this.oauthTokensByAccessToken.set(tokens.access_token, {
1534
- refreshToken: tokens.refresh_token,
1535
- expiresAt,
1536
- clientId,
1537
- scopes,
1538
- });
1539
- this.logger.debug('Stored OAuth tokens', {
1540
- hasRefreshToken: !!tokens.refresh_token,
1541
- expiresAt,
1542
- clientId,
1543
- scopesCount: scopes.length,
1544
- });
1545
- }
1546
- /**
1547
- * Cleanup expired sessions
1548
- *
1549
- * Why: Prevent memory leaks, enforce session timeout
1550
- *
1551
- * OAuth sessions with refresh tokens have extended or unlimited timeout
1552
- * to avoid forcing users to re-authenticate after periods of inactivity
1553
- */
1554
- cleanupExpiredSessions() {
1555
- const now = Date.now();
1556
- const expiredSessions = [];
1557
- // Default OAuth session timeout: 24 hours (or configurable)
1558
- const oauthSessionTimeoutMs = this.config.oauthSessionTimeoutMs
1559
- ?? (24 * 60 * 60 * 1000); // 24 hours default
1560
- for (const [sessionId, session] of this.sessions) {
1561
- const age = now - session.lastActivityAt;
1562
- // OAuth sessions with refresh tokens: use extended timeout or never expire
1563
- if (session.refreshToken) {
1564
- // If oauthSessionTimeoutMs is 0 or negative, never expire OAuth sessions
1565
- if (oauthSessionTimeoutMs > 0 && age > oauthSessionTimeoutMs) {
1566
- expiredSessions.push(sessionId);
1567
- }
1568
- // Otherwise, keep the session alive (unlimited timeout)
1569
- }
1570
- else {
1571
- // Non-OAuth sessions: use standard timeout
1572
- if (age > this.config.sessionTimeoutMs) {
1573
- expiredSessions.push(sessionId);
1574
- }
1575
- }
1576
- }
1577
- for (const sessionId of expiredSessions) {
1578
- this.destroySession(sessionId);
1579
- }
1580
- if (expiredSessions.length > 0) {
1581
- this.logger.info('Cleaned up expired sessions', { count: expiredSessions.length });
1582
- }
1583
- }
1584
- /**
1585
- * Get auth token from session
1586
- *
1587
- * Why public: Allows MCPServer to securely access session tokens without breaking encapsulation
1588
- */
1589
- getSessionToken(sessionId) {
1590
- const session = this.sessions.get(sessionId);
1591
- return session?.authToken;
1592
- }
1593
- /**
1594
- * Ensure session has a valid access token, refreshing if necessary
1595
- *
1596
- * Why: Transparently refresh expired OAuth tokens before making API calls
1597
- * Returns true if token is valid (or was successfully refreshed), false otherwise
1598
- */
1599
- async ensureValidSessionToken(sessionId) {
1600
- const session = this.sessions.get(sessionId);
1601
- if (!session) {
1602
- return false;
1603
- }
1604
- // If no expiration info, assume token is valid (non-OAuth scenarios)
1605
- if (!session.accessTokenExpiresAt) {
1606
- return true;
1607
- }
1608
- const now = Date.now();
1609
- const refreshThresholdMs = this.config.oauthRefreshThresholdMs ?? (60 * 1000); // Default: 60 seconds before expiration
1610
- const timeUntilExpiration = session.accessTokenExpiresAt - now;
1611
- // If token is expired or about to expire, refresh it
1612
- if (timeUntilExpiration <= refreshThresholdMs) {
1613
- this.logger.debug('Access token expired or expiring soon, refreshing', {
1614
- sessionId,
1615
- expiresAt: new Date(session.accessTokenExpiresAt).toISOString(),
1616
- timeUntilExpiration,
1617
- });
1618
- return await this.refreshAccessToken(sessionId);
1619
- }
1620
- return true;
1621
- }
1622
- /**
1623
- * Refresh access token using refresh token
1624
- *
1625
- * Why: Automatically renew expired OAuth access tokens without user intervention
1626
- * Returns true on success, false on failure
1627
- */
1628
- async refreshAccessToken(sessionId) {
1629
- const session = this.sessions.get(sessionId);
1630
- if (!session || !session.refreshToken || !this.oauthProvider) {
1631
- this.logger.warn('Cannot refresh token: missing session, refreshToken, or OAuth provider', {
1632
- sessionId,
1633
- hasSession: !!session,
1634
- hasRefreshToken: !!session?.refreshToken,
1635
- hasOAuthProvider: !!this.oauthProvider,
1636
- });
1637
- return false;
1638
- }
1639
- try {
1640
- // Get client from OAuth provider
1641
- // Try to find client by clientId stored in session, or use default client
1642
- let client;
1643
- if (session.oauthClientId) {
1644
- await this.oauthProvider.ensureEndpointsInitialized();
1645
- client = await this.oauthProvider.clientsStore.getClient(session.oauthClientId);
1646
- }
1647
- // Fallback to default client from config if session client not found
1648
- if (!client && this.oauthProvider) {
1649
- await this.oauthProvider.ensureEndpointsInitialized();
1650
- // Try common client IDs
1651
- const defaultClientIds = ['mcp-proxy-client'];
1652
- if (this.config.oauthConfig?.client_id) {
1653
- defaultClientIds.unshift(this.config.oauthConfig.client_id);
1654
- }
1655
- for (const clientId of defaultClientIds) {
1656
- client = await this.oauthProvider.clientsStore.getClient(clientId);
1657
- if (client)
1658
- break;
1659
- }
1660
- }
1661
- if (!client) {
1662
- this.logger.error('Cannot refresh token: OAuth client not found', undefined, {
1663
- sessionId,
1664
- oauthClientId: session.oauthClientId,
1665
- });
1666
- return false;
1667
- }
1668
- // Exchange refresh token for new tokens
1669
- const tokens = await this.oauthProvider.exchangeRefreshToken(client, session.refreshToken, session.scopes);
1670
- // Update session with new tokens
1671
- const oldAccessToken = session.authToken;
1672
- session.authToken = tokens.access_token;
1673
- session.refreshToken = tokens.refresh_token || session.refreshToken; // Keep old refresh token if new one not provided
1674
- session.accessTokenExpiresAt = tokens.expires_in
1675
- ? Date.now() + tokens.expires_in * 1000
1676
- : undefined;
1677
- // Update token map: remove old token, add new one
1678
- if (oldAccessToken) {
1679
- this.oauthTokensByAccessToken.delete(oldAccessToken);
1680
- }
1681
- this.storeOAuthTokens(tokens, client.client_id, session.scopes || []);
1682
- this.logger.info('Access token refreshed successfully', {
1683
- sessionId,
1684
- newExpiresAt: session.accessTokenExpiresAt ? new Date(session.accessTokenExpiresAt).toISOString() : undefined,
1685
- });
1686
- return true;
1687
- }
1688
- catch (error) {
1689
- this.logger.error('Token refresh failed', error instanceof Error ? error : new Error(String(error)), {
1690
- sessionId,
1691
- });
1692
- return false;
1693
- }
1694
- }
1695
- /**
1696
- * Set message handler for processing incoming JSON-RPC messages
1697
- */
1698
- setMessageHandler(handler) {
1699
- this.messageHandler = handler;
1700
- }
1701
- /**
1702
- * Check if OAuth provider is configured
1703
- */
1704
- hasOAuthProvider() {
1705
- return this.oauthProvider !== null;
1706
- }
1707
- /**
1708
- * Get server URL
1709
- */
1710
- getServerUrl() {
1711
- // Prefer base URL derived from OAuth redirect URI if available
1712
- // This ensures consistency with the public address used for OAuth callbacks
1713
- if (this.oauthProvider?.redirectUri) {
1714
- try {
1715
- return new URL(this.oauthProvider.redirectUri).origin;
1716
- }
1717
- catch (e) {
1718
- // Ignore invalid URL format
1719
- }
1720
- }
1721
- // Fallback to configured host/port
1722
- // If configured with 0.0.0.0, this will return http://0.0.0.0:port
1723
- // which is usually fine for internal communication but not for external clients
1724
- const protocol = this.config.host.includes('://') ? '' : 'http://';
1725
- const host = this.config.host.includes('://') ? this.config.host : this.config.host;
1726
- return `${protocol}${host}:${this.config.port}`;
1727
- }
1728
- /**
1729
- * Get OAuth authorization URL
1730
- */
1731
- getOAuthAuthorizationUrl() {
1732
- return this.oauthProvider?.authorizationEndpoint || '';
1733
- }
1734
- /**
1735
- * Get OAuth scopes
1736
- */
1737
- getOAuthScopes() {
1738
- return this.oauthProvider?.scopes || [];
1739
- }
1740
- /**
1741
- * Start HTTP server
1742
- */
1743
- async start() {
1744
- return new Promise((resolve, reject) => {
1745
- try {
1746
- // Check for SSL configuration from environment variables
1747
- const sslCertFile = process.env.MCP4_SSL_CERT_FILE;
1748
- const sslKeyFile = process.env.MCP4_SSL_KEY_FILE;
1749
- if (sslCertFile && sslKeyFile) {
1750
- // Start HTTPS server
1751
- this.logger.info('SSL configuration detected, starting HTTPS server', {
1752
- certFile: sslCertFile,
1753
- keyFile: sslKeyFile,
1754
- });
1755
- try {
1756
- const httpsOptions = {
1757
- cert: fs.readFileSync(sslCertFile),
1758
- key: fs.readFileSync(sslKeyFile),
1759
- };
1760
- this.server = https.createServer(httpsOptions, this.app);
1761
- this.server.listen(this.config.port, this.config.host, () => {
1762
- this.logger.info('HTTPS transport started', {
1763
- host: this.config.host,
1764
- port: this.config.port,
1765
- heartbeat: this.config.heartbeatEnabled,
1766
- metrics: this.config.metricsEnabled,
1767
- });
1768
- // Start session cleanup interval
1769
- this.cleanupInterval = setInterval(() => this.cleanupExpiredSessions(), TIMEOUTS.CLEANUP_INTERVAL_MS);
1770
- resolve();
1771
- });
1772
- }
1773
- catch (sslError) {
1774
- this.logger.error('Failed to start HTTPS server', sslError instanceof Error ? sslError : new Error(String(sslError)));
1775
- reject(sslError);
1776
- return;
1777
- }
1778
- }
1779
- else {
1780
- // Start HTTP server
1781
- this.server = this.app.listen(this.config.port, this.config.host, () => {
1782
- this.logger.info('HTTP transport started', {
1783
- host: this.config.host,
1784
- port: this.config.port,
1785
- heartbeat: this.config.heartbeatEnabled,
1786
- metrics: this.config.metricsEnabled,
1787
- });
1788
- // Start session cleanup interval
1789
- this.cleanupInterval = setInterval(() => this.cleanupExpiredSessions(), TIMEOUTS.CLEANUP_INTERVAL_MS);
1790
- resolve();
1791
- });
1792
- }
1793
- this.server.on('error', reject);
1794
- }
1795
- catch (error) {
1796
- reject(error);
1797
- }
1798
- });
1799
- }
1800
- /**
1801
- * Stop HTTP server
1802
- */
1803
- async stop() {
1804
- if (this.cleanupInterval) {
1805
- clearInterval(this.cleanupInterval);
1806
- this.cleanupInterval = null;
1807
- }
1808
- // Destroy all sessions
1809
- for (const sessionId of this.sessions.keys()) {
1810
- this.destroySession(sessionId);
1811
- }
1812
- if (this.server) {
1813
- return new Promise((resolve, reject) => {
1814
- this.server.close((err) => {
1815
- if (err)
1816
- reject(err);
1817
- else {
1818
- this.logger.info('HTTP transport stopped');
1819
- resolve();
1820
- }
1821
- });
1822
- });
1823
- }
1824
- }
1825
- }
1826
- //# sourceMappingURL=http-transport.js.map