akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (388) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/base/symbols.ts +4 -0
  4. package/client/clientRuntime.ts +8 -0
  5. package/client/cn.ts +7 -0
  6. package/client/csrTypes.ts +2 -0
  7. package/client/frameDebug.ts +3 -2
  8. package/common/Logger.ts +4 -0
  9. package/common/deepObjectify.ts +21 -7
  10. package/common/index.ts +14 -0
  11. package/common/mcpExposure.ts +99 -0
  12. package/common/pathSet.ts +17 -5
  13. package/common/routeConvention.ts +30 -0
  14. package/constant/crystalize.ts +3 -1
  15. package/constant/fieldInfo.ts +6 -0
  16. package/constant/getDefault.ts +37 -9
  17. package/constant/immerify.ts +1 -1
  18. package/constant/index.ts +2 -0
  19. package/constant/labelOf.ts +17 -0
  20. package/constant/mask.ts +60 -0
  21. package/constant/purify.ts +17 -11
  22. package/constant/types.ts +14 -12
  23. package/dictionary/DictionaryLookup.ts +30 -0
  24. package/dictionary/agent.dictionary.ts +31 -0
  25. package/dictionary/agentTurn.dictionary.ts +29 -0
  26. package/dictionary/base.dictionary.ts +8 -0
  27. package/dictionary/dictInfo.ts +8 -0
  28. package/dictionary/dictionary.ts +9 -3
  29. package/dictionary/index.ts +1 -0
  30. package/document/database.ts +21 -0
  31. package/document/filterMeta.ts +7 -0
  32. package/document/index.ts +1 -0
  33. package/document/into.ts +7 -1
  34. package/document/noDocumentError.ts +12 -0
  35. package/fetch/agentTurn.ts +19 -0
  36. package/fetch/client/fetchClient.ts +71 -13
  37. package/fetch/client/wsClient.ts +17 -2
  38. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  39. package/fetch/index.ts +1 -0
  40. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  41. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  42. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  43. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
  44. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  45. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
  46. package/package.json +11 -9
  47. package/server/akanApp.ts +30 -34
  48. package/server/akanOption.ts +65 -7
  49. package/server/akanServer.ts +172 -7
  50. package/server/animatedImage.ts +83 -0
  51. package/server/artifact/routeClientCache.ts +14 -0
  52. package/server/assetEncoding.ts +47 -0
  53. package/server/devtools/signalSerializer.ts +2 -2
  54. package/server/devtools/types.ts +2 -2
  55. package/server/di/diLifecycle.ts +31 -3
  56. package/server/di/predefinedAdaptor.ts +6 -0
  57. package/server/imageOptimizer.ts +33 -46
  58. package/server/index.ts +1 -2
  59. package/server/mcp/McpAuth.ts +177 -0
  60. package/server/mcp/McpDispatcher.ts +245 -0
  61. package/server/mcp/McpEventStream.ts +76 -0
  62. package/server/mcp/McpExecutionContext.ts +105 -0
  63. package/server/mcp/McpRouter.ts +591 -0
  64. package/server/mcp/index.ts +5 -0
  65. package/server/resolver/CascadeRunner.ts +1 -1
  66. package/server/resolver/database.resolver.ts +42 -5
  67. package/server/resolver/service.resolver.ts +29 -0
  68. package/server/resolver/signal.resolver.ts +16 -1
  69. package/server/routeTreeBuilder.ts +2 -44
  70. package/server/rscWorker.tsx +6 -2
  71. package/server/subRouteIndexDocument.tsx +61 -0
  72. package/server/systemPageDocument.tsx +2 -2
  73. package/server/systemPages.tsx +49 -16
  74. package/server/vendor/akanjs-fetch.ts +1 -0
  75. package/server/webRouter.ts +159 -35
  76. package/service/agent.service.ts +14 -0
  77. package/service/base.service.ts +2 -1
  78. package/service/index.ts +1 -0
  79. package/service/predefinedAdaptor/database.adaptor.ts +78 -24
  80. package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
  81. package/service/predefinedAdaptor/index.ts +3 -0
  82. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  83. package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
  84. package/service/predefinedAdaptor/role.adaptor.ts +2 -0
  85. package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
  86. package/service/serviceModule.ts +17 -0
  87. package/service/types.ts +4 -0
  88. package/signal/agent/AgentCatalogue.ts +165 -0
  89. package/signal/agent/index.ts +1 -0
  90. package/signal/agent.signal.ts +42 -0
  91. package/signal/agentTurn.ts +8 -0
  92. package/signal/agentTurnStream.ts +48 -0
  93. package/signal/base.signal.ts +2 -1
  94. package/signal/endpointInfo.ts +38 -2
  95. package/signal/guard.ts +13 -1
  96. package/signal/guards.ts +52 -1
  97. package/signal/index.ts +6 -0
  98. package/signal/mcp/McpDocument.ts +286 -0
  99. package/signal/mcp/McpProgress.ts +106 -0
  100. package/signal/mcp/McpUriTemplate.ts +85 -0
  101. package/signal/mcp/Msg.ts +368 -0
  102. package/signal/mcp/index.ts +5 -0
  103. package/signal/mcp/mcpProtocol.ts +120 -0
  104. package/signal/middleware.ts +7 -3
  105. package/signal/openapi/openapi.ts +32 -171
  106. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  107. package/signal/schema/index.ts +1 -0
  108. package/signal/signalContext.ts +85 -9
  109. package/signal/slice.ts +9 -9
  110. package/signal/types.ts +1 -1
  111. package/store/action.ts +11 -22
  112. package/store/actionTag.ts +33 -0
  113. package/store/agent/AgentBridge.ts +323 -0
  114. package/store/agent/AgentContext.ts +74 -0
  115. package/store/agent/AgentPrompts.ts +82 -0
  116. package/store/agent/AgentVisibility.ts +68 -0
  117. package/store/agent/ScreenReader.ts +208 -0
  118. package/store/agent/StoreCatalogue.ts +351 -0
  119. package/store/agent/StoreSurfaceSource.ts +122 -0
  120. package/store/agent/index.ts +9 -0
  121. package/store/agent/storeSurface.ts +28 -0
  122. package/store/agent/types.ts +50 -0
  123. package/store/agentic/StToolBuilder.ts +199 -0
  124. package/store/agentic/attachAgentic.ts +22 -0
  125. package/store/agentic/index.ts +5 -0
  126. package/store/agentic/readableValue.ts +17 -0
  127. package/store/agentic/useStExpose.ts +18 -0
  128. package/store/agentic/useStState.ts +35 -0
  129. package/store/baseSt.ts +7 -0
  130. package/store/databaseStateNames.ts +31 -0
  131. package/store/formSetterNames.ts +21 -0
  132. package/store/index.ts +8 -0
  133. package/store/rootStore.ts +3 -1
  134. package/store/sliceRole.ts +36 -0
  135. package/store/state.ts +2 -12
  136. package/store/store.ts +19 -1
  137. package/store/storeInstance.ts +165 -20
  138. package/store/storeRegistry.ts +16 -1
  139. package/store/types.ts +18 -0
  140. package/store/withSelector.ts +7 -8
  141. package/test/registerDom.ts +4 -0
  142. package/types/base/symbols.d.ts +4 -0
  143. package/types/client/cn.d.ts +5 -0
  144. package/types/client/csrTypes.d.ts +2 -0
  145. package/types/common/Logger.d.ts +2 -0
  146. package/types/common/deepObjectify.d.ts +4 -2
  147. package/types/common/index.d.ts +2 -1
  148. package/types/common/mcpExposure.d.ts +60 -0
  149. package/types/common/routeConvention.d.ts +8 -0
  150. package/types/constant/fieldInfo.d.ts +1 -0
  151. package/types/constant/getDefault.d.ts +5 -0
  152. package/types/constant/index.d.ts +2 -0
  153. package/types/constant/labelOf.d.ts +6 -0
  154. package/types/constant/mask.d.ts +34 -0
  155. package/types/constant/purify.d.ts +2 -2
  156. package/types/constant/types.d.ts +2 -2
  157. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  158. package/types/dictionary/agent.dictionary.d.ts +1 -0
  159. package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
  160. package/types/dictionary/base.dictionary.d.ts +1 -1
  161. package/types/dictionary/dictInfo.d.ts +6 -0
  162. package/types/dictionary/dictionary.d.ts +11 -9
  163. package/types/dictionary/index.d.ts +1 -0
  164. package/types/document/database.d.ts +20 -0
  165. package/types/document/filterMeta.d.ts +1 -0
  166. package/types/document/index.d.ts +1 -0
  167. package/types/document/into.d.ts +6 -1
  168. package/types/document/noDocumentError.d.ts +12 -0
  169. package/types/fetch/agentTurn.d.ts +593 -0
  170. package/types/fetch/client/fetchClient.d.ts +10 -0
  171. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  172. package/types/fetch/index.d.ts +1 -0
  173. package/types/server/akanOption.d.ts +35 -7
  174. package/types/server/akanServer.d.ts +44 -1
  175. package/types/server/animatedImage.d.ts +7 -0
  176. package/types/server/artifact/routeClientCache.d.ts +6 -0
  177. package/types/server/assetEncoding.d.ts +7 -0
  178. package/types/server/devtools/types.d.ts +2 -2
  179. package/types/server/di/predefinedAdaptor.d.ts +4 -1
  180. package/types/server/index.d.ts +0 -2
  181. package/types/server/mcp/McpAuth.d.ts +62 -0
  182. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  183. package/types/server/mcp/McpEventStream.d.ts +18 -0
  184. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  185. package/types/server/mcp/McpRouter.d.ts +60 -0
  186. package/types/server/mcp/index.d.ts +5 -0
  187. package/types/server/mcp.d.ts +1 -0
  188. package/types/server/subRouteIndexDocument.d.ts +8 -0
  189. package/types/server/systemPageDocument.d.ts +1 -0
  190. package/types/server/systemPages.d.ts +5 -0
  191. package/types/server/vendor/akanjs-fetch.d.ts +1 -0
  192. package/types/service/agent.service.d.ts +12 -0
  193. package/types/service/base.service.d.ts +4 -0
  194. package/types/service/index.d.ts +1 -0
  195. package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
  196. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
  197. package/types/service/predefinedAdaptor/index.d.ts +3 -0
  198. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  199. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
  200. package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
  201. package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
  202. package/types/service/types.d.ts +4 -1
  203. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  204. package/types/signal/agent/index.d.ts +1 -0
  205. package/types/signal/agent.d.ts +1 -0
  206. package/types/signal/agent.signal.d.ts +29 -0
  207. package/types/signal/agentTurn.d.ts +9 -0
  208. package/types/signal/agentTurnStream.d.ts +16 -0
  209. package/types/signal/base.signal.d.ts +1 -1
  210. package/types/signal/endpointInfo.d.ts +21 -3
  211. package/types/signal/guard.d.ts +11 -0
  212. package/types/signal/guards.d.ts +22 -1
  213. package/types/signal/index.d.ts +6 -0
  214. package/types/signal/mcp/McpDocument.d.ts +65 -0
  215. package/types/signal/mcp/McpProgress.d.ts +40 -0
  216. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  217. package/types/signal/mcp/Msg.d.ts +143 -0
  218. package/types/signal/mcp/index.d.ts +5 -0
  219. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  220. package/types/signal/mcp.d.ts +1 -0
  221. package/types/signal/openapi/openapi.d.ts +3 -3
  222. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  223. package/types/signal/schema/index.d.ts +1 -0
  224. package/types/signal/schema.d.ts +1 -0
  225. package/types/signal/signalContext.d.ts +16 -1
  226. package/types/signal/slice.d.ts +8 -8
  227. package/types/signal/types.d.ts +1 -1
  228. package/types/store/actionTag.d.ts +21 -0
  229. package/types/store/agent/AgentBridge.d.ts +77 -0
  230. package/types/store/agent/AgentContext.d.ts +16 -0
  231. package/types/store/agent/AgentPrompts.d.ts +31 -0
  232. package/types/store/agent/AgentVisibility.d.ts +21 -0
  233. package/types/store/agent/ScreenReader.d.ts +11 -0
  234. package/types/store/agent/StoreCatalogue.d.ts +26 -0
  235. package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
  236. package/types/store/agent/index.d.ts +9 -0
  237. package/types/store/agent/storeSurface.d.ts +14 -0
  238. package/types/store/agent/types.d.ts +49 -0
  239. package/types/store/agent.d.ts +1 -0
  240. package/types/store/agentic/StToolBuilder.d.ts +41 -0
  241. package/types/store/agentic/attachAgentic.d.ts +14 -0
  242. package/types/store/agentic/index.d.ts +5 -0
  243. package/types/store/agentic/readableValue.d.ts +3 -0
  244. package/types/store/agentic/useStExpose.d.ts +8 -0
  245. package/types/store/agentic/useStState.d.ts +12 -0
  246. package/types/store/agentic.d.ts +1 -0
  247. package/types/store/baseSt.d.ts +29 -22
  248. package/types/store/databaseStateNames.d.ts +25 -0
  249. package/types/store/formSetterNames.d.ts +16 -0
  250. package/types/store/index.d.ts +7 -0
  251. package/types/store/rootStore.d.ts +5 -1
  252. package/types/store/sliceRole.d.ts +25 -0
  253. package/types/store/store.d.ts +7 -2
  254. package/types/store/storeInstance.d.ts +33 -1
  255. package/types/store/storeRegistry.d.ts +5 -0
  256. package/types/store/types.d.ts +15 -0
  257. package/types/store/withSelector.d.ts +7 -8
  258. package/types/test/registerDom.d.ts +1 -0
  259. package/types/ui/Agent/Approval.d.ts +7 -0
  260. package/types/ui/Agent/Bubble.d.ts +13 -0
  261. package/types/ui/Agent/Chat.d.ts +24 -0
  262. package/types/ui/Agent/Context.d.ts +9 -0
  263. package/types/ui/Agent/Dock.d.ts +16 -0
  264. package/types/ui/Agent/Guide.d.ts +9 -0
  265. package/types/ui/Agent/Section.d.ts +10 -0
  266. package/types/ui/Agent/StateKey.d.ts +16 -0
  267. package/types/ui/Agent/Tool.d.ts +15 -0
  268. package/types/ui/Agent/Transcript.d.ts +13 -0
  269. package/types/ui/Agent/Zone.d.ts +23 -0
  270. package/types/ui/Agent/fetchRunner.d.ts +12 -0
  271. package/types/ui/Agent/index.d.ts +17 -0
  272. package/types/ui/Agent/index_.d.ts +1 -0
  273. package/types/ui/Agent/sessionHistory.d.ts +12 -0
  274. package/types/ui/Agent.d.ts +1 -0
  275. package/types/ui/Badge.d.ts +1 -1
  276. package/types/ui/Button.d.ts +1 -1
  277. package/types/ui/CsrImage.d.ts +1 -1
  278. package/types/ui/Dropdown.d.ts +2 -0
  279. package/types/ui/Image.d.ts +3 -3
  280. package/types/ui/Layout/BottomInset.d.ts +2 -1
  281. package/types/ui/Layout/index.d.ts +1 -1
  282. package/types/ui/Signal/style.d.ts +4 -1
  283. package/types/ui/UiOverride/context.d.ts +14 -2
  284. package/types/ui/agentAttrs.d.ts +14 -0
  285. package/types/ui/index.d.ts +4 -1
  286. package/types/ui/overlayLayer.d.ts +24 -0
  287. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  288. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  289. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  290. package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
  291. package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
  292. package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
  293. package/types/vendor/use-agentic/Agentic.d.ts +21 -0
  294. package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
  295. package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
  296. package/types/vendor/use-agentic/index.d.ts +13 -0
  297. package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
  298. package/types/vendor/use-agentic/types.d.ts +133 -0
  299. package/types/vendor/use-agentic/useAgent.d.ts +4 -0
  300. package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
  301. package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
  302. package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
  303. package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
  304. package/types/vendor/use-agentic.d.ts +1 -0
  305. package/types/webkit/index.d.ts +3 -0
  306. package/types/webkit/lazy.d.ts +12 -0
  307. package/types/webkit/useCsrValues.d.ts +3 -3
  308. package/types/webkit/useEscapeKey.d.ts +5 -0
  309. package/types/webkit/useFrameRuntime.d.ts +6 -1
  310. package/types/webkit/useScreenScope.d.ts +18 -0
  311. package/ui/Agent/Approval.tsx +29 -0
  312. package/ui/Agent/Bubble.tsx +74 -0
  313. package/ui/Agent/Chat.tsx +225 -0
  314. package/ui/Agent/Context.tsx +38 -0
  315. package/ui/Agent/Dock.tsx +72 -0
  316. package/ui/Agent/Guide.tsx +16 -0
  317. package/ui/Agent/Section.tsx +24 -0
  318. package/ui/Agent/StateKey.tsx +44 -0
  319. package/ui/Agent/Tool.tsx +66 -0
  320. package/ui/Agent/Transcript.tsx +33 -0
  321. package/ui/Agent/Zone.tsx +48 -0
  322. package/ui/Agent/fetchRunner.ts +30 -0
  323. package/ui/Agent/index.ts +12 -0
  324. package/ui/Agent/index_.tsx +4 -0
  325. package/ui/Agent/sessionHistory.ts +33 -0
  326. package/ui/Badge.tsx +3 -3
  327. package/ui/BottomSheet.tsx +5 -0
  328. package/ui/Button.tsx +6 -2
  329. package/ui/Constant/Doc.tsx +1 -1
  330. package/ui/CsrImage.tsx +1 -1
  331. package/ui/Data/ListContainer.tsx +1 -1
  332. package/ui/DatePicker.tsx +5 -4
  333. package/ui/Dialog/Modal.tsx +12 -15
  334. package/ui/DraggableList.tsx +3 -1
  335. package/ui/Dropdown.tsx +33 -11
  336. package/ui/Field.tsx +30 -16
  337. package/ui/Image.tsx +3 -3
  338. package/ui/Input.tsx +14 -5
  339. package/ui/Layout/BottomInset.tsx +6 -1
  340. package/ui/Load/Units.tsx +11 -2
  341. package/ui/Load/View.tsx +7 -2
  342. package/ui/Loading/ProgressBar.tsx +8 -1
  343. package/ui/Menu.tsx +7 -8
  344. package/ui/Model/EditModal.tsx +37 -2
  345. package/ui/Model/SureToRemove.tsx +2 -1
  346. package/ui/Model/index_.tsx +42 -15
  347. package/ui/ObjectId.tsx +3 -4
  348. package/ui/Pagination.tsx +3 -4
  349. package/ui/Popconfirm.tsx +8 -7
  350. package/ui/Select.tsx +6 -4
  351. package/ui/Signal/RestApi.tsx +43 -16
  352. package/ui/Signal/WebSocket.tsx +1 -1
  353. package/ui/Signal/style.ts +6 -1
  354. package/ui/Switch.tsx +2 -0
  355. package/ui/System/CSR.tsx +6 -2
  356. package/ui/System/ThemeToggle.tsx +55 -33
  357. package/ui/ToggleSelect.tsx +4 -3
  358. package/ui/Tooltip.tsx +2 -1
  359. package/ui/UiOverride/context.ts +14 -2
  360. package/ui/agentAttrs.ts +19 -0
  361. package/ui/index.ts +10 -1
  362. package/ui/overlayLayer.ts +39 -0
  363. package/ui/recipe/badgeRecipe.ts +22 -3
  364. package/ui/recipe/buttonRecipe.ts +51 -2
  365. package/ui/recipe/factory.ts +2 -2
  366. package/ui/recipe/inputRecipe.ts +18 -4
  367. package/vendor/use-agentic/AgentProvider.tsx +39 -0
  368. package/vendor/use-agentic/AgentScope.tsx +20 -0
  369. package/vendor/use-agentic/AgentSession.ts +292 -0
  370. package/vendor/use-agentic/Agentic.tsx +44 -0
  371. package/vendor/use-agentic/AgenticSurface.ts +294 -0
  372. package/vendor/use-agentic/WIRE.md +57 -0
  373. package/vendor/use-agentic/httpRunner.ts +115 -0
  374. package/vendor/use-agentic/index.ts +14 -0
  375. package/vendor/use-agentic/surfaceContext.ts +9 -0
  376. package/vendor/use-agentic/types.ts +144 -0
  377. package/vendor/use-agentic/useAgent.ts +17 -0
  378. package/vendor/use-agentic/useAgentGuide.ts +16 -0
  379. package/vendor/use-agentic/useAgentResource.ts +30 -0
  380. package/vendor/use-agentic/useAgentState.ts +61 -0
  381. package/vendor/use-agentic/useAgentTool.ts +55 -0
  382. package/webkit/bootCsr.tsx +2 -33
  383. package/webkit/index.ts +3 -0
  384. package/webkit/lazy.tsx +27 -3
  385. package/webkit/useCsrValues.ts +121 -4
  386. package/webkit/useEscapeKey.tsx +42 -0
  387. package/webkit/useFrameRuntime.ts +65 -32
  388. package/webkit/useScreenScope.tsx +49 -0
