mcp4openapi 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +143 -63
  2. package/dist/scripts/validate-profile.js +3 -3
  3. package/dist/scripts/validate-profile.js.map +1 -1
  4. package/dist/src/argument-normalizer.d.ts +5 -0
  5. package/dist/src/argument-normalizer.d.ts.map +1 -0
  6. package/dist/src/argument-normalizer.js +61 -0
  7. package/dist/src/argument-normalizer.js.map +1 -0
  8. package/dist/src/auth/oauth-provider.d.ts +131 -0
  9. package/dist/src/auth/oauth-provider.d.ts.map +1 -0
  10. package/dist/src/auth/oauth-provider.js +839 -0
  11. package/dist/src/auth/oauth-provider.js.map +1 -0
  12. package/dist/src/cli-config.d.ts +9 -0
  13. package/dist/src/cli-config.d.ts.map +1 -0
  14. package/dist/src/cli-config.js +111 -0
  15. package/dist/src/cli-config.js.map +1 -0
  16. package/dist/src/core/cli-config.d.ts +9 -0
  17. package/dist/src/core/cli-config.d.ts.map +1 -0
  18. package/dist/src/core/cli-config.js +125 -0
  19. package/dist/src/core/cli-config.js.map +1 -0
  20. package/dist/src/core/constants.d.ts +86 -0
  21. package/dist/src/core/constants.d.ts.map +1 -0
  22. package/dist/src/core/constants.js +86 -0
  23. package/dist/src/core/constants.js.map +1 -0
  24. package/dist/src/core/errors.d.ts +59 -0
  25. package/dist/src/core/errors.d.ts.map +1 -0
  26. package/dist/src/core/errors.js +119 -0
  27. package/dist/src/core/errors.js.map +1 -0
  28. package/dist/src/core/filtering.d.ts +19 -0
  29. package/dist/src/core/filtering.d.ts.map +1 -0
  30. package/dist/src/core/filtering.js +292 -0
  31. package/dist/src/core/filtering.js.map +1 -0
  32. package/dist/src/core/index.d.ts +26 -0
  33. package/dist/src/core/index.d.ts.map +1 -0
  34. package/dist/src/core/index.js +276 -0
  35. package/dist/src/core/index.js.map +1 -0
  36. package/dist/src/core/lib.d.ts +8 -0
  37. package/dist/src/core/lib.d.ts.map +1 -0
  38. package/dist/src/core/lib.js +7 -0
  39. package/dist/src/core/lib.js.map +1 -0
  40. package/dist/src/core/logger.d.ts +59 -0
  41. package/dist/src/core/logger.d.ts.map +1 -0
  42. package/dist/src/core/logger.js +197 -0
  43. package/dist/src/core/logger.js.map +1 -0
  44. package/dist/src/core/metrics.d.ts +97 -0
  45. package/dist/src/core/metrics.d.ts.map +1 -0
  46. package/dist/src/core/metrics.js +273 -0
  47. package/dist/src/core/metrics.js.map +1 -0
  48. package/dist/src/core/naming-warnings.d.ts +23 -0
  49. package/dist/src/core/naming-warnings.d.ts.map +1 -0
  50. package/dist/src/core/naming-warnings.js +83 -0
  51. package/dist/src/core/naming-warnings.js.map +1 -0
  52. package/dist/src/core/naming.d.ts +58 -0
  53. package/dist/src/core/naming.d.ts.map +1 -0
  54. package/dist/src/core/naming.js +510 -0
  55. package/dist/src/core/naming.js.map +1 -0
  56. package/dist/src/errors.d.ts +6 -0
  57. package/dist/src/errors.d.ts.map +1 -1
  58. package/dist/src/errors.js +15 -6
  59. package/dist/src/errors.js.map +1 -1
  60. package/dist/src/filtering.d.ts +19 -0
  61. package/dist/src/filtering.d.ts.map +1 -0
  62. package/dist/src/filtering.js +292 -0
  63. package/dist/src/filtering.js.map +1 -0
  64. package/dist/src/generated-schemas.d.ts +290 -79
  65. package/dist/src/generated-schemas.d.ts.map +1 -1
  66. package/dist/src/generated-schemas.js +17 -2
  67. package/dist/src/generated-schemas.js.map +1 -1
  68. package/dist/src/http-transport-config.d.ts +6 -0
  69. package/dist/src/http-transport-config.d.ts.map +1 -0
  70. package/dist/src/http-transport-config.js +47 -0
  71. package/dist/src/http-transport-config.js.map +1 -0
  72. package/dist/src/http-transport.d.ts +63 -13
  73. package/dist/src/http-transport.d.ts.map +1 -1
  74. package/dist/src/http-transport.js +1045 -482
  75. package/dist/src/http-transport.js.map +1 -1
  76. package/dist/src/index.d.ts +1 -6
  77. package/dist/src/index.d.ts.map +1 -1
  78. package/dist/src/index.js +1 -170
  79. package/dist/src/index.js.map +1 -1
  80. package/dist/src/interceptors.d.ts +1 -0
  81. package/dist/src/interceptors.d.ts.map +1 -1
  82. package/dist/src/interceptors.js +73 -63
  83. package/dist/src/interceptors.js.map +1 -1
  84. package/dist/src/lib.d.ts +1 -7
  85. package/dist/src/lib.d.ts.map +1 -1
  86. package/dist/src/lib.js +1 -6
  87. package/dist/src/lib.js.map +1 -1
  88. package/dist/src/logger.d.ts +5 -0
  89. package/dist/src/logger.d.ts.map +1 -1
  90. package/dist/src/logger.js +9 -1
  91. package/dist/src/logger.js.map +1 -1
  92. package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
  93. package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
  94. package/dist/src/mcp/mcp-server-manager.js +38 -0
  95. package/dist/src/mcp/mcp-server-manager.js.map +1 -0
  96. package/dist/src/mcp/mcp-server.d.ts +205 -0
  97. package/dist/src/mcp/mcp-server.d.ts.map +1 -0
  98. package/dist/src/mcp/mcp-server.js +1473 -0
  99. package/dist/src/mcp/mcp-server.js.map +1 -0
  100. package/dist/src/mcp-server-manager.d.ts +20 -0
  101. package/dist/src/mcp-server-manager.d.ts.map +1 -0
  102. package/dist/src/mcp-server-manager.js +38 -0
  103. package/dist/src/mcp-server-manager.js.map +1 -0
  104. package/dist/src/mcp-server.d.ts +28 -0
  105. package/dist/src/mcp-server.d.ts.map +1 -1
  106. package/dist/src/mcp-server.js +406 -109
  107. package/dist/src/mcp-server.js.map +1 -1
  108. package/dist/src/metrics.d.ts +11 -0
  109. package/dist/src/metrics.d.ts.map +1 -1
  110. package/dist/src/metrics.js +61 -0
  111. package/dist/src/metrics.js.map +1 -1
  112. package/dist/src/oauth-provider.d.ts +5 -0
  113. package/dist/src/oauth-provider.d.ts.map +1 -1
  114. package/dist/src/oauth-provider.js +29 -1
  115. package/dist/src/oauth-provider.js.map +1 -1
  116. package/dist/src/openapi/openapi-parser.d.ts +70 -0
  117. package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
  118. package/dist/src/openapi/openapi-parser.js +458 -0
  119. package/dist/src/openapi/openapi-parser.js.map +1 -0
  120. package/dist/src/profile/profile-loader.d.ts +78 -0
  121. package/dist/src/profile/profile-loader.d.ts.map +1 -0
  122. package/dist/src/profile/profile-loader.js +490 -0
  123. package/dist/src/profile/profile-loader.js.map +1 -0
  124. package/dist/src/profile/profile-registry.d.ts +19 -0
  125. package/dist/src/profile/profile-registry.d.ts.map +1 -0
  126. package/dist/src/profile/profile-registry.js +43 -0
  127. package/dist/src/profile/profile-registry.js.map +1 -0
  128. package/dist/src/profile/profile-resolver.d.ts +41 -0
  129. package/dist/src/profile/profile-resolver.d.ts.map +1 -0
  130. package/dist/src/profile/profile-resolver.js +324 -0
  131. package/dist/src/profile/profile-resolver.js.map +1 -0
  132. package/dist/src/profile/startup-profile.d.ts +17 -0
  133. package/dist/src/profile/startup-profile.d.ts.map +1 -0
  134. package/dist/src/profile/startup-profile.js +30 -0
  135. package/dist/src/profile/startup-profile.js.map +1 -0
  136. package/dist/src/profile/startup-validation.d.ts +11 -0
  137. package/dist/src/profile/startup-validation.d.ts.map +1 -0
  138. package/dist/src/profile/startup-validation.js +21 -0
  139. package/dist/src/profile/startup-validation.js.map +1 -0
  140. package/dist/src/profile-loader.d.ts +1 -0
  141. package/dist/src/profile-loader.d.ts.map +1 -1
  142. package/dist/src/profile-loader.js +14 -3
  143. package/dist/src/profile-loader.js.map +1 -1
  144. package/dist/src/profile-registry.d.ts +18 -0
  145. package/dist/src/profile-registry.d.ts.map +1 -0
  146. package/dist/src/profile-registry.js +26 -0
  147. package/dist/src/profile-registry.js.map +1 -0
  148. package/dist/src/profile-resolver.d.ts +19 -0
  149. package/dist/src/profile-resolver.d.ts.map +1 -0
  150. package/dist/src/profile-resolver.js +167 -0
  151. package/dist/src/profile-resolver.js.map +1 -0
  152. package/dist/src/proxy-executor.d.ts.map +1 -1
  153. package/dist/src/proxy-executor.js +7 -0
  154. package/dist/src/proxy-executor.js.map +1 -1
  155. package/dist/src/startup-profile.d.ts +17 -0
  156. package/dist/src/startup-profile.d.ts.map +1 -0
  157. package/dist/src/startup-profile.js +30 -0
  158. package/dist/src/startup-profile.js.map +1 -0
  159. package/dist/src/startup-validation.d.ts +11 -0
  160. package/dist/src/startup-validation.d.ts.map +1 -0
  161. package/dist/src/startup-validation.js +21 -0
  162. package/dist/src/startup-validation.js.map +1 -0
  163. package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
  164. package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
  165. package/dist/src/testing/dynamic-mock-server.js +138 -0
  166. package/dist/src/testing/dynamic-mock-server.js.map +1 -0
  167. package/dist/src/testing/request-assertions.d.ts +5 -0
  168. package/dist/src/testing/request-assertions.d.ts.map +1 -0
  169. package/dist/src/testing/request-assertions.js +165 -0
  170. package/dist/src/testing/request-assertions.js.map +1 -0
  171. package/dist/src/testing/template-utils.d.ts +10 -0
  172. package/dist/src/testing/template-utils.d.ts.map +1 -0
  173. package/dist/src/testing/template-utils.js +72 -0
  174. package/dist/src/testing/template-utils.js.map +1 -0
  175. package/dist/src/testing/test-http-utils.d.ts +1 -1
  176. package/dist/src/testing/test-http-utils.d.ts.map +1 -1
  177. package/dist/src/testing/test-http-utils.js +1 -1
  178. package/dist/src/testing/test-http-utils.js.map +1 -1
  179. package/dist/src/testing/test-loader.d.ts +6 -0
  180. package/dist/src/testing/test-loader.d.ts.map +1 -0
  181. package/dist/src/testing/test-loader.js +212 -0
  182. package/dist/src/testing/test-loader.js.map +1 -0
  183. package/dist/src/testing/test-schema.d.ts +1270 -0
  184. package/dist/src/testing/test-schema.d.ts.map +1 -0
  185. package/dist/src/testing/test-schema.js +76 -0
  186. package/dist/src/testing/test-schema.js.map +1 -0
  187. package/dist/src/tool-filter/compat.d.ts +49 -0
  188. package/dist/src/tool-filter/compat.d.ts.map +1 -0
  189. package/dist/src/tool-filter/compat.js +72 -0
  190. package/dist/src/tool-filter/compat.js.map +1 -0
  191. package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
  192. package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
  193. package/dist/src/tool-filter/config/env-config-parser.js +103 -0
  194. package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
  195. package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
  196. package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
  197. package/dist/src/tool-filter/config/header-config-parser.js +118 -0
  198. package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
  199. package/dist/src/tool-filter/errors.d.ts +18 -0
  200. package/dist/src/tool-filter/errors.d.ts.map +1 -0
  201. package/dist/src/tool-filter/errors.js +21 -0
  202. package/dist/src/tool-filter/errors.js.map +1 -0
  203. package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
  204. package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
  205. package/dist/src/tool-filter/filter/filter-engine.js +94 -0
  206. package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
  207. package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
  208. package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
  209. package/dist/src/tool-filter/filter/filter-rules.js +72 -0
  210. package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
  211. package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
  212. package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
  213. package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
  214. package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
  215. package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
  216. package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
  217. package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
  218. package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
  219. package/dist/src/tool-filter/index.d.ts +25 -0
  220. package/dist/src/tool-filter/index.d.ts.map +1 -0
  221. package/dist/src/tool-filter/index.js +30 -0
  222. package/dist/src/tool-filter/index.js.map +1 -0
  223. package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
  224. package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
  225. package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
  226. package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
  227. package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
  228. package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
  229. package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
  230. package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
  231. package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
  232. package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
  233. package/dist/src/tool-filter/operation/operation-detector.js +96 -0
  234. package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
  235. package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
  236. package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
  237. package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
  238. package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
  239. package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
  240. package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
  241. package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
  242. package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
  243. package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
  244. package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
  245. package/dist/src/tool-filter/regex/regex-validator.js +58 -0
  246. package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
  247. package/dist/src/tool-filter/types.d.ts +92 -0
  248. package/dist/src/tool-filter/types.d.ts.map +1 -0
  249. package/dist/src/tool-filter/types.js +5 -0
  250. package/dist/src/tool-filter/types.js.map +1 -0
  251. package/dist/src/tool-filter/utils.d.ts +11 -0
  252. package/dist/src/tool-filter/utils.d.ts.map +1 -0
  253. package/dist/src/tool-filter/utils.js +13 -0
  254. package/dist/src/tool-filter/utils.js.map +1 -0
  255. package/dist/src/tool-filter.d.ts +65 -0
  256. package/dist/src/tool-filter.d.ts.map +1 -0
  257. package/dist/src/tool-filter.js +471 -0
  258. package/dist/src/tool-filter.js.map +1 -0
  259. package/dist/src/tool-generator.d.ts +1 -0
  260. package/dist/src/tool-generator.d.ts.map +1 -1
  261. package/dist/src/tool-generator.js +15 -6
  262. package/dist/src/tool-generator.js.map +1 -1
  263. package/dist/src/tooling/composite-executor.d.ts +77 -0
  264. package/dist/src/tooling/composite-executor.d.ts.map +1 -0
  265. package/dist/src/tooling/composite-executor.js +198 -0
  266. package/dist/src/tooling/composite-executor.js.map +1 -0
  267. package/dist/src/tooling/dag-executor.d.ts +49 -0
  268. package/dist/src/tooling/dag-executor.d.ts.map +1 -0
  269. package/dist/src/tooling/dag-executor.js +138 -0
  270. package/dist/src/tooling/dag-executor.js.map +1 -0
  271. package/dist/src/tooling/proxy-executor.d.ts +86 -0
  272. package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
  273. package/dist/src/tooling/proxy-executor.js +501 -0
  274. package/dist/src/tooling/proxy-executor.js.map +1 -0
  275. package/dist/src/tooling/tool-generator.d.ts +67 -0
  276. package/dist/src/tooling/tool-generator.d.ts.map +1 -0
  277. package/dist/src/tooling/tool-generator.js +222 -0
  278. package/dist/src/tooling/tool-generator.js.map +1 -0
  279. package/dist/src/transport/http-client-factory.d.ts +65 -0
  280. package/dist/src/transport/http-client-factory.d.ts.map +1 -0
  281. package/dist/src/transport/http-client-factory.js +143 -0
  282. package/dist/src/transport/http-client-factory.js.map +1 -0
  283. package/dist/src/transport/http-transport-config.d.ts +6 -0
  284. package/dist/src/transport/http-transport-config.d.ts.map +1 -0
  285. package/dist/src/transport/http-transport-config.js +63 -0
  286. package/dist/src/transport/http-transport-config.js.map +1 -0
  287. package/dist/src/transport/http-transport.d.ts +329 -0
  288. package/dist/src/transport/http-transport.d.ts.map +1 -0
  289. package/dist/src/transport/http-transport.js +2584 -0
  290. package/dist/src/transport/http-transport.js.map +1 -0
  291. package/dist/src/transport/interceptors.d.ts +119 -0
  292. package/dist/src/transport/interceptors.d.ts.map +1 -0
  293. package/dist/src/transport/interceptors.js +413 -0
  294. package/dist/src/transport/interceptors.js.map +1 -0
  295. package/dist/src/transport/profile-index.d.ts +84 -0
  296. package/dist/src/transport/profile-index.d.ts.map +1 -0
  297. package/dist/src/transport/profile-index.js +405 -0
  298. package/dist/src/transport/profile-index.js.map +1 -0
  299. package/dist/src/types/http-transport.d.ts +26 -0
  300. package/dist/src/types/http-transport.d.ts.map +1 -1
  301. package/dist/src/types/openapi.d.ts +3 -0
  302. package/dist/src/types/openapi.d.ts.map +1 -1
  303. package/dist/src/types/profile.d.ts +16 -1
  304. package/dist/src/types/profile.d.ts.map +1 -1
  305. package/dist/src/validation/argument-normalizer.d.ts +6 -0
  306. package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
  307. package/dist/src/validation/argument-normalizer.js +70 -0
  308. package/dist/src/validation/argument-normalizer.js.map +1 -0
  309. package/dist/src/validation/jsonrpc-validator.d.ts +27 -0
  310. package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
  311. package/dist/src/validation/jsonrpc-validator.js +58 -0
  312. package/dist/src/validation/jsonrpc-validator.js.map +1 -0
  313. package/dist/src/validation/schema-validator.d.ts +30 -0
  314. package/dist/src/validation/schema-validator.d.ts.map +1 -0
  315. package/dist/src/validation/schema-validator.js +128 -0
  316. package/dist/src/validation/schema-validator.js.map +1 -0
  317. package/dist/src/validation/validation-utils.d.ts +49 -0
  318. package/dist/src/validation/validation-utils.d.ts.map +1 -0
  319. package/dist/src/validation/validation-utils.js +139 -0
  320. package/dist/src/validation/validation-utils.js.map +1 -0
  321. package/html/profile-index.html +386 -0
  322. package/package.json +10 -3
  323. package/profile-schema.json +77 -3
  324. package/profiles/gitlab/developer-profile-oauth.json +1520 -0
  325. package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
  326. package/profiles/gitlab/developer-profile.json +1508 -0
  327. package/profiles/gitlab/developer-profile.test.json +3432 -0
  328. package/profiles/gitlab/openapi.yaml +6891 -0
  329. package/profiles/n8n/openapi.yaml +2441 -0
  330. package/profiles/n8n/profile-optimized.json +965 -0
  331. package/profiles/n8n/profile-optimized.test.json +1078 -0
  332. package/profiles/n8n/profile.json +1033 -0
  333. package/profiles/n8n/profile.test.json +983 -0
  334. package/profiles/n8n-nodes/openapi.yaml +24 -0
  335. package/profiles/n8n-nodes/profile-nodes.json +44 -0
  336. package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
  337. package/profiles/semgrep/openapi.yaml +4706 -0
  338. package/profiles/semgrep/profile.json +692 -0
  339. package/profiles/semgrep/profile.test.json +471 -0
  340. package/profiles/youtrack/openapi.json +16976 -0
  341. package/profiles/youtrack/profile.json +608 -0
  342. package/profiles/youtrack/profile.test.json +1926 -0
  343. package/dist/src/testing/fixtures.d.ts +0 -684
  344. package/dist/src/testing/fixtures.d.ts.map +0 -1
  345. package/dist/src/testing/fixtures.js +0 -528
  346. package/dist/src/testing/fixtures.js.map +0 -1
  347. package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
  348. package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
  349. package/dist/src/testing/mock-gitlab-server.js +0 -1026
  350. package/dist/src/testing/mock-gitlab-server.js.map +0 -1
  351. package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
  352. package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
  353. package/dist/src/testing/mock-semgrep-server.js +0 -213
  354. package/dist/src/testing/mock-semgrep-server.js.map +0 -1
  355. package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
  356. package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
  357. package/dist/src/testing/mock-youtrack-server.js +0 -152
  358. package/dist/src/testing/mock-youtrack-server.js.map +0 -1
