mcp4openapi 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +143 -63
  2. package/dist/scripts/validate-profile.js +3 -3
  3. package/dist/scripts/validate-profile.js.map +1 -1
  4. package/dist/src/argument-normalizer.d.ts +5 -0
  5. package/dist/src/argument-normalizer.d.ts.map +1 -0
  6. package/dist/src/argument-normalizer.js +61 -0
  7. package/dist/src/argument-normalizer.js.map +1 -0
  8. package/dist/src/auth/oauth-provider.d.ts +131 -0
  9. package/dist/src/auth/oauth-provider.d.ts.map +1 -0
  10. package/dist/src/auth/oauth-provider.js +839 -0
  11. package/dist/src/auth/oauth-provider.js.map +1 -0
  12. package/dist/src/cli-config.d.ts +9 -0
  13. package/dist/src/cli-config.d.ts.map +1 -0
  14. package/dist/src/cli-config.js +111 -0
  15. package/dist/src/cli-config.js.map +1 -0
  16. package/dist/src/core/cli-config.d.ts +9 -0
  17. package/dist/src/core/cli-config.d.ts.map +1 -0
  18. package/dist/src/core/cli-config.js +125 -0
  19. package/dist/src/core/cli-config.js.map +1 -0
  20. package/dist/src/core/constants.d.ts +86 -0
  21. package/dist/src/core/constants.d.ts.map +1 -0
  22. package/dist/src/core/constants.js +86 -0
  23. package/dist/src/core/constants.js.map +1 -0
  24. package/dist/src/core/errors.d.ts +59 -0
  25. package/dist/src/core/errors.d.ts.map +1 -0
  26. package/dist/src/core/errors.js +119 -0
  27. package/dist/src/core/errors.js.map +1 -0
  28. package/dist/src/core/filtering.d.ts +19 -0
  29. package/dist/src/core/filtering.d.ts.map +1 -0
  30. package/dist/src/core/filtering.js +292 -0
  31. package/dist/src/core/filtering.js.map +1 -0
  32. package/dist/src/core/index.d.ts +26 -0
  33. package/dist/src/core/index.d.ts.map +1 -0
  34. package/dist/src/core/index.js +276 -0
  35. package/dist/src/core/index.js.map +1 -0
  36. package/dist/src/core/lib.d.ts +8 -0
  37. package/dist/src/core/lib.d.ts.map +1 -0
  38. package/dist/src/core/lib.js +7 -0
  39. package/dist/src/core/lib.js.map +1 -0
  40. package/dist/src/core/logger.d.ts +59 -0
  41. package/dist/src/core/logger.d.ts.map +1 -0
  42. package/dist/src/core/logger.js +197 -0
  43. package/dist/src/core/logger.js.map +1 -0
  44. package/dist/src/core/metrics.d.ts +97 -0
  45. package/dist/src/core/metrics.d.ts.map +1 -0
  46. package/dist/src/core/metrics.js +273 -0
  47. package/dist/src/core/metrics.js.map +1 -0
  48. package/dist/src/core/naming-warnings.d.ts +23 -0
  49. package/dist/src/core/naming-warnings.d.ts.map +1 -0
  50. package/dist/src/core/naming-warnings.js +83 -0
  51. package/dist/src/core/naming-warnings.js.map +1 -0
  52. package/dist/src/core/naming.d.ts +58 -0
  53. package/dist/src/core/naming.d.ts.map +1 -0
  54. package/dist/src/core/naming.js +510 -0
  55. package/dist/src/core/naming.js.map +1 -0
  56. package/dist/src/errors.d.ts +6 -0
  57. package/dist/src/errors.d.ts.map +1 -1
  58. package/dist/src/errors.js +15 -6
  59. package/dist/src/errors.js.map +1 -1
  60. package/dist/src/filtering.d.ts +19 -0
  61. package/dist/src/filtering.d.ts.map +1 -0
  62. package/dist/src/filtering.js +292 -0
  63. package/dist/src/filtering.js.map +1 -0
  64. package/dist/src/generated-schemas.d.ts +290 -79
  65. package/dist/src/generated-schemas.d.ts.map +1 -1
  66. package/dist/src/generated-schemas.js +17 -2
  67. package/dist/src/generated-schemas.js.map +1 -1
  68. package/dist/src/http-transport-config.d.ts +6 -0
  69. package/dist/src/http-transport-config.d.ts.map +1 -0
  70. package/dist/src/http-transport-config.js +47 -0
  71. package/dist/src/http-transport-config.js.map +1 -0
  72. package/dist/src/http-transport.d.ts +63 -13
  73. package/dist/src/http-transport.d.ts.map +1 -1
  74. package/dist/src/http-transport.js +1045 -482
  75. package/dist/src/http-transport.js.map +1 -1
  76. package/dist/src/index.d.ts +1 -6
  77. package/dist/src/index.d.ts.map +1 -1
  78. package/dist/src/index.js +1 -170
  79. package/dist/src/index.js.map +1 -1
  80. package/dist/src/interceptors.d.ts +1 -0
  81. package/dist/src/interceptors.d.ts.map +1 -1
  82. package/dist/src/interceptors.js +73 -63
  83. package/dist/src/interceptors.js.map +1 -1
  84. package/dist/src/lib.d.ts +1 -7
  85. package/dist/src/lib.d.ts.map +1 -1
  86. package/dist/src/lib.js +1 -6
  87. package/dist/src/lib.js.map +1 -1
  88. package/dist/src/logger.d.ts +5 -0
  89. package/dist/src/logger.d.ts.map +1 -1
  90. package/dist/src/logger.js +9 -1
  91. package/dist/src/logger.js.map +1 -1
  92. package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
  93. package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
  94. package/dist/src/mcp/mcp-server-manager.js +38 -0
  95. package/dist/src/mcp/mcp-server-manager.js.map +1 -0
  96. package/dist/src/mcp/mcp-server.d.ts +205 -0
  97. package/dist/src/mcp/mcp-server.d.ts.map +1 -0
  98. package/dist/src/mcp/mcp-server.js +1473 -0
  99. package/dist/src/mcp/mcp-server.js.map +1 -0
  100. package/dist/src/mcp-server-manager.d.ts +20 -0
  101. package/dist/src/mcp-server-manager.d.ts.map +1 -0
  102. package/dist/src/mcp-server-manager.js +38 -0
  103. package/dist/src/mcp-server-manager.js.map +1 -0
  104. package/dist/src/mcp-server.d.ts +28 -0
  105. package/dist/src/mcp-server.d.ts.map +1 -1
  106. package/dist/src/mcp-server.js +406 -109
  107. package/dist/src/mcp-server.js.map +1 -1
  108. package/dist/src/metrics.d.ts +11 -0
  109. package/dist/src/metrics.d.ts.map +1 -1
  110. package/dist/src/metrics.js +61 -0
  111. package/dist/src/metrics.js.map +1 -1
  112. package/dist/src/oauth-provider.d.ts +5 -0
  113. package/dist/src/oauth-provider.d.ts.map +1 -1
  114. package/dist/src/oauth-provider.js +29 -1
  115. package/dist/src/oauth-provider.js.map +1 -1
  116. package/dist/src/openapi/openapi-parser.d.ts +70 -0
  117. package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
  118. package/dist/src/openapi/openapi-parser.js +458 -0
  119. package/dist/src/openapi/openapi-parser.js.map +1 -0
  120. package/dist/src/profile/profile-loader.d.ts +78 -0
  121. package/dist/src/profile/profile-loader.d.ts.map +1 -0
  122. package/dist/src/profile/profile-loader.js +490 -0
  123. package/dist/src/profile/profile-loader.js.map +1 -0
  124. package/dist/src/profile/profile-registry.d.ts +19 -0
  125. package/dist/src/profile/profile-registry.d.ts.map +1 -0
  126. package/dist/src/profile/profile-registry.js +43 -0
  127. package/dist/src/profile/profile-registry.js.map +1 -0
  128. package/dist/src/profile/profile-resolver.d.ts +41 -0
  129. package/dist/src/profile/profile-resolver.d.ts.map +1 -0
  130. package/dist/src/profile/profile-resolver.js +324 -0
  131. package/dist/src/profile/profile-resolver.js.map +1 -0
  132. package/dist/src/profile/startup-profile.d.ts +17 -0
  133. package/dist/src/profile/startup-profile.d.ts.map +1 -0
  134. package/dist/src/profile/startup-profile.js +30 -0
  135. package/dist/src/profile/startup-profile.js.map +1 -0
  136. package/dist/src/profile/startup-validation.d.ts +11 -0
  137. package/dist/src/profile/startup-validation.d.ts.map +1 -0
  138. package/dist/src/profile/startup-validation.js +21 -0
  139. package/dist/src/profile/startup-validation.js.map +1 -0
  140. package/dist/src/profile-loader.d.ts +1 -0
  141. package/dist/src/profile-loader.d.ts.map +1 -1
  142. package/dist/src/profile-loader.js +14 -3
  143. package/dist/src/profile-loader.js.map +1 -1
  144. package/dist/src/profile-registry.d.ts +18 -0
  145. package/dist/src/profile-registry.d.ts.map +1 -0
  146. package/dist/src/profile-registry.js +26 -0
  147. package/dist/src/profile-registry.js.map +1 -0
  148. package/dist/src/profile-resolver.d.ts +19 -0
  149. package/dist/src/profile-resolver.d.ts.map +1 -0
  150. package/dist/src/profile-resolver.js +167 -0
  151. package/dist/src/profile-resolver.js.map +1 -0
  152. package/dist/src/proxy-executor.d.ts.map +1 -1
  153. package/dist/src/proxy-executor.js +7 -0
  154. package/dist/src/proxy-executor.js.map +1 -1
  155. package/dist/src/startup-profile.d.ts +17 -0
  156. package/dist/src/startup-profile.d.ts.map +1 -0
  157. package/dist/src/startup-profile.js +30 -0
  158. package/dist/src/startup-profile.js.map +1 -0
  159. package/dist/src/startup-validation.d.ts +11 -0
  160. package/dist/src/startup-validation.d.ts.map +1 -0
  161. package/dist/src/startup-validation.js +21 -0
  162. package/dist/src/startup-validation.js.map +1 -0
  163. package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
  164. package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
  165. package/dist/src/testing/dynamic-mock-server.js +138 -0
  166. package/dist/src/testing/dynamic-mock-server.js.map +1 -0
  167. package/dist/src/testing/request-assertions.d.ts +5 -0
  168. package/dist/src/testing/request-assertions.d.ts.map +1 -0
  169. package/dist/src/testing/request-assertions.js +165 -0
  170. package/dist/src/testing/request-assertions.js.map +1 -0
  171. package/dist/src/testing/template-utils.d.ts +10 -0
  172. package/dist/src/testing/template-utils.d.ts.map +1 -0
  173. package/dist/src/testing/template-utils.js +72 -0
  174. package/dist/src/testing/template-utils.js.map +1 -0
  175. package/dist/src/testing/test-http-utils.d.ts +1 -1
  176. package/dist/src/testing/test-http-utils.d.ts.map +1 -1
  177. package/dist/src/testing/test-http-utils.js +1 -1
  178. package/dist/src/testing/test-http-utils.js.map +1 -1
  179. package/dist/src/testing/test-loader.d.ts +6 -0
  180. package/dist/src/testing/test-loader.d.ts.map +1 -0
  181. package/dist/src/testing/test-loader.js +212 -0
  182. package/dist/src/testing/test-loader.js.map +1 -0
  183. package/dist/src/testing/test-schema.d.ts +1270 -0
  184. package/dist/src/testing/test-schema.d.ts.map +1 -0
  185. package/dist/src/testing/test-schema.js +76 -0
  186. package/dist/src/testing/test-schema.js.map +1 -0
  187. package/dist/src/tool-filter/compat.d.ts +49 -0
  188. package/dist/src/tool-filter/compat.d.ts.map +1 -0
  189. package/dist/src/tool-filter/compat.js +72 -0
  190. package/dist/src/tool-filter/compat.js.map +1 -0
  191. package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
  192. package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
  193. package/dist/src/tool-filter/config/env-config-parser.js +103 -0
  194. package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
  195. package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
  196. package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
  197. package/dist/src/tool-filter/config/header-config-parser.js +118 -0
  198. package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
  199. package/dist/src/tool-filter/errors.d.ts +18 -0
  200. package/dist/src/tool-filter/errors.d.ts.map +1 -0
  201. package/dist/src/tool-filter/errors.js +21 -0
  202. package/dist/src/tool-filter/errors.js.map +1 -0
  203. package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
  204. package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
  205. package/dist/src/tool-filter/filter/filter-engine.js +94 -0
  206. package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
  207. package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
  208. package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
  209. package/dist/src/tool-filter/filter/filter-rules.js +72 -0
  210. package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
  211. package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
  212. package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
  213. package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
  214. package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
  215. package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
  216. package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
  217. package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
  218. package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
  219. package/dist/src/tool-filter/index.d.ts +25 -0
  220. package/dist/src/tool-filter/index.d.ts.map +1 -0
  221. package/dist/src/tool-filter/index.js +30 -0
  222. package/dist/src/tool-filter/index.js.map +1 -0
  223. package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
  224. package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
  225. package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
  226. package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
  227. package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
  228. package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
  229. package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
  230. package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
  231. package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
  232. package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
  233. package/dist/src/tool-filter/operation/operation-detector.js +96 -0
  234. package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
  235. package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
  236. package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
  237. package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
  238. package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
  239. package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
  240. package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
  241. package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
  242. package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
  243. package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
  244. package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
  245. package/dist/src/tool-filter/regex/regex-validator.js +58 -0
  246. package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
  247. package/dist/src/tool-filter/types.d.ts +92 -0
  248. package/dist/src/tool-filter/types.d.ts.map +1 -0
  249. package/dist/src/tool-filter/types.js +5 -0
  250. package/dist/src/tool-filter/types.js.map +1 -0
  251. package/dist/src/tool-filter/utils.d.ts +11 -0
  252. package/dist/src/tool-filter/utils.d.ts.map +1 -0
  253. package/dist/src/tool-filter/utils.js +13 -0
  254. package/dist/src/tool-filter/utils.js.map +1 -0
  255. package/dist/src/tool-filter.d.ts +65 -0
  256. package/dist/src/tool-filter.d.ts.map +1 -0
  257. package/dist/src/tool-filter.js +471 -0
  258. package/dist/src/tool-filter.js.map +1 -0
  259. package/dist/src/tool-generator.d.ts +1 -0
  260. package/dist/src/tool-generator.d.ts.map +1 -1
  261. package/dist/src/tool-generator.js +15 -6
  262. package/dist/src/tool-generator.js.map +1 -1
  263. package/dist/src/tooling/composite-executor.d.ts +77 -0
  264. package/dist/src/tooling/composite-executor.d.ts.map +1 -0
  265. package/dist/src/tooling/composite-executor.js +198 -0
  266. package/dist/src/tooling/composite-executor.js.map +1 -0
  267. package/dist/src/tooling/dag-executor.d.ts +49 -0
  268. package/dist/src/tooling/dag-executor.d.ts.map +1 -0
  269. package/dist/src/tooling/dag-executor.js +138 -0
  270. package/dist/src/tooling/dag-executor.js.map +1 -0
  271. package/dist/src/tooling/proxy-executor.d.ts +86 -0
  272. package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
  273. package/dist/src/tooling/proxy-executor.js +501 -0
  274. package/dist/src/tooling/proxy-executor.js.map +1 -0
  275. package/dist/src/tooling/tool-generator.d.ts +67 -0
  276. package/dist/src/tooling/tool-generator.d.ts.map +1 -0
  277. package/dist/src/tooling/tool-generator.js +222 -0
  278. package/dist/src/tooling/tool-generator.js.map +1 -0
  279. package/dist/src/transport/http-client-factory.d.ts +65 -0
  280. package/dist/src/transport/http-client-factory.d.ts.map +1 -0
  281. package/dist/src/transport/http-client-factory.js +143 -0
  282. package/dist/src/transport/http-client-factory.js.map +1 -0
  283. package/dist/src/transport/http-transport-config.d.ts +6 -0
  284. package/dist/src/transport/http-transport-config.d.ts.map +1 -0
  285. package/dist/src/transport/http-transport-config.js +63 -0
  286. package/dist/src/transport/http-transport-config.js.map +1 -0
  287. package/dist/src/transport/http-transport.d.ts +329 -0
  288. package/dist/src/transport/http-transport.d.ts.map +1 -0
  289. package/dist/src/transport/http-transport.js +2584 -0
  290. package/dist/src/transport/http-transport.js.map +1 -0
  291. package/dist/src/transport/interceptors.d.ts +119 -0
  292. package/dist/src/transport/interceptors.d.ts.map +1 -0
  293. package/dist/src/transport/interceptors.js +413 -0
  294. package/dist/src/transport/interceptors.js.map +1 -0
  295. package/dist/src/transport/profile-index.d.ts +84 -0
  296. package/dist/src/transport/profile-index.d.ts.map +1 -0
  297. package/dist/src/transport/profile-index.js +405 -0
  298. package/dist/src/transport/profile-index.js.map +1 -0
  299. package/dist/src/types/http-transport.d.ts +26 -0
  300. package/dist/src/types/http-transport.d.ts.map +1 -1
  301. package/dist/src/types/openapi.d.ts +3 -0
  302. package/dist/src/types/openapi.d.ts.map +1 -1
  303. package/dist/src/types/profile.d.ts +16 -1
  304. package/dist/src/types/profile.d.ts.map +1 -1
  305. package/dist/src/validation/argument-normalizer.d.ts +6 -0
  306. package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
  307. package/dist/src/validation/argument-normalizer.js +70 -0
  308. package/dist/src/validation/argument-normalizer.js.map +1 -0
  309. package/dist/src/validation/jsonrpc-validator.d.ts +27 -0
  310. package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
  311. package/dist/src/validation/jsonrpc-validator.js +58 -0
  312. package/dist/src/validation/jsonrpc-validator.js.map +1 -0
  313. package/dist/src/validation/schema-validator.d.ts +30 -0
  314. package/dist/src/validation/schema-validator.d.ts.map +1 -0
  315. package/dist/src/validation/schema-validator.js +128 -0
  316. package/dist/src/validation/schema-validator.js.map +1 -0
  317. package/dist/src/validation/validation-utils.d.ts +49 -0
  318. package/dist/src/validation/validation-utils.d.ts.map +1 -0
  319. package/dist/src/validation/validation-utils.js +139 -0
  320. package/dist/src/validation/validation-utils.js.map +1 -0
  321. package/html/profile-index.html +386 -0
  322. package/package.json +10 -3
  323. package/profile-schema.json +77 -3
  324. package/profiles/gitlab/developer-profile-oauth.json +1520 -0
  325. package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
  326. package/profiles/gitlab/developer-profile.json +1508 -0
  327. package/profiles/gitlab/developer-profile.test.json +3432 -0
  328. package/profiles/gitlab/openapi.yaml +6891 -0
  329. package/profiles/n8n/openapi.yaml +2441 -0
  330. package/profiles/n8n/profile-optimized.json +965 -0
  331. package/profiles/n8n/profile-optimized.test.json +1078 -0
  332. package/profiles/n8n/profile.json +1033 -0
  333. package/profiles/n8n/profile.test.json +983 -0
  334. package/profiles/n8n-nodes/openapi.yaml +24 -0
  335. package/profiles/n8n-nodes/profile-nodes.json +44 -0
  336. package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
  337. package/profiles/semgrep/openapi.yaml +4706 -0
  338. package/profiles/semgrep/profile.json +692 -0
  339. package/profiles/semgrep/profile.test.json +471 -0
  340. package/profiles/youtrack/openapi.json +16976 -0
  341. package/profiles/youtrack/profile.json +608 -0
  342. package/profiles/youtrack/profile.test.json +1926 -0
  343. package/dist/src/testing/fixtures.d.ts +0 -684
  344. package/dist/src/testing/fixtures.d.ts.map +0 -1
  345. package/dist/src/testing/fixtures.js +0 -528
  346. package/dist/src/testing/fixtures.js.map +0 -1
  347. package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
  348. package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
  349. package/dist/src/testing/mock-gitlab-server.js +0 -1026
  350. package/dist/src/testing/mock-gitlab-server.js.map +0 -1
  351. package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
  352. package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
  353. package/dist/src/testing/mock-semgrep-server.js +0 -213
  354. package/dist/src/testing/mock-semgrep-server.js.map +0 -1
  355. package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
  356. package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
  357. package/dist/src/testing/mock-youtrack-server.js +0 -152
  358. package/dist/src/testing/mock-youtrack-server.js.map +0 -1