@@ -0,0 +1,591 @@
1
+ import type { BaseEnv } from "akanjs/base";
2
+ import { Logger } from "akanjs/common";
3
+ import { DictionaryLookup } from "akanjs/dictionary";
4
+ import type { InjectRegistry, LiveRegistry } from "akanjs/service";
5
+ import {
6
+ MCP_LEGACY_VERSION,
7
+ MCP_META_CLIENT_CAPABILITIES,
8
+ MCP_META_PROTOCOL_VERSION,
9
+ MCP_META_SERVER_INFO,
10
+ MCP_SUPPORTED_VERSIONS,
11
+ McpDocument,
12
+ type McpEra,
13
+ McpErrorCode,
14
+ type McpExposedEndpoint,
15
+ type McpJsonRpcRequest,
16
+ McpProgress,
17
+ type McpToolResult,
18
+ } from "../../signal/mcp";
19
+ import type { MiddlewareCls } from "../../signal/middleware";
20
+ import { FetchSerializer } from "../../signal/serializer";
21
+ import type { HttpRoutes } from "../types";
22
+ import { McpAuth, type McpAuthOption } from "./McpAuth";
23
+ import { McpAuthRequiredError, McpDispatcher, McpPromptError } from "./McpDispatcher";
24
+ import { McpEventStream } from "./McpEventStream";
25
+
26
+ export interface McpRouterProps {
27
+ registry: InjectRegistry;
28
+ env: BaseEnv;
29
+ live: LiveRegistry;
30
+ middleware: Map<string, MiddlewareCls>;
31
+ path?: string;
32
+ version?: string;
33
+ /** Free-text usage guidance handed to the model alongside the tool list. */
34
+ instructions?: string;
35
+ /** Extra origins allowed past the DNS-rebinding check, beyond the server's own host. */
36
+ allowedOrigins?: string[];
37
+ readOnly?: boolean;
38
+ /** Entries per catalogue page. A client that wants the whole list follows `nextCursor` until it stops. */
39
+ pageSize?: number;
40
+ /**
41
+ * The one language every title, description and domain error text is resolved in. The catalogue is built once
42
+ * at boot and cached by clients, so it is a server-wide choice rather than a per-request one: `Accept-Language`
43
+ * would mean a document per language, re-deriving every tool schema, for a surface a model reads and a human
44
+ * rarely sees. Defaults to `en`, falling back to the first registered language when the app has no `en`.
45
+ */
46
+ language?: string;
47
+ auth?: McpAuthOption;
48
+ }
49
+
50
+ interface McpCall {
51
+ id: string | number | null;
52
+ method: string;
53
+ params: Record<string, unknown>;
54
+ era: McpEra;
55
+ req: Request;
56
+ }
57
+
58
+ interface McpErrorOptions {
59
+ status?: number;
60
+ data?: unknown;
61
+ }
62
+
63
+ interface McpCacheHint {
64
+ ttlMs: number;
65
+ cacheScope: "public" | "private";
66
+ }
67
+
68
+ const notAllowed = () => new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
69
+ const defaultPageSize = 100;
70
+ /**
71
+ * Named rather than left to `DictionaryLookup`'s own fallback, which is whichever language registered first —
72
+ * true today only because dictionaries are written `[en, ko]`, and nothing keeps it true.
73
+ */
74
+ const defaultLanguage = "en";
75
+ /**
76
+ * The catalogue is fixed for the life of the process, so the ceiling is how long a client may keep serving a
77
+ * list from a server that has since been redeployed. `private` because the listing is filtered per credential
78
+ * (`filterForAccount`): a shared cache would hand one caller another's view of the shelf.
79
+ */
80
+ const listCache: McpCacheHint = { ttlMs: 300_000, cacheScope: "private" };
81
+ /** Capabilities name what this build implements — they do not vary by caller and change only with the binary. */
82
+ const discoverCache: McpCacheHint = { ttlMs: 3_600_000, cacheScope: "public" };
83
+
84
+ /**
85
+ * Serves the app's signals as one MCP endpoint.
86
+ *
87
+ * Answers both protocol eras from the same stateless handler. `2026-07-28` is stateless by design; the legacy
88
+ * revisions only *offer* sessions — a server may decline to issue `Mcp-Session-Id`, and a client that never
89
+ * receives one neither sends one back nor asks to resume a stream. So the legacy era costs an `initialize` reply
90
+ * and a laxer `_meta` check, not a session store. (Measured against Claude Code 2.1.226, which speaks it.)
91
+ */
92
+ export class McpRouter {
93
+ static readonly logger = new Logger("McpRouter");
94
+
95
+ readonly #props: McpRouterProps;
96
+ readonly #dispatcher: McpDispatcher;
97
+ readonly #auth: McpAuth;
98
+ #document: McpDocument | null = null;
99
+
100
+ constructor(props: McpRouterProps) {
101
+ this.#props = props;
102
+
103
+ this.#dispatcher = new McpDispatcher({ ...props, language: props.language ?? defaultLanguage });
104
+ this.#auth = new McpAuth({ ...props.auth, path: props.path ?? "/mcp" });
105
+ }
106
+
107
+ createRoutes(): HttpRoutes {
108
+ return {
109
+ [this.#props.path ?? "/mcp"]: {
110
+ POST: async (req: Request) => this.#cors(req, await this.#post(req)),
111
+ OPTIONS: (req: Request) => this.#preflight(req),
112
+
113
+ GET: (req: Request) => this.#cors(req, notAllowed()),
114
+ DELETE: (req: Request) => this.#cors(req, notAllowed()),
115
+ },
116
+ ...this.#auth.createRoutes(),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Says once, at boot, what this build actually published — and names every endpoint that was kept out.
122
+ *
123
+ * The rejections are fail-closed by design: an endpoint MCP cannot carry, or whose guards do not admit it, is
124
+ * simply not in the catalogue. That is the right default and the wrong silence, and it matters more now that
125
+ * exposure follows the guards — nobody wrote an opt-in whose absence would explain a missing tool, so this log
126
+ * is the only place the answer exists. A refusal turns on a resolved return type and a resolved guard list, so
127
+ * it reads only from here.
128
+ *
129
+ * An entry published with no description rides here for the same reason: the text every generated entry borrows
130
+ * is a *model* `.desc()`, which no source rule would read as that entry's description. This holds the resolved
131
+ * catalogue, so it can simply look.
132
+ *
133
+ * Called by whatever mounts the router rather than from `createRoutes`, so building a router to answer one
134
+ * request — which tests and tooling do — does not narrate a catalogue nobody asked about.
135
+ */
136
+ report() {
137
+ try {
138
+ const { tools, prompts, resourceTemplates, refusals, undescribed } = this.#getDocument();
139
+ const counts = `tools=${tools.length} prompts=${prompts.length} resourceTemplates=${resourceTemplates.length}`;
140
+ McpRouter.logger.info(`MCP catalogue: ${counts}${this.#props.readOnly ? " (read-only deployment)" : ""}`);
141
+
142
+ if (!tools.length && !prompts.length)
143
+ McpRouter.logger.warn(
144
+ "MCP is enabled but published nothing. Every candidate was refused; see the reasons below.",
145
+ );
146
+ for (const { key, reason } of refusals) McpRouter.logger.warn(`MCP did not expose "${key}": ${reason}`);
147
+ for (const { key, reason } of undescribed)
148
+ McpRouter.logger.warn(`MCP exposed "${key}" with no description: ${reason}`);
149
+ } catch (error) {
150
+
151
+ McpRouter.logger.warn(
152
+ `MCP catalogue could not be built at boot: ${error instanceof Error ? error.message : error}`,
153
+ );
154
+ }
155
+ }
156
+
157
+ /** Rebuilding per request would re-derive every tool schema on a list agents poll; the set is fixed at boot. */
158
+ #getDocument() {
159
+ if (this.#document) return this.#document;
160
+ const lookup = new DictionaryLookup(this.#props.language ?? defaultLanguage);
161
+ this.#document = new McpDocument(FetchSerializer.serializeRegistry(this.#props.live).signal, {
162
+ resolveDescription: (key) => lookup.text(key),
163
+ readOnly: this.#props.readOnly,
164
+ });
165
+ return this.#document;
166
+ }
167
+
168
+ async #post(req: Request) {
169
+ if (!this.#originAllowed(req)) return new Response("Forbidden", { status: 403 });
170
+ const rejected = this.#auth.reject(req);
171
+ if (rejected) return rejected;
172
+ let body: McpJsonRpcRequest;
173
+ try {
174
+ body = (await req.json()) as McpJsonRpcRequest;
175
+ } catch {
176
+ return McpRouter.#error(null, McpErrorCode.parse, "Invalid JSON body.", { status: 400 });
177
+ }
178
+ const id = body.id ?? null;
179
+ const method = body.method;
180
+ if (typeof method !== "string")
181
+ return McpRouter.#error(id, McpErrorCode.invalidRequest, "Missing JSON-RPC method.", { status: 400 });
182
+
183
+ if (method.startsWith("notifications/")) return new Response(null, { status: 202 });
184
+
185
+ const params = (body.params ?? {}) as Record<string, unknown>;
186
+
187
+ const meta = McpRouter.#meta(params);
188
+ const era: McpEra = method !== "initialize" && meta && MCP_META_PROTOCOL_VERSION in meta ? "modern" : "legacy";
189
+ const rejection = era === "modern" ? McpRouter.#validateModern(req, method, params, meta ?? {}, id) : null;
190
+ if (rejection) return rejection;
191
+
192
+ try {
193
+ return await this.#dispatch({ id, method, params, era, req });
194
+ } catch (error) {
195
+ if (error instanceof McpAuthRequiredError) return this.#auth.unauthorized(req);
196
+
197
+ if (error instanceof McpPromptError) return McpRouter.#error(id, error.code, error.message);
198
+ McpRouter.logger.error(
199
+ `MCP ${method} failed: ${error instanceof Error ? (error.stack ?? error.message) : error}`,
200
+ );
201
+ return McpRouter.#error(id, McpErrorCode.internal, "Internal server error.");
202
+ }
203
+ }
204
+
205
+ async #dispatch(call: McpCall) {
206
+ const document = this.#getDocument();
207
+ switch (call.method) {
208
+ case "initialize":
209
+
210
+ return this.#result(call, {
211
+ protocolVersion: McpRouter.#negotiate(call.params.protocolVersion),
212
+ capabilities: McpRouter.#capabilities(document),
213
+ serverInfo: this.#serverInfo(),
214
+ ...(this.#props.instructions ? { instructions: this.#props.instructions } : {}),
215
+ });
216
+ case "ping":
217
+
218
+ return this.#result(call, {});
219
+ case "server/discover":
220
+ return this.#result(
221
+ call,
222
+ {
223
+ supportedVersions: MCP_SUPPORTED_VERSIONS,
224
+ capabilities: McpRouter.#capabilities(document),
225
+ ...(this.#props.instructions ? { instructions: this.#props.instructions } : {}),
226
+ },
227
+ discoverCache,
228
+ );
229
+ case "tools/list":
230
+ return await this.#list(call, "tools", document.tools);
231
+ case "resources/list":
232
+ return await this.#list(call, "resources", document.resources);
233
+ case "resources/templates/list":
234
+ return await this.#list(call, "resourceTemplates", document.resourceTemplates);
235
+ case "prompts/list":
236
+ return await this.#list(call, "prompts", document.prompts);
237
+ case "tools/call":
238
+ return await this.#toolsCall(call, document);
239
+ case "prompts/get":
240
+ return await this.#promptsGet(call, document);
241
+ case "resources/read":
242
+ return await this.#resourcesRead(call, document);
243
+ default:
244
+
245
+ return McpRouter.#error(call.id, McpErrorCode.methodNotFound, McpRouter.#methodNotFound(call.method), {
246
+ status: call.era === "modern" ? 404 : 200,
247
+ });
248
+ }
249
+ }
250
+
251
+ /** Filter first, then page: an offset has to address the list the caller can actually see. */
252
+ async #list<T extends { name: string }>(call: McpCall, key: string, items: T[]) {
253
+ const visible = await this.#dispatcher.filterForAccount(items, call.req);
254
+ const page = McpRouter.#page(visible, call.params.cursor, this.#props.pageSize ?? defaultPageSize);
255
+ if (!page) return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Invalid cursor.");
256
+ const result = { [key]: page.items, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
257
+ return this.#result(call, result, listCache);
258
+ }
259
+
260
+ async #toolsCall(call: McpCall, document: McpDocument) {
261
+ const name = call.params.name;
262
+ if (typeof name !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing tool name.");
263
+ const exposed = document.findTool(name);
264
+
265
+ if (!exposed) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown tool: ${name}.`);
266
+ const args = McpRouter.#arguments(call.params);
267
+ if (!args) return McpRouter.#error(call.id, McpErrorCode.invalidParams, McpRouter.#badArguments);
268
+ const progressToken = McpRouter.#progressToken(call);
269
+ if (progressToken === undefined) return this.#result(call, await this.#dispatcher.call(exposed, args, call.req));
270
+ return await this.#streamedToolCall(call, exposed, args, progressToken);
271
+ }
272
+
273
+ /**
274
+ * Runs the tool and only commits to a stream once it actually reports progress.
275
+ *
276
+ * Deciding late is what keeps the failure modes intact: an HTTP status is fixed the moment the response is
277
+ * returned, so a call that opened a stream up front could no longer answer 401 with a `WWW-Authenticate`
278
+ * challenge. Guards run before an endpoint body can report anything, so by the time this switches to SSE the
279
+ * authorization decision has already been made — which is why the streamed path below need not carry one.
280
+ */
281
+ async #streamedToolCall(
282
+ call: McpCall,
283
+ exposed: McpExposedEndpoint,
284
+ args: Record<string, unknown>,
285
+ progressToken: string | number,
286
+ ) {
287
+ const channel = new McpProgress();
288
+ const settled = McpProgress.run(channel, async () => await this.#dispatcher.call(exposed, args, call.req))
289
+ .then(
290
+ (result) => ({ result }),
291
+ (error: unknown) => ({ error }),
292
+ )
293
+ .finally(() => channel.end());
294
+ const reported = await Promise.race([channel.started.then(() => true), settled.then(() => false)]);
295
+ if (!reported) {
296
+ const outcome = await settled;
297
+ if ("error" in outcome) throw outcome.error;
298
+ return this.#result(call, outcome.result);
299
+ }
300
+ const stream = new McpEventStream(() => channel.abort());
301
+
302
+ void this.#pump(call, channel, settled, stream, progressToken).catch((error: unknown) => {
303
+ McpRouter.logger.error(`MCP stream for ${exposed.key} failed: ${error instanceof Error ? error.stack : error}`);
304
+ });
305
+ return stream.response();
306
+ }
307
+
308
+ async #pump(
309
+ call: McpCall,
310
+ channel: McpProgress,
311
+ settled: Promise<{ result: McpToolResult } | { error: unknown }>,
312
+ stream: McpEventStream,
313
+ progressToken: string | number,
314
+ ) {
315
+ try {
316
+ for await (const report of channel.reports())
317
+ stream.write({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken, ...report } });
318
+ const outcome = await settled;
319
+
320
+ stream.write(
321
+ "error" in outcome
322
+ ? McpRouter.#errorBody(call.id, McpErrorCode.internal, "Internal server error.")
323
+ : this.#envelope(call, outcome.result),
324
+ );
325
+ } finally {
326
+ stream.close();
327
+ }
328
+ }
329
+
330
+ static readonly #badArguments = "`arguments` must be an object of named values.";
331
+
332
+ /**
333
+ * `arguments` is an object or it is not there at all; anything else is the caller's own mistake and is reported
334
+ * as one. Coercing it to `{}` ran the call with every argument missing — a tool that takes none then simply
335
+ * succeeded, and one that takes some answered "Missing required argument", which sends a model looking for a
336
+ * value it did send. An array is refused with the rest: MCP names its arguments, and positional ones would
337
+ * silently be read as the properties `0`, `1`, `2`.
338
+ */
339
+ static #arguments(params: Record<string, unknown>) {
340
+ const args = params.arguments;
341
+ if (args === undefined || args === null) return {};
342
+ if (typeof args !== "object" || Array.isArray(args)) return null;
343
+ return args as Record<string, unknown>;
344
+ }
345
+
346
+ /**
347
+ * Derived from the catalogue rather than fixed, so a server with no prompts does not invite a `prompts/list`
348
+ * that can only come back empty. Read from the unfiltered document on purpose: a capability says what this
349
+ * build implements, and one that narrowed per credential would contradict itself across a cached handshake.
350
+ */
351
+ static #capabilities(document: McpDocument) {
352
+ return {
353
+ ...(document.tools.length ? { tools: {} } : {}),
354
+ ...(document.resources.length || document.resourceTemplates.length ? { resources: {} } : {}),
355
+ ...(document.prompts.length ? { prompts: {} } : {}),
356
+ };
357
+ }
358
+
359
+ /**
360
+ * A client asks for progress by naming a token, and can only receive it over a stream — so both have to be
361
+ * true. Only `tools/call` is offered one: a listing is served from memory, and a prompt is a user-triggered
362
+ * read that a client renders as a slash command rather than something it watches run.
363
+ */
364
+ static #progressToken(call: McpCall) {
365
+ if (!call.req.headers.get("accept")?.includes("text/event-stream")) return undefined;
366
+ const token = McpRouter.#meta(call.params)?.progressToken;
367
+ return typeof token === "string" || typeof token === "number" ? token : undefined;
368
+ }
369
+
370
+ async #promptsGet(call: McpCall, document: McpDocument) {
371
+ const name = call.params.name;
372
+ if (typeof name !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing prompt name.");
373
+ const found = document.findPrompt(name);
374
+ if (!found) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown prompt: ${name}.`);
375
+
376
+ const args = McpRouter.#arguments(call.params);
377
+ if (!args) return McpRouter.#error(call.id, McpErrorCode.invalidParams, McpRouter.#badArguments);
378
+ const missing = (found.prompt.arguments ?? [])
379
+ .filter((arg) => arg.required && args[arg.name] === undefined)
380
+ .map((arg) => arg.name);
381
+ if (missing.length)
382
+ return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Missing prompt arguments: ${missing.join(", ")}.`);
383
+ const messages = await this.#dispatcher.prompt(found.exposed, args, call.req);
384
+ return this.#result(call, {
385
+ ...(found.prompt.description ? { description: found.prompt.description } : {}),
386
+ messages,
387
+ });
388
+ }
389
+
390
+ async #resourcesRead(call: McpCall, document: McpDocument) {
391
+ const uri = call.params.uri;
392
+ if (typeof uri !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing resource uri.");
393
+ const resolved = document.resolveResource(uri);
394
+ if (!resolved) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown resource: ${uri}.`);
395
+ const result = await this.#dispatcher.call(resolved.exposed, resolved.args, call.req);
396
+
397
+ if (result.isError)
398
+ return McpRouter.#error(call.id, McpErrorCode.invalidParams, result.content[0]?.text ?? "Read failed.");
399
+ return this.#result(call, {
400
+ contents: [{ uri, mimeType: "application/json", text: result.content[0]?.text ?? "null" }],
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Cross-origin gate. MCP clients are not browsers and normally send no `Origin` at all, so absence is allowed;
406
+ * a present one has to name this server or an explicitly configured peer.
407
+ *
408
+ * Matching against our own host stops a page served from somewhere else, and that is all it stops — a rebinding
409
+ * attack points its *own* name at this server, so the `Origin` it sends and the host it arrives on agree and it
410
+ * passes. `allowedOrigins` is the list that actually decides who may drive this server from a browser; leave it
411
+ * unset unless a browser-hosted client needs it.
412
+ */
413
+ #originAllowed(req: Request) {
414
+ const origin = req.headers.get("origin");
415
+ if (!origin) return true;
416
+ if ((this.#props.allowedOrigins ?? []).includes(origin)) return true;
417
+ try {
418
+
419
+ return new URL(origin).host === new URL(McpAuth.origin(req)).host;
420
+ } catch {
421
+ return false;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Answers the preflight a browser-hosted client is forced to make. `content-type: application/json` and the
427
+ * `mcp-*` mirror headers each put the call past the simple-request bar, so without this the request never
428
+ * leaves the browser and `allowedOrigins` grants nothing it can use.
429
+ *
430
+ * The requested header list is echoed rather than fixed: the decision that matters is the origin, and a fixed
431
+ * list only breaks clients that send one more header than we predicted.
432
+ */
433
+ #preflight(req: Request) {
434
+ if (!req.headers.get("origin") || !this.#originAllowed(req)) return new Response("Forbidden", { status: 403 });
435
+ return this.#cors(
436
+ req,
437
+ new Response(null, {
438
+ status: 204,
439
+ headers: {
440
+ "access-control-allow-methods": "POST, OPTIONS",
441
+ "access-control-allow-headers":
442
+ req.headers.get("access-control-request-headers") ?? "authorization, content-type",
443
+ "access-control-max-age": "600",
444
+ },
445
+ }),
446
+ );
447
+ }
448
+
449
+ /**
450
+ * Grants the one origin that already passed `#originAllowed`, so the rebinding defence stays exactly as wide
451
+ * as it was. Never `access-control-allow-credentials`: an MCP client presents its bearer token in a header it
452
+ * sets deliberately, and allowing ambient cookies is what would turn a permitted origin into one that can ride
453
+ * a signed-in user's session.
454
+ */
455
+ #cors(req: Request, res: Response) {
456
+ const origin = req.headers.get("origin");
457
+ if (!origin || !this.#originAllowed(req)) return res;
458
+ res.headers.set("access-control-allow-origin", origin);
459
+ res.headers.set("vary", "origin");
460
+ return res;
461
+ }
462
+
463
+ #serverInfo() {
464
+ return { name: `${this.#props.env.appName}-mcp`, version: this.#props.version ?? "0.0.0" };
465
+ }
466
+
467
+ #result(call: McpCall, result: object, cache?: McpCacheHint) {
468
+ return Response.json(this.#envelope(call, result, cache));
469
+ }
470
+
471
+ #envelope(call: McpCall, result: object, cache?: McpCacheHint) {
472
+
473
+ const meta =
474
+ call.era === "modern"
475
+ ? { resultType: "complete", _meta: { [MCP_META_SERVER_INFO]: this.#serverInfo() }, ...cache }
476
+ : {};
477
+ return { jsonrpc: "2.0", id: call.id, result: { ...meta, ...result } };
478
+ }
479
+
480
+ /**
481
+ * The cursor is an offset into the filtered list, base64url-wrapped so a client treats it as opaque rather
482
+ * than arithmetic it may do itself. The catalogue is built once at boot, so an offset stays meaningful for the
483
+ * life of the process; one minted by an earlier process may address a position that no longer exists, and that
484
+ * is refused rather than clamped — a silently shortened page reads as "the list ends here".
485
+ */
486
+ static #page<T>(items: T[], cursor: unknown, size: number) {
487
+ const offset = McpRouter.#offset(cursor);
488
+ if (offset === null || offset > items.length) return null;
489
+ const next = offset + size;
490
+ return {
491
+ items: items.slice(offset, next),
492
+ ...(next < items.length ? { nextCursor: Buffer.from(String(next)).toString("base64url") } : {}),
493
+ };
494
+ }
495
+
496
+ static #offset(cursor: unknown) {
497
+ if (cursor === undefined || cursor === null) return 0;
498
+ if (typeof cursor !== "string") return null;
499
+ const decoded = Buffer.from(cursor, "base64url").toString("utf8");
500
+
501
+ if (!decoded) return null;
502
+ const offset = Number(decoded);
503
+ return Number.isInteger(offset) && offset >= 0 ? offset : null;
504
+ }
505
+
506
+ static #meta(params: Record<string, unknown>) {
507
+ const meta = params._meta;
508
+ return meta && typeof meta === "object" ? (meta as Record<string, unknown>) : null;
509
+ }
510
+
511
+ /**
512
+ * Legacy clients propose a version; answer with theirs when we speak it, and otherwise with whichever end of our
513
+ * list they are likelier to accept.
514
+ *
515
+ * A client proposes the newest revision *it* speaks, so an unknown proposal is either newer than everything here
516
+ * — in which case the newest we have is the closest thing it may still know — or older than everything here, in
517
+ * which case the newest is hopeless and the oldest is the only candidate. The spec's "SHOULD be the latest
518
+ * version supported by the server" covers the first case and would strand the second, which is the one an old
519
+ * client is actually in. Revision names are ISO dates, so they order as strings.
520
+ */
521
+ static #negotiate(requested: unknown) {
522
+ if (typeof requested !== "string") return MCP_LEGACY_VERSION;
523
+ if (MCP_SUPPORTED_VERSIONS.includes(requested as (typeof MCP_SUPPORTED_VERSIONS)[number])) return requested;
524
+ const [newest] = MCP_SUPPORTED_VERSIONS;
525
+ return requested > newest ? newest : MCP_SUPPORTED_VERSIONS[MCP_SUPPORTED_VERSIONS.length - 1];
526
+ }
527
+
528
+ /** Legacy clients have no way to fall forward, so the one diagnostic they get is this message. */
529
+ static #methodNotFound(method: string) {
530
+ return `Method not found: ${method}. This server speaks MCP ${MCP_SUPPORTED_VERSIONS.join(", ")}.`;
531
+ }
532
+
533
+ /**
534
+ * Modern requests mirror parts of the body into headers so a proxy can route and audit without parsing JSON.
535
+ * A mismatch is rejected rather than resolved in the body's favour: whatever a gateway in front of us allowed
536
+ * was decided from the header, so honouring a body that disagrees is how that check gets bypassed. A mirror
537
+ * that is simply absent is refused on the same ground — see `#headerMismatch`.
538
+ */
539
+ static #validateModern(
540
+ req: Request,
541
+ method: string,
542
+ params: Record<string, unknown>,
543
+ meta: Record<string, unknown>,
544
+ id: string | number | null,
545
+ ) {
546
+ const version = meta[MCP_META_PROTOCOL_VERSION];
547
+ if (typeof version !== "string" || !(MCP_META_CLIENT_CAPABILITIES in meta))
548
+ return McpRouter.#error(id, McpErrorCode.invalidParams, "Missing required `_meta` fields.", { status: 400 });
549
+ if (!MCP_SUPPORTED_VERSIONS.includes(version as (typeof MCP_SUPPORTED_VERSIONS)[number]))
550
+ return McpRouter.#error(id, McpErrorCode.unsupportedProtocolVersion, "Unsupported protocol version.", {
551
+ status: 400,
552
+ data: { requested: version, supported: MCP_SUPPORTED_VERSIONS },
553
+ });
554
+ const named = params.name ?? params.uri;
555
+ const mismatch =
556
+ McpRouter.#headerMismatch(req, "mcp-protocol-version", version) ??
557
+ McpRouter.#headerMismatch(req, "mcp-method", method) ??
558
+ (typeof named === "string" ? McpRouter.#headerMismatch(req, "mcp-name", named) : undefined);
559
+ return mismatch ? McpRouter.#error(id, McpErrorCode.headerMismatch, mismatch, { status: 400 }) : null;
560
+ }
561
+
562
+ static #headerMismatch(req: Request, header: string, expected: string) {
563
+ const raw = req.headers.get(header);
564
+
565
+ if (raw === null) return `Header \`${header}\` is required by this protocol version and was not sent.`;
566
+ return McpRouter.#decodeHeader(raw) === expected ? undefined : `Header \`${header}\` does not match the body.`;
567
+ }
568
+
569
+ /** Values that are not ASCII-safe travel wrapped in a lowercase base64 sentinel: `=?base64?…?=`. */
570
+ static #decodeHeader(value: string) {
571
+ if (!value.startsWith("=?base64?") || !value.endsWith("?=")) return value;
572
+ try {
573
+ return Buffer.from(value.slice(9, -2), "base64").toString("utf8");
574
+ } catch {
575
+ return value;
576
+ }
577
+ }
578
+
579
+ /**
580
+ * A JSON-RPC error body is what tells a client that a 400 or 404 came from an MCP server rather than from a
581
+ * proxy in front of it, so the status never travels alone. Tool-level failures stay at 200 and are carried in
582
+ * the JSON-RPC error itself; only envelope problems escalate to an HTTP status.
583
+ */
584
+ static #error(id: string | number | null, code: number, message: string, { status, data }: McpErrorOptions = {}) {
585
+ return Response.json(McpRouter.#errorBody(id, code, message, data), { status: status ?? 200 });
586
+ }
587
+
588
+ static #errorBody(id: string | number | null, code: number, message: string, data?: unknown) {
589
+ return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } };
590
+ }
591
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./McpAuth";
2
+ export * from "./McpDispatcher";
3
+ export * from "./McpEventStream";
4
+ export * from "./McpExecutionContext";
5
+ export * from "./McpRouter";
@@ -186,7 +186,7 @@ export class CascadeRunner {
186
186
  }
187
187
  if (!lines.length) return;
188
188
  const bulk = lines.filter((line) => line.endsWith("(bulk)")).length;
189
- this.#logger.info(`${lines.length} cascade edge(s), ${bulk} in one query`);
189
+ this.#logger.verbose(`${lines.length} cascade edge(s), ${bulk} in one query`);
190
190
  for (const line of lines) this.#logger.verbose(line);
191
191
  }
192
192