mcp4openapi 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) 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/{oauth-provider.d.ts → auth/oauth-provider.d.ts} +7 -2
  5. package/dist/src/auth/oauth-provider.d.ts.map +1 -0
  6. package/dist/src/{oauth-provider.js → auth/oauth-provider.js} +30 -2
  7. package/dist/src/auth/oauth-provider.js.map +1 -0
  8. package/dist/src/core/cli-config.d.ts +9 -0
  9. package/dist/src/core/cli-config.d.ts.map +1 -0
  10. package/dist/src/core/cli-config.js +124 -0
  11. package/dist/src/core/cli-config.js.map +1 -0
  12. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -0
  13. package/dist/src/core/constants.d.ts.map +1 -0
  14. package/dist/src/{constants.js → core/constants.js} +1 -0
  15. package/dist/src/core/constants.js.map +1 -0
  16. package/dist/src/{errors.d.ts → core/errors.d.ts} +6 -0
  17. package/dist/src/core/errors.d.ts.map +1 -0
  18. package/dist/src/{errors.js → core/errors.js} +15 -6
  19. package/dist/src/core/errors.js.map +1 -0
  20. package/dist/src/core/filtering.d.ts +19 -0
  21. package/dist/src/core/filtering.d.ts.map +1 -0
  22. package/dist/src/core/filtering.js +292 -0
  23. package/dist/src/core/filtering.js.map +1 -0
  24. package/dist/src/core/index.d.ts +26 -0
  25. package/dist/src/core/index.d.ts.map +1 -0
  26. package/dist/src/core/index.js +275 -0
  27. package/dist/src/core/index.js.map +1 -0
  28. package/dist/src/core/lib.d.ts +8 -0
  29. package/dist/src/core/lib.d.ts.map +1 -0
  30. package/dist/src/core/lib.js +7 -0
  31. package/dist/src/core/lib.js.map +1 -0
  32. package/dist/src/{logger.d.ts → core/logger.d.ts} +6 -1
  33. package/dist/src/core/logger.d.ts.map +1 -0
  34. package/dist/src/{logger.js → core/logger.js} +30 -2
  35. package/dist/src/core/logger.js.map +1 -0
  36. package/dist/src/{metrics.d.ts → core/metrics.d.ts} +11 -0
  37. package/dist/src/core/metrics.d.ts.map +1 -0
  38. package/dist/src/{metrics.js → core/metrics.js} +61 -0
  39. package/dist/src/core/metrics.js.map +1 -0
  40. package/dist/src/core/naming-warnings.d.ts.map +1 -0
  41. package/dist/src/core/naming-warnings.js.map +1 -0
  42. package/dist/src/core/naming.d.ts.map +1 -0
  43. package/dist/src/core/naming.js.map +1 -0
  44. package/dist/src/generated-schemas.d.ts +245 -79
  45. package/dist/src/generated-schemas.d.ts.map +1 -1
  46. package/dist/src/generated-schemas.js +14 -2
  47. package/dist/src/generated-schemas.js.map +1 -1
  48. package/dist/src/index.d.ts +1 -6
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/dist/src/index.js +1 -170
  51. package/dist/src/index.js.map +1 -1
  52. package/dist/src/lib.d.ts +1 -7
  53. package/dist/src/lib.d.ts.map +1 -1
  54. package/dist/src/lib.js +1 -6
  55. package/dist/src/lib.js.map +1 -1
  56. package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
  57. package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
  58. package/dist/src/mcp/mcp-server-manager.js +38 -0
  59. package/dist/src/mcp/mcp-server-manager.js.map +1 -0
  60. package/dist/src/{mcp-server.d.ts → mcp/mcp-server.d.ts} +31 -1
  61. package/dist/src/mcp/mcp-server.d.ts.map +1 -0
  62. package/dist/src/{mcp-server.js → mcp/mcp-server.js} +547 -146
  63. package/dist/src/mcp/mcp-server.js.map +1 -0
  64. package/dist/src/{openapi-parser.d.ts → openapi/openapi-parser.d.ts} +1 -1
  65. package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
  66. package/dist/src/{openapi-parser.js → openapi/openapi-parser.js} +2 -2
  67. package/dist/src/openapi/openapi-parser.js.map +1 -0
  68. package/dist/src/{profile-loader.d.ts → profile/profile-loader.d.ts} +3 -2
  69. package/dist/src/profile/profile-loader.d.ts.map +1 -0
  70. package/dist/src/{profile-loader.js → profile/profile-loader.js} +17 -6
  71. package/dist/src/profile/profile-loader.js.map +1 -0
  72. package/dist/src/profile/profile-registry.d.ts +18 -0
  73. package/dist/src/profile/profile-registry.d.ts.map +1 -0
  74. package/dist/src/profile/profile-registry.js +26 -0
  75. package/dist/src/profile/profile-registry.js.map +1 -0
  76. package/dist/src/profile/profile-resolver.d.ts +25 -0
  77. package/dist/src/profile/profile-resolver.d.ts.map +1 -0
  78. package/dist/src/profile/profile-resolver.js +204 -0
  79. package/dist/src/profile/profile-resolver.js.map +1 -0
  80. package/dist/src/profile/startup-profile.d.ts +17 -0
  81. package/dist/src/profile/startup-profile.d.ts.map +1 -0
  82. package/dist/src/profile/startup-profile.js +30 -0
  83. package/dist/src/profile/startup-profile.js.map +1 -0
  84. package/dist/src/profile/startup-validation.d.ts +11 -0
  85. package/dist/src/profile/startup-validation.d.ts.map +1 -0
  86. package/dist/src/profile/startup-validation.js +21 -0
  87. package/dist/src/profile/startup-validation.js.map +1 -0
  88. package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
  89. package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
  90. package/dist/src/testing/dynamic-mock-server.js +138 -0
  91. package/dist/src/testing/dynamic-mock-server.js.map +1 -0
  92. package/dist/src/testing/request-assertions.d.ts +5 -0
  93. package/dist/src/testing/request-assertions.d.ts.map +1 -0
  94. package/dist/src/testing/request-assertions.js +165 -0
  95. package/dist/src/testing/request-assertions.js.map +1 -0
  96. package/dist/src/testing/template-utils.d.ts +10 -0
  97. package/dist/src/testing/template-utils.d.ts.map +1 -0
  98. package/dist/src/testing/template-utils.js +72 -0
  99. package/dist/src/testing/template-utils.js.map +1 -0
  100. package/dist/src/testing/test-http-utils.d.ts +1 -1
  101. package/dist/src/testing/test-http-utils.d.ts.map +1 -1
  102. package/dist/src/testing/test-http-utils.js +1 -1
  103. package/dist/src/testing/test-http-utils.js.map +1 -1
  104. package/dist/src/testing/test-loader.d.ts +6 -0
  105. package/dist/src/testing/test-loader.d.ts.map +1 -0
  106. package/dist/src/testing/test-loader.js +212 -0
  107. package/dist/src/testing/test-loader.js.map +1 -0
  108. package/dist/src/testing/test-schema.d.ts +1270 -0
  109. package/dist/src/testing/test-schema.d.ts.map +1 -0
  110. package/dist/src/testing/test-schema.js +76 -0
  111. package/dist/src/testing/test-schema.js.map +1 -0
  112. package/dist/src/tool-filter/compat.d.ts +49 -0
  113. package/dist/src/tool-filter/compat.d.ts.map +1 -0
  114. package/dist/src/tool-filter/compat.js +72 -0
  115. package/dist/src/tool-filter/compat.js.map +1 -0
  116. package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
  117. package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
  118. package/dist/src/tool-filter/config/env-config-parser.js +103 -0
  119. package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
  120. package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
  121. package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
  122. package/dist/src/tool-filter/config/header-config-parser.js +118 -0
  123. package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
  124. package/dist/src/tool-filter/errors.d.ts +18 -0
  125. package/dist/src/tool-filter/errors.d.ts.map +1 -0
  126. package/dist/src/tool-filter/errors.js +21 -0
  127. package/dist/src/tool-filter/errors.js.map +1 -0
  128. package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
  129. package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
  130. package/dist/src/tool-filter/filter/filter-engine.js +94 -0
  131. package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
  132. package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
  133. package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
  134. package/dist/src/tool-filter/filter/filter-rules.js +72 -0
  135. package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
  136. package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
  137. package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
  138. package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
  139. package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
  140. package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
  141. package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
  142. package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
  143. package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
  144. package/dist/src/tool-filter/index.d.ts +25 -0
  145. package/dist/src/tool-filter/index.d.ts.map +1 -0
  146. package/dist/src/tool-filter/index.js +30 -0
  147. package/dist/src/tool-filter/index.js.map +1 -0
  148. package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
  149. package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
  150. package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
  151. package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
  152. package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
  153. package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
  154. package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
  155. package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
  156. package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
  157. package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
  158. package/dist/src/tool-filter/operation/operation-detector.js +96 -0
  159. package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
  160. package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
  161. package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
  162. package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
  163. package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
  164. package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
  165. package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
  166. package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
  167. package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
  168. package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
  169. package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
  170. package/dist/src/tool-filter/regex/regex-validator.js +58 -0
  171. package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
  172. package/dist/src/tool-filter/types.d.ts +92 -0
  173. package/dist/src/tool-filter/types.d.ts.map +1 -0
  174. package/dist/src/tool-filter/types.js +5 -0
  175. package/dist/src/tool-filter/types.js.map +1 -0
  176. package/dist/src/tool-filter/utils.d.ts +11 -0
  177. package/dist/src/tool-filter/utils.d.ts.map +1 -0
  178. package/dist/src/tool-filter/utils.js +13 -0
  179. package/dist/src/tool-filter/utils.js.map +1 -0
  180. package/dist/src/{composite-executor.d.ts → tooling/composite-executor.d.ts} +3 -3
  181. package/dist/src/tooling/composite-executor.d.ts.map +1 -0
  182. package/dist/src/{composite-executor.js → tooling/composite-executor.js} +1 -1
  183. package/dist/src/tooling/composite-executor.js.map +1 -0
  184. package/dist/src/{dag-executor.d.ts → tooling/dag-executor.d.ts} +1 -1
  185. package/dist/src/tooling/dag-executor.d.ts.map +1 -0
  186. package/dist/src/tooling/dag-executor.js.map +1 -0
  187. package/dist/src/{proxy-executor.d.ts → tooling/proxy-executor.d.ts} +2 -2
  188. package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
  189. package/dist/src/{proxy-executor.js → tooling/proxy-executor.js} +8 -1
  190. package/dist/src/tooling/proxy-executor.js.map +1 -0
  191. package/dist/src/{tool-generator.d.ts → tooling/tool-generator.d.ts} +4 -3
  192. package/dist/src/tooling/tool-generator.d.ts.map +1 -0
  193. package/dist/src/{tool-generator.js → tooling/tool-generator.js} +23 -7
  194. package/dist/src/tooling/tool-generator.js.map +1 -0
  195. package/dist/src/{http-client-factory.d.ts → transport/http-client-factory.d.ts} +4 -1
  196. package/dist/src/transport/http-client-factory.d.ts.map +1 -0
  197. package/dist/src/{http-client-factory.js → transport/http-client-factory.js} +13 -3
  198. package/dist/src/transport/http-client-factory.js.map +1 -0
  199. package/dist/src/transport/http-transport-config.d.ts +6 -0
  200. package/dist/src/transport/http-transport-config.d.ts.map +1 -0
  201. package/dist/src/transport/http-transport-config.js +62 -0
  202. package/dist/src/transport/http-transport-config.js.map +1 -0
  203. package/dist/src/{http-transport.d.ts → transport/http-transport.d.ts} +72 -14
  204. package/dist/src/transport/http-transport.d.ts.map +1 -0
  205. package/dist/src/{http-transport.js → transport/http-transport.js} +1166 -493
  206. package/dist/src/transport/http-transport.js.map +1 -0
  207. package/dist/src/{interceptors.d.ts → transport/interceptors.d.ts} +6 -2
  208. package/dist/src/transport/interceptors.d.ts.map +1 -0
  209. package/dist/src/{interceptors.js → transport/interceptors.js} +72 -41
  210. package/dist/src/transport/interceptors.js.map +1 -0
  211. package/dist/src/types/http-transport.d.ts +25 -0
  212. package/dist/src/types/http-transport.d.ts.map +1 -1
  213. package/dist/src/types/profile.d.ts +13 -1
  214. package/dist/src/types/profile.d.ts.map +1 -1
  215. package/dist/src/validation/argument-normalizer.d.ts +6 -0
  216. package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
  217. package/dist/src/validation/argument-normalizer.js +70 -0
  218. package/dist/src/validation/argument-normalizer.js.map +1 -0
  219. package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
  220. package/dist/src/validation/jsonrpc-validator.js.map +1 -0
  221. package/dist/src/{schema-validator.d.ts → validation/schema-validator.d.ts} +2 -2
  222. package/dist/src/validation/schema-validator.d.ts.map +1 -0
  223. package/dist/src/validation/schema-validator.js.map +1 -0
  224. package/dist/src/validation/validation-utils.d.ts.map +1 -0
  225. package/dist/src/validation/validation-utils.js.map +1 -0
  226. package/package.json +9 -3
  227. package/profile-schema.json +63 -3
  228. package/profiles/gitlab/developer-profile-oauth.json +1520 -0
  229. package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
  230. package/profiles/gitlab/openapi.yaml +6891 -0
  231. package/profiles/n8n/openapi.yaml +2441 -0
  232. package/profiles/n8n/profile-optimized.json +965 -0
  233. package/profiles/n8n/profile-optimized.test.json +1078 -0
  234. package/profiles/n8n/profile.json +1033 -0
  235. package/profiles/n8n/profile.test.json +983 -0
  236. package/profiles/n8n-nodes/openapi.yaml +24 -0
  237. package/profiles/n8n-nodes/profile-nodes.json +44 -0
  238. package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
  239. package/profiles/semgrep/openapi.yaml +4706 -0
  240. package/profiles/semgrep/profile.json +692 -0
  241. package/profiles/semgrep/profile.test.json +471 -0
  242. package/profiles/youtrack/openapi.json +16976 -0
  243. package/profiles/youtrack/profile.json +608 -0
  244. package/profiles/youtrack/profile.test.json +1926 -0
  245. package/dist/src/composite-executor.d.ts.map +0 -1
  246. package/dist/src/composite-executor.js.map +0 -1
  247. package/dist/src/constants.d.ts.map +0 -1
  248. package/dist/src/constants.js.map +0 -1
  249. package/dist/src/dag-executor.d.ts.map +0 -1
  250. package/dist/src/dag-executor.js.map +0 -1
  251. package/dist/src/errors.d.ts.map +0 -1
  252. package/dist/src/errors.js.map +0 -1
  253. package/dist/src/http-client-factory.d.ts.map +0 -1
  254. package/dist/src/http-client-factory.js.map +0 -1
  255. package/dist/src/http-transport.d.ts.map +0 -1
  256. package/dist/src/http-transport.js.map +0 -1
  257. package/dist/src/interceptors.d.ts.map +0 -1
  258. package/dist/src/interceptors.js.map +0 -1
  259. package/dist/src/jsonrpc-validator.d.ts.map +0 -1
  260. package/dist/src/jsonrpc-validator.js.map +0 -1
  261. package/dist/src/logger.d.ts.map +0 -1
  262. package/dist/src/logger.js.map +0 -1
  263. package/dist/src/mcp-server.d.ts.map +0 -1
  264. package/dist/src/mcp-server.js.map +0 -1
  265. package/dist/src/metrics.d.ts.map +0 -1
  266. package/dist/src/metrics.js.map +0 -1
  267. package/dist/src/naming-warnings.d.ts.map +0 -1
  268. package/dist/src/naming-warnings.js.map +0 -1
  269. package/dist/src/naming.d.ts.map +0 -1
  270. package/dist/src/naming.js.map +0 -1
  271. package/dist/src/oauth-provider.d.ts.map +0 -1
  272. package/dist/src/oauth-provider.js.map +0 -1
  273. package/dist/src/openapi-parser.d.ts.map +0 -1
  274. package/dist/src/openapi-parser.js.map +0 -1
  275. package/dist/src/profile-loader.d.ts.map +0 -1
  276. package/dist/src/profile-loader.js.map +0 -1
  277. package/dist/src/proxy-executor.d.ts.map +0 -1
  278. package/dist/src/proxy-executor.js.map +0 -1
  279. package/dist/src/schema-validator.d.ts.map +0 -1
  280. package/dist/src/schema-validator.js.map +0 -1
  281. package/dist/src/testing/fixtures.d.ts +0 -684
  282. package/dist/src/testing/fixtures.d.ts.map +0 -1
  283. package/dist/src/testing/fixtures.js +0 -528
  284. package/dist/src/testing/fixtures.js.map +0 -1
  285. package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
  286. package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
  287. package/dist/src/testing/mock-gitlab-server.js +0 -1026
  288. package/dist/src/testing/mock-gitlab-server.js.map +0 -1
  289. package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
  290. package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
  291. package/dist/src/testing/mock-semgrep-server.js +0 -213
  292. package/dist/src/testing/mock-semgrep-server.js.map +0 -1
  293. package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
  294. package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
  295. package/dist/src/testing/mock-youtrack-server.js +0 -152
  296. package/dist/src/testing/mock-youtrack-server.js.map +0 -1
  297. package/dist/src/tool-generator.d.ts.map +0 -1
  298. package/dist/src/tool-generator.js.map +0 -1
  299. package/dist/src/validation-utils.d.ts.map +0 -1
  300. package/dist/src/validation-utils.js.map +0 -1
  301. /package/dist/src/{naming-warnings.d.ts → core/naming-warnings.d.ts} +0 -0
  302. /package/dist/src/{naming-warnings.js → core/naming-warnings.js} +0 -0
  303. /package/dist/src/{naming.d.ts → core/naming.d.ts} +0 -0
  304. /package/dist/src/{naming.js → core/naming.js} +0 -0
  305. /package/dist/src/{dag-executor.js → tooling/dag-executor.js} +0 -0
  306. /package/dist/src/{jsonrpc-validator.d.ts → validation/jsonrpc-validator.d.ts} +0 -0
  307. /package/dist/src/{jsonrpc-validator.js → validation/jsonrpc-validator.js} +0 -0
  308. /package/dist/src/{schema-validator.js → validation/schema-validator.js} +0 -0
  309. /package/dist/src/{validation-utils.d.ts → validation/validation-utils.d.ts} +0 -0
  310. /package/dist/src/{validation-utils.js → validation/validation-utils.js} +0 -0