@@ -18,20 +18,20 @@ import { MetricsCollector } from './metrics.js';
18
18
  import { ExternalOAuthProvider } from './oauth-provider.js';
19
19
  import { HTTP_STATUS, MIME_TYPES, OAUTH_PATHS, TIMEOUTS, OAUTH_RATE_LIMIT } from './constants.js';
20
20
  import { escapeHtmlSafe } from './validation-utils.js';
21
- import { AuthenticationError, AuthorizationError, RateLimitError, ValidationError, generateCorrelationId, } from './errors.js';
22
- // Default maximum token length (1000 characters)
21
+ import { AuthenticationError, AuthorizationError, ConfigurationError, RateLimitError, ValidationError, generateCorrelationId, } from './errors.js';
22
+ import { parseFilteringHeader, normalizeFilteringHeaderValue } from './filtering.js';
23
+ import { ToolFilterService, EnvConfigParser, HeaderConfigParser, RegexCompiler, RegexValidator, OperationClassifier, OpenAPIOperationResolver, OperationDetector, normalizeToolFilterHeaderValue, parseSessionToolFilterHeader, } from './tool-filter/index.js';
23
24
  const DEFAULT_MAX_TOKEN_LENGTH = 1000;
24
25
  export class HttpTransport {
25
26
  constructor(config, logger) {
26
27
  this.server = null;
27
- this.sessions = new Map();
28
28
  this.metrics = null;
29
29
  this.cleanupInterval = null;
30
30
  this.messageHandler = null;
31
- this.oauthProvider = null;
32
- // Map access_token -> { refreshToken, expiresAt, clientId, scopes }
33
- // Used to bridge /oauth/token endpoint (where we see OAuthTokens) and session initialization (where we only see access token)
34
- this.oauthTokensByAccessToken = new Map();
31
+ this.profileContextProvider = null;
32
+ this.profileStates = new Map();
33
+ this.oauthRedirectHostCache = new Map();
34
+ this.warnedMissingOAuthRedirectEnvVars = new Set();
35
35
  this.hasWarnedAboutBinding = false;
36
36
  /**
37
37
  * Session destruction listeners for cleanup in other components
@@ -47,20 +47,6 @@ export class HttpTransport {
47
47
  prefix: 'mcp_',
48
48
  });
49
49
  }
50
- // Initialize OAuth provider if configured
51
- if (config.oauthConfig) {
52
- this.logger.info('Initializing OAuth provider with config', { hasClientId: !!config.oauthConfig.client_id });
53
- this.oauthProvider = new ExternalOAuthProvider(config.oauthConfig, logger);
54
- // Note: authorizationEndpoint may be undefined at this point if config uses issuer-based discovery
55
- // It will be resolved lazily on first OAuth operation (authorize/token)
56
- this.logger.info('OAuth provider initialized', {
57
- endpoint: this.oauthProvider.authorizationEndpoint || '(to be derived from issuer)',
58
- hasIssuer: !!config.oauthConfig.issuer,
59
- });
60
- }
61
- else {
62
- this.logger.info('No OAuth config provided - OAuth provider not initialized');
63
- }
64
50
  this.app = express();
65
51
  this.setupMiddleware();
66
52
  this.setupRoutes();
@@ -71,6 +57,21 @@ export class HttpTransport {
71
57
  * Why: Security (Origin validation, rate limiting), JSON parsing, session extraction, metrics
72
58
  */
73
59
  setupMiddleware() {
60
+ // Security: standard headers
61
+ this.app.disable('x-powered-by');
62
+ this.app.use((req, res, next) => {
63
+ res.setHeader('Content-Security-Policy', "default-src 'self'; frame-ancestors 'none'");
64
+ res.setHeader('X-Frame-Options', 'DENY');
65
+ res.setHeader('X-Content-Type-Options', 'nosniff');
66
+ res.setHeader('Referrer-Policy', 'no-referrer');
67
+ res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=()');
68
+ // Additional security headers
69
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
70
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
71
+ res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
72
+ res.setHeader('X-DNS-Prefetch-Control', 'off');
73
+ next();
74
+ });
74
75
  // Request logging (before any middleware)
75
76
  this.app.use((req, res, next) => {
76
77
  this.logger.debug('Request received', {
@@ -103,7 +104,9 @@ export class HttpTransport {
103
104
  next();
104
105
  });
105
106
  // JSON body parser
106
- this.app.use(express.json());
107
+ // Limit set to 10MB to support large tool inputs/results (e.g. file content)
108
+ // while preventing massive DoS attacks. Default is 100kb.
109
+ this.app.use(express.json({ limit: '10mb' }));
107
110
  // Metrics: Track request start time
108
111
  this.app.use((req, res, next) => {
109
112
  req.startTime = Date.now();
@@ -157,19 +160,31 @@ export class HttpTransport {
157
160
  this.logger.warn('HTTP transport bound to 0.0.0.0 - accessible from network. Ensure firewall protection.');
158
161
  this.hasWarnedAboutBinding = true;
159
162
  }
160
- // Skip Origin check for localhost
161
- if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') {
162
- return next();
163
+ // Validate Origin header
164
+ // We do not skip this check for localhost, as requests to localhost can still be CSRF targets
165
+ if (!origin) {
166
+ next();
167
+ return;
163
168
  }
164
- // Validate Origin header for non-localhost
165
- if (origin && !this.isAllowedOrigin(origin)) {
166
- this.logger.warn('Rejected request from disallowed origin', { origin, ip: req.ip });
167
- return res.status(HTTP_STATUS.FORBIDDEN).json({
169
+ this.isAllowedOriginForRequest(origin, req)
170
+ .then((allowed) => {
171
+ if (!allowed) {
172
+ this.logger.warn('Rejected request from disallowed origin', { origin, ip: req.ip });
173
+ res.status(HTTP_STATUS.FORBIDDEN).json({
174
+ error: 'Forbidden',
175
+ message: 'Origin not allowed'
176
+ });
177
+ return;
178
+ }
179
+ next();
180
+ })
181
+ .catch((error) => {
182
+ this.logger.error('Origin validation failed', error instanceof Error ? error : new Error(String(error)));
183
+ res.status(HTTP_STATUS.FORBIDDEN).json({
168
184
  error: 'Forbidden',
169
185
  message: 'Origin not allowed'
170
186
  });
171
- }
172
- next();
187
+ });
173
188
  });
174
189
  // Extract session ID from header
175
190
  this.app.use((req, res, next) => {
@@ -180,6 +195,101 @@ export class HttpTransport {
180
195
  next();
181
196
  });
182
197
  }
198
+ setProfileContextProvider(provider) {
199
+ this.profileContextProvider = provider;
200
+ }
201
+ getDefaultProfileId() {
202
+ if (this.config.defaultProfileId) {
203
+ return this.config.defaultProfileId;
204
+ }
205
+ if (this.config.profileRoutingEnabled) {
206
+ return undefined;
207
+ }
208
+ return 'default';
209
+ }
210
+ buildDefaultProfileContext() {
211
+ const profileId = this.getDefaultProfileId();
212
+ if (!profileId) {
213
+ return null;
214
+ }
215
+ return {
216
+ profileId,
217
+ oauthConfig: this.config.oauthConfig,
218
+ authConfigs: this.config.authConfigs,
219
+ baseUrl: this.config.baseUrl,
220
+ rateLimitOAuthMax: this.config.rateLimitOAuthMax,
221
+ rateLimitOAuthWindowMs: this.config.rateLimitOAuthWindowMs,
222
+ resourceName: this.config.resourceName,
223
+ resourceDocumentation: this.config.resourceDocumentation,
224
+ parser: this.config.parser,
225
+ };
226
+ }
227
+ async getProfileState(profileId) {
228
+ const existing = this.profileStates.get(profileId);
229
+ if (existing) {
230
+ return existing;
231
+ }
232
+ let context = null;
233
+ if (this.profileContextProvider) {
234
+ try {
235
+ context = await this.profileContextProvider(profileId);
236
+ }
237
+ catch (error) {
238
+ if (error instanceof ConfigurationError && error.message === 'Profile not found') {
239
+ this.logger.warn('Profile not found during request', { profileId });
240
+ return null;
241
+ }
242
+ throw error;
243
+ }
244
+ }
245
+ else {
246
+ const defaultContext = this.buildDefaultProfileContext();
247
+ if (defaultContext?.profileId === profileId) {
248
+ context = defaultContext;
249
+ }
250
+ }
251
+ if (!context) {
252
+ return null;
253
+ }
254
+ let oauthProvider = null;
255
+ if (context.oauthConfig) {
256
+ this.logger.info('Initializing OAuth provider with config', {
257
+ profileId,
258
+ hasClientId: !!context.oauthConfig.client_id,
259
+ });
260
+ oauthProvider = new ExternalOAuthProvider(context.oauthConfig, this.logger);
261
+ this.logger.info('OAuth provider initialized', {
262
+ profileId,
263
+ endpoint: oauthProvider.authorizationEndpoint || '(to be derived from issuer)',
264
+ hasIssuer: !!context.oauthConfig.issuer,
265
+ });
266
+ }
267
+ else {
268
+ this.logger.info('No OAuth config provided - OAuth provider not initialized', { profileId });
269
+ }
270
+ const state = {
271
+ profileId,
272
+ context,
273
+ oauthProvider,
274
+ oauthTokensByAccessToken: new Map(),
275
+ sessions: new Map(),
276
+ };
277
+ this.profileStates.set(profileId, state);
278
+ return state;
279
+ }
280
+ getProfileIdForRequest(req) {
281
+ if (req.profileId) {
282
+ return req.profileId;
283
+ }
284
+ return this.getDefaultProfileId();
285
+ }
286
+ async getProfileStateForRequest(req) {
287
+ const profileId = this.getProfileIdForRequest(req);
288
+ if (!profileId) {
289
+ return null;
290
+ }
291
+ return await this.getProfileState(profileId);
292
+ }
183
293
  /**
184
294
  * Check if origin is allowed
185
295
  *
@@ -203,16 +313,10 @@ export class HttpTransport {
203
313
  if (hostname === this.config.host) {
204
314
  return true;
205
315
  }
206
- // Allow OAuth redirect URI host if configured
207
- if (this.oauthProvider?.redirectUri) {
208
- try {
209
- const redirectUrl = new URL(this.oauthProvider.redirectUri);
210
- if (hostname === redirectUrl.hostname) {
211
- return true;
212
- }
213
- }
214
- catch {
215
- // Invalid URL, ignore
316
+ // Allow OAuth redirect URI hosts (initialized + cached + configured)
317
+ for (const redirectHost of this.getOAuthRedirectHostPatterns()) {
318
+ if (this.matchOrigin(hostname, redirectHost)) {
319
+ return true;
216
320
  }
217
321
  }
218
322
  // Check custom allowed origins
@@ -229,6 +333,139 @@ export class HttpTransport {
229
333
  return false;
230
334
  }
231
335
  }
336
+ getOAuthRedirectHostPatterns() {
337
+ const hosts = new Set();
338
+ const defaultProfileId = this.getDefaultProfileId() ?? 'default';
339
+ for (const state of this.profileStates.values()) {
340
+ const redirectUri = state.oauthProvider?.redirectUri;
341
+ if (!redirectUri) {
342
+ continue;
343
+ }
344
+ try {
345
+ const redirectUrl = new URL(redirectUri);
346
+ hosts.add(redirectUrl.hostname);
347
+ }
348
+ catch {
349
+ // Ignore invalid URL
350
+ }
351
+ }
352
+ if (this.config.oauthConfig) {
353
+ for (const pattern of this.extractRedirectHostPatterns(this.config.oauthConfig, defaultProfileId)) {
354
+ hosts.add(pattern);
355
+ }
356
+ }
357
+ for (const patterns of this.oauthRedirectHostCache.values()) {
358
+ for (const pattern of patterns) {
359
+ hosts.add(pattern);
360
+ }
361
+ }
362
+ return Array.from(hosts);
363
+ }
364
+ extractRedirectHostPatterns(oauthConfig, profileId) {
365
+ if (!oauthConfig?.redirect_uri) {
366
+ return [];
367
+ }
368
+ const resolvedRedirectUri = this.resolveRedirectUriFromEnv(oauthConfig.redirect_uri, profileId);
369
+ if (!resolvedRedirectUri) {
370
+ return [];
371
+ }
372
+ try {
373
+ const redirectUrl = new URL(resolvedRedirectUri);
374
+ return [redirectUrl.hostname];
375
+ }
376
+ catch {
377
+ return [];
378
+ }
379
+ }
380
+ resolveRedirectUriFromEnv(value, profileId) {
381
+ const match = value.match(/^\$\{env:([^}]+)\}$/);
382
+ if (!match) {
383
+ return value;
384
+ }
385
+ const envVar = match[1];
386
+ const envValue = process.env[envVar];
387
+ if (!envValue || envValue.trim().length === 0) {
388
+ const warningKey = `${profileId}:${envVar}`;
389
+ if (!this.warnedMissingOAuthRedirectEnvVars.has(warningKey)) {
390
+ this.warnedMissingOAuthRedirectEnvVars.add(warningKey);
391
+ this.logger.warn('OAuth redirect_uri environment variable is empty', {
392
+ profileId,
393
+ envVar,
394
+ });
395
+ }
396
+ return undefined;
397
+ }
398
+ return envValue;
399
+ }
400
+ resolveProfileIdFromPath(pathname) {
401
+ if (!this.config.profileRoutingEnabled) {
402
+ return null;
403
+ }
404
+ const match = /^\/profile\/([^/]+)/.exec(pathname);
405
+ if (!match) {
406
+ return null;
407
+ }
408
+ try {
409
+ return decodeURIComponent(match[1]);
410
+ }
411
+ catch {
412
+ return null;
413
+ }
414
+ }
415
+ resolveProfileIdForOriginCheck(req) {
416
+ const pathProfileId = this.resolveProfileIdFromPath(req.path);
417
+ if (pathProfileId) {
418
+ return pathProfileId;
419
+ }
420
+ const resource = req.query?.resource;
421
+ if (typeof resource === 'string') {
422
+ const profileId = this.resolveProfileIdFromResourceUrl(resource);
423
+ if (profileId) {
424
+ return profileId;
425
+ }
426
+ }
427
+ return this.getDefaultProfileId() ?? null;
428
+ }
429
+ async primeOAuthRedirectHosts(profileId) {
430
+ if (!this.profileContextProvider) {
431
+ return;
432
+ }
433
+ if (this.oauthRedirectHostCache.has(profileId)) {
434
+ return;
435
+ }
436
+ try {
437
+ const context = await this.profileContextProvider(profileId);
438
+ if (!context?.oauthConfig) {
439
+ this.oauthRedirectHostCache.set(profileId, []);
440
+ return;
441
+ }
442
+ const patterns = this.extractRedirectHostPatterns(context.oauthConfig, profileId);
443
+ this.oauthRedirectHostCache.set(profileId, patterns);
444
+ }
445
+ catch (error) {
446
+ if (error instanceof ConfigurationError && error.message === 'Profile not found') {
447
+ return;
448
+ }
449
+ this.logger.warn('Failed to preload OAuth redirect hosts', {
450
+ profileId,
451
+ error: String(error),
452
+ });
453
+ }
454
+ }
455
+ async isAllowedOriginForRequest(origin, req) {
456
+ if (this.isAllowedOrigin(origin)) {
457
+ return true;
458
+ }
459
+ if (!this.config.profileRoutingEnabled) {
460
+ return false;
461
+ }
462
+ const profileId = this.resolveProfileIdForOriginCheck(req);
463
+ if (!profileId) {
464
+ return false;
465
+ }
466
+ await this.primeOAuthRedirectHosts(profileId);
467
+ return this.isAllowedOrigin(origin);
468
+ }
232
469
  /**
233
470
  * Match hostname against allowed origin pattern
234
471
  *
@@ -280,9 +517,12 @@ export class HttpTransport {
280
517
  }
281
518
  const ipInt = this.ipv4ToInt(ip);
282
519
  const rangeInt = this.ipv4ToInt(range);
520
+ /* c8 ignore start - defensive check for edge cases where isIP() passes but parsing fails
521
+ * This should never happen in practice, but serves as a fail-safe */
283
522
  if (ipInt === null || rangeInt === null) {
284
523
  return false;
285
524
  }
525
+ /* c8 ignore end */
286
526
  const mask = (0xFFFFFFFF << (32 - maskBits)) >>> 0;
287
527
  return (ipInt & mask) === (rangeInt & mask);
288
528
  }
@@ -292,9 +532,12 @@ export class HttpTransport {
292
532
  }
293
533
  const ipInt = this.ipv6ToBigInt(ip);
294
534
  const rangeInt = this.ipv6ToBigInt(range);
535
+ /* c8 ignore start - defensive check for edge cases where isIP() passes but parsing fails
536
+ * This should never happen in practice, but serves as a fail-safe */
295
537
  if (ipInt === null || rangeInt === null) {
296
538
  return false;
297
539
  }
540
+ /* c8 ignore end */
298
541
  const mask = this.ipv6Mask(maskBits);
299
542
  return (ipInt & mask) === (rangeInt & mask);
300
543
  }
@@ -368,17 +611,21 @@ export class HttpTransport {
368
611
  return null;
369
612
  }
370
613
  segments = [...headVals, ...Array(missing).fill(0), ...tailVals];
614
+ /* c8 ignore start - defensive check that should never trigger if logic above is correct */
371
615
  if (segments.length !== totalSegmentsNeeded) {
372
616
  return null;
373
617
  }
618
+ /* c8 ignore end */
374
619
  if (ipv4Tail !== null) {
375
620
  const high = (ipv4Tail >>> 16) & 0xFFFF;
376
621
  const low = ipv4Tail & 0xFFFF;
377
622
  segments.push(high, low);
378
623
  }
624
+ /* c8 ignore start - defensive check that should never trigger if logic above is correct */
379
625
  if (segments.length !== 8) {
380
626
  return null;
381
627
  }
628
+ /* c8 ignore end */
382
629
  let value = 0n;
383
630
  for (const part of segments) {
384
631
  value = (value << 16n) + BigInt(part);
@@ -428,6 +675,83 @@ export class HttpTransport {
428
675
  formatRateLimitMessage(scope, maxRequests, windowMs) {
429
676
  return `Rate limit exceeded for ${scope}. Max ${maxRequests} requests per ${windowMs / 1000} seconds.`;
430
677
  }
678
+ getProfilePrefix(profileId, options) {
679
+ if (!this.config.profileRoutingEnabled) {
680
+ return '';
681
+ }
682
+ if (options?.forceProfilePrefix && profileId) {
683
+ return `/profile/${encodeURIComponent(profileId)}`;
684
+ }
685
+ const defaultProfileId = this.getDefaultProfileId();
686
+ if (!profileId || (defaultProfileId && profileId === defaultProfileId)) {
687
+ return '';
688
+ }
689
+ return `/profile/${encodeURIComponent(profileId)}`;
690
+ }
691
+ buildProfilePath(profileId, path, options) {
692
+ const prefix = this.getProfilePrefix(profileId, options);
693
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
694
+ return `${prefix}${normalizedPath}`;
695
+ }
696
+ getServerOrigin(profileId) {
697
+ if (profileId) {
698
+ const state = this.profileStates.get(profileId);
699
+ if (state?.oauthProvider?.redirectUri) {
700
+ try {
701
+ return new URL(state.oauthProvider.redirectUri).origin;
702
+ }
703
+ catch {
704
+ // Ignore invalid URL
705
+ }
706
+ }
707
+ }
708
+ const protocol = this.config.host.includes('://') ? '' : 'http://';
709
+ const host = this.config.host.includes('://') ? this.config.host : this.config.host;
710
+ return `${protocol}${host}:${this.config.port}`;
711
+ }
712
+ buildProfileUrl(profileId, path, options) {
713
+ return `${this.getServerOrigin(profileId)}${this.buildProfilePath(profileId, path, options)}`;
714
+ }
715
+ normalizeResourcePath(pathname) {
716
+ const normalized = pathname.replace(/\/+$/, '');
717
+ return normalized === '' ? '/' : normalized;
718
+ }
719
+ resolveProfileIdFromResourceUrl(resource) {
720
+ let url;
721
+ try {
722
+ url = new URL(resource);
723
+ }
724
+ catch {
725
+ return null;
726
+ }
727
+ const path = this.normalizeResourcePath(url.pathname);
728
+ if (path === '/mcp') {
729
+ return this.getDefaultProfileId() ?? null;
730
+ }
731
+ if (!this.config.profileRoutingEnabled) {
732
+ return null;
733
+ }
734
+ const match = /^\/profile\/([^/]+)\/mcp$/.exec(path);
735
+ if (!match) {
736
+ return null;
737
+ }
738
+ try {
739
+ return decodeURIComponent(match[1]);
740
+ }
741
+ catch {
742
+ return null;
743
+ }
744
+ }
745
+ getOAuthProtectedResourceUrl(profileId) {
746
+ const effectiveProfileId = profileId ?? this.getDefaultProfileId();
747
+ return this.buildProfileUrl(effectiveProfileId, OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE);
748
+ }
749
+ respondProfileNotFound(res, profileId) {
750
+ res.status(HTTP_STATUS.NOT_FOUND).json({
751
+ error: 'Not Found',
752
+ message: profileId ? `Profile '${profileId}' not found` : 'Profile not found',
753
+ });
754
+ }
431
755
  /**
432
756
  * Setup MCP endpoint routes
433
757
  *
@@ -437,303 +761,100 @@ export class HttpTransport {
437
761
  this.logger.info('Setting up HTTP routes');
438
762
  // Security: Rate limiting setup (needed for OAuth routes)
439
763
  const rateLimitEnabled = this.config.rateLimitEnabled !== false; // default: true
440
- // Rate limiter for OAuth endpoints (stricter limits for security)
441
- // OAuth endpoints are sensitive and should have lower limits than general API
442
- // Configuration priority: profile > env vars > defaults
443
- const oauthWindowMs = this.config.rateLimitOAuthWindowMs || OAUTH_RATE_LIMIT.WINDOW_MS;
444
- const oauthMaxRequests = this.config.rateLimitOAuthMax || OAUTH_RATE_LIMIT.MAX_REQUESTS;
445
- const oauthRateLimiter = this.createRateLimiter({
446
- enabled: rateLimitEnabled,
447
- windowMs: oauthWindowMs,
448
- maxRequests: oauthMaxRequests,
449
- logMessage: 'Rate limit exceeded for OAuth',
450
- responseMessage: `Too many OAuth requests. Limit: ${oauthMaxRequests} requests per ${Math.round(oauthWindowMs / 60000)} minutes. Please try again later.`,
451
- });
452
- // OAuth 2.0 routes (if configured)
453
- if (this.oauthProvider) {
454
- // Build redirect URI
455
- const redirectUri = this.oauthProvider.redirectUri ||
456
- `http://${this.config.host}:${this.config.port}/oauth/callback`;
457
- // Derive serverUrl from redirectUri
458
- const baseUrl = new URL(redirectUri).origin;
459
- const serverUrl = new URL(`${baseUrl}/mcp`);
460
- // issuerUrl should be the base URL of the authorization server (e.g. https://gitlab.com),
461
- // NOT the authorization endpoint (e.g. https://gitlab.com/oauth/authorize)
462
- // We try to derive it from authorizationEndpoint if not explicitly configured
463
- // Note: authorizationEndpoint might not be ready yet (async initialization),
464
- // so we use serverUrl.origin as fallback
465
- let issuerUrl;
466
- try {
467
- const authEndpoint = this.oauthProvider.authorizationEndpoint;
468
- if (authEndpoint) {
469
- // Try to extract base URL from auth endpoint
470
- const authUrl = new URL(authEndpoint);
471
- issuerUrl = new URL(authUrl.origin);
472
- }
473
- else {
474
- // Fallback: use server origin (will be updated after async init)
475
- issuerUrl = new URL(serverUrl.origin);
476
- }
477
- }
478
- catch (e) {
479
- // Fallback: use server origin
480
- issuerUrl = new URL(serverUrl.origin);
764
+ const profileRoutingEnabled = this.config.profileRoutingEnabled === true;
765
+ const defaultProfileId = this.getDefaultProfileId();
766
+ const attachProfileId = (req, _res, next) => {
767
+ req.profileId = req.params.profileId;
768
+ next();
769
+ };
770
+ const oauthRateLimiterByProfile = new Map();
771
+ const getOAuthRateLimiter = (profileState) => {
772
+ const existing = oauthRateLimiterByProfile.get(profileState.profileId);
773
+ if (existing) {
774
+ return existing;
481
775
  }
482
- this.logger.info('Setting up OAuth routes', {
483
- serverUrl: serverUrl.toString(),
484
- issuerUrl: issuerUrl.toString(),
485
- redirectUri,
486
- });
487
- // Install MCP OAuth router
488
- // This adds standard OAuth endpoints:
489
- // - /.well-known/oauth-authorization-server
490
- // - /.well-known/oauth-protected-resource
491
- // - /oauth/authorize
492
- // - /oauth/token
493
- // - /oauth/register (dynamic client registration)
494
- // - /oauth/revoke (token revocation)
495
- // Only register resource server endpoints, not authorization server endpoints
496
- // since our MCP server is not an OAuth authorization server
497
- this.app.get(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, oauthRateLimiter, (req, res) => {
498
- // Build metadata object with only defined fields (RFC 8707)
499
- const metadata = {
500
- resource: serverUrl.href,
501
- authorization_servers: [serverUrl.origin], // We are the authorization server (proxy)
502
- bearer_methods_supported: ['header'],
503
- };
504
- // Optional: scopes_supported (only if scopes are defined)
505
- if (this.oauthProvider?.scopes && this.oauthProvider.scopes.length > 0) {
506
- metadata.scopes_supported = this.oauthProvider.scopes;
507
- }
508
- // Optional: resource_name (from config, already has fallback in mcp-server.ts)
509
- if (this.config.resourceName) {
510
- metadata.resource_name = this.config.resourceName;
511
- }
512
- // Optional: resource_documentation (from config, may be undefined)
513
- if (this.config.resourceDocumentation) {
514
- metadata.resource_documentation = this.config.resourceDocumentation;
515
- }
516
- res.json(metadata);
517
- });
518
- // Authorization endpoint
519
- // Initiates the OAuth flow by redirecting the user to the external provider
520
- this.app.get(OAUTH_PATHS.AUTHORIZE, oauthRateLimiter, async (req, res) => {
521
- try {
522
- const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query;
523
- if (!client_id || typeof client_id !== 'string') {
524
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing client_id');
525
- return;
526
- }
527
- if (!redirect_uri || typeof redirect_uri !== 'string') {
528
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing redirect_uri');
529
- return;
530
- }
531
- if (this.oauthProvider) {
532
- // Ensure provider is initialized before client validation
533
- // This registers configured client_id if present
534
- await this.oauthProvider.ensureEndpointsInitialized();
535
- // Find the client to validate configuration
536
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
537
- if (!client) {
538
- res.status(HTTP_STATUS.BAD_REQUEST).send('Invalid client_id');
539
- return;
540
- }
541
- // Prepare parameters for provider authorization
542
- const scopeStr = (scope || '').trim();
543
- const params = {
544
- responseType: response_type || 'code',
545
- clientId: client_id,
546
- redirectUri: redirect_uri,
547
- scope: scopeStr ? scopeStr.split(' ') : [],
548
- state: state,
549
- codeChallenge: code_challenge,
550
- codeChallengeMethod: code_challenge_method,
551
- scopes: scopeStr ? scopeStr.split(' ') : [],
552
- };
553
- // Call provider authorize method which handles the redirect logic
554
- await this.oauthProvider.authorize(client, params, res);
555
- }
556
- else {
557
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth provider not initialized');
558
- }
559
- }
560
- catch (error) {
561
- this.logger.error('OAuth authorize error', error instanceof Error ? error : new Error(String(error)));
562
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth authorization failed');
563
- }
564
- });
565
- // Token endpoint
566
- // Exchanges authorization code or refresh token for access token
567
- this.app.post(OAUTH_PATHS.TOKEN, oauthRateLimiter, express.urlencoded({ extended: false }), async (req, res) => {
568
- try {
569
- const { grant_type, code, redirect_uri, client_id, code_verifier, refresh_token } = req.body;
570
- this.logger.debug('OAuth token request', {
571
- grant_type,
572
- client_id,
573
- has_code: !!code,
574
- has_code_verifier: !!code_verifier,
575
- redirect_uri,
576
- });
577
- if (grant_type === 'authorization_code') {
578
- // Authorization Code Flow
579
- if (!code) {
580
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing code' });
581
- return;
582
- }
583
- if (this.oauthProvider) {
584
- // Ensure provider is initialized before client validation
585
- await this.oauthProvider.ensureEndpointsInitialized();
586
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
587
- if (!client) {
588
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
589
- return;
590
- }
591
- const tokens = await this.oauthProvider.exchangeAuthorizationCode(client, code, code_verifier, redirect_uri);
592
- // Store OAuth tokens for later session initialization
593
- this.storeOAuthTokens(tokens, client.client_id, client.scope?.split(' ') || []);
594
- res.json(tokens);
595
- }
596
- else {
597
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
598
- }
599
- }
600
- else if (grant_type === 'refresh_token') {
601
- // Refresh Token Flow
602
- if (!refresh_token) {
603
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing refresh_token' });
604
- return;
605
- }
606
- if (this.oauthProvider) {
607
- // Ensure provider is initialized before client validation
608
- await this.oauthProvider.ensureEndpointsInitialized();
609
- const client = await this.oauthProvider.clientsStore.getClient(client_id);
610
- if (!client) {
611
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
612
- return;
613
- }
614
- const tokens = await this.oauthProvider.exchangeRefreshToken(client, refresh_token);
615
- // Store OAuth tokens for later session initialization
616
- // Note: When refreshing, the old access token should be invalidated
617
- // but we don't track it here - the new token replaces it in the map
618
- this.storeOAuthTokens(tokens, client.client_id, client.scope?.split(' ') || []);
619
- res.json(tokens);
620
- }
621
- else {
622
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
623
- }
624
- }
625
- else {
626
- this.logger.warn('Unsupported grant type', { grant_type, expected: 'authorization_code or refresh_token' });
627
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'unsupported_grant_type' });
628
- return;
629
- }
630
- }
631
- catch (error) {
632
- this.logger.error('OAuth token exchange error', error instanceof Error ? error : new Error(String(error)));
633
- res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_grant', error_description: String(error) });
634
- }
776
+ const oauthWindowMs = profileState.context.rateLimitOAuthWindowMs || OAUTH_RATE_LIMIT.WINDOW_MS;
777
+ const oauthMaxRequests = profileState.context.rateLimitOAuthMax || OAUTH_RATE_LIMIT.MAX_REQUESTS;
778
+ const limiter = this.createRateLimiter({
779
+ enabled: rateLimitEnabled,
780
+ windowMs: oauthWindowMs,
781
+ maxRequests: oauthMaxRequests,
782
+ logMessage: 'Rate limit exceeded for OAuth',
783
+ responseMessage: `Too many OAuth requests. Limit: ${oauthMaxRequests} requests per ${Math.round(oauthWindowMs / 60000)} minutes. Please try again later.`,
635
784
  });
636
- // OAuth callback endpoint to receive tokens from authorization server
637
- this.app.get(OAUTH_PATHS.CALLBACK, oauthRateLimiter, async (req, res) => {
638
- try {
639
- const { code, state, error, error_description } = req.query;
640
- this.logger.info('OAuth callback received', {
641
- hasCode: !!code,
642
- hasState: !!state,
643
- error: error,
644
- errorDescription: error_description
645
- });
646
- if (error) {
647
- // Sanitize error messages to prevent XSS
648
- const safeError = escapeHtmlSafe(error);
649
- const safeErrorDesc = escapeHtmlSafe(error_description);
650
- res.status(HTTP_STATUS.BAD_REQUEST).json({
651
- error: safeError,
652
- error_description: safeErrorDesc || safeError
653
- });
654
- return;
655
- }
656
- if (!code || typeof code !== 'string') {
657
- res.status(HTTP_STATUS.BAD_REQUEST).send('Missing authorization code');
658
- return;
659
- }
660
- // Delegate to OAuth provider to handle token exchange and redirect back to client (Cursor)
661
- if (this.oauthProvider) {
662
- await this.oauthProvider.handleCallback(req, res);
663
- }
664
- else {
665
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth provider not initialized');
666
- }
667
- }
668
- catch (error) {
669
- this.logger.error('OAuth callback error', error instanceof Error ? error : new Error(String(error)));
670
- if (!res.headersSent) {
671
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth callback failed');
672
- }
673
- }
674
- });
675
- // Provide authorization server metadata
676
- // We advertise the MCP server itself as the authorization server (Proxy Mode)
677
- // This allows us to handle the redirect dance between Cursor -> MCP -> GitLab -> MCP -> Cursor
678
- this.app.get(OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER, async (req, res) => {
679
- try {
680
- res.json({
681
- issuer: serverUrl.origin, // We are the issuer for the client
682
- authorization_endpoint: new URL(OAUTH_PATHS.AUTHORIZE, serverUrl.origin).href,
683
- token_endpoint: new URL(OAUTH_PATHS.TOKEN, serverUrl.origin).href,
684
- registration_endpoint: new URL(OAUTH_PATHS.REGISTER, serverUrl.origin).href,
685
- response_types_supported: ['code'],
686
- code_challenge_methods_supported: ['S256'],
687
- grant_types_supported: ['authorization_code', 'refresh_token'],
688
- scopes_supported: this.oauthProvider?.scopes || ['api'],
689
- });
690
- }
691
- catch (error) {
692
- this.logger.error('OAuth authorization server metadata error', error instanceof Error ? error : new Error(String(error)));
693
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth metadata failed');
694
- }
695
- });
696
- // Dynamic Client Registration endpoint
697
- // Cursor requires this to register itself with a redirect URI
698
- this.app.post(OAUTH_PATHS.REGISTER, express.json(), async (req, res) => {
699
- try {
700
- const { redirect_uris } = req.body;
701
- this.logger.info('Dynamic client registration request', { redirect_uris });
702
- // We don't actually strictly enforce registration in this proxy mode,
703
- // but we return a valid client configuration to satisfy the client.
704
- // We use a static client ID for the internal mapping.
705
- const clientId = 'mcp-proxy-client';
706
- const clientSecret = 'mcp-proxy-secret';
707
- // Register this client in our internal store so authorize requests pass validation
708
- if (this.oauthProvider) {
709
- const client = {
710
- client_id: clientId,
711
- client_secret: clientSecret,
712
- redirect_uris: redirect_uris || [],
713
- grant_types: ['authorization_code', 'refresh_token'],
714
- response_types: ['code'],
715
- scope: (this.oauthProvider.scopes || []).join(' '),
716
- };
717
- // We need to cast to any because registerClient might not be exposed on the interface
718
- // but we know ExternalOAuthProvider uses InMemoryClientsStore
719
- await this.oauthProvider.clientsStore.registerClient(client);
720
- }
721
- res.status(HTTP_STATUS.CREATED).json({
722
- client_id: clientId,
723
- client_secret: clientSecret,
724
- redirect_uris: redirect_uris,
725
- grant_types: ['authorization_code', 'refresh_token'],
726
- response_types: ['code'],
727
- scope: (this.oauthProvider?.scopes || []).join(' '),
728
- token_endpoint_auth_method: 'client_secret_post'
729
- });
730
- }
731
- catch (error) {
732
- this.logger.error('Client registration failed', error instanceof Error ? error : new Error(String(error)));
733
- res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'Registration failed' });
785
+ oauthRateLimiterByProfile.set(profileState.profileId, limiter);
786
+ return limiter;
787
+ };
788
+ const oauthRateLimiter = async (req, res, next) => {
789
+ const profileState = await this.getProfileStateForRequest(req);
790
+ if (!profileState) {
791
+ this.respondProfileNotFound(res, req.profileId);
792
+ return;
793
+ }
794
+ const limiter = getOAuthRateLimiter(profileState);
795
+ return limiter(req, res, next);
796
+ };
797
+ const withProfileState = (handler) => {
798
+ return async (req, res) => {
799
+ const profileState = await this.getProfileStateForRequest(req);
800
+ if (!profileState) {
801
+ this.respondProfileNotFound(res, req.profileId);
802
+ return;
734
803
  }
804
+ await handler(req, res, profileState);
805
+ };
806
+ };
807
+ const registerOAuthRoutes = (basePath, includeProfileParam, includeProtectedResource) => {
808
+ const middlewares = includeProfileParam ? [attachProfileId] : [];
809
+ const withOAuthRateLimit = [...middlewares, oauthRateLimiter];
810
+ const withProfile = (handler) => {
811
+ return [...middlewares, oauthRateLimiter, withProfileState(handler)];
812
+ };
813
+ if (includeProtectedResource) {
814
+ this.app.get(`${basePath}${OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE}`, ...withProfile((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
815
+ }
816
+ this.app.get(`${basePath}${OAUTH_PATHS.AUTHORIZE}`, ...withProfile((req, res, profileState) => this.handleOAuthAuthorize(req, res, profileState)));
817
+ this.app.post(`${basePath}${OAUTH_PATHS.TOKEN}`, ...middlewares, oauthRateLimiter, express.urlencoded({ extended: false, limit: '50kb' }), withProfileState((req, res, profileState) => this.handleOAuthToken(req, res, profileState)));
818
+ this.app.get(`${basePath}${OAUTH_PATHS.CALLBACK}`, ...withProfile((req, res, profileState) => this.handleOAuthCallback(req, res, profileState)));
819
+ this.app.get(`${basePath}${OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER}`, ...withProfile((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
820
+ this.app.post(`${basePath}${OAUTH_PATHS.REGISTER}`, ...middlewares, oauthRateLimiter, express.json(), withProfileState((req, res, profileState) => this.handleOAuthRegister(req, res, profileState)));
821
+ this.logger.info('OAuth routes registered', {
822
+ basePath,
823
+ profileRoutingEnabled,
735
824
  });
736
- this.logger.info('OAuth routes registered');
825
+ };
826
+ if (defaultProfileId) {
827
+ registerOAuthRoutes('', false, false);
828
+ }
829
+ if (profileRoutingEnabled) {
830
+ registerOAuthRoutes('/profile/:profileId', true, true);
831
+ }
832
+ const attachProfileFromResourceQuery = (req, res, next) => {
833
+ const { resource } = req.query;
834
+ if (resource === undefined) {
835
+ next();
836
+ return;
837
+ }
838
+ if (typeof resource !== 'string') {
839
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
840
+ error: 'invalid_request',
841
+ message: 'Invalid resource query parameter',
842
+ });
843
+ return;
844
+ }
845
+ const profileId = this.resolveProfileIdFromResourceUrl(resource);
846
+ if (!profileId) {
847
+ res.status(HTTP_STATUS.NOT_FOUND).json({
848
+ error: 'Not Found',
849
+ message: 'OAuth metadata unavailable for requested resource',
850
+ });
851
+ return;
852
+ }
853
+ req.profileId = profileId;
854
+ next();
855
+ };
856
+ if (defaultProfileId || profileRoutingEnabled) {
857
+ this.app.get(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, attachProfileFromResourceQuery, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
737
858
  }
738
859
  // Security: Rate limiting setup (for MCP endpoints)
739
860
  const windowMs = this.config.rateLimitWindowMs || TIMEOUTS.RATE_LIMIT_WINDOW_MS;
@@ -761,47 +882,67 @@ export class HttpTransport {
761
882
  logMessage: 'Rate limit exceeded for metrics',
762
883
  responseMessage: this.formatRateLimitMessage('metrics', metricsMaxRequests, windowMs),
763
884
  });
764
- // Main MCP endpoint - POST for sending messages
765
- this.app.post('/mcp', mcpRateLimiter, this.handlePost.bind(this));
766
- // CORS preflight handler
767
- this.app.options('/mcp', (req, res) => {
768
- const origin = req.headers.origin;
769
- // Only send CORS headers for explicitly allowed origins; otherwise reject
770
- if (origin && this.isAllowedOrigin(origin)) {
771
- // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration
772
- res.setHeader('Access-Control-Allow-Origin', origin);
773
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
774
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
775
- // We do not allow credentials; prevents cookie-based attacks by default
776
- res.setHeader('Access-Control-Allow-Credentials', 'false');
777
- res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache
778
- return res.status(HTTP_STATUS.OK).send();
779
- }
780
- // Disallowed origin: do not echo origin or emit permissive headers
781
- res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
782
- });
783
- this.logger.info('Registered POST /mcp route');
784
- // Main MCP endpoint - GET for SSE streaming
785
- this.app.get('/mcp', mcpRateLimiter, this.handleGet.bind(this));
786
- // Session termination
787
- this.app.delete('/mcp', mcpRateLimiter, this.handleDelete.bind(this));
788
- // Legacy alias endpoints - deprecated
789
- // Why: Backward compatibility for clients using /sse during migration
790
- this.app.post('/sse', mcpRateLimiter, (req, res, next) => {
791
- this.logger.warn('Deprecated endpoint used: POST /sse. Please migrate to POST /mcp');
792
- this.logger.info('Handling POST /sse request');
793
- return this.handlePost(req, res, next);
794
- });
795
- this.app.get('/sse', mcpRateLimiter, (req, res, next) => {
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
- });
885
+ const registerMcpRoutes = (basePath, includeProfileParam, isDefault) => {
886
+ const middlewares = includeProfileParam ? [attachProfileId] : [];
887
+ const pathPrefix = basePath || '';
888
+ // Main MCP endpoint - POST for sending messages
889
+ this.app.post(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handlePost.bind(this));
890
+ // CORS preflight handler
891
+ this.app.options(`${pathPrefix}/mcp`, ...middlewares, async (req, res) => {
892
+ const origin = req.headers.origin;
893
+ try {
894
+ // Only send CORS headers for explicitly allowed origins; otherwise reject
895
+ if (origin && await this.isAllowedOriginForRequest(origin, req)) {
896
+ // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration
897
+ res.setHeader('Access-Control-Allow-Origin', origin);
898
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
899
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
900
+ // We do not allow credentials; prevents cookie-based attacks by default
901
+ res.setHeader('Access-Control-Allow-Credentials', 'false');
902
+ res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache
903
+ return res.status(HTTP_STATUS.OK).send();
904
+ }
905
+ // Disallowed origin: do not echo origin or emit permissive headers
906
+ res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
907
+ }
908
+ catch (error) {
909
+ this.logger.error('Origin validation failed', error instanceof Error ? error : new Error(String(error)));
910
+ res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
911
+ }
912
+ });
913
+ // Main MCP endpoint - GET for SSE streaming
914
+ this.app.get(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handleGet.bind(this));
915
+ // Session termination
916
+ this.app.delete(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handleDelete.bind(this));
917
+ // Legacy alias endpoints - deprecated
918
+ // Why: Backward compatibility for clients using /sse during migration
919
+ this.app.post(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
920
+ this.logger.warn('Deprecated endpoint used: POST /sse. Please migrate to POST /mcp');
921
+ this.logger.info('Handling POST /sse request');
922
+ return this.handlePost(req, res, next);
923
+ });
924
+ this.app.get(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
925
+ this.logger.warn('Deprecated endpoint used: GET /sse. Please migrate to GET /mcp');
926
+ this.logger.info(`Handling GET /sse request from: ${req.ip}`);
927
+ return this.handleGet(req, res, next);
928
+ });
929
+ this.app.delete(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
930
+ this.logger.warn('Deprecated endpoint used: DELETE /sse. Please migrate to DELETE /mcp');
931
+ return this.handleDelete(req, res, next);
932
+ });
933
+ if (isDefault) {
934
+ this.logger.info('Registered MCP routes for default profile', { pathPrefix: pathPrefix || '/mcp' });
935
+ }
936
+ else {
937
+ this.logger.info('Registered MCP routes for profile routing', { pathPrefix: `${pathPrefix}/mcp` });
938
+ }
939
+ };
940
+ if (defaultProfileId) {
941
+ registerMcpRoutes('', false, true);
942
+ }
943
+ if (profileRoutingEnabled) {
944
+ registerMcpRoutes('/profile/:profileId', true, false);
945
+ }
805
946
  // Metrics endpoint (if enabled)
806
947
  if (this.config.metricsEnabled) {
807
948
  this.app.get(this.config.metricsPath, metricsRateLimiter, this.handleMetrics.bind(this));
@@ -809,7 +950,11 @@ export class HttpTransport {
809
950
  // Health check (with rate limiting)
810
951
  this.app.get('/health', mcpRateLimiter, (req, res) => {
811
952
  const startTime = Date.now();
812
- res.json({ status: 'ok', sessions: this.sessions.size });
953
+ let totalSessions = 0;
954
+ for (const state of this.profileStates.values()) {
955
+ totalSessions += state.sessions.size;
956
+ }
957
+ res.json({ status: 'ok', sessions: totalSessions });
813
958
  if (this.metrics) {
814
959
  const duration = (Date.now() - startTime) / 1000;
815
960
  this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
@@ -833,6 +978,243 @@ export class HttpTransport {
833
978
  });
834
979
  });
835
980
  }
981
+ getProfileIssuerUrl(profileId, options) {
982
+ const origin = this.getServerOrigin(profileId);
983
+ const prefix = this.getProfilePrefix(profileId, options);
984
+ return `${origin}${prefix}`;
985
+ }
986
+ async handleOAuthProtectedResource(req, res, profileState) {
987
+ if (!profileState.oauthProvider) {
988
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'OAuth not configured for this profile' });
989
+ return;
990
+ }
991
+ const profileId = profileState.profileId;
992
+ const isProfileScoped = typeof req.params?.profileId === 'string';
993
+ const urlOptions = isProfileScoped ? { forceProfilePrefix: true } : undefined;
994
+ const serverUrl = new URL(this.buildProfileUrl(profileId, '/mcp', urlOptions));
995
+ const issuerUrl = this.getProfileIssuerUrl(profileId, urlOptions);
996
+ const metadata = {
997
+ resource: serverUrl.href,
998
+ authorization_servers: [issuerUrl],
999
+ bearer_methods_supported: ['header'],
1000
+ };
1001
+ if (profileState.oauthProvider.scopes && profileState.oauthProvider.scopes.length > 0) {
1002
+ metadata.scopes_supported = profileState.oauthProvider.scopes;
1003
+ }
1004
+ if (profileState.context.resourceName) {
1005
+ metadata.resource_name = profileState.context.resourceName;
1006
+ }
1007
+ if (profileState.context.resourceDocumentation) {
1008
+ metadata.resource_documentation = profileState.context.resourceDocumentation;
1009
+ }
1010
+ res.json(metadata);
1011
+ }
1012
+ async handleOAuthAuthorize(req, res, profileState) {
1013
+ if (!profileState.oauthProvider) {
1014
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth not configured for this profile');
1015
+ return;
1016
+ }
1017
+ try {
1018
+ res.setHeader('Cache-Control', 'no-store');
1019
+ const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query;
1020
+ if (!client_id || typeof client_id !== 'string') {
1021
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing client_id');
1022
+ return;
1023
+ }
1024
+ if (!response_type || typeof response_type !== 'string') {
1025
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing response_type');
1026
+ return;
1027
+ }
1028
+ if (response_type !== 'code') {
1029
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Unsupported response_type');
1030
+ return;
1031
+ }
1032
+ if (!redirect_uri || typeof redirect_uri !== 'string') {
1033
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing redirect_uri');
1034
+ return;
1035
+ }
1036
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1037
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1038
+ if (!client) {
1039
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Invalid client_id');
1040
+ return;
1041
+ }
1042
+ const scopeStr = (scope || '').trim();
1043
+ const params = {
1044
+ responseType: response_type,
1045
+ clientId: client_id,
1046
+ redirectUri: redirect_uri,
1047
+ scope: scopeStr ? scopeStr.split(' ') : [],
1048
+ state: state,
1049
+ codeChallenge: code_challenge,
1050
+ codeChallengeMethod: code_challenge_method,
1051
+ scopes: scopeStr ? scopeStr.split(' ') : [],
1052
+ };
1053
+ await profileState.oauthProvider.authorize(client, params, res);
1054
+ }
1055
+ catch (error) {
1056
+ this.logger.error('OAuth authorize error', error instanceof Error ? error : new Error(String(error)));
1057
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth authorization failed');
1058
+ }
1059
+ }
1060
+ async handleOAuthToken(req, res, profileState) {
1061
+ if (!profileState.oauthProvider) {
1062
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
1063
+ return;
1064
+ }
1065
+ try {
1066
+ res.setHeader('Cache-Control', 'no-store');
1067
+ const { grant_type, code, redirect_uri, client_id, code_verifier, refresh_token } = req.body;
1068
+ this.logger.debug('OAuth token request', {
1069
+ profileId: profileState.profileId,
1070
+ grant_type,
1071
+ client_id,
1072
+ has_code: !!code,
1073
+ has_code_verifier: !!code_verifier,
1074
+ redirect_uri,
1075
+ });
1076
+ if (grant_type === 'authorization_code') {
1077
+ if (!code) {
1078
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing code' });
1079
+ return;
1080
+ }
1081
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1082
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1083
+ if (!client) {
1084
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
1085
+ return;
1086
+ }
1087
+ const tokens = await profileState.oauthProvider.exchangeAuthorizationCode(client, code, code_verifier, redirect_uri);
1088
+ this.storeOAuthTokens(profileState, tokens, client.client_id, client.scope?.split(' ') || []);
1089
+ res.json(tokens);
1090
+ return;
1091
+ }
1092
+ if (grant_type === 'refresh_token') {
1093
+ if (!refresh_token) {
1094
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing refresh_token' });
1095
+ return;
1096
+ }
1097
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1098
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1099
+ if (!client) {
1100
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
1101
+ return;
1102
+ }
1103
+ const tokens = await profileState.oauthProvider.exchangeRefreshToken(client, refresh_token);
1104
+ this.storeOAuthTokens(profileState, tokens, client.client_id, client.scope?.split(' ') || []);
1105
+ res.json(tokens);
1106
+ return;
1107
+ }
1108
+ this.logger.warn('Unsupported grant type', { grant_type, expected: 'authorization_code or refresh_token' });
1109
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'unsupported_grant_type' });
1110
+ }
1111
+ catch (error) {
1112
+ this.logger.error('OAuth token exchange error', error instanceof Error ? error : new Error(String(error)));
1113
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1114
+ error: 'invalid_grant',
1115
+ error_description: 'Token exchange failed',
1116
+ });
1117
+ }
1118
+ }
1119
+ async handleOAuthCallback(req, res, profileState) {
1120
+ if (!profileState.oauthProvider) {
1121
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth provider not initialized');
1122
+ return;
1123
+ }
1124
+ try {
1125
+ const { code, state, error, error_description } = req.query;
1126
+ this.logger.info('OAuth callback received', {
1127
+ profileId: profileState.profileId,
1128
+ hasCode: !!code,
1129
+ hasState: !!state,
1130
+ error: error,
1131
+ errorDescription: error_description,
1132
+ });
1133
+ if (error) {
1134
+ const safeError = escapeHtmlSafe(error);
1135
+ const safeErrorDesc = escapeHtmlSafe(error_description);
1136
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1137
+ error: safeError,
1138
+ error_description: safeErrorDesc || safeError,
1139
+ });
1140
+ return;
1141
+ }
1142
+ if (!code || typeof code !== 'string') {
1143
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing authorization code');
1144
+ return;
1145
+ }
1146
+ await profileState.oauthProvider.handleCallback(req, res);
1147
+ }
1148
+ catch (error) {
1149
+ this.logger.error('OAuth callback error', error instanceof Error ? error : new Error(String(error)));
1150
+ if (!res.headersSent) {
1151
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth callback failed');
1152
+ }
1153
+ }
1154
+ }
1155
+ async handleOAuthAuthorizationServerMetadata(req, res, profileState) {
1156
+ if (!profileState.oauthProvider) {
1157
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth metadata unavailable');
1158
+ return;
1159
+ }
1160
+ try {
1161
+ const profileId = profileState.profileId;
1162
+ const isProfileScoped = typeof req.params?.profileId === 'string';
1163
+ const urlOptions = isProfileScoped ? { forceProfilePrefix: true } : undefined;
1164
+ const issuer = this.getProfileIssuerUrl(profileId, urlOptions);
1165
+ res.json({
1166
+ issuer,
1167
+ authorization_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.AUTHORIZE, urlOptions),
1168
+ token_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.TOKEN, urlOptions),
1169
+ registration_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.REGISTER, urlOptions),
1170
+ response_types_supported: ['code'],
1171
+ code_challenge_methods_supported: ['S256'],
1172
+ grant_types_supported: ['authorization_code', 'refresh_token'],
1173
+ scopes_supported: profileState.oauthProvider.scopes || ['api'],
1174
+ });
1175
+ }
1176
+ catch (error) {
1177
+ this.logger.error('OAuth authorization server metadata error', error instanceof Error ? error : new Error(String(error)));
1178
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth metadata failed');
1179
+ }
1180
+ }
1181
+ async handleOAuthRegister(req, res, profileState) {
1182
+ if (!profileState.oauthProvider) {
1183
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'server_error', error_description: 'Registration unavailable' });
1184
+ return;
1185
+ }
1186
+ try {
1187
+ const { redirect_uris } = req.body;
1188
+ this.logger.info('Dynamic client registration request', {
1189
+ profileId: profileState.profileId,
1190
+ redirect_uris,
1191
+ });
1192
+ const clientId = 'mcp-proxy-client';
1193
+ const clientSecret = 'mcp-proxy-secret';
1194
+ const client = {
1195
+ client_id: clientId,
1196
+ client_secret: clientSecret,
1197
+ redirect_uris: redirect_uris || [],
1198
+ grant_types: ['authorization_code', 'refresh_token'],
1199
+ response_types: ['code'],
1200
+ scope: (profileState.oauthProvider.scopes || []).join(' '),
1201
+ };
1202
+ await profileState.oauthProvider.clientsStore.registerClient(client);
1203
+ res.status(HTTP_STATUS.CREATED).json({
1204
+ client_id: clientId,
1205
+ client_secret: clientSecret,
1206
+ redirect_uris: redirect_uris,
1207
+ grant_types: ['authorization_code', 'refresh_token'],
1208
+ response_types: ['code'],
1209
+ scope: (profileState.oauthProvider.scopes || []).join(' '),
1210
+ token_endpoint_auth_method: 'client_secret_post',
1211
+ });
1212
+ }
1213
+ catch (error) {
1214
+ this.logger.error('Client registration failed', error instanceof Error ? error : new Error(String(error)));
1215
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'Registration failed' });
1216
+ }
1217
+ }
836
1218
  /**
837
1219
  * Handle metrics endpoint
838
1220
  *
@@ -851,8 +1233,14 @@ export class HttpTransport {
851
1233
  // Don't record metrics call in metrics (avoid recursion)
852
1234
  }
853
1235
  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 });
1236
+ const correlationId = generateCorrelationId();
1237
+ this.logger.error('Metrics endpoint error', error, { correlationId });
1238
+ res.setHeader('Cache-Control', 'no-store');
1239
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
1240
+ error: 'Internal Server Error',
1241
+ message: `Internal error (correlation ID: ${correlationId})`,
1242
+ correlationId
1243
+ });
856
1244
  }
857
1245
  }
858
1246
  /**
@@ -969,11 +1357,11 @@ export class HttpTransport {
969
1357
  *
970
1358
  * Returns: { type: 'bearer' | 'oauth' | 'api-token', token: string, sessionId?: string }
971
1359
  */
972
- extractAuthToken(req) {
1360
+ extractAuthToken(req, profileState) {
973
1361
  // 1. Check for OAuth session first (highest priority for authenticated sessions)
974
1362
  const sessionId = req.sessionId || req.headers['mcp-session-id'];
975
- if (sessionId && this.sessions.has(sessionId)) {
976
- const session = this.sessions.get(sessionId);
1363
+ if (sessionId && profileState.sessions.has(sessionId)) {
1364
+ const session = profileState.sessions.get(sessionId);
977
1365
  if (session && session.authToken) {
978
1366
  return { type: 'oauth', token: session.authToken, sessionId };
979
1367
  }
@@ -1008,6 +1396,26 @@ export class HttpTransport {
1008
1396
  }
1009
1397
  return { type: 'none' };
1010
1398
  }
1399
+ /**
1400
+ * Lazy initialization of ToolFilterService
1401
+ */
1402
+ getToolFilterService(profileState) {
1403
+ if (!profileState.toolFilterService) {
1404
+ const validator = new RegexValidator();
1405
+ const compiler = new RegexCompiler(validator);
1406
+ const envParser = new EnvConfigParser(compiler);
1407
+ const headerParser = new HeaderConfigParser(compiler);
1408
+ // Create OperationDetector for category filtering (if parser available)
1409
+ let detector;
1410
+ if (profileState.context.parser) {
1411
+ const classifier = new OperationClassifier();
1412
+ const resolver = new OpenAPIOperationResolver(profileState.context.parser);
1413
+ detector = new OperationDetector(classifier, resolver);
1414
+ }
1415
+ profileState.toolFilterService = new ToolFilterService(envParser, headerParser, this.logger, detector);
1416
+ }
1417
+ return profileState.toolFilterService;
1418
+ }
1011
1419
  /**
1012
1420
  * Handle POST requests - Client sending messages to server
1013
1421
  *
@@ -1017,8 +1425,19 @@ export class HttpTransport {
1017
1425
  const startTime = Date.now();
1018
1426
  try {
1019
1427
  this.logger.debug('handlePost called', { method: req.method, path: req.path, sessionId: req.sessionId, accept: req.headers.accept });
1428
+ const profileState = await this.getProfileStateForRequest(req);
1429
+ if (!profileState) {
1430
+ this.respondProfileNotFound(res, req.profileId);
1431
+ return;
1432
+ }
1433
+ const requestProfileId = req.profileId ?? profileState.profileId;
1020
1434
  const sessionId = req.sessionId;
1021
1435
  const body = req.body;
1436
+ const filteringHeader = normalizeFilteringHeaderValue(this.getFilteringHeaderValue(req));
1437
+ const parsedFiltering = filteringHeader ? parseFilteringHeader(filteringHeader) : undefined;
1438
+ const toolFilterHeader = normalizeToolFilterHeaderValue(this.getToolFilterHeaderValue(req));
1439
+ const parsedToolFilter = toolFilterHeader !== undefined ? parseSessionToolFilterHeader(toolFilterHeader) : undefined;
1440
+ const normalizedToolFilterHeader = parsedToolFilter?.normalizedHeader;
1022
1441
  // Validate Accept header per MCP Streamable HTTP specification
1023
1442
  const accept = req.headers.accept || '';
1024
1443
  // POST requests can return either JSON or SSE, so must accept both if specified
@@ -1047,12 +1466,22 @@ export class HttpTransport {
1047
1466
  this.logger.debug('Session validation', { isInitialization, sessionId, bodyMethod: body?.method });
1048
1467
  // Validate session (except for initialization)
1049
1468
  if (!isInitialization && sessionId) {
1050
- const session = this.sessions.get(sessionId);
1469
+ const session = profileState.sessions.get(sessionId);
1051
1470
  if (!session) {
1052
1471
  res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1053
1472
  return;
1054
1473
  }
1055
- this.updateSessionActivity(sessionId);
1474
+ if (filteringHeader !== undefined) {
1475
+ if (!session.filteringHeader || session.filteringHeader !== filteringHeader) {
1476
+ throw new ValidationError('X-Mcp4-Params header mismatch for existing session.');
1477
+ }
1478
+ }
1479
+ if (normalizedToolFilterHeader !== undefined) {
1480
+ if (!session.toolFilterHeader || session.toolFilterHeader !== normalizedToolFilterHeader) {
1481
+ throw new ValidationError(`X-Mcp4-Tools header mismatch for existing session. Expected: '${session.toolFilterHeader ?? ''}', Got: '${normalizedToolFilterHeader}'.`);
1482
+ }
1483
+ }
1484
+ this.updateSessionActivity(profileState, sessionId);
1056
1485
  }
1057
1486
  else if (!isInitialization && !sessionId) {
1058
1487
  this.logger.debug('Session validation failed: non-init request without sessionId');
@@ -1065,7 +1494,7 @@ export class HttpTransport {
1065
1494
  // If only notifications/responses, return 202 Accepted
1066
1495
  if (messageType === 'notification-only' || messageType === 'response-only') {
1067
1496
  if (this.messageHandler) {
1068
- await this.messageHandler(body);
1497
+ await this.messageHandler(body, undefined, requestProfileId);
1069
1498
  }
1070
1499
  res.status(HTTP_STATUS.ACCEPTED).send();
1071
1500
  return;
@@ -1080,14 +1509,14 @@ export class HttpTransport {
1080
1509
  let newSessionId;
1081
1510
  if (isInitialization) {
1082
1511
  // Extract and validate auth token from headers
1083
- const authInfo = this.extractAuthToken(req);
1512
+ const authInfo = this.extractAuthToken(req, profileState);
1084
1513
  this.logger.debug('Auth token extracted', { authType: authInfo?.type, hasToken: !!authInfo?.token });
1085
1514
  // If OAuth is configured, require authentication for initialization
1086
1515
  // This ensures clients like Cursor properly handle OAuth flow
1087
- if (this.oauthProvider && !authInfo.token) {
1516
+ if (profileState.oauthProvider && !authInfo.token) {
1088
1517
  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(' ')}"`);
1518
+ const resourceMetadataUrl = this.getOAuthProtectedResourceUrl(requestProfileId);
1519
+ res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="${profileState.oauthProvider.scopes.join(' ')}"`);
1091
1520
  res.status(HTTP_STATUS.UNAUTHORIZED).json({
1092
1521
  error: 'Unauthorized',
1093
1522
  message: 'Authentication required for OAuth'
@@ -1096,17 +1525,17 @@ export class HttpTransport {
1096
1525
  }
1097
1526
  // Allow initialization without token for non-OAuth scenarios
1098
1527
  // Validate token if auth is configured and token is provided
1099
- if (authInfo && authInfo.token && this.config.authConfigs && this.config.baseUrl) {
1528
+ if (authInfo && authInfo.token && profileState.context.authConfigs && profileState.context.baseUrl) {
1100
1529
  // Find matching auth config based on priority (authConfigs is sorted)
1101
1530
  // For 'bearer' token type, 'oauth' config is also a match
1102
- const authConfig = this.config.authConfigs.find(c => c.type === authInfo.type ||
1531
+ const authConfig = profileState.context.authConfigs.find(c => c.type === authInfo.type ||
1103
1532
  (authInfo.type === 'bearer' && c.type === 'oauth'));
1104
1533
  if (authConfig && authConfig.validation_endpoint) {
1105
1534
  this.logger.info('Validating auth token during initialization', {
1106
1535
  authType: authConfig.type, // Use config type for logging
1107
1536
  endpoint: authConfig.validation_endpoint,
1108
1537
  });
1109
- const isValid = await this.validateAuthToken(authConfig, authInfo.token, this.config.baseUrl);
1538
+ const isValid = await this.validateAuthToken(authConfig, authInfo.token, profileState.context.baseUrl);
1110
1539
  if (!isValid) {
1111
1540
  this.logger.warn('Auth token validation failed during initialization', {
1112
1541
  authType: authInfo.type,
@@ -1126,7 +1555,7 @@ export class HttpTransport {
1126
1555
  let scopes;
1127
1556
  let oauthClientId;
1128
1557
  if (authInfo.token && (authInfo.type === 'oauth' || authInfo.type === 'bearer')) {
1129
- const tokenData = this.oauthTokensByAccessToken.get(authInfo.token);
1558
+ const tokenData = profileState.oauthTokensByAccessToken.get(authInfo.token);
1130
1559
  if (tokenData) {
1131
1560
  refreshToken = tokenData.refreshToken;
1132
1561
  accessTokenExpiresAt = tokenData.expiresAt;
@@ -1144,21 +1573,21 @@ export class HttpTransport {
1144
1573
  });
1145
1574
  }
1146
1575
  }
1147
- newSessionId = this.createSession(authInfo.token, refreshToken, accessTokenExpiresAt, scopes, oauthClientId);
1576
+ newSessionId = this.createSession(profileState, authInfo.token, refreshToken, accessTokenExpiresAt, scopes, oauthClientId, parsedFiltering?.filtering, parsedFiltering?.normalizedHeader, parsedToolFilter, normalizedToolFilterHeader);
1148
1577
  }
1149
1578
  this.logger.debug('Calling messageHandler', { body, sessionId: isInitialization ? newSessionId : sessionId });
1150
- const response = await this.messageHandler(body, isInitialization ? newSessionId : sessionId);
1579
+ const response = await this.messageHandler(body, isInitialization ? newSessionId : sessionId, requestProfileId);
1151
1580
  this.logger.debug('MessageHandler response', { response });
1152
1581
  // Debug: Check OAuth conditions
1153
1582
  this.logger.debug('Checking OAuth conditions', {
1154
1583
  responseError: response.error,
1155
- hasOAuthProvider: !!this.oauthProvider,
1156
- oauthProviderType: typeof this.oauthProvider
1584
+ hasOAuthProvider: !!profileState.oauthProvider,
1585
+ oauthProviderType: typeof profileState.oauthProvider
1157
1586
  });
1158
1587
  // Check if response contains OAuth error and add WWW-Authenticate header
1159
1588
  const responseObj = response;
1160
1589
  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;
1590
+ const resourceMetadataUrl = this.getOAuthProtectedResourceUrl(requestProfileId);
1162
1591
  res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="api"`);
1163
1592
  res.status(HTTP_STATUS.UNAUTHORIZED); // Set 401 status for OAuth errors
1164
1593
  }
@@ -1175,6 +1604,8 @@ export class HttpTransport {
1175
1604
  if (newSessionId) {
1176
1605
  res.setHeader('Mcp-Session-Id', newSessionId);
1177
1606
  }
1607
+ // Security: Prevent caching of sensitive API responses
1608
+ res.setHeader('Cache-Control', 'no-store');
1178
1609
  this.logger.debug('Sending JSON response', { response, newSessionId });
1179
1610
  res.json(response);
1180
1611
  }
@@ -1185,6 +1616,7 @@ export class HttpTransport {
1185
1616
  catch (error) {
1186
1617
  const correlationId = generateCorrelationId();
1187
1618
  this.logger.error('POST request error', error, { correlationId });
1619
+ res.setHeader('Cache-Control', 'no-store');
1188
1620
  let status = 500;
1189
1621
  let errorLabel = 'Internal Server Error';
1190
1622
  let message = `Internal error (correlation ID: ${correlationId})`;
@@ -1231,6 +1663,11 @@ export class HttpTransport {
1231
1663
  async handleGet(req, res) {
1232
1664
  const startTime = Date.now();
1233
1665
  try {
1666
+ const profileState = await this.getProfileStateForRequest(req);
1667
+ if (!profileState) {
1668
+ this.respondProfileNotFound(res, req.profileId);
1669
+ return;
1670
+ }
1234
1671
  const sessionId = req.sessionId;
1235
1672
  const lastEventId = req.headers['last-event-id'];
1236
1673
  // Validate Accept header
@@ -1244,14 +1681,14 @@ export class HttpTransport {
1244
1681
  res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
1245
1682
  return;
1246
1683
  }
1247
- const session = this.sessions.get(sessionId);
1684
+ const session = profileState.sessions.get(sessionId);
1248
1685
  if (!session) {
1249
1686
  res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1250
1687
  return;
1251
1688
  }
1252
- this.updateSessionActivity(sessionId);
1689
+ this.updateSessionActivity(profileState, sessionId);
1253
1690
  // Start SSE stream
1254
- this.startSSEStream(res, sessionId, lastEventId);
1691
+ this.startSSEStream(res, sessionId, lastEventId, profileState);
1255
1692
  // Record metrics for successful SSE start
1256
1693
  if (this.metrics) {
1257
1694
  const duration = (Date.now() - startTime) / 1000;
@@ -1259,10 +1696,16 @@ export class HttpTransport {
1259
1696
  }
1260
1697
  }
1261
1698
  catch (error) {
1262
- this.logger.error('GET request error', error);
1699
+ const correlationId = generateCorrelationId();
1700
+ this.logger.error('GET request error', error, { correlationId });
1263
1701
  const status = 500;
1264
1702
  if (!res.headersSent) {
1265
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
1703
+ res.setHeader('Cache-Control', 'no-store');
1704
+ res.status(status).json({
1705
+ error: 'Internal Server Error',
1706
+ message: `Internal error (correlation ID: ${correlationId})`,
1707
+ correlationId
1708
+ });
1266
1709
  }
1267
1710
  // Record error metrics
1268
1711
  if (this.metrics) {
@@ -1279,6 +1722,11 @@ export class HttpTransport {
1279
1722
  handleDelete(req, res) {
1280
1723
  const startTime = Date.now();
1281
1724
  const sessionId = req.sessionId;
1725
+ const profileId = this.getProfileIdForRequest(req);
1726
+ if (!profileId) {
1727
+ this.respondProfileNotFound(res, req.profileId);
1728
+ return;
1729
+ }
1282
1730
  if (!sessionId) {
1283
1731
  const status = 400;
1284
1732
  res.status(status).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
@@ -1288,7 +1736,12 @@ export class HttpTransport {
1288
1736
  }
1289
1737
  return;
1290
1738
  }
1291
- const session = this.sessions.get(sessionId);
1739
+ const profileState = this.profileStates.get(profileId);
1740
+ if (!profileState) {
1741
+ this.respondProfileNotFound(res, profileId);
1742
+ return;
1743
+ }
1744
+ const session = profileState.sessions.get(sessionId);
1292
1745
  if (!session) {
1293
1746
  const status = 404;
1294
1747
  res.status(status).json({ error: 'Not Found', message: 'Session not found' });
@@ -1298,7 +1751,7 @@ export class HttpTransport {
1298
1751
  }
1299
1752
  return;
1300
1753
  }
1301
- this.destroySession(sessionId);
1754
+ this.destroySession(profileState, sessionId);
1302
1755
  const status = 204;
1303
1756
  res.status(status).send();
1304
1757
  if (this.metrics) {
@@ -1330,12 +1783,12 @@ export class HttpTransport {
1330
1783
  *
1331
1784
  * Why: Allows server to send requests/notifications to client
1332
1785
  */
1333
- startSSEStream(res, sessionId, lastEventId) {
1786
+ startSSEStream(res, sessionId, lastEventId, profileState) {
1334
1787
  res.setHeader('Content-Type', MIME_TYPES.EVENT_STREAM);
1335
1788
  res.setHeader('Cache-Control', 'no-cache');
1336
1789
  res.setHeader('Connection', 'keep-alive');
1337
1790
  const streamId = crypto.randomBytes(16).toString('hex');
1338
- const session = this.sessions.get(sessionId);
1791
+ const session = profileState.sessions.get(sessionId);
1339
1792
  const streamState = {
1340
1793
  streamId,
1341
1794
  lastEventId: lastEventId ? parseInt(lastEventId, 10) : 0,
@@ -1385,10 +1838,10 @@ export class HttpTransport {
1385
1838
  *
1386
1839
  * Why: Server-initiated requests/notifications
1387
1840
  */
1388
- sendToClient(sessionId, message) {
1389
- const session = this.sessions.get(sessionId);
1841
+ sendToClient(profileId, sessionId, message) {
1842
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
1390
1843
  if (!session) {
1391
- this.logger.warn('Cannot send to client: session not found', { sessionId });
1844
+ this.logger.warn('Cannot send to client: session not found', { profileId, sessionId });
1392
1845
  return;
1393
1846
  }
1394
1847
  const eventId = Date.now();
@@ -1437,12 +1890,38 @@ export class HttpTransport {
1437
1890
  }
1438
1891
  return 'unknown';
1439
1892
  }
1893
+ getFilteringHeaderValue(req) {
1894
+ const headerValue = req.headers['x-mcp4-params'];
1895
+ if (Array.isArray(headerValue)) {
1896
+ if (headerValue.length === 0) {
1897
+ return undefined;
1898
+ }
1899
+ if (headerValue.length > 1) {
1900
+ throw new ValidationError('Invalid X-Mcp4-Params header. Expected comma-separated key=value pairs.');
1901
+ }
1902
+ return headerValue[0];
1903
+ }
1904
+ return headerValue;
1905
+ }
1906
+ getToolFilterHeaderValue(req) {
1907
+ const headerValue = req.headers['x-mcp4-tools'];
1908
+ if (Array.isArray(headerValue)) {
1909
+ if (headerValue.length === 0) {
1910
+ return undefined;
1911
+ }
1912
+ if (headerValue.length > 1) {
1913
+ throw new ValidationError('Invalid X-Mcp4-Tools header. Expected comma-separated tool names.');
1914
+ }
1915
+ return headerValue[0];
1916
+ }
1917
+ return headerValue;
1918
+ }
1440
1919
  /**
1441
1920
  * Create new session
1442
1921
  *
1443
1922
  * Why: Stateful sessions for MCP protocol
1444
1923
  */
1445
- createSession(authToken, refreshToken, accessTokenExpiresAt, scopes, oauthClientId) {
1924
+ createSession(profileState, authToken, refreshToken, accessTokenExpiresAt, scopes, oauthClientId, filtering, filteringHeader, toolFilterRequest, toolFilterHeader) {
1446
1925
  // Validate token if provided (defense in depth)
1447
1926
  if (authToken) {
1448
1927
  this.validateToken(authToken, 'Session auth token');
@@ -1458,9 +1937,14 @@ export class HttpTransport {
1458
1937
  accessTokenExpiresAt,
1459
1938
  scopes,
1460
1939
  oauthClientId,
1940
+ filtering,
1941
+ filteringHeader,
1942
+ toolFilterRequest,
1943
+ toolFilterHeader,
1461
1944
  };
1462
- this.sessions.set(sessionId, session);
1945
+ profileState.sessions.set(sessionId, session);
1463
1946
  this.logger.info('Session created', {
1947
+ profileId: profileState.profileId,
1464
1948
  sessionId,
1465
1949
  hasAuthToken: !!authToken,
1466
1950
  hasRefreshToken: !!refreshToken,
@@ -1475,8 +1959,8 @@ export class HttpTransport {
1475
1959
  /**
1476
1960
  * Update session activity timestamp
1477
1961
  */
1478
- updateSessionActivity(sessionId) {
1479
- const session = this.sessions.get(sessionId);
1962
+ updateSessionActivity(profileState, sessionId) {
1963
+ const session = profileState.sessions.get(sessionId);
1480
1964
  if (session) {
1481
1965
  session.lastActivityAt = Date.now();
1482
1966
  }
@@ -1486,8 +1970,8 @@ export class HttpTransport {
1486
1970
  *
1487
1971
  * Why: Free memory, close streams
1488
1972
  */
1489
- destroySession(sessionId) {
1490
- const session = this.sessions.get(sessionId);
1973
+ destroySession(profileState, sessionId) {
1974
+ const session = profileState.sessions.get(sessionId);
1491
1975
  if (session) {
1492
1976
  // Close all active SSE streams
1493
1977
  for (const [, streamState] of session.sseStreams) {
@@ -1506,15 +1990,16 @@ export class HttpTransport {
1506
1990
  session.sseStreams.clear();
1507
1991
  // Clean up OAuth token from map if present
1508
1992
  if (session.authToken) {
1509
- this.oauthTokensByAccessToken.delete(session.authToken);
1993
+ profileState.oauthTokensByAccessToken.delete(session.authToken);
1510
1994
  }
1511
- this.sessions.delete(sessionId);
1512
- this.logger.info('Session destroyed', { sessionId });
1995
+ profileState.sessions.delete(sessionId);
1996
+ this.logger.info('Session destroyed', { profileId: profileState.profileId, sessionId });
1513
1997
  // Notify session destruction listeners (for cleanup in MCPServer)
1514
- this.notifySessionDestroyed(sessionId);
1998
+ this.notifySessionDestroyed(profileState.profileId, sessionId);
1515
1999
  // Record metrics
1516
2000
  if (this.metrics) {
1517
2001
  this.metrics.recordSessionDestroyed();
2002
+ this.metrics.clearToolsSession(sessionId);
1518
2003
  }
1519
2004
  }
1520
2005
  }
@@ -1529,10 +2014,10 @@ export class HttpTransport {
1529
2014
  /**
1530
2015
  * Notify all listeners about session destruction
1531
2016
  */
1532
- notifySessionDestroyed(sessionId) {
2017
+ notifySessionDestroyed(profileId, sessionId) {
1533
2018
  for (const listener of this.sessionDestroyedListeners) {
1534
2019
  try {
1535
- listener(sessionId);
2020
+ listener(profileId, sessionId);
1536
2021
  }
1537
2022
  catch (error) {
1538
2023
  this.logger.error('Session destroyed listener error', error);
@@ -1545,7 +2030,7 @@ export class HttpTransport {
1545
2030
  * Why: Bridge between /oauth/token endpoint (where we see OAuthTokens)
1546
2031
  * and session initialization (where we only see access token in Authorization header)
1547
2032
  */
1548
- storeOAuthTokens(tokens, clientId, scopes) {
2033
+ storeOAuthTokens(profileState, tokens, clientId, scopes) {
1549
2034
  if (!tokens.access_token) {
1550
2035
  this.logger.warn('OAuth tokens missing access_token, skipping storage');
1551
2036
  return;
@@ -1553,13 +2038,14 @@ export class HttpTransport {
1553
2038
  const expiresAt = tokens.expires_in
1554
2039
  ? Date.now() + tokens.expires_in * 1000
1555
2040
  : undefined;
1556
- this.oauthTokensByAccessToken.set(tokens.access_token, {
2041
+ profileState.oauthTokensByAccessToken.set(tokens.access_token, {
1557
2042
  refreshToken: tokens.refresh_token,
1558
2043
  expiresAt,
1559
2044
  clientId,
1560
2045
  scopes,
1561
2046
  });
1562
2047
  this.logger.debug('Stored OAuth tokens', {
2048
+ profileId: profileState.profileId,
1563
2049
  hasRefreshToken: !!tokens.refresh_token,
1564
2050
  expiresAt,
1565
2051
  clientId,
@@ -1580,25 +2066,33 @@ export class HttpTransport {
1580
2066
  // Default OAuth session timeout: 24 hours (or configurable)
1581
2067
  const oauthSessionTimeoutMs = this.config.oauthSessionTimeoutMs
1582
2068
  ?? (24 * 60 * 60 * 1000); // 24 hours default
1583
- for (const [sessionId, session] of this.sessions) {
1584
- const age = now - session.lastActivityAt;
1585
- // OAuth sessions with refresh tokens: use extended timeout or never expire
1586
- if (session.refreshToken) {
1587
- // If oauthSessionTimeoutMs is 0 or negative, never expire OAuth sessions
1588
- if (oauthSessionTimeoutMs > 0 && age > oauthSessionTimeoutMs) {
1589
- expiredSessions.push(sessionId);
1590
- }
1591
- // Otherwise, keep the session alive (unlimited timeout)
2069
+ for (const profileState of this.profileStates.values()) {
2070
+ // Cleanup OAuth provider resources (states, codes, tokens)
2071
+ if (profileState.oauthProvider) {
2072
+ profileState.oauthProvider.cleanup();
1592
2073
  }
1593
- else {
1594
- // Non-OAuth sessions: use standard timeout
1595
- if (age > this.config.sessionTimeoutMs) {
1596
- expiredSessions.push(sessionId);
2074
+ for (const [sessionId, session] of profileState.sessions) {
2075
+ const age = now - session.lastActivityAt;
2076
+ // OAuth sessions with refresh tokens: use extended timeout or never expire
2077
+ if (session.refreshToken) {
2078
+ // If oauthSessionTimeoutMs is 0 or negative, never expire OAuth sessions
2079
+ if (oauthSessionTimeoutMs > 0 && age > oauthSessionTimeoutMs) {
2080
+ expiredSessions.push({ profileId: profileState.profileId, sessionId });
2081
+ }
2082
+ }
2083
+ else {
2084
+ // Non-OAuth sessions: use standard timeout
2085
+ if (age > this.config.sessionTimeoutMs) {
2086
+ expiredSessions.push({ profileId: profileState.profileId, sessionId });
2087
+ }
1597
2088
  }
1598
2089
  }
1599
2090
  }
1600
- for (const sessionId of expiredSessions) {
1601
- this.destroySession(sessionId);
2091
+ for (const entry of expiredSessions) {
2092
+ const state = this.profileStates.get(entry.profileId);
2093
+ if (state) {
2094
+ this.destroySession(state, entry.sessionId);
2095
+ }
1602
2096
  }
1603
2097
  if (expiredSessions.length > 0) {
1604
2098
  this.logger.info('Cleaned up expired sessions', { count: expiredSessions.length });
@@ -1609,18 +2103,71 @@ export class HttpTransport {
1609
2103
  *
1610
2104
  * Why public: Allows MCPServer to securely access session tokens without breaking encapsulation
1611
2105
  */
1612
- getSessionToken(sessionId) {
1613
- const session = this.sessions.get(sessionId);
2106
+ getSessionToken(profileId, sessionId) {
2107
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
1614
2108
  return session?.authToken;
1615
2109
  }
2110
+ getSessionFiltering(profileId, sessionId) {
2111
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2112
+ return session?.filtering;
2113
+ }
2114
+ getSessionFilteringHeader(profileId, sessionId) {
2115
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2116
+ return session?.filteringHeader;
2117
+ }
2118
+ getSessionToolFilterRequest(profileId, sessionId) {
2119
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2120
+ return session?.toolFilterRequest;
2121
+ }
2122
+ getSessionToolFilter(profileId, sessionId) {
2123
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2124
+ return session?.toolFilter;
2125
+ }
2126
+ getSessionToolFilterHeader(profileId, sessionId) {
2127
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2128
+ return session?.toolFilterHeader;
2129
+ }
2130
+ setSessionToolFilter(profileId, sessionId, toolFilter) {
2131
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2132
+ if (!session) {
2133
+ return;
2134
+ }
2135
+ session.toolFilter = toolFilter;
2136
+ }
2137
+ recordGlobalToolFilterMetrics(summary) {
2138
+ if (!this.metrics) {
2139
+ return;
2140
+ }
2141
+ this.metrics.recordToolsTotal('profile', summary.originalCount);
2142
+ this.metrics.recordToolsFiltered('global_env', 'allowed', summary.allowedCount);
2143
+ this.metrics.recordToolsFiltered('global_env', 'denied', summary.removedCount);
2144
+ for (const [type, count] of Object.entries(summary.patternCounts)) {
2145
+ this.metrics.recordToolFilterPatternCount(type, count);
2146
+ }
2147
+ }
2148
+ recordSessionToolFilterMetrics(sessionId, allowedCount, request) {
2149
+ if (!this.metrics) {
2150
+ return;
2151
+ }
2152
+ this.metrics.recordToolsSession(sessionId, allowedCount);
2153
+ this.metrics.recordToolFilterPatternCount('session_allow_list', request.exactNames.size);
2154
+ this.metrics.recordToolFilterPatternCount('session_allow_regex', request.regexPatterns.length);
2155
+ }
2156
+ recordToolFilterRejection(tool, source) {
2157
+ if (!this.metrics) {
2158
+ return;
2159
+ }
2160
+ this.metrics.recordToolFilterRejection(tool, source);
2161
+ }
1616
2162
  /**
1617
2163
  * Ensure session has a valid access token, refreshing if necessary
1618
2164
  *
1619
2165
  * Why: Transparently refresh expired OAuth tokens before making API calls
1620
2166
  * Returns true if token is valid (or was successfully refreshed), false otherwise
1621
2167
  */
1622
- async ensureValidSessionToken(sessionId) {
1623
- const session = this.sessions.get(sessionId);
2168
+ async ensureValidSessionToken(profileId, sessionId) {
2169
+ const profileState = this.profileStates.get(profileId);
2170
+ const session = profileState?.sessions.get(sessionId);
1624
2171
  if (!session) {
1625
2172
  return false;
1626
2173
  }
@@ -1634,11 +2181,12 @@ export class HttpTransport {
1634
2181
  // If token is expired or about to expire, refresh it
1635
2182
  if (timeUntilExpiration <= refreshThresholdMs) {
1636
2183
  this.logger.debug('Access token expired or expiring soon, refreshing', {
2184
+ profileId,
1637
2185
  sessionId,
1638
2186
  expiresAt: new Date(session.accessTokenExpiresAt).toISOString(),
1639
2187
  timeUntilExpiration,
1640
2188
  });
1641
- return await this.refreshAccessToken(sessionId);
2189
+ return await this.refreshAccessToken(profileId, sessionId);
1642
2190
  }
1643
2191
  return true;
1644
2192
  }
@@ -1648,14 +2196,16 @@ export class HttpTransport {
1648
2196
  * Why: Automatically renew expired OAuth access tokens without user intervention
1649
2197
  * Returns true on success, false on failure
1650
2198
  */
1651
- async refreshAccessToken(sessionId) {
1652
- const session = this.sessions.get(sessionId);
1653
- if (!session || !session.refreshToken || !this.oauthProvider) {
2199
+ async refreshAccessToken(profileId, sessionId) {
2200
+ const profileState = this.profileStates.get(profileId);
2201
+ const session = profileState?.sessions.get(sessionId);
2202
+ if (!profileState || !session || !session.refreshToken || !profileState.oauthProvider) {
1654
2203
  this.logger.warn('Cannot refresh token: missing session, refreshToken, or OAuth provider', {
2204
+ profileId,
1655
2205
  sessionId,
1656
2206
  hasSession: !!session,
1657
2207
  hasRefreshToken: !!session?.refreshToken,
1658
- hasOAuthProvider: !!this.oauthProvider,
2208
+ hasOAuthProvider: !!profileState?.oauthProvider,
1659
2209
  });
1660
2210
  return false;
1661
2211
  }
@@ -1664,32 +2214,33 @@ export class HttpTransport {
1664
2214
  // Try to find client by clientId stored in session, or use default client
1665
2215
  let client;
1666
2216
  if (session.oauthClientId) {
1667
- await this.oauthProvider.ensureEndpointsInitialized();
1668
- client = await this.oauthProvider.clientsStore.getClient(session.oauthClientId);
2217
+ await profileState.oauthProvider.ensureEndpointsInitialized();
2218
+ client = await profileState.oauthProvider.clientsStore.getClient(session.oauthClientId);
1669
2219
  }
1670
2220
  // Fallback to default client from config if session client not found
1671
- if (!client && this.oauthProvider) {
1672
- await this.oauthProvider.ensureEndpointsInitialized();
2221
+ if (!client && profileState.oauthProvider) {
2222
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1673
2223
  // Try common client IDs
1674
2224
  const defaultClientIds = ['mcp-proxy-client'];
1675
- if (this.config.oauthConfig?.client_id) {
1676
- defaultClientIds.unshift(this.config.oauthConfig.client_id);
2225
+ if (profileState.context.oauthConfig?.client_id) {
2226
+ defaultClientIds.unshift(profileState.context.oauthConfig.client_id);
1677
2227
  }
1678
2228
  for (const clientId of defaultClientIds) {
1679
- client = await this.oauthProvider.clientsStore.getClient(clientId);
2229
+ client = await profileState.oauthProvider.clientsStore.getClient(clientId);
1680
2230
  if (client)
1681
2231
  break;
1682
2232
  }
1683
2233
  }
1684
2234
  if (!client) {
1685
2235
  this.logger.error('Cannot refresh token: OAuth client not found', undefined, {
2236
+ profileId,
1686
2237
  sessionId,
1687
2238
  oauthClientId: session.oauthClientId,
1688
2239
  });
1689
2240
  return false;
1690
2241
  }
1691
2242
  // Exchange refresh token for new tokens
1692
- const tokens = await this.oauthProvider.exchangeRefreshToken(client, session.refreshToken, session.scopes);
2243
+ const tokens = await profileState.oauthProvider.exchangeRefreshToken(client, session.refreshToken, session.scopes);
1693
2244
  // Update session with new tokens
1694
2245
  const oldAccessToken = session.authToken;
1695
2246
  session.authToken = tokens.access_token;
@@ -1699,10 +2250,11 @@ export class HttpTransport {
1699
2250
  : undefined;
1700
2251
  // Update token map: remove old token, add new one
1701
2252
  if (oldAccessToken) {
1702
- this.oauthTokensByAccessToken.delete(oldAccessToken);
2253
+ profileState.oauthTokensByAccessToken.delete(oldAccessToken);
1703
2254
  }
1704
- this.storeOAuthTokens(tokens, client.client_id, session.scopes || []);
2255
+ this.storeOAuthTokens(profileState, tokens, client.client_id, session.scopes || []);
1705
2256
  this.logger.info('Access token refreshed successfully', {
2257
+ profileId,
1706
2258
  sessionId,
1707
2259
  newExpiresAt: session.accessTokenExpiresAt ? new Date(session.accessTokenExpiresAt).toISOString() : undefined,
1708
2260
  });
@@ -1710,6 +2262,7 @@ export class HttpTransport {
1710
2262
  }
1711
2263
  catch (error) {
1712
2264
  this.logger.error('Token refresh failed', error instanceof Error ? error : new Error(String(error)), {
2265
+ profileId,
1713
2266
  sessionId,
1714
2267
  });
1715
2268
  return false;
@@ -1724,41 +2277,49 @@ export class HttpTransport {
1724
2277
  /**
1725
2278
  * Check if OAuth provider is configured
1726
2279
  */
1727
- hasOAuthProvider() {
1728
- return this.oauthProvider !== null;
2280
+ hasOAuthProvider(profileId) {
2281
+ if (!profileId) {
2282
+ const defaultProfileId = this.getDefaultProfileId();
2283
+ if (!defaultProfileId) {
2284
+ return false;
2285
+ }
2286
+ const state = this.profileStates.get(defaultProfileId);
2287
+ return state ? state.oauthProvider !== null : false;
2288
+ }
2289
+ const state = this.profileStates.get(profileId);
2290
+ return state ? state.oauthProvider !== null : false;
1729
2291
  }
1730
2292
  /**
1731
2293
  * Get server URL
1732
2294
  */
1733
- getServerUrl() {
1734
- // Prefer base URL derived from OAuth redirect URI if available
1735
- // This ensures consistency with the public address used for OAuth callbacks
1736
- if (this.oauthProvider?.redirectUri) {
1737
- try {
1738
- return new URL(this.oauthProvider.redirectUri).origin;
1739
- }
1740
- catch (e) {
1741
- // Ignore invalid URL format
1742
- }
1743
- }
1744
- // Fallback to configured host/port
1745
- // If configured with 0.0.0.0, this will return http://0.0.0.0:port
1746
- // which is usually fine for internal communication but not for external clients
1747
- const protocol = this.config.host.includes('://') ? '' : 'http://';
1748
- const host = this.config.host.includes('://') ? this.config.host : this.config.host;
1749
- return `${protocol}${host}:${this.config.port}`;
2295
+ getServerUrl(profileId) {
2296
+ return this.getServerOrigin(profileId);
1750
2297
  }
1751
2298
  /**
1752
2299
  * Get OAuth authorization URL
1753
2300
  */
1754
- getOAuthAuthorizationUrl() {
1755
- return this.oauthProvider?.authorizationEndpoint || '';
2301
+ getOAuthAuthorizationUrl(profileId) {
2302
+ if (!profileId) {
2303
+ const defaultProfileId = this.getDefaultProfileId();
2304
+ if (!defaultProfileId) {
2305
+ return '';
2306
+ }
2307
+ return this.profileStates.get(defaultProfileId)?.oauthProvider?.authorizationEndpoint || '';
2308
+ }
2309
+ return this.profileStates.get(profileId)?.oauthProvider?.authorizationEndpoint || '';
1756
2310
  }
1757
2311
  /**
1758
2312
  * Get OAuth scopes
1759
2313
  */
1760
- getOAuthScopes() {
1761
- return this.oauthProvider?.scopes || [];
2314
+ getOAuthScopes(profileId) {
2315
+ if (!profileId) {
2316
+ const defaultProfileId = this.getDefaultProfileId();
2317
+ if (!defaultProfileId) {
2318
+ return [];
2319
+ }
2320
+ return this.profileStates.get(defaultProfileId)?.oauthProvider?.scopes || [];
2321
+ }
2322
+ return this.profileStates.get(profileId)?.oauthProvider?.scopes || [];
1762
2323
  }
1763
2324
  /**
1764
2325
  * Start HTTP server
@@ -1829,8 +2390,10 @@ export class HttpTransport {
1829
2390
  this.cleanupInterval = null;
1830
2391
  }
1831
2392
  // Destroy all sessions
1832
- for (const sessionId of this.sessions.keys()) {
1833
- this.destroySession(sessionId);
2393
+ for (const profileState of this.profileStates.values()) {
2394
+ for (const sessionId of profileState.sessions.keys()) {
2395
+ this.destroySession(profileState, sessionId);
2396
+ }
1834
2397
  }
1835
2398
  if (this.server) {
1836
2399
  return new Promise((resolve, reject) => {