@@ -0,0 +1,1473 @@
1
+ /**
2
+ * Main MCP server implementation
3
+ *
4
+ * Why: Coordinates OpenAPI parser, profile loader, tool generator, and request execution.
5
+ * Single entry point for tool registration and invocation.
6
+ */
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
10
+ import { OpenAPIParser } from '../openapi/openapi-parser.js';
11
+ import { ProfileLoader } from '../profile/profile-loader.js';
12
+ import { ToolGenerator } from '../tooling/tool-generator.js';
13
+ import { applyParameterDefaults, normalizeArguments } from '../validation/argument-normalizer.js';
14
+ import { CompositeExecutor } from '../tooling/composite-executor.js';
15
+ import { ProxyDownloadExecutor } from '../tooling/proxy-executor.js';
16
+ import { enforceFiltering, parseFilteringHeader } from '../core/filtering.js';
17
+ import { ConfigurationError, OperationNotFoundError, ResourceNotFoundError, ValidationError, AuthenticationError, AuthorizationError, RateLimitError, NetworkError, generateCorrelationId } from '../core/errors.js';
18
+ import { OAUTH_RATE_LIMIT } from '../core/constants.js';
19
+ import { HttpClientFactory } from '../transport/http-client-factory.js';
20
+ import { SchemaValidator } from '../validation/schema-validator.js';
21
+ import { ConsoleLogger, JsonLogger } from '../core/logger.js';
22
+ import { isInitializeRequest, isToolCallRequest } from '../validation/jsonrpc-validator.js';
23
+ import { generateNameWarnings } from '../core/naming-warnings.js';
24
+ import { NamingStrategy } from '../core/naming.js';
25
+ import { isSafePropertyName } from '../validation/validation-utils.js';
26
+ import { ToolFilterService, EnvConfigParser, HeaderConfigParser, RegexCompiler, RegexValidator, OperationClassifier, OpenAPIOperationResolver, OperationDetector, applySessionToolFilter, } from '../tool-filter/index.js';
27
+ import { buildHttpTransportBaseConfig } from '../transport/http-transport-config.js';
28
+ export class MCPServer {
29
+ /**
30
+ * Execute a tools/call request via the JSON-RPC handler.
31
+ * Intended for internal use and tests to avoid accessing private methods.
32
+ */
33
+ async callToolRpc(name, args, sessionId, requestId = 1) {
34
+ const message = {
35
+ jsonrpc: '2.0',
36
+ id: requestId,
37
+ method: 'tools/call',
38
+ params: {
39
+ name,
40
+ arguments: args,
41
+ },
42
+ };
43
+ return this.handleToolCall(message, sessionId);
44
+ }
45
+ /**
46
+ * Filter response payload to include only specified fields.
47
+ *
48
+ * Supports YouTrack-style field selectors like:
49
+ * - "author(id,login)"
50
+ * - "comments(id,text,author(id,login))"
51
+ *
52
+ * Recurses into nested objects and arrays when subfields are specified.
53
+ */
54
+ filterFields(data, fields) {
55
+ const selection = this.parseFieldSelection(fields);
56
+ return this.applyFieldSelection(data, selection);
57
+ }
58
+ parseFieldSelection(fields) {
59
+ const root = Object.create(null);
60
+ for (const field of fields) {
61
+ const trimmed = field.trim();
62
+ if (!trimmed)
63
+ continue;
64
+ this.mergeFieldSelector(root, trimmed);
65
+ }
66
+ return root;
67
+ }
68
+ mergeFieldSelector(target, selector) {
69
+ const parsed = this.parseFieldSelector(selector);
70
+ const baseName = parsed.baseName;
71
+ if (!baseName)
72
+ return;
73
+ if (!isSafePropertyName(baseName))
74
+ return;
75
+ if (!parsed.inner) {
76
+ target[baseName] = true;
77
+ return;
78
+ }
79
+ const inner = parsed.inner;
80
+ const subSelectors = this.splitTopLevel(inner);
81
+ const subTree = Object.create(null);
82
+ for (const sub of subSelectors) {
83
+ this.mergeFieldSelector(subTree, sub);
84
+ }
85
+ const existing = target[baseName];
86
+ if (existing === true)
87
+ return;
88
+ if (!existing) {
89
+ target[baseName] = subTree;
90
+ return;
91
+ }
92
+ this.mergeSelectionTrees(existing, subTree);
93
+ }
94
+ parseFieldSelector(selector) {
95
+ const trimmed = selector.trim();
96
+ if (!trimmed)
97
+ return { baseName: '' };
98
+ if (trimmed.startsWith('"')) {
99
+ const parsedQuoted = this.parseQuotedBase(trimmed);
100
+ if (parsedQuoted) {
101
+ const { baseName, rest } = parsedQuoted;
102
+ const remaining = rest.trim();
103
+ if (!remaining) {
104
+ return { baseName };
105
+ }
106
+ if (remaining.startsWith('(') && remaining.endsWith(')')) {
107
+ const inner = remaining.slice(1, -1).trim();
108
+ return inner ? { baseName, inner } : { baseName };
109
+ }
110
+ return { baseName };
111
+ }
112
+ }
113
+ const openParen = trimmed.indexOf('(');
114
+ if (openParen === -1) {
115
+ return { baseName: trimmed };
116
+ }
117
+ const closeParen = trimmed.lastIndexOf(')');
118
+ if (closeParen === -1 || closeParen <= openParen) {
119
+ return { baseName: trimmed.slice(0, openParen).trim() };
120
+ }
121
+ const baseName = trimmed.slice(0, openParen).trim();
122
+ const inner = trimmed.slice(openParen + 1, closeParen).trim();
123
+ return inner ? { baseName, inner } : { baseName };
124
+ }
125
+ parseQuotedBase(input) {
126
+ let escaped = false;
127
+ let base = '';
128
+ for (let i = 1; i < input.length; i += 1) {
129
+ const ch = input[i];
130
+ if (escaped) {
131
+ base += ch;
132
+ escaped = false;
133
+ continue;
134
+ }
135
+ if (ch === '\\') {
136
+ escaped = true;
137
+ continue;
138
+ }
139
+ if (ch === '"') {
140
+ const rest = input.slice(i + 1);
141
+ return { baseName: base, rest };
142
+ }
143
+ base += ch;
144
+ }
145
+ return undefined;
146
+ }
147
+ mergeSelectionTrees(target, incoming) {
148
+ for (const [key, val] of Object.entries(incoming)) {
149
+ if (!isSafePropertyName(key))
150
+ continue;
151
+ const existing = target[key];
152
+ if (!existing) {
153
+ target[key] = val;
154
+ continue;
155
+ }
156
+ if (existing === true || val === true) {
157
+ target[key] = true;
158
+ continue;
159
+ }
160
+ this.mergeSelectionTrees(existing, val);
161
+ }
162
+ }
163
+ splitTopLevel(input) {
164
+ const result = [];
165
+ let depth = 0;
166
+ let current = '';
167
+ let inQuote = false;
168
+ let escaped = false;
169
+ for (const ch of input) {
170
+ if (escaped) {
171
+ current += ch;
172
+ escaped = false;
173
+ continue;
174
+ }
175
+ if (ch === '\\' && inQuote) {
176
+ current += ch;
177
+ escaped = true;
178
+ continue;
179
+ }
180
+ if (ch === '"') {
181
+ inQuote = !inQuote;
182
+ current += ch;
183
+ continue;
184
+ }
185
+ if (!inQuote) {
186
+ if (ch === '(')
187
+ depth += 1;
188
+ if (ch === ')')
189
+ depth = Math.max(0, depth - 1);
190
+ }
191
+ if (!inQuote && ch === ',' && depth === 0) {
192
+ const trimmed = current.trim();
193
+ if (trimmed)
194
+ result.push(trimmed);
195
+ current = '';
196
+ continue;
197
+ }
198
+ current += ch;
199
+ }
200
+ const last = current.trim();
201
+ if (last)
202
+ result.push(last);
203
+ return result;
204
+ }
205
+ applyFieldSelection(data, selection) {
206
+ if (!data || typeof data !== 'object') {
207
+ return data;
208
+ }
209
+ if (Array.isArray(data)) {
210
+ return data.map(item => this.applyFieldSelection(item, selection));
211
+ }
212
+ const obj = data;
213
+ const filtered = Object.create(null);
214
+ for (const [key, sel] of Object.entries(selection)) {
215
+ if (!isSafePropertyName(key))
216
+ continue;
217
+ if (!Object.prototype.hasOwnProperty.call(obj, key))
218
+ continue;
219
+ const value = obj[key];
220
+ if (sel === true) {
221
+ filtered[key] = value;
222
+ }
223
+ else {
224
+ filtered[key] = this.applyFieldSelection(value, sel);
225
+ }
226
+ }
227
+ return filtered;
228
+ }
229
+ /**
230
+ * Format error message for client with correlation ID
231
+ *
232
+ * Why: Categorize errors as "safe" (4xx client errors) vs "unsafe" (5xx server errors)
233
+ * Safe errors show API message to help user fix the issue
234
+ * Unsafe errors show generic message to avoid leaking sensitive info
235
+ */
236
+ formatErrorForClient(error, correlationId) {
237
+ // Authentication errors - safe to show (token expired, invalid credentials)
238
+ if (error instanceof AuthenticationError) {
239
+ return `Authentication failed: ${error.message} (correlation ID: ${correlationId})`;
240
+ }
241
+ // Authorization errors - safe to show (insufficient permissions)
242
+ if (error instanceof AuthorizationError) {
243
+ return `Authorization failed: ${error.message} (correlation ID: ${correlationId})`;
244
+ }
245
+ // Rate limit errors - safe to show (helps user understand backoff)
246
+ if (error instanceof RateLimitError) {
247
+ const retryInfo = error.details?.retryAfter
248
+ ? ` Retry after ${error.details.retryAfter} seconds.`
249
+ : '';
250
+ return `Rate limit exceeded: ${error.message}${retryInfo} (correlation ID: ${correlationId})`;
251
+ }
252
+ // Network errors with 4xx status - safe to show (client errors)
253
+ if (error instanceof NetworkError && error.details?.statusCode) {
254
+ const statusCode = error.details.statusCode;
255
+ if (statusCode >= 400 && statusCode < 500) {
256
+ return `Request failed: ${error.message} (correlation ID: ${correlationId})`;
257
+ }
258
+ }
259
+ // Validation errors - safe to show (helps user fix input)
260
+ if (error instanceof ValidationError) {
261
+ return `Validation error: ${error.message} (correlation ID: ${correlationId})`;
262
+ }
263
+ // Operation not found - safe to show (configuration issue)
264
+ if (error instanceof OperationNotFoundError) {
265
+ return `Operation not found: ${error.message} (correlation ID: ${correlationId})`;
266
+ }
267
+ if (error instanceof ResourceNotFoundError) {
268
+ return `${error.message} (correlation ID: ${correlationId})`;
269
+ }
270
+ // Configuration errors - safe to show (helps admin fix setup)
271
+ if (error instanceof ConfigurationError) {
272
+ return `Configuration error: ${error.message} (correlation ID: ${correlationId})`;
273
+ }
274
+ // Generic/unknown errors - hide details, show only correlation ID
275
+ return `Internal error (correlation ID: ${correlationId})`;
276
+ }
277
+ constructor(logger) {
278
+ this.httpClientFactory = new HttpClientFactory();
279
+ this.httpTransport = null;
280
+ this.logger = logger || new ConsoleLogger();
281
+ this.schemaValidator = new SchemaValidator();
282
+ this.server = new Server({
283
+ name: 'mcp4openapi',
284
+ version: '0.1.0',
285
+ }, {
286
+ capabilities: {
287
+ tools: {},
288
+ },
289
+ });
290
+ this.parser = new OpenAPIParser();
291
+ this.toolGenerator = new ToolGenerator(this.parser);
292
+ this.setupHandlers();
293
+ }
294
+ async initialize(specPath, profilePath) {
295
+ // Load OpenAPI spec
296
+ await this.parser.load(specPath);
297
+ this.logger.info('Loaded OpenAPI spec', { specPath });
298
+ // Load or create MCP profile
299
+ if (profilePath) {
300
+ const loader = new ProfileLoader();
301
+ this.profile = await loader.load(profilePath);
302
+ this.logger.info('Loaded profile', {
303
+ profile: this.profile.profile_name,
304
+ toolCount: this.profile.tools.length,
305
+ });
306
+ }
307
+ else {
308
+ this.profile = ProfileLoader.createDefaultProfile('default', this.parser);
309
+ this.logger.info('Using auto-generated default profile', {
310
+ profile: this.profile.profile_name,
311
+ toolCount: this.profile.tools.length,
312
+ });
313
+ // Check if we should warn about long names
314
+ this.checkToolNameLengths();
315
+ }
316
+ this.applyGlobalToolFiltering();
317
+ // Re-create logger with auth config for token redaction
318
+ const authConfigs = this.getAuthConfigs();
319
+ if (authConfigs.length > 0) {
320
+ // Use first auth config for logger (primary)
321
+ this.logger = this.createLoggerWithAuth(authConfigs[0]);
322
+ this.logger.info('Logger re-configured with auth token redaction', {
323
+ authMethods: authConfigs.length,
324
+ });
325
+ }
326
+ // Setup HTTP client with interceptors
327
+ // For stdio transport, create client with env token
328
+ // For HTTP transport, clients are created per-session with user's token
329
+ const baseUrl = this.getBaseUrl();
330
+ const envAuthConfig = this.getEnvBackedAuthConfig();
331
+ const envVarName = envAuthConfig?.value_from_env;
332
+ const envToken = envVarName ? process.env[envVarName] : undefined;
333
+ if ((envAuthConfig && envToken) || authConfigs.length === 0) {
334
+ // Token available in env (stdio) or no auth required - create global client
335
+ const httpClient = this.httpClientFactory.createGlobalClient({
336
+ profile: this.profile,
337
+ baseUrl,
338
+ });
339
+ this.compositeExecutor = new CompositeExecutor(this.parser, httpClient, this.profile.parameter_aliases);
340
+ }
341
+ else {
342
+ // No env token or no auth - will use per-session clients (HTTP transport)
343
+ this.compositeExecutor = new CompositeExecutor(this.parser, undefined, this.profile.parameter_aliases);
344
+ }
345
+ this.logger.info('MCP server initialized', {
346
+ baseUrl,
347
+ toolCount: this.profile.tools.length,
348
+ });
349
+ }
350
+ /**
351
+ * Create logger with auth configuration for token redaction
352
+ *
353
+ * Why: Prevents sensitive tokens from appearing in logs
354
+ */
355
+ createLoggerWithAuth(authConfig) {
356
+ const logFormat = process.env.MCP4_LOG_FORMAT || 'console';
357
+ const logLevel = this.logger instanceof ConsoleLogger || this.logger instanceof JsonLogger
358
+ ? this.logger.level
359
+ : undefined;
360
+ return logFormat === 'json'
361
+ ? new JsonLogger(logLevel, authConfig)
362
+ : new ConsoleLogger(logLevel, authConfig);
363
+ }
364
+ /**
365
+ * Check tool name lengths and warn if needed
366
+ */
367
+ checkToolNameLengths() {
368
+ const maxLength = parseInt(process.env.MCP4_TOOLNAME_MAX || '45', 10);
369
+ const strategy = (process.env.MCP4_TOOLNAME_STRATEGY || 'none').toLowerCase();
370
+ const warnOnly = (process.env.MCP4_TOOLNAME_WARN_ONLY || 'true').toLowerCase() === 'true';
371
+ // Only warn if strategy is 'none' or warn-only mode is enabled
372
+ if (strategy !== NamingStrategy.None && !warnOnly) {
373
+ return; // Names already shortened, no need to warn
374
+ }
375
+ // Get all operations as OperationForNaming
376
+ const operations = this.parser.getAllOperations();
377
+ const opsForNaming = operations.map(op => ({
378
+ operationId: op.operationId,
379
+ method: op.method,
380
+ path: op.path,
381
+ tags: op.tags,
382
+ }));
383
+ const warningOptions = {
384
+ maxLength,
385
+ similarTopN: parseInt(process.env.MCP4_TOOLNAME_SIMILAR_TOP || '3', 10),
386
+ similarityThreshold: parseFloat(process.env.MCP4_TOOLNAME_SIMILARITY_THRESHOLD || '0.75'),
387
+ minParts: parseInt(process.env.MCP4_TOOLNAME_MIN_PARTS || '3', 10),
388
+ minLength: parseInt(process.env.MCP4_TOOLNAME_MIN_LENGTH || '20', 10),
389
+ };
390
+ generateNameWarnings(opsForNaming, warningOptions, this.logger);
391
+ }
392
+ /**
393
+ * Get base URL from profile config or OpenAPI spec
394
+ */
395
+ getBaseUrl() {
396
+ const baseUrlConfig = this.profile?.interceptors?.base_url;
397
+ if (baseUrlConfig) {
398
+ const envValue = process.env[baseUrlConfig.value_from_env];
399
+ if (envValue)
400
+ return envValue;
401
+ if (baseUrlConfig.default)
402
+ return baseUrlConfig.default;
403
+ }
404
+ return this.parser.getBaseUrl();
405
+ }
406
+ /**
407
+ * Get auth configurations as array (supports single or multiple auth methods)
408
+ * Returns array sorted by priority (lower = higher priority)
409
+ */
410
+ getAuthConfigs() {
411
+ const auth = this.profile?.interceptors?.auth;
412
+ if (!auth)
413
+ return [];
414
+ const configs = Array.isArray(auth) ? auth : [auth];
415
+ // Sort by priority (lower = higher priority)
416
+ return configs.sort((a, b) => (a.priority || 0) - (b.priority || 0));
417
+ }
418
+ /**
419
+ * Get primary (highest priority) auth configuration
420
+ */
421
+ getPrimaryAuthConfig() {
422
+ const configs = this.getAuthConfigs();
423
+ return configs[0];
424
+ }
425
+ /**
426
+ * Get highest priority auth configuration that reads token from environment
427
+ */
428
+ getEnvBackedAuthConfig() {
429
+ const configs = this.getAuthConfigs();
430
+ return configs.find(config => config.type !== 'oauth' && !!config.value_from_env);
431
+ }
432
+ /**
433
+ * Get OAuth configuration from auth configs (if any)
434
+ */
435
+ getOAuthConfig() {
436
+ const configs = this.getAuthConfigs();
437
+ const oauthConfig = configs.find(c => c.type === 'oauth');
438
+ return oauthConfig?.oauth_config;
439
+ }
440
+ buildOAuthConfigWithAllowedRedirectHosts(oauthConfig) {
441
+ if (!oauthConfig) {
442
+ return undefined;
443
+ }
444
+ return {
445
+ ...oauthConfig,
446
+ allowed_redirect_hosts: oauthConfig.allowed_redirect_hosts
447
+ || (process.env.MCP4_ALLOWED_ORIGINS
448
+ ? this.extractHostsFromOrigins(process.env.MCP4_ALLOWED_ORIGINS)
449
+ : undefined),
450
+ };
451
+ }
452
+ getProfileIdValue() {
453
+ if (!this.profile) {
454
+ throw new ConfigurationError('Profile not initialized. Call initialize() first.');
455
+ }
456
+ const profileId = this.profile.profile_id?.trim() || this.profile.profile_name;
457
+ if (!profileId) {
458
+ throw new ConfigurationError('Profile is missing profile_id and profile_name.');
459
+ }
460
+ return profileId;
461
+ }
462
+ getOAuthRateLimitConfig() {
463
+ const authConfigs = this.getAuthConfigs();
464
+ const oauthAuthConfig = authConfigs.find(c => c.type === 'oauth');
465
+ const oauthRateLimit = oauthAuthConfig?.oauth_rate_limit;
466
+ const max = oauthRateLimit?.max_requests
467
+ || parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_MAX || String(OAUTH_RATE_LIMIT.MAX_REQUESTS), 10);
468
+ const windowMs = oauthRateLimit?.window_ms
469
+ || parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_WINDOW_MS || String(OAUTH_RATE_LIMIT.WINDOW_MS), 10);
470
+ return { max, windowMs };
471
+ }
472
+ getHttpProfileContext() {
473
+ if (!this.profile) {
474
+ throw new ConfigurationError('Profile not initialized. Call initialize() first.');
475
+ }
476
+ const authConfigs = this.getAuthConfigs();
477
+ const baseUrl = this.getBaseUrl();
478
+ const oauthConfig = this.buildOAuthConfigWithAllowedRedirectHosts(this.getOAuthConfig());
479
+ const resourceMetadata = this.parser.getResourceMetadata();
480
+ const oauthRateLimit = this.getOAuthRateLimitConfig();
481
+ return {
482
+ profileId: this.getProfileIdValue(),
483
+ oauthConfig,
484
+ authConfigs,
485
+ baseUrl,
486
+ rateLimitOAuthMax: oauthRateLimit.max,
487
+ rateLimitOAuthWindowMs: oauthRateLimit.windowMs,
488
+ resourceName: this.profile.resource_name || resourceMetadata.name || 'MCP Server',
489
+ resourceDocumentation: this.profile.resource_documentation || resourceMetadata.documentation,
490
+ parser: this.parser,
491
+ };
492
+ }
493
+ /**
494
+ * Extract hostnames from origin patterns for OAuth redirect validation
495
+ * e.g., "http://localhost:*,https://app.example.com" -> ["localhost", "app.example.com"]
496
+ *
497
+ * Filters out CIDR blocks (e.g., "127.0.0.1/8") which are valid for origin validation
498
+ * but not for OAuth redirect URI validation
499
+ */
500
+ extractHostsFromOrigins(origins) {
501
+ const hosts = [];
502
+ for (const origin of origins.split(',')) {
503
+ const trimmed = origin.trim();
504
+ try {
505
+ // Handle wildcard ports: http://localhost:* -> localhost
506
+ const normalized = trimmed.replace(/:\*$/, ':80');
507
+ const url = new URL(normalized);
508
+ // Preserve wildcards in hostname
509
+ if (trimmed.includes('*.')) {
510
+ const match = trimmed.match(/\*\.[^:/]+/);
511
+ if (match) {
512
+ hosts.push(match[0]);
513
+ }
514
+ }
515
+ else {
516
+ hosts.push(url.hostname);
517
+ }
518
+ }
519
+ catch {
520
+ // If not a URL, treat as hostname/pattern directly
521
+ // Skip CIDR blocks (e.g., 127.0.0.1/8, 10.0.0.0/8, 2a06:2140::/29)
522
+ if (trimmed && !trimmed.includes(' ') && !trimmed.includes('/')) {
523
+ hosts.push(trimmed);
524
+ }
525
+ }
526
+ }
527
+ return [...new Set(hosts)]; // Dedupe
528
+ }
529
+ /**
530
+ * Get or create HTTP client for session
531
+ */
532
+ async getHttpClientForSession(sessionId, profileId) {
533
+ if (!sessionId) {
534
+ // Fallback to global client for stdio transport
535
+ if (!this.httpClientFactory.hasGlobalClient()) {
536
+ const hasHttpTransport = !!this.httpTransport;
537
+ const transport = hasHttpTransport ? 'http' : 'stdio';
538
+ const envAuthConfig = this.getEnvBackedAuthConfig();
539
+ const envVarName = envAuthConfig?.value_from_env || 'MCP4_API_TOKEN';
540
+ const hasEnvToken = !!process.env[envVarName];
541
+ throw new ConfigurationError(`HTTP client not initialized. ` +
542
+ `Transport: ${transport}, ` +
543
+ `HasEnvToken(${envVarName}): ${hasEnvToken}, ` +
544
+ `Suggestion: ${hasHttpTransport
545
+ ? 'Send token in Authorization header during initialization'
546
+ : `Set ${envVarName} environment variable`}`, { transport, hasEnvToken, envVarName, hasHttpTransport });
547
+ }
548
+ return this.httpClientFactory.getGlobalClient();
549
+ }
550
+ // Validate profile exists
551
+ if (!this.profile) {
552
+ throw new ConfigurationError('Profile not initialized. Call initialize() first.');
553
+ }
554
+ // Get auth token from session (ensures token is valid/refreshed)
555
+ const authToken = await this.getAuthTokenFromSession(sessionId, profileId);
556
+ // Create or get session client using factory
557
+ return this.httpClientFactory.getOrCreateSessionClient(sessionId, {
558
+ profile: this.profile,
559
+ baseUrl: this.getBaseUrl(),
560
+ sessionToken: authToken,
561
+ });
562
+ }
563
+ /**
564
+ * Get auth token from HTTP transport session
565
+ * Ensures token is valid (refreshes if expired) before returning
566
+ */
567
+ async getAuthTokenFromSession(sessionId, profileId) {
568
+ // Early return if sessionId is missing/empty
569
+ // Prevents misleading warn logs with empty sessionId
570
+ if (!sessionId) {
571
+ return undefined;
572
+ }
573
+ if (!this.httpTransport) {
574
+ return undefined;
575
+ }
576
+ // Ensure token is valid (refresh if expired)
577
+ const effectiveProfileId = profileId || this.getProfileIdValue();
578
+ const isValid = await this.httpTransport.ensureValidSessionToken(effectiveProfileId, sessionId);
579
+ if (!isValid) {
580
+ this.logger.warn('Session token validation/refresh failed', { profileId: effectiveProfileId, sessionId });
581
+ // Still return token if available - let the API call fail with proper error
582
+ }
583
+ // Use public API instead of type casting
584
+ return this.httpTransport.getSessionToken(effectiveProfileId, sessionId);
585
+ }
586
+ /**
587
+ * Cleanup HTTP client for destroyed session
588
+ *
589
+ * Why: Prevent memory leak - sessions expire but cached clients stay forever
590
+ */
591
+ cleanupSessionClient(profileId, sessionId) {
592
+ const removed = this.httpClientFactory.cleanupSessionClient(sessionId);
593
+ if (removed) {
594
+ this.logger.info('Cleaned up session HTTP client', { profileId, sessionId });
595
+ }
596
+ }
597
+ /**
598
+ * Setup MCP request handlers
599
+ */
600
+ setupHandlers() {
601
+ // List available tools
602
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
603
+ try {
604
+ if (!this.profile) {
605
+ throw new ConfigurationError('Server not initialized. Call initialize() first.');
606
+ }
607
+ const tools = this.profile.tools.map(toolDef => this.toolGenerator.generateTool(toolDef));
608
+ return { tools };
609
+ }
610
+ catch (err) {
611
+ // Generate correlation ID only on error (lazy)
612
+ const correlationId = generateCorrelationId();
613
+ this.logger.error('ListTools handler error', err, { correlationId });
614
+ // Always return generic error to clients
615
+ throw new Error(`Internal error (correlation ID: ${correlationId})`);
616
+ }
617
+ });
618
+ // Execute tool
619
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
620
+ try {
621
+ if (!this.profile || !this.compositeExecutor) {
622
+ throw new ConfigurationError('Server not initialized. Call initialize() first.');
623
+ }
624
+ const toolDef = this.profile.tools.find(t => t.name === request.params.name);
625
+ if (!toolDef) {
626
+ throw new OperationNotFoundError(request.params.name);
627
+ }
628
+ const rawArgs = request.params.arguments || {};
629
+ const args = applyParameterDefaults(toolDef, rawArgs);
630
+ // Validate arguments
631
+ this.toolGenerator.validateArguments(toolDef, args);
632
+ // Execute composite or simple tool
633
+ let result;
634
+ if (toolDef.composite && toolDef.steps) {
635
+ const compositeResult = await this.compositeExecutor.execute(toolDef.steps, args, toolDef.partial_results || false);
636
+ // Include metadata about completion
637
+ result = {
638
+ ...compositeResult.data,
639
+ _metadata: {
640
+ completed_steps: compositeResult.completed_steps,
641
+ total_steps: compositeResult.total_steps,
642
+ success: compositeResult.completed_steps === compositeResult.total_steps,
643
+ errors: compositeResult.errors,
644
+ },
645
+ };
646
+ }
647
+ else {
648
+ result = await this.executeSimpleTool(toolDef, args);
649
+ }
650
+ return {
651
+ content: [
652
+ {
653
+ type: 'text',
654
+ text: JSON.stringify(result, null, 2),
655
+ },
656
+ ],
657
+ };
658
+ }
659
+ catch (err) {
660
+ // Generate correlation ID only on error (lazy)
661
+ const correlationId = generateCorrelationId();
662
+ this.logger.error('CallTool handler error', err, {
663
+ correlationId,
664
+ toolName: request.params.name,
665
+ action: request.params.arguments?.action
666
+ });
667
+ // Return user-friendly error message with correlation ID
668
+ const errorMessage = this.formatErrorForClient(err, correlationId);
669
+ throw new Error(errorMessage);
670
+ }
671
+ });
672
+ }
673
+ /**
674
+ * Execute simple (non-composite) tool
675
+ *
676
+ * Why separate: Simple tools map directly to single OpenAPI operation.
677
+ * No result aggregation needed.
678
+ */
679
+ async executeSimpleTool(toolDef, args, sessionId, profileId) {
680
+ const normalizedArgs = normalizeArguments(toolDef, args);
681
+ this.logger.debug('Executing simple tool', {
682
+ toolName: toolDef.name,
683
+ action: normalizedArgs['action'],
684
+ resourceType: normalizedArgs['resource_type'],
685
+ sessionId
686
+ });
687
+ // Get operation definition (can be string or ProxyDownloadOperation)
688
+ const operationDef = this.toolGenerator.getOperationDefinition(toolDef, normalizedArgs);
689
+ if (!operationDef) {
690
+ throw new ValidationError(`Could not map tool action to operation`, {
691
+ toolName: toolDef.name,
692
+ action: args['action'],
693
+ resourceType: args['resource_type'],
694
+ availableOperations: Object.keys(toolDef.operations || {})
695
+ });
696
+ }
697
+ // Check if this is a proxy download operation
698
+ if (typeof operationDef === 'object' && operationDef.type === 'proxy_download') {
699
+ return this.executeProxyDownload(operationDef, normalizedArgs, sessionId, profileId);
700
+ }
701
+ // Regular string operation
702
+ const operationId = operationDef;
703
+ const operation = this.parser.getOperation(operationId);
704
+ if (!operation) {
705
+ throw new OperationNotFoundError(operationId);
706
+ }
707
+ // Build request
708
+ const path = this.resolvePath(operation.path, normalizedArgs);
709
+ const queryParams = this.extractQueryParams(operation, normalizedArgs);
710
+ const body = this.extractBody(operation, normalizedArgs, toolDef);
711
+ this.logger.debug('Executing HTTP request', {
712
+ operationId,
713
+ method: operation.method,
714
+ path,
715
+ hasQueryParams: Object.keys(queryParams).length > 0,
716
+ hasBody: !!body
717
+ });
718
+ // Validate request body against schema
719
+ if (body && operation.requestBody) {
720
+ const validationResult = this.schemaValidator.validateRequestBody(operation, body);
721
+ if (!validationResult.valid && validationResult.errors) {
722
+ const errorDetails = validationResult.errors
723
+ .map(e => ` - ${e.path}: ${e.message}`)
724
+ .join('\n');
725
+ throw new ValidationError(`Request body validation failed:\n${errorDetails}`, { operationId, validationErrors: validationResult.errors });
726
+ }
727
+ }
728
+ // Execute with session-specific client
729
+ const httpClient = await this.getHttpClientForSession(sessionId, profileId);
730
+ // Set fields parameter if response_fields are configured for this action AND enabled
731
+ const action = normalizedArgs.action;
732
+ if (toolDef.send_response_fields_as_param && toolDef.response_fields && action && toolDef.response_fields[action]) {
733
+ const fields = toolDef.response_fields[action];
734
+ queryParams.fields = fields.join(',');
735
+ }
736
+ const response = await httpClient.request(operation.method, path, {
737
+ params: queryParams,
738
+ body,
739
+ operationId: operationId,
740
+ });
741
+ // Apply response field filtering if configured
742
+ let result = response.body;
743
+ if (toolDef.response_fields) {
744
+ const action = normalizedArgs.action;
745
+ if (action && toolDef.response_fields[action]) {
746
+ const fields = toolDef.response_fields[action];
747
+ result = this.filterFields(result, fields);
748
+ }
749
+ }
750
+ return result;
751
+ }
752
+ /**
753
+ * Execute proxy download operation
754
+ *
755
+ * Why: Some APIs return authenticated URLs that LLMs cannot fetch directly.
756
+ * This proxies the download through the MCP server.
757
+ */
758
+ async executeProxyDownload(operation, args, sessionId, profileId) {
759
+ this.logger.debug('Executing proxy download', {
760
+ metadataEndpoint: operation.metadata_endpoint,
761
+ urlField: operation.url_field,
762
+ sessionId
763
+ });
764
+ // Get the metadata operation to build the path
765
+ const metadataOp = this.parser.getOperation(operation.metadata_endpoint);
766
+ if (!metadataOp) {
767
+ throw new OperationNotFoundError(operation.metadata_endpoint);
768
+ }
769
+ // Build path for metadata endpoint
770
+ const metadataPath = this.resolvePath(metadataOp.path, args);
771
+ const metadataMethod = metadataOp.method;
772
+ let directDownloadRequest;
773
+ if (operation.download_endpoint) {
774
+ const downloadOp = this.parser.getOperation(operation.download_endpoint);
775
+ if (!downloadOp) {
776
+ throw new OperationNotFoundError(operation.download_endpoint);
777
+ }
778
+ directDownloadRequest = {
779
+ path: this.resolvePath(downloadOp.path, args),
780
+ method: downloadOp.method,
781
+ };
782
+ }
783
+ // Get auth credentials for download
784
+ const httpClient = await this.getHttpClientForSession(sessionId, profileId);
785
+ const authCredentials = httpClient.getAuthCredentials();
786
+ // Execute proxy download
787
+ const proxyExecutor = new ProxyDownloadExecutor(httpClient, this.logger);
788
+ const result = await proxyExecutor.execute(operation, { path: metadataPath, method: metadataMethod }, authCredentials, directDownloadRequest);
789
+ this.logger.debug('Proxy download completed', {
790
+ fileName: result.fileName,
791
+ mimeType: result.mimeType,
792
+ size: result.size
793
+ });
794
+ return result;
795
+ }
796
+ /**
797
+ * Encode path segment if it contains special characters (like slashes)
798
+ *
799
+ * Why: GitLab and other APIs require path parameters (like project paths)
800
+ * to be URL-encoded when used in URL path.
801
+ */
802
+ encodePathSegment(value) {
803
+ const val = String(value);
804
+ return val.includes('/') ? encodeURIComponent(val) : val;
805
+ }
806
+ /**
807
+ * Resolve path parameters using profile aliases
808
+ *
809
+ * Why aliases: Different tools may use different parameter names for same path param.
810
+ * Example: GitLab uses "resource_id", "project_id", "group_id" all mapping to "{id}"
811
+ */
812
+ resolvePath(template, args) {
813
+ const aliases = this.profile?.parameter_aliases || {};
814
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
815
+ // Try direct match first
816
+ if (args[key] !== undefined) {
817
+ return this.encodePathSegment(args[key]);
818
+ }
819
+ // Try aliases from profile
820
+ const possibleAliases = aliases[key] || [];
821
+ for (const alias of possibleAliases) {
822
+ if (args[alias] !== undefined) {
823
+ return this.encodePathSegment(args[alias]);
824
+ }
825
+ }
826
+ throw new ValidationError(`Missing path parameter: ${key}` +
827
+ (possibleAliases.length > 0 ? `. Tried aliases: ${possibleAliases.join(', ')}` : ''), { paramName: key, possibleAliases });
828
+ });
829
+ }
830
+ /**
831
+ * Extract query parameters from args
832
+ *
833
+ * Why: Separate query params from body params. Array handling is done by HttpClient
834
+ * based on profile's array_format setting.
835
+ */
836
+ extractQueryParams(operation, args) {
837
+ const params = {};
838
+ const aliases = this.profile?.parameter_aliases || {};
839
+ for (const param of operation.parameters) {
840
+ if (param.in === 'query') {
841
+ let value = args[param.name];
842
+ // If not found by direct name, check aliases
843
+ if (value === undefined) {
844
+ const possibleAliases = aliases[param.name] || [];
845
+ for (const alias of possibleAliases) {
846
+ if (args[alias] !== undefined) {
847
+ value = args[alias];
848
+ break;
849
+ }
850
+ }
851
+ }
852
+ if (value !== undefined) {
853
+ // Pass arrays as-is, HttpClient will serialize based on array_format
854
+ if (Array.isArray(value)) {
855
+ params[param.name] = value.map(String);
856
+ }
857
+ else {
858
+ params[param.name] = String(value);
859
+ }
860
+ }
861
+ }
862
+ }
863
+ return params;
864
+ }
865
+ /**
866
+ * Extract request body from args
867
+ *
868
+ * Why: For create/update operations, collect non-metadata fields into body.
869
+ * Metadata (action, resource_type, etc.) are not sent to API.
870
+ * Path/query parameters are excluded from body UNLESS they're also in request body schema.
871
+ *
872
+ * Uses metadata_params from tool definition, defaults to ['action', 'resource_type']
873
+ */
874
+ extractBody(operation, args, toolDef) {
875
+ // Metadata fields from tool definition (or defaults)
876
+ const metadataList = toolDef.metadata_params || ['action', 'resource_type'];
877
+ const metadata = new Set(metadataList);
878
+ // Collect parameter names that go in path or query
879
+ const pathOrQuery = new Set();
880
+ for (const param of operation.parameters) {
881
+ if (param.in === 'path' || param.in === 'query') {
882
+ pathOrQuery.add(param.name);
883
+ }
884
+ }
885
+ // Get body schema and properties to check if path/query params should also be in body
886
+ let bodySchema;
887
+ const bodySchemaProps = new Set();
888
+ if (operation.requestBody?.content) {
889
+ // Prefer application/json but accept any schema present
890
+ const jsonSchema = operation.requestBody.content['application/json']?.schema;
891
+ bodySchema = jsonSchema;
892
+ if (!bodySchema) {
893
+ for (const mediaType of Object.values(operation.requestBody.content)) {
894
+ if (mediaType.schema) {
895
+ bodySchema = mediaType.schema;
896
+ break;
897
+ }
898
+ }
899
+ }
900
+ if (bodySchema?.type === 'object' && bodySchema.properties) {
901
+ for (const propName of Object.keys(bodySchema.properties)) {
902
+ bodySchemaProps.add(propName);
903
+ }
904
+ }
905
+ }
906
+ // Root array body support
907
+ if (bodySchema?.type === 'array') {
908
+ const explicit = args['body'] ?? args['items'];
909
+ if (explicit !== undefined) {
910
+ return explicit;
911
+ }
912
+ const arrayCandidates = [];
913
+ for (const [key, value] of Object.entries(args)) {
914
+ if (metadata.has(key))
915
+ continue;
916
+ if (pathOrQuery.has(key))
917
+ continue;
918
+ if (Array.isArray(value)) {
919
+ arrayCandidates.push(value);
920
+ }
921
+ }
922
+ if (arrayCandidates.length === 1) {
923
+ return arrayCandidates[0];
924
+ }
925
+ return undefined;
926
+ }
927
+ const body = {};
928
+ let hasBody = false;
929
+ for (const [key, value] of Object.entries(args)) {
930
+ // Include field if:
931
+ // - Not metadata
932
+ // - Not in path/query OR is in path/query but also required in body schema
933
+ // - Value is defined
934
+ const isPathOrQuery = pathOrQuery.has(key);
935
+ const isInBodySchema = bodySchemaProps.has(key);
936
+ if (!metadata.has(key) && (!isPathOrQuery || isInBodySchema) && value !== undefined) {
937
+ body[key] = value;
938
+ hasBody = true;
939
+ }
940
+ }
941
+ return hasBody ? body : undefined;
942
+ }
943
+ /**
944
+ * Start server with stdio transport
945
+ */
946
+ async runStdio() {
947
+ const transport = new StdioServerTransport();
948
+ await this.server.connect(transport);
949
+ this.logger.info('MCP server running on stdio');
950
+ }
951
+ /**
952
+ * Start server with HTTP transport
953
+ *
954
+ * Implements MCP Specification 2025-03-26 Streamable HTTP transport
955
+ *
956
+ * Why: Enables remote MCP server access with SSE streaming, session management,
957
+ * and resumability for reliable communication over HTTP.
958
+ */
959
+ async runHttp(host, port) {
960
+ const { HttpTransport } = await import('../transport/http-transport.js');
961
+ const profileContext = this.getHttpProfileContext();
962
+ if (profileContext.oauthConfig) {
963
+ this.logger.info('OAuth authentication enabled for HTTP transport');
964
+ }
965
+ const baseConfig = buildHttpTransportBaseConfig(host, port);
966
+ const config = {
967
+ ...baseConfig,
968
+ profileRoutingEnabled: false,
969
+ defaultProfileId: profileContext.profileId,
970
+ // OAuth rate limiting (priority: profile > env vars > defaults)
971
+ rateLimitOAuthMax: profileContext.rateLimitOAuthMax,
972
+ rateLimitOAuthWindowMs: profileContext.rateLimitOAuthWindowMs,
973
+ // OAuth config already merged with allowed_redirect_hosts
974
+ oauthConfig: profileContext.oauthConfig,
975
+ baseUrl: profileContext.baseUrl,
976
+ authConfigs: profileContext.authConfigs,
977
+ resourceName: profileContext.resourceName,
978
+ resourceDocumentation: profileContext.resourceDocumentation,
979
+ parser: profileContext.parser,
980
+ };
981
+ // Warn if binding to non-localhost without explicit MCP4_ALLOWED_ORIGINS
982
+ const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
983
+ const hasAllowedOrigins = Array.isArray(config.allowedOrigins) && config.allowedOrigins.length > 0;
984
+ if (!isLocalhost && !hasAllowedOrigins) {
985
+ this.logger.warn('Binding to non-localhost with empty MCP4_ALLOWED_ORIGINS. Set MCP4_ALLOWED_ORIGINS or bind to localhost.');
986
+ }
987
+ this.httpTransport = new HttpTransport(config, this.logger);
988
+ const metricsCollector = this.httpTransport.getMetricsCollector?.() || null;
989
+ this.httpClientFactory.setMetricsCollector(metricsCollector);
990
+ this.recordGlobalToolFilterMetrics();
991
+ // Set message handler to process JSON-RPC messages
992
+ this.httpTransport.setMessageHandler(async (message, sessionId, profileId) => {
993
+ return await this.handleJsonRpcMessage(message, sessionId, profileId);
994
+ });
995
+ // Register cleanup listener for session destruction (memory leak prevention)
996
+ this.httpTransport.onSessionDestroyed((profileId, sessionId) => {
997
+ this.cleanupSessionClient(profileId, sessionId);
998
+ });
999
+ await this.httpTransport.start();
1000
+ this.logger.info('MCP server running on HTTP', { host, port });
1001
+ }
1002
+ attachHttpTransport(transport) {
1003
+ this.httpTransport = transport;
1004
+ const metricsCollector = this.httpTransport.getMetricsCollector?.() || null;
1005
+ this.httpClientFactory.setMetricsCollector(metricsCollector);
1006
+ }
1007
+ handleSessionDestroyed(profileId, sessionId) {
1008
+ this.cleanupSessionClient(profileId, sessionId);
1009
+ }
1010
+ /**
1011
+ * Handle JSON-RPC message from HTTP transport
1012
+ *
1013
+ * Why: Unified message handling for both stdio and HTTP transports
1014
+ */
1015
+ async handleJsonRpcMessage(message, sessionId, profileId) {
1016
+ // Handle initialize
1017
+ if (isInitializeRequest(message)) {
1018
+ return this.handleInitialize(message, sessionId, profileId);
1019
+ }
1020
+ // Handle tool calls
1021
+ if (isToolCallRequest(message)) {
1022
+ return await this.handleToolCall(message, sessionId, profileId);
1023
+ }
1024
+ // Handle other JSON-RPC requests
1025
+ // (tools/list, prompts/list, etc.)
1026
+ return await this.handleOtherRequest(message, sessionId, profileId);
1027
+ }
1028
+ async handleHttpMessage(message, sessionId, profileId) {
1029
+ return this.handleJsonRpcMessage(message, sessionId, profileId);
1030
+ }
1031
+ handleInitialize(message, sessionId, profileId) {
1032
+ const req = message;
1033
+ const params = req.params;
1034
+ if (!this.httpTransport && params?.filtering !== undefined) {
1035
+ if (typeof params.filtering !== 'string') {
1036
+ throw new ValidationError('Invalid X-Mcp4-Params header. Expected comma-separated key=value pairs.');
1037
+ }
1038
+ const parsed = parseFilteringHeader(params.filtering);
1039
+ this.stdioFiltering = parsed.filtering;
1040
+ }
1041
+ if (this.httpTransport && sessionId) {
1042
+ this.applySessionToolFiltering(sessionId, profileId);
1043
+ }
1044
+ const result = {
1045
+ protocolVersion: '2025-03-26',
1046
+ serverInfo: {
1047
+ name: 'mcp4openapi',
1048
+ version: '0.1.0',
1049
+ },
1050
+ capabilities: {
1051
+ tools: {},
1052
+ },
1053
+ };
1054
+ // OAuth capability is communicated via 401 responses with WWW-Authenticate header
1055
+ // as per MCP Authorization specification
1056
+ // Include sessionId if available (for HTTP transport)
1057
+ if (sessionId) {
1058
+ result.sessionId = sessionId;
1059
+ }
1060
+ return {
1061
+ jsonrpc: '2.0',
1062
+ id: req.id,
1063
+ result,
1064
+ };
1065
+ }
1066
+ async handleToolCall(message, sessionId, profileId) {
1067
+ const req = message;
1068
+ const params = req.params;
1069
+ const toolName = params.name;
1070
+ const rawArgs = params.arguments || {};
1071
+ // Check OAuth authentication for tool operations
1072
+ if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
1073
+ const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
1074
+ if (!authToken) {
1075
+ // Return OAuth required error with WWW-Authenticate header
1076
+ // This should trigger the OAuth flow in the client
1077
+ const errorResponse = {
1078
+ jsonrpc: '2.0',
1079
+ id: req.id,
1080
+ error: {
1081
+ code: -32001, // Application error
1082
+ message: 'Authentication required. Please authorize via OAuth.',
1083
+ data: {
1084
+ oauth_required: true,
1085
+ resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
1086
+ scope: 'api'
1087
+ }
1088
+ }
1089
+ };
1090
+ return errorResponse;
1091
+ }
1092
+ }
1093
+ let args = rawArgs;
1094
+ try {
1095
+ // Find tool definition
1096
+ const toolDef = this.profile?.tools.find(t => t.name === toolName);
1097
+ if (!toolDef) {
1098
+ throw new ResourceNotFoundError(toolName, 'Tool');
1099
+ }
1100
+ args = applyParameterDefaults(toolDef, rawArgs);
1101
+ const toolFilter = this.getToolFilterForSession(sessionId, profileId);
1102
+ if (toolFilter && !toolFilter.allowedToolNames.has(toolName)) {
1103
+ this.recordToolFilterRejection(toolName, 'session');
1104
+ const reason = toolFilter.reasons.get(toolName)?.[0];
1105
+ const reasonSuffix = reason ? ` Blocked by: ${reason}.` : '';
1106
+ throw new AuthorizationError(`Tool '${toolName}' not allowed by X-Mcp4-Tools filter.${reasonSuffix}`);
1107
+ }
1108
+ const filtering = this.getFilteringForSession(sessionId, profileId);
1109
+ if (filtering) {
1110
+ const operation = this.getFilteringOperationInfo(toolDef, args);
1111
+ enforceFiltering({
1112
+ filtering,
1113
+ toolDef,
1114
+ args,
1115
+ parameterAliases: this.profile?.parameter_aliases,
1116
+ operation,
1117
+ });
1118
+ }
1119
+ // Execute tool (reuse existing execution logic)
1120
+ let result;
1121
+ if (toolDef.composite && toolDef.steps) {
1122
+ const httpClient = await this.getHttpClientForSession(sessionId, profileId);
1123
+ const compositeResult = await this.compositeExecutor.execute(toolDef.steps, args, toolDef.partial_results || false, httpClient);
1124
+ result = {
1125
+ data: compositeResult.data,
1126
+ completed_steps: compositeResult.completed_steps,
1127
+ total_steps: compositeResult.total_steps,
1128
+ success: compositeResult.completed_steps === compositeResult.total_steps,
1129
+ errors: compositeResult.errors,
1130
+ };
1131
+ }
1132
+ else {
1133
+ result = await this.executeSimpleTool(toolDef, args, sessionId, profileId);
1134
+ }
1135
+ return {
1136
+ jsonrpc: '2.0',
1137
+ id: req.id,
1138
+ result: {
1139
+ content: [
1140
+ {
1141
+ type: 'text',
1142
+ text: JSON.stringify(result, null, 2),
1143
+ },
1144
+ ],
1145
+ },
1146
+ };
1147
+ }
1148
+ catch (error) {
1149
+ // Generate correlation ID only on error (lazy)
1150
+ const correlationId = generateCorrelationId();
1151
+ // Log internal error details with correlation ID
1152
+ this.logger.error('Tool call error', error, {
1153
+ correlationId,
1154
+ toolName,
1155
+ action: args?.action,
1156
+ resourceType: args?.resource_type,
1157
+ sessionId
1158
+ });
1159
+ // Return user-friendly error message with correlation ID
1160
+ const errorMessage = this.formatErrorForClient(error, correlationId);
1161
+ // Map error type to JSON-RPC error code
1162
+ let errorCode = -32603; // Internal error (default)
1163
+ if (error instanceof AuthenticationError) {
1164
+ errorCode = -32001; // Authentication error
1165
+ }
1166
+ else if (error instanceof AuthorizationError) {
1167
+ errorCode = -32002; // Authorization error
1168
+ }
1169
+ else if (error instanceof ValidationError) {
1170
+ errorCode = -32602; // Invalid params
1171
+ }
1172
+ else if (error instanceof RateLimitError) {
1173
+ errorCode = -32003; // Rate limit error
1174
+ }
1175
+ else if (error instanceof OperationNotFoundError) {
1176
+ errorCode = -32601; // Method not found
1177
+ }
1178
+ else if (error instanceof ResourceNotFoundError) {
1179
+ errorCode = -32601; // Method not found
1180
+ }
1181
+ return {
1182
+ jsonrpc: '2.0',
1183
+ id: req.id,
1184
+ error: {
1185
+ code: errorCode,
1186
+ message: errorMessage,
1187
+ },
1188
+ };
1189
+ }
1190
+ }
1191
+ getFilteringForSession(sessionId, profileId) {
1192
+ if (this.httpTransport && sessionId) {
1193
+ const effectiveProfileId = profileId || this.getProfileIdValue();
1194
+ return this.httpTransport.getSessionFiltering(effectiveProfileId, sessionId);
1195
+ }
1196
+ return this.stdioFiltering;
1197
+ }
1198
+ getToolFilterForSession(sessionId, profileId) {
1199
+ if (this.httpTransport && sessionId && typeof this.httpTransport.getSessionToolFilter === 'function') {
1200
+ const effectiveProfileId = profileId || this.getProfileIdValue();
1201
+ return this.httpTransport.getSessionToolFilter(effectiveProfileId, sessionId);
1202
+ }
1203
+ return undefined;
1204
+ }
1205
+ getFilteringOperationInfo(toolDef, args) {
1206
+ if (toolDef.composite) {
1207
+ return undefined;
1208
+ }
1209
+ const operationId = this.toolGenerator.mapActionToOperation(toolDef, args);
1210
+ if (!operationId) {
1211
+ return undefined;
1212
+ }
1213
+ return this.parser.getOperation(operationId);
1214
+ }
1215
+ async handleOtherRequest(message, sessionId, profileId) {
1216
+ const req = message;
1217
+ // Check OAuth authentication for other operations (like tools/list)
1218
+ if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
1219
+ const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
1220
+ if (!authToken) {
1221
+ // Return OAuth required error with WWW-Authenticate header
1222
+ // This should trigger the OAuth flow in the client
1223
+ const errorResponse = {
1224
+ jsonrpc: '2.0',
1225
+ id: req.id,
1226
+ error: {
1227
+ code: -32001, // Application error
1228
+ message: 'Authentication required. Please authorize via OAuth.',
1229
+ data: {
1230
+ oauth_required: true,
1231
+ resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
1232
+ scope: 'api'
1233
+ }
1234
+ }
1235
+ };
1236
+ return errorResponse;
1237
+ }
1238
+ }
1239
+ // Handle tools/list
1240
+ if (req.method === 'tools/list') {
1241
+ const sessionFilter = this.getToolFilterForSession(sessionId, profileId);
1242
+ const allowedSet = sessionFilter?.allowedToolNames;
1243
+ const tools = this.profile?.tools
1244
+ .filter(toolDef => !allowedSet || allowedSet.has(toolDef.name))
1245
+ .map(toolDef => this.toolGenerator.generateTool(toolDef)) || [];
1246
+ return {
1247
+ jsonrpc: '2.0',
1248
+ id: req.id,
1249
+ result: {
1250
+ tools,
1251
+ },
1252
+ };
1253
+ }
1254
+ // Unknown method
1255
+ return {
1256
+ jsonrpc: '2.0',
1257
+ id: req.id,
1258
+ error: {
1259
+ code: -32601,
1260
+ message: `Method not found: ${req.method}`,
1261
+ },
1262
+ };
1263
+ }
1264
+ applyGlobalToolFiltering() {
1265
+ if (!this.profile) {
1266
+ return;
1267
+ }
1268
+ // Initialize ToolFilterService if not already done
1269
+ if (!this.toolFilterService) {
1270
+ const validator = new RegexValidator();
1271
+ const compiler = new RegexCompiler(validator);
1272
+ const envParser = new EnvConfigParser(compiler);
1273
+ const headerParser = new HeaderConfigParser(compiler);
1274
+ // Create OperationDetector for category filtering
1275
+ const classifier = new OperationClassifier();
1276
+ const resolver = new OpenAPIOperationResolver(this.parser);
1277
+ const detector = new OperationDetector(classifier, resolver);
1278
+ this.toolFilterService = new ToolFilterService(envParser, headerParser, this.logger, detector);
1279
+ }
1280
+ const originalTools = this.profile.tools;
1281
+ const originalCount = originalTools.length;
1282
+ // Apply filtering using new service
1283
+ const filteredTools = this.toolFilterService.applyGlobalFilter(originalTools, process.env);
1284
+ const allowedCount = filteredTools.length;
1285
+ const removedCount = originalCount - allowedCount;
1286
+ // Early return if no filtering config present (service returned same tools)
1287
+ if (filteredTools === originalTools) {
1288
+ return;
1289
+ }
1290
+ // Validation: check if filter has no effect
1291
+ if (originalCount > 0 && allowedCount === originalCount && removedCount === 0) {
1292
+ throw new ConfigurationError(`Tool filter configuration has no effect. Original tool count: ${originalCount}, filtered: ${allowedCount}. Check MCP4_TOOL_FILTER_* patterns.`);
1293
+ }
1294
+ // Validation: check if all tools filtered
1295
+ if (originalCount > 0 && allowedCount === 0) {
1296
+ throw new ConfigurationError(`All tools filtered out (original: ${originalCount}). Check MCP4_TOOL_FILTER_* settings.`);
1297
+ }
1298
+ // Validate composite tools against filtered operations
1299
+ const resolver = this.buildToolFilterResolver();
1300
+ this.validateCompositeToolsAgainstFilteredOperations(originalTools, filteredTools, resolver);
1301
+ // Update profile
1302
+ this.profile.tools = filteredTools;
1303
+ // Record summary for metrics
1304
+ this.globalToolFilterSummary = {
1305
+ originalCount,
1306
+ allowedCount,
1307
+ removedCount,
1308
+ patternCounts: {
1309
+ // Note: counts not available from new service, using simplified version
1310
+ filtered: removedCount
1311
+ }
1312
+ };
1313
+ // Warn if high percentage filtered
1314
+ const warnThreshold = this.getToolFilterWarnThresholdPct();
1315
+ if (originalCount > 0) {
1316
+ const percentFiltered = (removedCount / originalCount) * 100;
1317
+ if (percentFiltered >= warnThreshold) {
1318
+ this.logger.warn('Tool filter removed high percentage of tools', {
1319
+ original: originalCount,
1320
+ surviving: allowedCount,
1321
+ threshold_pct: warnThreshold,
1322
+ removed_count: removedCount
1323
+ });
1324
+ }
1325
+ }
1326
+ if (this.httpTransport) {
1327
+ this.recordGlobalToolFilterMetrics();
1328
+ }
1329
+ }
1330
+ applySessionToolFiltering(sessionId, profileId) {
1331
+ if (!this.httpTransport || !this.profile) {
1332
+ return;
1333
+ }
1334
+ if (typeof this.httpTransport.getSessionToolFilterRequest !== 'function') {
1335
+ return;
1336
+ }
1337
+ const effectiveProfileId = profileId || this.getProfileIdValue();
1338
+ const request = this.httpTransport.getSessionToolFilterRequest(effectiveProfileId, sessionId);
1339
+ if (!request) {
1340
+ return;
1341
+ }
1342
+ const originalCount = this.profile.tools.length;
1343
+ const resolver = this.buildToolFilterResolver();
1344
+ const sessionFilter = applySessionToolFilter(this.profile.tools, request, resolver);
1345
+ const allowedCount = sessionFilter.allowedToolNames.size;
1346
+ if (allowedCount === originalCount) {
1347
+ throw new ValidationError(`X-Mcp4-Tools filter has no effect for this session. Available tools: ${originalCount}, after filter: ${allowedCount}. Check patterns.`);
1348
+ }
1349
+ if (originalCount > 0 && allowedCount === 0) {
1350
+ const sources = request.rawEntries.length > 0 ? request.rawEntries.join(', ') : 'none';
1351
+ throw new ValidationError(`X-Mcp4-Tools filtered out all tools (original: ${originalCount}). Removed by: ${sources}. Check session filter configuration.`);
1352
+ }
1353
+ this.httpTransport.setSessionToolFilter(effectiveProfileId, sessionId, sessionFilter);
1354
+ this.logger.info('Session tool filter applied', {
1355
+ sessionId,
1356
+ originalCount,
1357
+ allowedCount,
1358
+ patterns: request.rawEntries,
1359
+ });
1360
+ this.recordSessionToolFilterMetrics(sessionId, allowedCount, request);
1361
+ }
1362
+ buildToolFilterResolver() {
1363
+ return {
1364
+ getOperationById: (operationId) => this.parser.getOperation(operationId),
1365
+ getOperationForCall: (call) => {
1366
+ const [method, path] = call.split(' ');
1367
+ if (!method || !path) {
1368
+ return undefined;
1369
+ }
1370
+ const pathInfo = this.parser.getPath(path);
1371
+ return pathInfo?.operations[method.toLowerCase()];
1372
+ },
1373
+ };
1374
+ }
1375
+ validateCompositeToolsAgainstFilteredOperations(originalTools, allowedTools, resolver) {
1376
+ const operationToTools = new Map();
1377
+ for (const tool of originalTools) {
1378
+ if (!tool.operations) {
1379
+ continue;
1380
+ }
1381
+ for (const operationId of Object.values(tool.operations)) {
1382
+ if (typeof operationId !== 'string') {
1383
+ continue;
1384
+ }
1385
+ const names = operationToTools.get(operationId) ?? [];
1386
+ names.push(tool.name);
1387
+ operationToTools.set(operationId, names);
1388
+ }
1389
+ }
1390
+ const allowedOperationIds = new Set();
1391
+ for (const tool of allowedTools) {
1392
+ if (!tool.operations) {
1393
+ continue;
1394
+ }
1395
+ for (const operationId of Object.values(tool.operations)) {
1396
+ if (typeof operationId !== 'string') {
1397
+ continue;
1398
+ }
1399
+ allowedOperationIds.add(operationId);
1400
+ }
1401
+ }
1402
+ for (const tool of allowedTools) {
1403
+ if (!tool.composite || !tool.steps) {
1404
+ continue;
1405
+ }
1406
+ for (const step of tool.steps) {
1407
+ const operation = resolver.getOperationForCall(step.call);
1408
+ if (!operation) {
1409
+ continue;
1410
+ }
1411
+ if (allowedOperationIds.has(operation.operationId)) {
1412
+ continue;
1413
+ }
1414
+ const removedTools = operationToTools.get(operation.operationId);
1415
+ if (!removedTools || removedTools.length === 0) {
1416
+ continue;
1417
+ }
1418
+ const removedList = removedTools.join(', ');
1419
+ throw new ConfigurationError(`Composite tool '${tool.name}' step '${step.call}' calls filtered tool '${removedList}'. ` +
1420
+ `Add '${removedList}' to filter or include _allow_list or _allow_read if it is a list or read operation.`);
1421
+ }
1422
+ }
1423
+ }
1424
+ getToolFilterWarnThresholdPct() {
1425
+ const raw = process.env.MCP4_TOOL_FILTER_WARN_THRESHOLD_PCT;
1426
+ if (raw === undefined) {
1427
+ return 90;
1428
+ }
1429
+ const parsed = Number(raw);
1430
+ if (Number.isNaN(parsed) || parsed <= 0) {
1431
+ throw new ConfigurationError(`Invalid MCP4_TOOL_FILTER_WARN_THRESHOLD_PCT: expected positive number, got '${raw}'.`);
1432
+ }
1433
+ return parsed;
1434
+ }
1435
+ recordGlobalToolFilterMetrics() {
1436
+ if (!this.httpTransport || !this.globalToolFilterSummary) {
1437
+ return;
1438
+ }
1439
+ if (typeof this.httpTransport.recordGlobalToolFilterMetrics !== 'function') {
1440
+ return;
1441
+ }
1442
+ this.httpTransport.recordGlobalToolFilterMetrics(this.globalToolFilterSummary);
1443
+ }
1444
+ recordSessionToolFilterMetrics(sessionId, allowedCount, request) {
1445
+ if (!this.httpTransport) {
1446
+ return;
1447
+ }
1448
+ if (typeof this.httpTransport.recordSessionToolFilterMetrics !== 'function') {
1449
+ return;
1450
+ }
1451
+ this.httpTransport.recordSessionToolFilterMetrics(sessionId, allowedCount, request);
1452
+ }
1453
+ recordToolFilterRejection(toolName, source) {
1454
+ if (!this.httpTransport) {
1455
+ return;
1456
+ }
1457
+ if (typeof this.httpTransport.recordToolFilterRejection !== 'function') {
1458
+ return;
1459
+ }
1460
+ this.httpTransport.recordToolFilterRejection(toolName, source);
1461
+ }
1462
+ /**
1463
+ * Stop the MCP server gracefully
1464
+ *
1465
+ * Why: Cleanup resources, close connections, allow graceful shutdown
1466
+ */
1467
+ async stop() {
1468
+ if (this.httpTransport) {
1469
+ await this.httpTransport.stop();
1470
+ }
1471
+ }
1472
+ }
1473
+ //# sourceMappingURL=mcp-server.js.map