@@ -13,25 +13,27 @@ import fs from 'fs';
13
13
  import crypto from 'crypto';
14
14
  import { isIP } from 'node:net';
15
15
  import rateLimit from 'express-rate-limit';
16
- import { isInitializeRequest } from './jsonrpc-validator.js';
17
- import { MetricsCollector } from './metrics.js';
18
- import { ExternalOAuthProvider } from './oauth-provider.js';
19
- import { HTTP_STATUS, MIME_TYPES, OAUTH_PATHS, TIMEOUTS, OAUTH_RATE_LIMIT } from './constants.js';
20
- import { escapeHtmlSafe } from './validation-utils.js';
21
- import { AuthenticationError, AuthorizationError, RateLimitError, ValidationError, generateCorrelationId, } from './errors.js';
22
- // Default maximum token length (1000 characters)
16
+ import { isInitializeRequest } from '../validation/jsonrpc-validator.js';
17
+ import { MetricsCollector } from '../core/metrics.js';
18
+ import { ExternalOAuthProvider } from '../auth/oauth-provider.js';
19
+ import { HTTP_STATUS, MIME_TYPES, OAUTH_PATHS, TIMEOUTS, OAUTH_RATE_LIMIT } from '../core/constants.js';
20
+ import { escapeHtmlSafe, isSafePropertyName } from '../validation/validation-utils.js';
21
+ import { AuthenticationError, AuthorizationError, ConfigurationError, RateLimitError, ValidationError, generateCorrelationId, } from '../core/errors.js';
22
+ import { parseFilteringHeader, normalizeFilteringHeaderValue } from '../core/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 {
26
+ static { this.PROFILE_HINT_TTL_MS = 10 * 60 * 1000; }
25
27
  constructor(config, logger) {
26
28
  this.server = null;
27
- this.sessions = new Map();
28
29
  this.metrics = null;
29
30
  this.cleanupInterval = null;
30
31
  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();
32
+ this.profileContextProvider = null;
33
+ this.profileStates = new Map();
34
+ this.oauthRedirectHostCache = new Map();
35
+ this.warnedMissingOAuthRedirectEnvVars = new Set();
36
+ this.profileHintsByClient = new Map();
35
37
  this.hasWarnedAboutBinding = false;
36
38
  /**
37
39
  * Session destruction listeners for cleanup in other components
@@ -47,30 +49,37 @@ export class HttpTransport {
47
49
  prefix: 'mcp_',
48
50
  });
49
51
  }
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
52
  this.app = express();
53
+ if (this.config.trustProxy !== undefined) {
54
+ this.app.set('trust proxy', this.config.trustProxy);
55
+ }
65
56
  this.setupMiddleware();
66
57
  this.setupRoutes();
67
58
  }
59
+ getMetricsCollector() {
60
+ return this.metrics;
61
+ }
68
62
  /**
69
63
  * Setup Express middleware
70
64
  *
71
65
  * Why: Security (Origin validation, rate limiting), JSON parsing, session extraction, metrics
72
66
  */
73
67
  setupMiddleware() {
68
+ // Security: standard headers
69
+ this.app.disable('x-powered-by');
70
+ this.app.use((req, res, next) => {
71
+ res.setHeader('Content-Security-Policy', "default-src 'self'; frame-ancestors 'none'");
72
+ res.setHeader('X-Frame-Options', 'DENY');
73
+ res.setHeader('X-Content-Type-Options', 'nosniff');
74
+ res.setHeader('Referrer-Policy', 'no-referrer');
75
+ res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=()');
76
+ // Additional security headers
77
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
78
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
79
+ res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
80
+ res.setHeader('X-DNS-Prefetch-Control', 'off');
81
+ next();
82
+ });
74
83
  // Request logging (before any middleware)
75
84
  this.app.use((req, res, next) => {
76
85
  this.logger.debug('Request received', {
@@ -103,7 +112,9 @@ export class HttpTransport {
103
112
  next();
104
113
  });
105
114
  // JSON body parser
106
- this.app.use(express.json());
115
+ // Limit set to 10MB to support large tool inputs/results (e.g. file content)
116
+ // while preventing massive DoS attacks. Default is 100kb.
117
+ this.app.use(express.json({ limit: '10mb' }));
107
118
  // Metrics: Track request start time
108
119
  this.app.use((req, res, next) => {
109
120
  req.startTime = Date.now();
@@ -149,6 +160,16 @@ export class HttpTransport {
149
160
  });
150
161
  next();
151
162
  });
163
+ // Capture profile hints for clients using profile routing
164
+ this.app.use((req, _res, next) => {
165
+ if (this.config.profileRoutingEnabled) {
166
+ const profileId = this.resolveProfileIdFromPath(req.path);
167
+ if (profileId) {
168
+ this.storeProfileHint(req, profileId);
169
+ }
170
+ }
171
+ next();
172
+ });
152
173
  // Security: Origin validation (DNS rebinding protection)
153
174
  this.app.use((req, res, next) => {
154
175
  const origin = req.headers.origin;
@@ -157,19 +178,31 @@ export class HttpTransport {
157
178
  this.logger.warn('HTTP transport bound to 0.0.0.0 - accessible from network. Ensure firewall protection.');
158
179
  this.hasWarnedAboutBinding = true;
159
180
  }
160
- // Skip Origin check for localhost
161
- if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') {
162
- return next();
181
+ // Validate Origin header
182
+ // We do not skip this check for localhost, as requests to localhost can still be CSRF targets
183
+ if (!origin) {
184
+ next();
185
+ return;
163
186
  }
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({
187
+ this.isAllowedOriginForRequest(origin, req)
188
+ .then((allowed) => {
189
+ if (!allowed) {
190
+ this.logger.warn('Rejected request from disallowed origin', { origin, ip: req.ip });
191
+ res.status(HTTP_STATUS.FORBIDDEN).json({
192
+ error: 'Forbidden',
193
+ message: 'Origin not allowed'
194
+ });
195
+ return;
196
+ }
197
+ next();
198
+ })
199
+ .catch((error) => {
200
+ this.logger.error('Origin validation failed', error instanceof Error ? error : new Error(String(error)));
201
+ res.status(HTTP_STATUS.FORBIDDEN).json({
168
202
  error: 'Forbidden',
169
203
  message: 'Origin not allowed'
170
204
  });
171
- }
172
- next();
205
+ });
173
206
  });
174
207
  // Extract session ID from header
175
208
  this.app.use((req, res, next) => {
@@ -180,6 +213,101 @@ export class HttpTransport {
180
213
  next();
181
214
  });
182
215
  }
216
+ setProfileContextProvider(provider) {
217
+ this.profileContextProvider = provider;
218
+ }
219
+ getDefaultProfileId() {
220
+ if (this.config.defaultProfileId) {
221
+ return this.config.defaultProfileId;
222
+ }
223
+ if (this.config.profileRoutingEnabled) {
224
+ return undefined;
225
+ }
226
+ return 'default';
227
+ }
228
+ buildDefaultProfileContext() {
229
+ const profileId = this.getDefaultProfileId();
230
+ if (!profileId) {
231
+ return null;
232
+ }
233
+ return {
234
+ profileId,
235
+ oauthConfig: this.config.oauthConfig,
236
+ authConfigs: this.config.authConfigs,
237
+ baseUrl: this.config.baseUrl,
238
+ rateLimitOAuthMax: this.config.rateLimitOAuthMax,
239
+ rateLimitOAuthWindowMs: this.config.rateLimitOAuthWindowMs,
240
+ resourceName: this.config.resourceName,
241
+ resourceDocumentation: this.config.resourceDocumentation,
242
+ parser: this.config.parser,
243
+ };
244
+ }
245
+ async getProfileState(profileId) {
246
+ const existing = this.profileStates.get(profileId);
247
+ if (existing) {
248
+ return existing;
249
+ }
250
+ let context = null;
251
+ if (this.profileContextProvider) {
252
+ try {
253
+ context = await this.profileContextProvider(profileId);
254
+ }
255
+ catch (error) {
256
+ if (error instanceof ConfigurationError && error.message === 'Profile not found') {
257
+ this.logger.warn('Profile not found during request', { profileId });
258
+ return null;
259
+ }
260
+ throw error;
261
+ }
262
+ }
263
+ else {
264
+ const defaultContext = this.buildDefaultProfileContext();
265
+ if (defaultContext?.profileId === profileId) {
266
+ context = defaultContext;
267
+ }
268
+ }
269
+ if (!context) {
270
+ return null;
271
+ }
272
+ let oauthProvider = null;
273
+ if (context.oauthConfig) {
274
+ this.logger.info('Initializing OAuth provider with config', {
275
+ profileId,
276
+ hasClientId: !!context.oauthConfig.client_id,
277
+ });
278
+ oauthProvider = new ExternalOAuthProvider(context.oauthConfig, this.logger);
279
+ this.logger.info('OAuth provider initialized', {
280
+ profileId,
281
+ endpoint: oauthProvider.authorizationEndpoint || '(to be derived from issuer)',
282
+ hasIssuer: !!context.oauthConfig.issuer,
283
+ });
284
+ }
285
+ else {
286
+ this.logger.info('No OAuth config provided - OAuth provider not initialized', { profileId });
287
+ }
288
+ const state = {
289
+ profileId,
290
+ context,
291
+ oauthProvider,
292
+ oauthTokensByAccessToken: new Map(),
293
+ sessions: new Map(),
294
+ };
295
+ this.profileStates.set(profileId, state);
296
+ return state;
297
+ }
298
+ getProfileIdForRequest(req) {
299
+ if (req.profileId) {
300
+ return req.profileId;
301
+ }
302
+ return this.getDefaultProfileId();
303
+ }
304
+ async getProfileStateForRequest(req) {
305
+ const profileId = this.getProfileIdForRequest(req);
306
+ if (!profileId) {
307
+ return null;
308
+ }
309
+ return await this.getProfileState(profileId);
310
+ }
183
311
  /**
184
312
  * Check if origin is allowed
185
313
  *
@@ -203,16 +331,10 @@ export class HttpTransport {
203
331
  if (hostname === this.config.host) {
204
332
  return true;
205
333
  }
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
334
+ // Allow OAuth redirect URI hosts (initialized + cached + configured)
335
+ for (const redirectHost of this.getOAuthRedirectHostPatterns()) {
336
+ if (this.matchOrigin(hostname, redirectHost)) {
337
+ return true;
216
338
  }
217
339
  }
218
340
  // Check custom allowed origins
@@ -229,6 +351,159 @@ export class HttpTransport {
229
351
  return false;
230
352
  }
231
353
  }
354
+ getOAuthRedirectHostPatterns() {
355
+ const hosts = new Set();
356
+ const defaultProfileId = this.getDefaultProfileId() ?? 'default';
357
+ for (const state of this.profileStates.values()) {
358
+ const redirectUri = state.oauthProvider?.redirectUri;
359
+ if (!redirectUri) {
360
+ continue;
361
+ }
362
+ try {
363
+ const redirectUrl = new URL(redirectUri);
364
+ hosts.add(redirectUrl.hostname);
365
+ }
366
+ catch {
367
+ // Ignore invalid URL
368
+ }
369
+ }
370
+ if (this.config.oauthConfig) {
371
+ for (const pattern of this.extractRedirectHostPatterns(this.config.oauthConfig, defaultProfileId)) {
372
+ hosts.add(pattern);
373
+ }
374
+ }
375
+ for (const patterns of this.oauthRedirectHostCache.values()) {
376
+ for (const pattern of patterns) {
377
+ hosts.add(pattern);
378
+ }
379
+ }
380
+ return Array.from(hosts);
381
+ }
382
+ extractRedirectHostPatterns(oauthConfig, profileId) {
383
+ if (!oauthConfig?.redirect_uri) {
384
+ return [];
385
+ }
386
+ const resolvedRedirectUri = this.resolveRedirectUriFromEnv(oauthConfig.redirect_uri, profileId);
387
+ if (!resolvedRedirectUri) {
388
+ return [];
389
+ }
390
+ try {
391
+ const redirectUrl = new URL(resolvedRedirectUri);
392
+ return [redirectUrl.hostname];
393
+ }
394
+ catch {
395
+ return [];
396
+ }
397
+ }
398
+ resolveRedirectUriFromEnv(value, profileId) {
399
+ const match = value.match(/^\$\{env:([^}]+)\}$/);
400
+ if (!match) {
401
+ return value;
402
+ }
403
+ const envVar = match[1];
404
+ const envValue = process.env[envVar];
405
+ if (!envValue || envValue.trim().length === 0) {
406
+ const warningKey = `${profileId}:${envVar}`;
407
+ if (!this.warnedMissingOAuthRedirectEnvVars.has(warningKey)) {
408
+ this.warnedMissingOAuthRedirectEnvVars.add(warningKey);
409
+ this.logger.warn('OAuth redirect_uri environment variable is empty', {
410
+ profileId,
411
+ envVar,
412
+ });
413
+ }
414
+ return undefined;
415
+ }
416
+ return envValue;
417
+ }
418
+ resolveProfileIdFromPath(pathname) {
419
+ if (!this.config.profileRoutingEnabled) {
420
+ return null;
421
+ }
422
+ const match = /^\/profile\/([^/]+)/.exec(pathname);
423
+ if (!match) {
424
+ return null;
425
+ }
426
+ try {
427
+ return decodeURIComponent(match[1]);
428
+ }
429
+ catch {
430
+ return null;
431
+ }
432
+ }
433
+ getClientHintKey(req) {
434
+ const userAgent = req.get('user-agent') || '';
435
+ return `${req.ip}|${userAgent}`;
436
+ }
437
+ storeProfileHint(req, profileId) {
438
+ const key = this.getClientHintKey(req);
439
+ this.profileHintsByClient.set(key, { profileId, lastSeen: Date.now() });
440
+ }
441
+ resolveProfileIdFromHint(req) {
442
+ const key = this.getClientHintKey(req);
443
+ const hint = this.profileHintsByClient.get(key);
444
+ if (!hint) {
445
+ return null;
446
+ }
447
+ if (Date.now() - hint.lastSeen > HttpTransport.PROFILE_HINT_TTL_MS) {
448
+ this.profileHintsByClient.delete(key);
449
+ return null;
450
+ }
451
+ return hint.profileId;
452
+ }
453
+ resolveProfileIdForOriginCheck(req) {
454
+ const pathProfileId = this.resolveProfileIdFromPath(req.path);
455
+ if (pathProfileId) {
456
+ return pathProfileId;
457
+ }
458
+ const resource = req.query?.resource;
459
+ if (typeof resource === 'string') {
460
+ const info = this.resolveProfileInfoFromResourceUrl(resource);
461
+ if (info?.profileId) {
462
+ return info.profileId;
463
+ }
464
+ }
465
+ return this.getDefaultProfileId() ?? this.resolveProfileIdFromHint(req) ?? null;
466
+ }
467
+ async primeOAuthRedirectHosts(profileId) {
468
+ if (!this.profileContextProvider) {
469
+ return;
470
+ }
471
+ if (this.oauthRedirectHostCache.has(profileId)) {
472
+ return;
473
+ }
474
+ try {
475
+ const context = await this.profileContextProvider(profileId);
476
+ if (!context?.oauthConfig) {
477
+ this.oauthRedirectHostCache.set(profileId, []);
478
+ return;
479
+ }
480
+ const patterns = this.extractRedirectHostPatterns(context.oauthConfig, profileId);
481
+ this.oauthRedirectHostCache.set(profileId, patterns);
482
+ }
483
+ catch (error) {
484
+ if (error instanceof ConfigurationError && error.message === 'Profile not found') {
485
+ return;
486
+ }
487
+ this.logger.warn('Failed to preload OAuth redirect hosts', {
488
+ profileId,
489
+ error: String(error),
490
+ });
491
+ }
492
+ }
493
+ async isAllowedOriginForRequest(origin, req) {
494
+ if (this.isAllowedOrigin(origin)) {
495
+ return true;
496
+ }
497
+ if (!this.config.profileRoutingEnabled) {
498
+ return false;
499
+ }
500
+ const profileId = this.resolveProfileIdForOriginCheck(req);
501
+ if (!profileId) {
502
+ return false;
503
+ }
504
+ await this.primeOAuthRedirectHosts(profileId);
505
+ return this.isAllowedOrigin(origin);
506
+ }
232
507
  /**
233
508
  * Match hostname against allowed origin pattern
234
509
  *
@@ -280,9 +555,12 @@ export class HttpTransport {
280
555
  }
281
556
  const ipInt = this.ipv4ToInt(ip);
282
557
  const rangeInt = this.ipv4ToInt(range);
558
+ /* c8 ignore start - defensive check for edge cases where isIP() passes but parsing fails
559
+ * This should never happen in practice, but serves as a fail-safe */
283
560
  if (ipInt === null || rangeInt === null) {
284
561
  return false;
285
562
  }
563
+ /* c8 ignore end */
286
564
  const mask = (0xFFFFFFFF << (32 - maskBits)) >>> 0;
287
565
  return (ipInt & mask) === (rangeInt & mask);
288
566
  }
@@ -292,9 +570,12 @@ export class HttpTransport {
292
570
  }
293
571
  const ipInt = this.ipv6ToBigInt(ip);
294
572
  const rangeInt = this.ipv6ToBigInt(range);
573
+ /* c8 ignore start - defensive check for edge cases where isIP() passes but parsing fails
574
+ * This should never happen in practice, but serves as a fail-safe */
295
575
  if (ipInt === null || rangeInt === null) {
296
576
  return false;
297
577
  }
578
+ /* c8 ignore end */
298
579
  const mask = this.ipv6Mask(maskBits);
299
580
  return (ipInt & mask) === (rangeInt & mask);
300
581
  }
@@ -368,17 +649,21 @@ export class HttpTransport {
368
649
  return null;
369
650
  }
370
651
  segments = [...headVals, ...Array(missing).fill(0), ...tailVals];
652
+ /* c8 ignore start - defensive check that should never trigger if logic above is correct */
371
653
  if (segments.length !== totalSegmentsNeeded) {
372
654
  return null;
373
655
  }
656
+ /* c8 ignore end */
374
657
  if (ipv4Tail !== null) {
375
658
  const high = (ipv4Tail >>> 16) & 0xFFFF;
376
659
  const low = ipv4Tail & 0xFFFF;
377
660
  segments.push(high, low);
378
661
  }
662
+ /* c8 ignore start - defensive check that should never trigger if logic above is correct */
379
663
  if (segments.length !== 8) {
380
664
  return null;
381
665
  }
666
+ /* c8 ignore end */
382
667
  let value = 0n;
383
668
  for (const part of segments) {
384
669
  value = (value << 16n) + BigInt(part);
@@ -428,6 +713,90 @@ export class HttpTransport {
428
713
  formatRateLimitMessage(scope, maxRequests, windowMs) {
429
714
  return `Rate limit exceeded for ${scope}. Max ${maxRequests} requests per ${windowMs / 1000} seconds.`;
430
715
  }
716
+ getProfilePrefix(profileId, options) {
717
+ if (!this.config.profileRoutingEnabled) {
718
+ return '';
719
+ }
720
+ if (options?.forceProfilePrefix && profileId) {
721
+ return `/profile/${encodeURIComponent(profileId)}`;
722
+ }
723
+ const defaultProfileId = this.getDefaultProfileId();
724
+ if (!profileId || (defaultProfileId && profileId === defaultProfileId)) {
725
+ return '';
726
+ }
727
+ return `/profile/${encodeURIComponent(profileId)}`;
728
+ }
729
+ buildProfilePath(profileId, path, options) {
730
+ const prefix = this.getProfilePrefix(profileId, options);
731
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
732
+ return `${prefix}${normalizedPath}`;
733
+ }
734
+ getServerOrigin(profileId) {
735
+ if (profileId) {
736
+ const state = this.profileStates.get(profileId);
737
+ if (state?.oauthProvider?.redirectUri) {
738
+ try {
739
+ return new URL(state.oauthProvider.redirectUri).origin;
740
+ }
741
+ catch {
742
+ // Ignore invalid URL
743
+ }
744
+ }
745
+ }
746
+ const protocol = this.config.host.includes('://') ? '' : 'http://';
747
+ const host = this.config.host.includes('://') ? this.config.host : this.config.host;
748
+ return `${protocol}${host}:${this.config.port}`;
749
+ }
750
+ buildProfileUrl(profileId, path, options) {
751
+ return `${this.getServerOrigin(profileId)}${this.buildProfilePath(profileId, path, options)}`;
752
+ }
753
+ normalizeResourcePath(pathname) {
754
+ const normalized = pathname.replace(/\/+$/, '');
755
+ return normalized === '' ? '/' : normalized;
756
+ }
757
+ resolveProfileInfoFromResourceUrl(resource) {
758
+ let url;
759
+ try {
760
+ url = new URL(resource);
761
+ }
762
+ catch {
763
+ return null;
764
+ }
765
+ const path = this.normalizeResourcePath(url.pathname);
766
+ if (path === '/mcp') {
767
+ const defaultProfileId = this.getDefaultProfileId();
768
+ if (!defaultProfileId) {
769
+ return null;
770
+ }
771
+ return { profileId: defaultProfileId, forceProfilePrefix: false };
772
+ }
773
+ if (!this.config.profileRoutingEnabled) {
774
+ return null;
775
+ }
776
+ const match = /^\/profile\/([^/]+)\/mcp$/.exec(path);
777
+ if (!match) {
778
+ return null;
779
+ }
780
+ try {
781
+ return { profileId: decodeURIComponent(match[1]), forceProfilePrefix: true };
782
+ }
783
+ catch {
784
+ return null;
785
+ }
786
+ }
787
+ resolveProfileIdFromResourceUrl(resource) {
788
+ return this.resolveProfileInfoFromResourceUrl(resource)?.profileId ?? null;
789
+ }
790
+ getOAuthProtectedResourceUrl(profileId) {
791
+ const effectiveProfileId = profileId ?? this.getDefaultProfileId();
792
+ return this.buildProfileUrl(effectiveProfileId, OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE);
793
+ }
794
+ respondProfileNotFound(res, profileId) {
795
+ res.status(HTTP_STATUS.NOT_FOUND).json({
796
+ error: 'Not Found',
797
+ message: profileId ? `Profile '${profileId}' not found` : 'Profile not found',
798
+ });
799
+ }
431
800
  /**
432
801
  * Setup MCP endpoint routes
433
802
  *
@@ -437,303 +806,130 @@ export class HttpTransport {
437
806
  this.logger.info('Setting up HTTP routes');
438
807
  // Security: Rate limiting setup (needed for OAuth routes)
439
808
  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);
809
+ const profileRoutingEnabled = this.config.profileRoutingEnabled === true;
810
+ const defaultProfileId = this.getDefaultProfileId();
811
+ const attachProfileId = (req, _res, next) => {
812
+ req.profileId = req.params.profileId;
813
+ next();
814
+ };
815
+ const oauthRateLimiterByProfile = new Map();
816
+ const getOAuthRateLimiter = (profileState) => {
817
+ const existing = oauthRateLimiterByProfile.get(profileState.profileId);
818
+ if (existing) {
819
+ return existing;
481
820
  }
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
- }
635
- });
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
- }
821
+ const oauthWindowMs = profileState.context.rateLimitOAuthWindowMs || OAUTH_RATE_LIMIT.WINDOW_MS;
822
+ const oauthMaxRequests = profileState.context.rateLimitOAuthMax || OAUTH_RATE_LIMIT.MAX_REQUESTS;
823
+ const limiter = this.createRateLimiter({
824
+ enabled: rateLimitEnabled,
825
+ windowMs: oauthWindowMs,
826
+ maxRequests: oauthMaxRequests,
827
+ logMessage: 'Rate limit exceeded for OAuth',
828
+ responseMessage: `Too many OAuth requests. Limit: ${oauthMaxRequests} requests per ${Math.round(oauthWindowMs / 60000)} minutes. Please try again later.`,
695
829
  });
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' });
830
+ oauthRateLimiterByProfile.set(profileState.profileId, limiter);
831
+ return limiter;
832
+ };
833
+ const oauthRateLimiter = async (req, res, next) => {
834
+ const profileState = await this.getProfileStateForRequest(req);
835
+ if (!profileState) {
836
+ this.respondProfileNotFound(res, req.profileId);
837
+ return;
838
+ }
839
+ const limiter = getOAuthRateLimiter(profileState);
840
+ return limiter(req, res, next);
841
+ };
842
+ const withProfileState = (handler) => {
843
+ return async (req, res) => {
844
+ const profileState = await this.getProfileStateForRequest(req);
845
+ if (!profileState) {
846
+ this.respondProfileNotFound(res, req.profileId);
847
+ return;
734
848
  }
849
+ await handler(req, res, profileState);
850
+ };
851
+ };
852
+ const registerOAuthRoutes = (basePath, includeProfileParam, includeProtectedResource) => {
853
+ const middlewares = includeProfileParam ? [attachProfileId] : [];
854
+ const withOAuthRateLimit = [...middlewares, oauthRateLimiter];
855
+ const withProfile = (handler) => {
856
+ return [...middlewares, oauthRateLimiter, withProfileState(handler)];
857
+ };
858
+ if (includeProtectedResource) {
859
+ this.app.get(`${basePath}${OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE}`, ...withProfile((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
860
+ }
861
+ this.app.get(`${basePath}${OAUTH_PATHS.AUTHORIZE}`, ...withProfile((req, res, profileState) => this.handleOAuthAuthorize(req, res, profileState)));
862
+ this.app.post(`${basePath}${OAUTH_PATHS.TOKEN}`, ...middlewares, oauthRateLimiter, express.urlencoded({ extended: false, limit: '50kb' }), withProfileState((req, res, profileState) => this.handleOAuthToken(req, res, profileState)));
863
+ this.app.get(`${basePath}${OAUTH_PATHS.CALLBACK}`, ...withProfile((req, res, profileState) => this.handleOAuthCallback(req, res, profileState)));
864
+ this.app.get(`${basePath}${OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER}`, ...withProfile((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
865
+ this.app.get(`${basePath}${OAUTH_PATHS.WELL_KNOWN_OPENID_CONFIGURATION}`, ...withProfile((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
866
+ this.app.post(`${basePath}${OAUTH_PATHS.REGISTER}`, ...middlewares, oauthRateLimiter, express.json(), withProfileState((req, res, profileState) => this.handleOAuthRegister(req, res, profileState)));
867
+ this.logger.info('OAuth routes registered', {
868
+ basePath,
869
+ profileRoutingEnabled,
735
870
  });
736
- this.logger.info('OAuth routes registered');
871
+ };
872
+ if (defaultProfileId) {
873
+ registerOAuthRoutes('', false, false);
874
+ }
875
+ if (profileRoutingEnabled) {
876
+ registerOAuthRoutes('/profile/:profileId', true, true);
877
+ }
878
+ const attachProfileFromResourceQuery = (req, res, next) => {
879
+ const { resource } = req.query;
880
+ if (resource === undefined) {
881
+ next();
882
+ return;
883
+ }
884
+ if (typeof resource !== 'string') {
885
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
886
+ error: 'invalid_request',
887
+ message: 'Invalid resource query parameter',
888
+ });
889
+ return;
890
+ }
891
+ const info = this.resolveProfileInfoFromResourceUrl(resource);
892
+ if (!info?.profileId) {
893
+ res.status(HTTP_STATUS.NOT_FOUND).json({
894
+ error: 'Not Found',
895
+ message: 'OAuth metadata unavailable for requested resource',
896
+ });
897
+ return;
898
+ }
899
+ req.profileId = info.profileId;
900
+ req.forceProfilePrefix = info.forceProfilePrefix;
901
+ this.storeProfileHint(req, info.profileId);
902
+ next();
903
+ };
904
+ const attachProfileFromHint = (req, res, next) => {
905
+ const profileId = this.resolveProfileIdFromHint(req);
906
+ if (!profileId) {
907
+ res.status(HTTP_STATUS.NOT_FOUND).json({
908
+ error: 'Not Found',
909
+ message: 'OAuth metadata unavailable for requested resource',
910
+ });
911
+ return;
912
+ }
913
+ req.profileId = profileId;
914
+ req.forceProfilePrefix = true;
915
+ next();
916
+ };
917
+ if (defaultProfileId || profileRoutingEnabled) {
918
+ this.app.get(OAUTH_PATHS.WELL_KNOWN_PROTECTED_RESOURCE, attachProfileFromResourceQuery, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
919
+ }
920
+ if (profileRoutingEnabled) {
921
+ this.app.get('/.well-known/oauth-protected-resource', attachProfileFromResourceQuery, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
922
+ this.app.get('/.well-known/oauth-protected-resource/profile/:profileId/mcp', attachProfileId, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthProtectedResource(req, res, profileState)));
923
+ if (!defaultProfileId) {
924
+ this.app.get(OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER, attachProfileFromHint, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
925
+ this.app.get(OAUTH_PATHS.WELL_KNOWN_OPENID_CONFIGURATION, attachProfileFromHint, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
926
+ this.app.post(OAUTH_PATHS.REGISTER, attachProfileFromHint, oauthRateLimiter, express.json(), withProfileState((req, res, profileState) => this.handleOAuthRegister(req, res, profileState)));
927
+ this.app.get(OAUTH_PATHS.AUTHORIZE, attachProfileFromHint, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthAuthorize(req, res, profileState)));
928
+ this.app.post(OAUTH_PATHS.TOKEN, attachProfileFromHint, oauthRateLimiter, express.urlencoded({ extended: false, limit: '50kb' }), withProfileState((req, res, profileState) => this.handleOAuthToken(req, res, profileState)));
929
+ this.app.get(OAUTH_PATHS.CALLBACK, attachProfileFromHint, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthCallback(req, res, profileState)));
930
+ }
931
+ this.app.get(`${OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER}/profile/:profileId`, attachProfileId, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
932
+ this.app.get(`${OAUTH_PATHS.WELL_KNOWN_OPENID_CONFIGURATION}/profile/:profileId`, attachProfileId, oauthRateLimiter, withProfileState((req, res, profileState) => this.handleOAuthAuthorizationServerMetadata(req, res, profileState)));
737
933
  }
738
934
  // Security: Rate limiting setup (for MCP endpoints)
739
935
  const windowMs = this.config.rateLimitWindowMs || TIMEOUTS.RATE_LIMIT_WINDOW_MS;
@@ -761,47 +957,67 @@ export class HttpTransport {
761
957
  logMessage: 'Rate limit exceeded for metrics',
762
958
  responseMessage: this.formatRateLimitMessage('metrics', metricsMaxRequests, windowMs),
763
959
  });
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
- });
960
+ const registerMcpRoutes = (basePath, includeProfileParam, isDefault) => {
961
+ const middlewares = includeProfileParam ? [attachProfileId] : [];
962
+ const pathPrefix = basePath || '';
963
+ // Main MCP endpoint - POST for sending messages
964
+ this.app.post(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handlePost.bind(this));
965
+ // CORS preflight handler
966
+ this.app.options(`${pathPrefix}/mcp`, ...middlewares, async (req, res) => {
967
+ const origin = req.headers.origin;
968
+ try {
969
+ // Only send CORS headers for explicitly allowed origins; otherwise reject
970
+ if (origin && await this.isAllowedOriginForRequest(origin, req)) {
971
+ // nosemgrep: javascript.express.security.cors-misconfiguration.cors-misconfiguration
972
+ res.setHeader('Access-Control-Allow-Origin', origin);
973
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
974
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Mcp-Session-Id');
975
+ // We do not allow credentials; prevents cookie-based attacks by default
976
+ res.setHeader('Access-Control-Allow-Credentials', 'false');
977
+ res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache
978
+ return res.status(HTTP_STATUS.OK).send();
979
+ }
980
+ // Disallowed origin: do not echo origin or emit permissive headers
981
+ res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
982
+ }
983
+ catch (error) {
984
+ this.logger.error('Origin validation failed', error instanceof Error ? error : new Error(String(error)));
985
+ res.status(HTTP_STATUS.FORBIDDEN).json({ error: 'Forbidden', message: 'Origin not allowed' });
986
+ }
987
+ });
988
+ // Main MCP endpoint - GET for SSE streaming
989
+ this.app.get(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handleGet.bind(this));
990
+ // Session termination
991
+ this.app.delete(`${pathPrefix}/mcp`, ...middlewares, mcpRateLimiter, this.handleDelete.bind(this));
992
+ // Legacy alias endpoints - deprecated
993
+ // Why: Backward compatibility for clients using /sse during migration
994
+ this.app.post(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
995
+ this.logger.warn('Deprecated endpoint used: POST /sse. Please migrate to POST /mcp');
996
+ this.logger.info('Handling POST /sse request');
997
+ return this.handlePost(req, res, next);
998
+ });
999
+ this.app.get(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
1000
+ this.logger.warn('Deprecated endpoint used: GET /sse. Please migrate to GET /mcp');
1001
+ this.logger.info(`Handling GET /sse request from: ${req.ip}`);
1002
+ return this.handleGet(req, res, next);
1003
+ });
1004
+ this.app.delete(`${pathPrefix}/sse`, ...middlewares, mcpRateLimiter, (req, res, next) => {
1005
+ this.logger.warn('Deprecated endpoint used: DELETE /sse. Please migrate to DELETE /mcp');
1006
+ return this.handleDelete(req, res, next);
1007
+ });
1008
+ if (isDefault) {
1009
+ this.logger.info('Registered MCP routes for default profile', { pathPrefix: pathPrefix || '/mcp' });
1010
+ }
1011
+ else {
1012
+ this.logger.info('Registered MCP routes for profile routing', { pathPrefix: `${pathPrefix}/mcp` });
1013
+ }
1014
+ };
1015
+ if (defaultProfileId) {
1016
+ registerMcpRoutes('', false, true);
1017
+ }
1018
+ if (profileRoutingEnabled) {
1019
+ registerMcpRoutes('/profile/:profileId', true, false);
1020
+ }
805
1021
  // Metrics endpoint (if enabled)
806
1022
  if (this.config.metricsEnabled) {
807
1023
  this.app.get(this.config.metricsPath, metricsRateLimiter, this.handleMetrics.bind(this));
@@ -809,7 +1025,11 @@ export class HttpTransport {
809
1025
  // Health check (with rate limiting)
810
1026
  this.app.get('/health', mcpRateLimiter, (req, res) => {
811
1027
  const startTime = Date.now();
812
- res.json({ status: 'ok', sessions: this.sessions.size });
1028
+ let totalSessions = 0;
1029
+ for (const state of this.profileStates.values()) {
1030
+ totalSessions += state.sessions.size;
1031
+ }
1032
+ res.json({ status: 'ok', sessions: totalSessions });
813
1033
  if (this.metrics) {
814
1034
  const duration = (Date.now() - startTime) / 1000;
815
1035
  this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
@@ -833,6 +1053,245 @@ export class HttpTransport {
833
1053
  });
834
1054
  });
835
1055
  }
1056
+ getProfileIssuerUrl(profileId, options) {
1057
+ const origin = this.getServerOrigin(profileId);
1058
+ const prefix = this.getProfilePrefix(profileId, options);
1059
+ return `${origin}${prefix}`;
1060
+ }
1061
+ async handleOAuthProtectedResource(req, res, profileState) {
1062
+ if (!profileState.oauthProvider) {
1063
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'OAuth not configured for this profile' });
1064
+ return;
1065
+ }
1066
+ const profileId = profileState.profileId;
1067
+ const isProfileScoped = typeof req.params?.profileId === 'string';
1068
+ const forceProfilePrefix = isProfileScoped || req.forceProfilePrefix === true;
1069
+ const urlOptions = forceProfilePrefix ? { forceProfilePrefix: true } : undefined;
1070
+ const serverUrl = new URL(this.buildProfileUrl(profileId, '/mcp', urlOptions));
1071
+ const issuerUrl = this.getProfileIssuerUrl(profileId, urlOptions);
1072
+ const metadata = {
1073
+ resource: serverUrl.href,
1074
+ authorization_servers: [issuerUrl],
1075
+ bearer_methods_supported: ['header'],
1076
+ };
1077
+ if (profileState.oauthProvider.scopes && profileState.oauthProvider.scopes.length > 0) {
1078
+ metadata.scopes_supported = profileState.oauthProvider.scopes;
1079
+ }
1080
+ if (profileState.context.resourceName) {
1081
+ metadata.resource_name = profileState.context.resourceName;
1082
+ }
1083
+ if (profileState.context.resourceDocumentation) {
1084
+ metadata.resource_documentation = profileState.context.resourceDocumentation;
1085
+ }
1086
+ res.json(metadata);
1087
+ }
1088
+ async handleOAuthAuthorize(req, res, profileState) {
1089
+ if (!profileState.oauthProvider) {
1090
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth not configured for this profile');
1091
+ return;
1092
+ }
1093
+ try {
1094
+ res.setHeader('Cache-Control', 'no-store');
1095
+ const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query;
1096
+ if (!client_id || typeof client_id !== 'string') {
1097
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing client_id');
1098
+ return;
1099
+ }
1100
+ if (!response_type || typeof response_type !== 'string') {
1101
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing response_type');
1102
+ return;
1103
+ }
1104
+ if (response_type !== 'code') {
1105
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Unsupported response_type');
1106
+ return;
1107
+ }
1108
+ if (!redirect_uri || typeof redirect_uri !== 'string') {
1109
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing redirect_uri');
1110
+ return;
1111
+ }
1112
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1113
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1114
+ if (!client) {
1115
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Invalid client_id');
1116
+ return;
1117
+ }
1118
+ const scopeStr = (scope || '').trim();
1119
+ const params = {
1120
+ responseType: response_type,
1121
+ clientId: client_id,
1122
+ redirectUri: redirect_uri,
1123
+ scope: scopeStr ? scopeStr.split(' ') : [],
1124
+ state: state,
1125
+ codeChallenge: code_challenge,
1126
+ codeChallengeMethod: code_challenge_method,
1127
+ scopes: scopeStr ? scopeStr.split(' ') : [],
1128
+ };
1129
+ await profileState.oauthProvider.authorize(client, params, res);
1130
+ }
1131
+ catch (error) {
1132
+ this.logger.error('OAuth authorize error', error instanceof Error ? error : new Error(String(error)));
1133
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth authorization failed');
1134
+ }
1135
+ }
1136
+ async handleOAuthToken(req, res, profileState) {
1137
+ if (!profileState.oauthProvider) {
1138
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'server_error', error_description: 'OAuth provider not initialized' });
1139
+ return;
1140
+ }
1141
+ try {
1142
+ res.setHeader('Cache-Control', 'no-store');
1143
+ const { grant_type, code, redirect_uri, client_id, code_verifier, refresh_token } = req.body;
1144
+ this.logger.debug('OAuth token request', {
1145
+ profileId: profileState.profileId,
1146
+ grant_type,
1147
+ client_id,
1148
+ has_code: !!code,
1149
+ has_code_verifier: !!code_verifier,
1150
+ redirect_uri,
1151
+ });
1152
+ if (grant_type === 'authorization_code') {
1153
+ if (!code) {
1154
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing code' });
1155
+ return;
1156
+ }
1157
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1158
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1159
+ if (!client) {
1160
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
1161
+ return;
1162
+ }
1163
+ const tokens = await profileState.oauthProvider.exchangeAuthorizationCode(client, code, code_verifier, redirect_uri);
1164
+ this.storeOAuthTokens(profileState, tokens, client.client_id, client.scope?.split(' ') || []);
1165
+ res.json(tokens);
1166
+ return;
1167
+ }
1168
+ if (grant_type === 'refresh_token') {
1169
+ if (!refresh_token) {
1170
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_request', error_description: 'Missing refresh_token' });
1171
+ return;
1172
+ }
1173
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1174
+ const client = await profileState.oauthProvider.clientsStore.getClient(client_id);
1175
+ if (!client) {
1176
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'invalid_client' });
1177
+ return;
1178
+ }
1179
+ const tokens = await profileState.oauthProvider.exchangeRefreshToken(client, refresh_token);
1180
+ this.storeOAuthTokens(profileState, tokens, client.client_id, client.scope?.split(' ') || []);
1181
+ res.json(tokens);
1182
+ return;
1183
+ }
1184
+ this.logger.warn('Unsupported grant type', { grant_type, expected: 'authorization_code or refresh_token' });
1185
+ res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'unsupported_grant_type' });
1186
+ }
1187
+ catch (error) {
1188
+ this.logger.error('OAuth token exchange error', error instanceof Error ? error : new Error(String(error)));
1189
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1190
+ error: 'invalid_grant',
1191
+ error_description: 'Token exchange failed',
1192
+ });
1193
+ }
1194
+ }
1195
+ async handleOAuthCallback(req, res, profileState) {
1196
+ if (!profileState.oauthProvider) {
1197
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth provider not initialized');
1198
+ return;
1199
+ }
1200
+ try {
1201
+ const { code, state, error, error_description } = req.query;
1202
+ this.logger.info('OAuth callback received', {
1203
+ profileId: profileState.profileId,
1204
+ hasCode: !!code,
1205
+ hasState: !!state,
1206
+ error: error,
1207
+ errorDescription: error_description,
1208
+ });
1209
+ if (error) {
1210
+ const safeError = escapeHtmlSafe(error);
1211
+ const safeErrorDesc = escapeHtmlSafe(error_description);
1212
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1213
+ error: safeError,
1214
+ error_description: safeErrorDesc || safeError,
1215
+ });
1216
+ return;
1217
+ }
1218
+ if (!code || typeof code !== 'string') {
1219
+ res.status(HTTP_STATUS.BAD_REQUEST).send('Missing authorization code');
1220
+ return;
1221
+ }
1222
+ await profileState.oauthProvider.handleCallback(req, res);
1223
+ }
1224
+ catch (error) {
1225
+ this.logger.error('OAuth callback error', error instanceof Error ? error : new Error(String(error)));
1226
+ if (!res.headersSent) {
1227
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth callback failed');
1228
+ }
1229
+ }
1230
+ }
1231
+ async handleOAuthAuthorizationServerMetadata(req, res, profileState) {
1232
+ if (!profileState.oauthProvider) {
1233
+ res.status(HTTP_STATUS.NOT_FOUND).send('OAuth metadata unavailable');
1234
+ return;
1235
+ }
1236
+ try {
1237
+ const profileId = profileState.profileId;
1238
+ const isProfileScoped = typeof req.params?.profileId === 'string';
1239
+ const forceProfilePrefix = isProfileScoped || req.forceProfilePrefix === true;
1240
+ const urlOptions = forceProfilePrefix ? { forceProfilePrefix: true } : undefined;
1241
+ const issuer = this.getProfileIssuerUrl(profileId, urlOptions);
1242
+ res.json({
1243
+ issuer,
1244
+ authorization_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.AUTHORIZE, urlOptions),
1245
+ token_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.TOKEN, urlOptions),
1246
+ registration_endpoint: this.buildProfileUrl(profileId, OAUTH_PATHS.REGISTER, urlOptions),
1247
+ response_types_supported: ['code'],
1248
+ code_challenge_methods_supported: ['S256'],
1249
+ grant_types_supported: ['authorization_code', 'refresh_token'],
1250
+ scopes_supported: profileState.oauthProvider.scopes || ['api'],
1251
+ });
1252
+ }
1253
+ catch (error) {
1254
+ this.logger.error('OAuth authorization server metadata error', error instanceof Error ? error : new Error(String(error)));
1255
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send('OAuth metadata failed');
1256
+ }
1257
+ }
1258
+ async handleOAuthRegister(req, res, profileState) {
1259
+ if (!profileState.oauthProvider) {
1260
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'server_error', error_description: 'Registration unavailable' });
1261
+ return;
1262
+ }
1263
+ try {
1264
+ const { redirect_uris } = req.body;
1265
+ this.logger.info('Dynamic client registration request', {
1266
+ profileId: profileState.profileId,
1267
+ redirect_uris,
1268
+ });
1269
+ const clientId = 'mcp-proxy-client';
1270
+ const clientSecret = 'mcp-proxy-secret';
1271
+ const client = {
1272
+ client_id: clientId,
1273
+ client_secret: clientSecret,
1274
+ redirect_uris: redirect_uris || [],
1275
+ grant_types: ['authorization_code', 'refresh_token'],
1276
+ response_types: ['code'],
1277
+ scope: (profileState.oauthProvider.scopes || []).join(' '),
1278
+ };
1279
+ await profileState.oauthProvider.clientsStore.registerClient(client);
1280
+ res.status(HTTP_STATUS.CREATED).json({
1281
+ client_id: clientId,
1282
+ client_secret: clientSecret,
1283
+ redirect_uris: redirect_uris,
1284
+ grant_types: ['authorization_code', 'refresh_token'],
1285
+ response_types: ['code'],
1286
+ scope: (profileState.oauthProvider.scopes || []).join(' '),
1287
+ token_endpoint_auth_method: 'client_secret_post',
1288
+ });
1289
+ }
1290
+ catch (error) {
1291
+ this.logger.error('Client registration failed', error instanceof Error ? error : new Error(String(error)));
1292
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'server_error', error_description: 'Registration failed' });
1293
+ }
1294
+ }
836
1295
  /**
837
1296
  * Handle metrics endpoint
838
1297
  *
@@ -851,8 +1310,14 @@ export class HttpTransport {
851
1310
  // Don't record metrics call in metrics (avoid recursion)
852
1311
  }
853
1312
  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 });
1313
+ const correlationId = generateCorrelationId();
1314
+ this.logger.error('Metrics endpoint error', error, { correlationId });
1315
+ res.setHeader('Cache-Control', 'no-store');
1316
+ res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
1317
+ error: 'Internal Server Error',
1318
+ message: `Internal error (correlation ID: ${correlationId})`,
1319
+ correlationId
1320
+ });
856
1321
  }
857
1322
  }
858
1323
  /**
@@ -969,16 +1434,13 @@ export class HttpTransport {
969
1434
  *
970
1435
  * Returns: { type: 'bearer' | 'oauth' | 'api-token', token: string, sessionId?: string }
971
1436
  */
972
- extractAuthToken(req) {
973
- // 1. Check for OAuth session first (highest priority for authenticated sessions)
1437
+ extractAuthToken(req, profileState) {
974
1438
  const sessionId = req.sessionId || req.headers['mcp-session-id'];
975
- if (sessionId && this.sessions.has(sessionId)) {
976
- const session = this.sessions.get(sessionId);
977
- if (session && session.authToken) {
978
- return { type: 'oauth', token: session.authToken, sessionId };
979
- }
980
- }
981
- // 2. Check Authorization: Bearer header
1439
+ const session = sessionId ? profileState.sessions.get(sessionId) : undefined;
1440
+ const authConfigs = profileState.context.authConfigs;
1441
+ const configs = authConfigs ? (Array.isArray(authConfigs) ? authConfigs : [authConfigs]) : [];
1442
+ const hasOAuth = !!profileState.oauthProvider || configs.some(config => config.type === 'oauth');
1443
+ // 1. Check Authorization: Bearer header
982
1444
  const authHeader = req.headers.authorization;
983
1445
  if (authHeader) {
984
1446
  // Defense against ReDoS: Check length before regex
@@ -997,6 +1459,25 @@ export class HttpTransport {
997
1459
  this.validateToken(token, 'Authorization token');
998
1460
  return { type: 'bearer', token };
999
1461
  }
1462
+ // 2. Check configured custom header (if any)
1463
+ if (configs.length > 0) {
1464
+ const sortedConfigs = configs.sort((a, b) => (a.priority || 0) - (b.priority || 0));
1465
+ const customHeaderConfig = sortedConfigs.find(c => c.type === 'custom-header' && c.header_name);
1466
+ if (customHeaderConfig && customHeaderConfig.header_name) {
1467
+ if (!isSafePropertyName(customHeaderConfig.header_name)) {
1468
+ throw new ValidationError(`Invalid custom auth header name: ${customHeaderConfig.header_name}`);
1469
+ }
1470
+ const headerKey = customHeaderConfig.header_name.toLowerCase();
1471
+ const headerValue = req.headers[headerKey];
1472
+ if (headerValue) {
1473
+ if (typeof headerValue !== 'string') {
1474
+ throw new ValidationError(`${customHeaderConfig.header_name} must be a string`);
1475
+ }
1476
+ this.validateToken(headerValue, customHeaderConfig.header_name);
1477
+ return { type: 'api-token', token: headerValue };
1478
+ }
1479
+ }
1480
+ }
1000
1481
  // 3. Check X-API-Token header (for custom implementations)
1001
1482
  const apiTokenHeader = req.headers['x-api-token'];
1002
1483
  if (apiTokenHeader) {
@@ -1006,8 +1487,32 @@ export class HttpTransport {
1006
1487
  this.validateToken(apiTokenHeader, 'X-API-Token');
1007
1488
  return { type: 'api-token', token: apiTokenHeader };
1008
1489
  }
1490
+ // 4. Fall back to session token (OAuth only when configured)
1491
+ if (session && session.authToken) {
1492
+ return { type: hasOAuth ? 'oauth' : 'api-token', token: session.authToken, sessionId };
1493
+ }
1009
1494
  return { type: 'none' };
1010
1495
  }
1496
+ /**
1497
+ * Lazy initialization of ToolFilterService
1498
+ */
1499
+ getToolFilterService(profileState) {
1500
+ if (!profileState.toolFilterService) {
1501
+ const validator = new RegexValidator();
1502
+ const compiler = new RegexCompiler(validator);
1503
+ const envParser = new EnvConfigParser(compiler);
1504
+ const headerParser = new HeaderConfigParser(compiler);
1505
+ // Create OperationDetector for category filtering (if parser available)
1506
+ let detector;
1507
+ if (profileState.context.parser) {
1508
+ const classifier = new OperationClassifier();
1509
+ const resolver = new OpenAPIOperationResolver(profileState.context.parser);
1510
+ detector = new OperationDetector(classifier, resolver);
1511
+ }
1512
+ profileState.toolFilterService = new ToolFilterService(envParser, headerParser, this.logger, detector);
1513
+ }
1514
+ return profileState.toolFilterService;
1515
+ }
1011
1516
  /**
1012
1517
  * Handle POST requests - Client sending messages to server
1013
1518
  *
@@ -1017,8 +1522,19 @@ export class HttpTransport {
1017
1522
  const startTime = Date.now();
1018
1523
  try {
1019
1524
  this.logger.debug('handlePost called', { method: req.method, path: req.path, sessionId: req.sessionId, accept: req.headers.accept });
1525
+ const profileState = await this.getProfileStateForRequest(req);
1526
+ if (!profileState) {
1527
+ this.respondProfileNotFound(res, req.profileId);
1528
+ return;
1529
+ }
1530
+ const requestProfileId = req.profileId ?? profileState.profileId;
1020
1531
  const sessionId = req.sessionId;
1021
1532
  const body = req.body;
1533
+ const filteringHeader = normalizeFilteringHeaderValue(this.getFilteringHeaderValue(req));
1534
+ const parsedFiltering = filteringHeader ? parseFilteringHeader(filteringHeader) : undefined;
1535
+ const toolFilterHeader = normalizeToolFilterHeaderValue(this.getToolFilterHeaderValue(req));
1536
+ const parsedToolFilter = toolFilterHeader !== undefined ? parseSessionToolFilterHeader(toolFilterHeader) : undefined;
1537
+ const normalizedToolFilterHeader = parsedToolFilter?.normalizedHeader;
1022
1538
  // Validate Accept header per MCP Streamable HTTP specification
1023
1539
  const accept = req.headers.accept || '';
1024
1540
  // POST requests can return either JSON or SSE, so must accept both if specified
@@ -1047,12 +1563,35 @@ export class HttpTransport {
1047
1563
  this.logger.debug('Session validation', { isInitialization, sessionId, bodyMethod: body?.method });
1048
1564
  // Validate session (except for initialization)
1049
1565
  if (!isInitialization && sessionId) {
1050
- const session = this.sessions.get(sessionId);
1566
+ const session = profileState.sessions.get(sessionId);
1051
1567
  if (!session) {
1052
1568
  res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1053
1569
  return;
1054
1570
  }
1055
- this.updateSessionActivity(sessionId);
1571
+ if (filteringHeader !== undefined) {
1572
+ if (!session.filteringHeader || session.filteringHeader !== filteringHeader) {
1573
+ throw new ValidationError('X-Mcp4-Params header mismatch for existing session.');
1574
+ }
1575
+ }
1576
+ if (normalizedToolFilterHeader !== undefined) {
1577
+ if (!session.toolFilterHeader || session.toolFilterHeader !== normalizedToolFilterHeader) {
1578
+ throw new ValidationError(`X-Mcp4-Tools header mismatch for existing session. Expected: '${session.toolFilterHeader ?? ''}', Got: '${normalizedToolFilterHeader}'.`);
1579
+ }
1580
+ }
1581
+ this.updateSessionActivity(profileState, sessionId);
1582
+ const authInfo = this.extractAuthToken(req, profileState);
1583
+ if (authInfo.token && authInfo.type !== 'oauth') {
1584
+ const previousToken = session.authToken;
1585
+ if (!previousToken || previousToken !== authInfo.token) {
1586
+ session.authToken = authInfo.token;
1587
+ this.logger.info('Session auth token updated', {
1588
+ profileId: profileState.profileId,
1589
+ sessionId,
1590
+ authType: authInfo.type,
1591
+ replaced: !!previousToken,
1592
+ });
1593
+ }
1594
+ }
1056
1595
  }
1057
1596
  else if (!isInitialization && !sessionId) {
1058
1597
  this.logger.debug('Session validation failed: non-init request without sessionId');
@@ -1065,7 +1604,7 @@ export class HttpTransport {
1065
1604
  // If only notifications/responses, return 202 Accepted
1066
1605
  if (messageType === 'notification-only' || messageType === 'response-only') {
1067
1606
  if (this.messageHandler) {
1068
- await this.messageHandler(body);
1607
+ await this.messageHandler(body, undefined, requestProfileId);
1069
1608
  }
1070
1609
  res.status(HTTP_STATUS.ACCEPTED).send();
1071
1610
  return;
@@ -1080,14 +1619,14 @@ export class HttpTransport {
1080
1619
  let newSessionId;
1081
1620
  if (isInitialization) {
1082
1621
  // Extract and validate auth token from headers
1083
- const authInfo = this.extractAuthToken(req);
1622
+ const authInfo = this.extractAuthToken(req, profileState);
1084
1623
  this.logger.debug('Auth token extracted', { authType: authInfo?.type, hasToken: !!authInfo?.token });
1085
1624
  // If OAuth is configured, require authentication for initialization
1086
1625
  // This ensures clients like Cursor properly handle OAuth flow
1087
- if (this.oauthProvider && !authInfo.token) {
1626
+ if (profileState.oauthProvider && !authInfo.token) {
1088
1627
  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(' ')}"`);
1628
+ const resourceMetadataUrl = this.getOAuthProtectedResourceUrl(requestProfileId);
1629
+ res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="${profileState.oauthProvider.scopes.join(' ')}"`);
1091
1630
  res.status(HTTP_STATUS.UNAUTHORIZED).json({
1092
1631
  error: 'Unauthorized',
1093
1632
  message: 'Authentication required for OAuth'
@@ -1096,17 +1635,17 @@ export class HttpTransport {
1096
1635
  }
1097
1636
  // Allow initialization without token for non-OAuth scenarios
1098
1637
  // Validate token if auth is configured and token is provided
1099
- if (authInfo && authInfo.token && this.config.authConfigs && this.config.baseUrl) {
1638
+ if (authInfo && authInfo.token && profileState.context.authConfigs && profileState.context.baseUrl) {
1100
1639
  // Find matching auth config based on priority (authConfigs is sorted)
1101
1640
  // For 'bearer' token type, 'oauth' config is also a match
1102
- const authConfig = this.config.authConfigs.find(c => c.type === authInfo.type ||
1641
+ const authConfig = profileState.context.authConfigs.find(c => c.type === authInfo.type ||
1103
1642
  (authInfo.type === 'bearer' && c.type === 'oauth'));
1104
1643
  if (authConfig && authConfig.validation_endpoint) {
1105
1644
  this.logger.info('Validating auth token during initialization', {
1106
1645
  authType: authConfig.type, // Use config type for logging
1107
1646
  endpoint: authConfig.validation_endpoint,
1108
1647
  });
1109
- const isValid = await this.validateAuthToken(authConfig, authInfo.token, this.config.baseUrl);
1648
+ const isValid = await this.validateAuthToken(authConfig, authInfo.token, profileState.context.baseUrl);
1110
1649
  if (!isValid) {
1111
1650
  this.logger.warn('Auth token validation failed during initialization', {
1112
1651
  authType: authInfo.type,
@@ -1126,7 +1665,7 @@ export class HttpTransport {
1126
1665
  let scopes;
1127
1666
  let oauthClientId;
1128
1667
  if (authInfo.token && (authInfo.type === 'oauth' || authInfo.type === 'bearer')) {
1129
- const tokenData = this.oauthTokensByAccessToken.get(authInfo.token);
1668
+ const tokenData = profileState.oauthTokensByAccessToken.get(authInfo.token);
1130
1669
  if (tokenData) {
1131
1670
  refreshToken = tokenData.refreshToken;
1132
1671
  accessTokenExpiresAt = tokenData.expiresAt;
@@ -1144,21 +1683,21 @@ export class HttpTransport {
1144
1683
  });
1145
1684
  }
1146
1685
  }
1147
- newSessionId = this.createSession(authInfo.token, refreshToken, accessTokenExpiresAt, scopes, oauthClientId);
1686
+ newSessionId = this.createSession(profileState, authInfo.token, refreshToken, accessTokenExpiresAt, scopes, oauthClientId, parsedFiltering?.filtering, parsedFiltering?.normalizedHeader, parsedToolFilter, normalizedToolFilterHeader);
1148
1687
  }
1149
1688
  this.logger.debug('Calling messageHandler', { body, sessionId: isInitialization ? newSessionId : sessionId });
1150
- const response = await this.messageHandler(body, isInitialization ? newSessionId : sessionId);
1689
+ const response = await this.messageHandler(body, isInitialization ? newSessionId : sessionId, requestProfileId);
1151
1690
  this.logger.debug('MessageHandler response', { response });
1152
1691
  // Debug: Check OAuth conditions
1153
1692
  this.logger.debug('Checking OAuth conditions', {
1154
1693
  responseError: response.error,
1155
- hasOAuthProvider: !!this.oauthProvider,
1156
- oauthProviderType: typeof this.oauthProvider
1694
+ hasOAuthProvider: !!profileState.oauthProvider,
1695
+ oauthProviderType: typeof profileState.oauthProvider
1157
1696
  });
1158
1697
  // Check if response contains OAuth error and add WWW-Authenticate header
1159
1698
  const responseObj = response;
1160
1699
  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;
1700
+ const resourceMetadataUrl = this.getOAuthProtectedResourceUrl(requestProfileId);
1162
1701
  res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadataUrl}", scope="api"`);
1163
1702
  res.status(HTTP_STATUS.UNAUTHORIZED); // Set 401 status for OAuth errors
1164
1703
  }
@@ -1175,6 +1714,8 @@ export class HttpTransport {
1175
1714
  if (newSessionId) {
1176
1715
  res.setHeader('Mcp-Session-Id', newSessionId);
1177
1716
  }
1717
+ // Security: Prevent caching of sensitive API responses
1718
+ res.setHeader('Cache-Control', 'no-store');
1178
1719
  this.logger.debug('Sending JSON response', { response, newSessionId });
1179
1720
  res.json(response);
1180
1721
  }
@@ -1185,6 +1726,7 @@ export class HttpTransport {
1185
1726
  catch (error) {
1186
1727
  const correlationId = generateCorrelationId();
1187
1728
  this.logger.error('POST request error', error, { correlationId });
1729
+ res.setHeader('Cache-Control', 'no-store');
1188
1730
  let status = 500;
1189
1731
  let errorLabel = 'Internal Server Error';
1190
1732
  let message = `Internal error (correlation ID: ${correlationId})`;
@@ -1231,6 +1773,11 @@ export class HttpTransport {
1231
1773
  async handleGet(req, res) {
1232
1774
  const startTime = Date.now();
1233
1775
  try {
1776
+ const profileState = await this.getProfileStateForRequest(req);
1777
+ if (!profileState) {
1778
+ this.respondProfileNotFound(res, req.profileId);
1779
+ return;
1780
+ }
1234
1781
  const sessionId = req.sessionId;
1235
1782
  const lastEventId = req.headers['last-event-id'];
1236
1783
  // Validate Accept header
@@ -1244,14 +1791,14 @@ export class HttpTransport {
1244
1791
  res.status(HTTP_STATUS.BAD_REQUEST).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
1245
1792
  return;
1246
1793
  }
1247
- const session = this.sessions.get(sessionId);
1794
+ const session = profileState.sessions.get(sessionId);
1248
1795
  if (!session) {
1249
1796
  res.status(HTTP_STATUS.NOT_FOUND).json({ error: 'Not Found', message: 'Session not found or expired' });
1250
1797
  return;
1251
1798
  }
1252
- this.updateSessionActivity(sessionId);
1799
+ this.updateSessionActivity(profileState, sessionId);
1253
1800
  // Start SSE stream
1254
- this.startSSEStream(res, sessionId, lastEventId);
1801
+ this.startSSEStream(res, sessionId, lastEventId, profileState);
1255
1802
  // Record metrics for successful SSE start
1256
1803
  if (this.metrics) {
1257
1804
  const duration = (Date.now() - startTime) / 1000;
@@ -1259,10 +1806,16 @@ export class HttpTransport {
1259
1806
  }
1260
1807
  }
1261
1808
  catch (error) {
1262
- this.logger.error('GET request error', error);
1809
+ const correlationId = generateCorrelationId();
1810
+ this.logger.error('GET request error', error, { correlationId });
1263
1811
  const status = 500;
1264
1812
  if (!res.headersSent) {
1265
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
1813
+ res.setHeader('Cache-Control', 'no-store');
1814
+ res.status(status).json({
1815
+ error: 'Internal Server Error',
1816
+ message: `Internal error (correlation ID: ${correlationId})`,
1817
+ correlationId
1818
+ });
1266
1819
  }
1267
1820
  // Record error metrics
1268
1821
  if (this.metrics) {
@@ -1279,6 +1832,11 @@ export class HttpTransport {
1279
1832
  handleDelete(req, res) {
1280
1833
  const startTime = Date.now();
1281
1834
  const sessionId = req.sessionId;
1835
+ const profileId = this.getProfileIdForRequest(req);
1836
+ if (!profileId) {
1837
+ this.respondProfileNotFound(res, req.profileId);
1838
+ return;
1839
+ }
1282
1840
  if (!sessionId) {
1283
1841
  const status = 400;
1284
1842
  res.status(status).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
@@ -1288,7 +1846,12 @@ export class HttpTransport {
1288
1846
  }
1289
1847
  return;
1290
1848
  }
1291
- const session = this.sessions.get(sessionId);
1849
+ const profileState = this.profileStates.get(profileId);
1850
+ if (!profileState) {
1851
+ this.respondProfileNotFound(res, profileId);
1852
+ return;
1853
+ }
1854
+ const session = profileState.sessions.get(sessionId);
1292
1855
  if (!session) {
1293
1856
  const status = 404;
1294
1857
  res.status(status).json({ error: 'Not Found', message: 'Session not found' });
@@ -1298,7 +1861,7 @@ export class HttpTransport {
1298
1861
  }
1299
1862
  return;
1300
1863
  }
1301
- this.destroySession(sessionId);
1864
+ this.destroySession(profileState, sessionId);
1302
1865
  const status = 204;
1303
1866
  res.status(status).send();
1304
1867
  if (this.metrics) {
@@ -1330,12 +1893,12 @@ export class HttpTransport {
1330
1893
  *
1331
1894
  * Why: Allows server to send requests/notifications to client
1332
1895
  */
1333
- startSSEStream(res, sessionId, lastEventId) {
1896
+ startSSEStream(res, sessionId, lastEventId, profileState) {
1334
1897
  res.setHeader('Content-Type', MIME_TYPES.EVENT_STREAM);
1335
1898
  res.setHeader('Cache-Control', 'no-cache');
1336
1899
  res.setHeader('Connection', 'keep-alive');
1337
1900
  const streamId = crypto.randomBytes(16).toString('hex');
1338
- const session = this.sessions.get(sessionId);
1901
+ const session = profileState.sessions.get(sessionId);
1339
1902
  const streamState = {
1340
1903
  streamId,
1341
1904
  lastEventId: lastEventId ? parseInt(lastEventId, 10) : 0,
@@ -1385,10 +1948,10 @@ export class HttpTransport {
1385
1948
  *
1386
1949
  * Why: Server-initiated requests/notifications
1387
1950
  */
1388
- sendToClient(sessionId, message) {
1389
- const session = this.sessions.get(sessionId);
1951
+ sendToClient(profileId, sessionId, message) {
1952
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
1390
1953
  if (!session) {
1391
- this.logger.warn('Cannot send to client: session not found', { sessionId });
1954
+ this.logger.warn('Cannot send to client: session not found', { profileId, sessionId });
1392
1955
  return;
1393
1956
  }
1394
1957
  const eventId = Date.now();
@@ -1437,12 +2000,38 @@ export class HttpTransport {
1437
2000
  }
1438
2001
  return 'unknown';
1439
2002
  }
2003
+ getFilteringHeaderValue(req) {
2004
+ const headerValue = req.headers['x-mcp4-params'];
2005
+ if (Array.isArray(headerValue)) {
2006
+ if (headerValue.length === 0) {
2007
+ return undefined;
2008
+ }
2009
+ if (headerValue.length > 1) {
2010
+ throw new ValidationError('Invalid X-Mcp4-Params header. Expected comma-separated key=value pairs.');
2011
+ }
2012
+ return headerValue[0];
2013
+ }
2014
+ return headerValue;
2015
+ }
2016
+ getToolFilterHeaderValue(req) {
2017
+ const headerValue = req.headers['x-mcp4-tools'];
2018
+ if (Array.isArray(headerValue)) {
2019
+ if (headerValue.length === 0) {
2020
+ return undefined;
2021
+ }
2022
+ if (headerValue.length > 1) {
2023
+ throw new ValidationError('Invalid X-Mcp4-Tools header. Expected comma-separated tool names.');
2024
+ }
2025
+ return headerValue[0];
2026
+ }
2027
+ return headerValue;
2028
+ }
1440
2029
  /**
1441
2030
  * Create new session
1442
2031
  *
1443
2032
  * Why: Stateful sessions for MCP protocol
1444
2033
  */
1445
- createSession(authToken, refreshToken, accessTokenExpiresAt, scopes, oauthClientId) {
2034
+ createSession(profileState, authToken, refreshToken, accessTokenExpiresAt, scopes, oauthClientId, filtering, filteringHeader, toolFilterRequest, toolFilterHeader) {
1446
2035
  // Validate token if provided (defense in depth)
1447
2036
  if (authToken) {
1448
2037
  this.validateToken(authToken, 'Session auth token');
@@ -1458,9 +2047,14 @@ export class HttpTransport {
1458
2047
  accessTokenExpiresAt,
1459
2048
  scopes,
1460
2049
  oauthClientId,
2050
+ filtering,
2051
+ filteringHeader,
2052
+ toolFilterRequest,
2053
+ toolFilterHeader,
1461
2054
  };
1462
- this.sessions.set(sessionId, session);
2055
+ profileState.sessions.set(sessionId, session);
1463
2056
  this.logger.info('Session created', {
2057
+ profileId: profileState.profileId,
1464
2058
  sessionId,
1465
2059
  hasAuthToken: !!authToken,
1466
2060
  hasRefreshToken: !!refreshToken,
@@ -1475,8 +2069,8 @@ export class HttpTransport {
1475
2069
  /**
1476
2070
  * Update session activity timestamp
1477
2071
  */
1478
- updateSessionActivity(sessionId) {
1479
- const session = this.sessions.get(sessionId);
2072
+ updateSessionActivity(profileState, sessionId) {
2073
+ const session = profileState.sessions.get(sessionId);
1480
2074
  if (session) {
1481
2075
  session.lastActivityAt = Date.now();
1482
2076
  }
@@ -1486,8 +2080,8 @@ export class HttpTransport {
1486
2080
  *
1487
2081
  * Why: Free memory, close streams
1488
2082
  */
1489
- destroySession(sessionId) {
1490
- const session = this.sessions.get(sessionId);
2083
+ destroySession(profileState, sessionId) {
2084
+ const session = profileState.sessions.get(sessionId);
1491
2085
  if (session) {
1492
2086
  // Close all active SSE streams
1493
2087
  for (const [, streamState] of session.sseStreams) {
@@ -1506,15 +2100,16 @@ export class HttpTransport {
1506
2100
  session.sseStreams.clear();
1507
2101
  // Clean up OAuth token from map if present
1508
2102
  if (session.authToken) {
1509
- this.oauthTokensByAccessToken.delete(session.authToken);
2103
+ profileState.oauthTokensByAccessToken.delete(session.authToken);
1510
2104
  }
1511
- this.sessions.delete(sessionId);
1512
- this.logger.info('Session destroyed', { sessionId });
2105
+ profileState.sessions.delete(sessionId);
2106
+ this.logger.info('Session destroyed', { profileId: profileState.profileId, sessionId });
1513
2107
  // Notify session destruction listeners (for cleanup in MCPServer)
1514
- this.notifySessionDestroyed(sessionId);
2108
+ this.notifySessionDestroyed(profileState.profileId, sessionId);
1515
2109
  // Record metrics
1516
2110
  if (this.metrics) {
1517
2111
  this.metrics.recordSessionDestroyed();
2112
+ this.metrics.clearToolsSession(sessionId);
1518
2113
  }
1519
2114
  }
1520
2115
  }
@@ -1529,10 +2124,10 @@ export class HttpTransport {
1529
2124
  /**
1530
2125
  * Notify all listeners about session destruction
1531
2126
  */
1532
- notifySessionDestroyed(sessionId) {
2127
+ notifySessionDestroyed(profileId, sessionId) {
1533
2128
  for (const listener of this.sessionDestroyedListeners) {
1534
2129
  try {
1535
- listener(sessionId);
2130
+ listener(profileId, sessionId);
1536
2131
  }
1537
2132
  catch (error) {
1538
2133
  this.logger.error('Session destroyed listener error', error);
@@ -1545,7 +2140,7 @@ export class HttpTransport {
1545
2140
  * Why: Bridge between /oauth/token endpoint (where we see OAuthTokens)
1546
2141
  * and session initialization (where we only see access token in Authorization header)
1547
2142
  */
1548
- storeOAuthTokens(tokens, clientId, scopes) {
2143
+ storeOAuthTokens(profileState, tokens, clientId, scopes) {
1549
2144
  if (!tokens.access_token) {
1550
2145
  this.logger.warn('OAuth tokens missing access_token, skipping storage');
1551
2146
  return;
@@ -1553,13 +2148,14 @@ export class HttpTransport {
1553
2148
  const expiresAt = tokens.expires_in
1554
2149
  ? Date.now() + tokens.expires_in * 1000
1555
2150
  : undefined;
1556
- this.oauthTokensByAccessToken.set(tokens.access_token, {
2151
+ profileState.oauthTokensByAccessToken.set(tokens.access_token, {
1557
2152
  refreshToken: tokens.refresh_token,
1558
2153
  expiresAt,
1559
2154
  clientId,
1560
2155
  scopes,
1561
2156
  });
1562
2157
  this.logger.debug('Stored OAuth tokens', {
2158
+ profileId: profileState.profileId,
1563
2159
  hasRefreshToken: !!tokens.refresh_token,
1564
2160
  expiresAt,
1565
2161
  clientId,
@@ -1580,25 +2176,33 @@ export class HttpTransport {
1580
2176
  // Default OAuth session timeout: 24 hours (or configurable)
1581
2177
  const oauthSessionTimeoutMs = this.config.oauthSessionTimeoutMs
1582
2178
  ?? (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)
2179
+ for (const profileState of this.profileStates.values()) {
2180
+ // Cleanup OAuth provider resources (states, codes, tokens)
2181
+ if (profileState.oauthProvider) {
2182
+ profileState.oauthProvider.cleanup();
1592
2183
  }
1593
- else {
1594
- // Non-OAuth sessions: use standard timeout
1595
- if (age > this.config.sessionTimeoutMs) {
1596
- expiredSessions.push(sessionId);
2184
+ for (const [sessionId, session] of profileState.sessions) {
2185
+ const age = now - session.lastActivityAt;
2186
+ // OAuth sessions with refresh tokens: use extended timeout or never expire
2187
+ if (session.refreshToken) {
2188
+ // If oauthSessionTimeoutMs is 0 or negative, never expire OAuth sessions
2189
+ if (oauthSessionTimeoutMs > 0 && age > oauthSessionTimeoutMs) {
2190
+ expiredSessions.push({ profileId: profileState.profileId, sessionId });
2191
+ }
2192
+ }
2193
+ else {
2194
+ // Non-OAuth sessions: use standard timeout
2195
+ if (age > this.config.sessionTimeoutMs) {
2196
+ expiredSessions.push({ profileId: profileState.profileId, sessionId });
2197
+ }
1597
2198
  }
1598
2199
  }
1599
2200
  }
1600
- for (const sessionId of expiredSessions) {
1601
- this.destroySession(sessionId);
2201
+ for (const entry of expiredSessions) {
2202
+ const state = this.profileStates.get(entry.profileId);
2203
+ if (state) {
2204
+ this.destroySession(state, entry.sessionId);
2205
+ }
1602
2206
  }
1603
2207
  if (expiredSessions.length > 0) {
1604
2208
  this.logger.info('Cleaned up expired sessions', { count: expiredSessions.length });
@@ -1609,18 +2213,71 @@ export class HttpTransport {
1609
2213
  *
1610
2214
  * Why public: Allows MCPServer to securely access session tokens without breaking encapsulation
1611
2215
  */
1612
- getSessionToken(sessionId) {
1613
- const session = this.sessions.get(sessionId);
2216
+ getSessionToken(profileId, sessionId) {
2217
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
1614
2218
  return session?.authToken;
1615
2219
  }
2220
+ getSessionFiltering(profileId, sessionId) {
2221
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2222
+ return session?.filtering;
2223
+ }
2224
+ getSessionFilteringHeader(profileId, sessionId) {
2225
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2226
+ return session?.filteringHeader;
2227
+ }
2228
+ getSessionToolFilterRequest(profileId, sessionId) {
2229
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2230
+ return session?.toolFilterRequest;
2231
+ }
2232
+ getSessionToolFilter(profileId, sessionId) {
2233
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2234
+ return session?.toolFilter;
2235
+ }
2236
+ getSessionToolFilterHeader(profileId, sessionId) {
2237
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2238
+ return session?.toolFilterHeader;
2239
+ }
2240
+ setSessionToolFilter(profileId, sessionId, toolFilter) {
2241
+ const session = this.profileStates.get(profileId)?.sessions.get(sessionId);
2242
+ if (!session) {
2243
+ return;
2244
+ }
2245
+ session.toolFilter = toolFilter;
2246
+ }
2247
+ recordGlobalToolFilterMetrics(summary) {
2248
+ if (!this.metrics) {
2249
+ return;
2250
+ }
2251
+ this.metrics.recordToolsTotal('profile', summary.originalCount);
2252
+ this.metrics.recordToolsFiltered('global_env', 'allowed', summary.allowedCount);
2253
+ this.metrics.recordToolsFiltered('global_env', 'denied', summary.removedCount);
2254
+ for (const [type, count] of Object.entries(summary.patternCounts)) {
2255
+ this.metrics.recordToolFilterPatternCount(type, count);
2256
+ }
2257
+ }
2258
+ recordSessionToolFilterMetrics(sessionId, allowedCount, request) {
2259
+ if (!this.metrics) {
2260
+ return;
2261
+ }
2262
+ this.metrics.recordToolsSession(sessionId, allowedCount);
2263
+ this.metrics.recordToolFilterPatternCount('session_allow_list', request.exactNames.size);
2264
+ this.metrics.recordToolFilterPatternCount('session_allow_regex', request.regexPatterns.length);
2265
+ }
2266
+ recordToolFilterRejection(tool, source) {
2267
+ if (!this.metrics) {
2268
+ return;
2269
+ }
2270
+ this.metrics.recordToolFilterRejection(tool, source);
2271
+ }
1616
2272
  /**
1617
2273
  * Ensure session has a valid access token, refreshing if necessary
1618
2274
  *
1619
2275
  * Why: Transparently refresh expired OAuth tokens before making API calls
1620
2276
  * Returns true if token is valid (or was successfully refreshed), false otherwise
1621
2277
  */
1622
- async ensureValidSessionToken(sessionId) {
1623
- const session = this.sessions.get(sessionId);
2278
+ async ensureValidSessionToken(profileId, sessionId) {
2279
+ const profileState = this.profileStates.get(profileId);
2280
+ const session = profileState?.sessions.get(sessionId);
1624
2281
  if (!session) {
1625
2282
  return false;
1626
2283
  }
@@ -1634,11 +2291,12 @@ export class HttpTransport {
1634
2291
  // If token is expired or about to expire, refresh it
1635
2292
  if (timeUntilExpiration <= refreshThresholdMs) {
1636
2293
  this.logger.debug('Access token expired or expiring soon, refreshing', {
2294
+ profileId,
1637
2295
  sessionId,
1638
2296
  expiresAt: new Date(session.accessTokenExpiresAt).toISOString(),
1639
2297
  timeUntilExpiration,
1640
2298
  });
1641
- return await this.refreshAccessToken(sessionId);
2299
+ return await this.refreshAccessToken(profileId, sessionId);
1642
2300
  }
1643
2301
  return true;
1644
2302
  }
@@ -1648,14 +2306,16 @@ export class HttpTransport {
1648
2306
  * Why: Automatically renew expired OAuth access tokens without user intervention
1649
2307
  * Returns true on success, false on failure
1650
2308
  */
1651
- async refreshAccessToken(sessionId) {
1652
- const session = this.sessions.get(sessionId);
1653
- if (!session || !session.refreshToken || !this.oauthProvider) {
2309
+ async refreshAccessToken(profileId, sessionId) {
2310
+ const profileState = this.profileStates.get(profileId);
2311
+ const session = profileState?.sessions.get(sessionId);
2312
+ if (!profileState || !session || !session.refreshToken || !profileState.oauthProvider) {
1654
2313
  this.logger.warn('Cannot refresh token: missing session, refreshToken, or OAuth provider', {
2314
+ profileId,
1655
2315
  sessionId,
1656
2316
  hasSession: !!session,
1657
2317
  hasRefreshToken: !!session?.refreshToken,
1658
- hasOAuthProvider: !!this.oauthProvider,
2318
+ hasOAuthProvider: !!profileState?.oauthProvider,
1659
2319
  });
1660
2320
  return false;
1661
2321
  }
@@ -1664,32 +2324,33 @@ export class HttpTransport {
1664
2324
  // Try to find client by clientId stored in session, or use default client
1665
2325
  let client;
1666
2326
  if (session.oauthClientId) {
1667
- await this.oauthProvider.ensureEndpointsInitialized();
1668
- client = await this.oauthProvider.clientsStore.getClient(session.oauthClientId);
2327
+ await profileState.oauthProvider.ensureEndpointsInitialized();
2328
+ client = await profileState.oauthProvider.clientsStore.getClient(session.oauthClientId);
1669
2329
  }
1670
2330
  // Fallback to default client from config if session client not found
1671
- if (!client && this.oauthProvider) {
1672
- await this.oauthProvider.ensureEndpointsInitialized();
2331
+ if (!client && profileState.oauthProvider) {
2332
+ await profileState.oauthProvider.ensureEndpointsInitialized();
1673
2333
  // Try common client IDs
1674
2334
  const defaultClientIds = ['mcp-proxy-client'];
1675
- if (this.config.oauthConfig?.client_id) {
1676
- defaultClientIds.unshift(this.config.oauthConfig.client_id);
2335
+ if (profileState.context.oauthConfig?.client_id) {
2336
+ defaultClientIds.unshift(profileState.context.oauthConfig.client_id);
1677
2337
  }
1678
2338
  for (const clientId of defaultClientIds) {
1679
- client = await this.oauthProvider.clientsStore.getClient(clientId);
2339
+ client = await profileState.oauthProvider.clientsStore.getClient(clientId);
1680
2340
  if (client)
1681
2341
  break;
1682
2342
  }
1683
2343
  }
1684
2344
  if (!client) {
1685
2345
  this.logger.error('Cannot refresh token: OAuth client not found', undefined, {
2346
+ profileId,
1686
2347
  sessionId,
1687
2348
  oauthClientId: session.oauthClientId,
1688
2349
  });
1689
2350
  return false;
1690
2351
  }
1691
2352
  // Exchange refresh token for new tokens
1692
- const tokens = await this.oauthProvider.exchangeRefreshToken(client, session.refreshToken, session.scopes);
2353
+ const tokens = await profileState.oauthProvider.exchangeRefreshToken(client, session.refreshToken, session.scopes);
1693
2354
  // Update session with new tokens
1694
2355
  const oldAccessToken = session.authToken;
1695
2356
  session.authToken = tokens.access_token;
@@ -1699,10 +2360,11 @@ export class HttpTransport {
1699
2360
  : undefined;
1700
2361
  // Update token map: remove old token, add new one
1701
2362
  if (oldAccessToken) {
1702
- this.oauthTokensByAccessToken.delete(oldAccessToken);
2363
+ profileState.oauthTokensByAccessToken.delete(oldAccessToken);
1703
2364
  }
1704
- this.storeOAuthTokens(tokens, client.client_id, session.scopes || []);
2365
+ this.storeOAuthTokens(profileState, tokens, client.client_id, session.scopes || []);
1705
2366
  this.logger.info('Access token refreshed successfully', {
2367
+ profileId,
1706
2368
  sessionId,
1707
2369
  newExpiresAt: session.accessTokenExpiresAt ? new Date(session.accessTokenExpiresAt).toISOString() : undefined,
1708
2370
  });
@@ -1710,6 +2372,7 @@ export class HttpTransport {
1710
2372
  }
1711
2373
  catch (error) {
1712
2374
  this.logger.error('Token refresh failed', error instanceof Error ? error : new Error(String(error)), {
2375
+ profileId,
1713
2376
  sessionId,
1714
2377
  });
1715
2378
  return false;
@@ -1724,41 +2387,49 @@ export class HttpTransport {
1724
2387
  /**
1725
2388
  * Check if OAuth provider is configured
1726
2389
  */
1727
- hasOAuthProvider() {
1728
- return this.oauthProvider !== null;
2390
+ hasOAuthProvider(profileId) {
2391
+ if (!profileId) {
2392
+ const defaultProfileId = this.getDefaultProfileId();
2393
+ if (!defaultProfileId) {
2394
+ return false;
2395
+ }
2396
+ const state = this.profileStates.get(defaultProfileId);
2397
+ return state ? state.oauthProvider !== null : false;
2398
+ }
2399
+ const state = this.profileStates.get(profileId);
2400
+ return state ? state.oauthProvider !== null : false;
1729
2401
  }
1730
2402
  /**
1731
2403
  * Get server URL
1732
2404
  */
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}`;
2405
+ getServerUrl(profileId) {
2406
+ return this.getServerOrigin(profileId);
1750
2407
  }
1751
2408
  /**
1752
2409
  * Get OAuth authorization URL
1753
2410
  */
1754
- getOAuthAuthorizationUrl() {
1755
- return this.oauthProvider?.authorizationEndpoint || '';
2411
+ getOAuthAuthorizationUrl(profileId) {
2412
+ if (!profileId) {
2413
+ const defaultProfileId = this.getDefaultProfileId();
2414
+ if (!defaultProfileId) {
2415
+ return '';
2416
+ }
2417
+ return this.profileStates.get(defaultProfileId)?.oauthProvider?.authorizationEndpoint || '';
2418
+ }
2419
+ return this.profileStates.get(profileId)?.oauthProvider?.authorizationEndpoint || '';
1756
2420
  }
1757
2421
  /**
1758
2422
  * Get OAuth scopes
1759
2423
  */
1760
- getOAuthScopes() {
1761
- return this.oauthProvider?.scopes || [];
2424
+ getOAuthScopes(profileId) {
2425
+ if (!profileId) {
2426
+ const defaultProfileId = this.getDefaultProfileId();
2427
+ if (!defaultProfileId) {
2428
+ return [];
2429
+ }
2430
+ return this.profileStates.get(defaultProfileId)?.oauthProvider?.scopes || [];
2431
+ }
2432
+ return this.profileStates.get(profileId)?.oauthProvider?.scopes || [];
1762
2433
  }
1763
2434
  /**
1764
2435
  * Start HTTP server
@@ -1829,8 +2500,10 @@ export class HttpTransport {
1829
2500
  this.cleanupInterval = null;
1830
2501
  }
1831
2502
  // Destroy all sessions
1832
- for (const sessionId of this.sessions.keys()) {
1833
- this.destroySession(sessionId);
2503
+ for (const profileState of this.profileStates.values()) {
2504
+ for (const sessionId of profileState.sessions.keys()) {
2505
+ this.destroySession(profileState, sessionId);
2506
+ }
1834
2507
  }
1835
2508
  if (this.server) {
1836
2509
  return new Promise((resolve, reject) => {