@akon-labs/nexus-cli 0.1.9 → 0.1.10

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 (320) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +1 -0
  3. package/README.md +3 -2
  4. package/dist/app-bridge.bundle.js +67 -0
  5. package/dist/cli.js +20263 -2158
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +1 -1
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/auto-model.d.ts +1 -1
  10. package/dist/core/auto-model.d.ts.map +1 -1
  11. package/dist/core/auto-model.js +1 -1
  12. package/dist/core/auto-model.js.map +1 -1
  13. package/dist/core/model-resolver.d.ts +2 -2
  14. package/dist/core/model-resolver.d.ts.map +1 -1
  15. package/dist/core/model-resolver.js +3 -3
  16. package/dist/core/model-resolver.js.map +1 -1
  17. package/dist/core/package-manager.d.ts +15 -0
  18. package/dist/core/package-manager.d.ts.map +1 -1
  19. package/dist/core/package-manager.js +50 -0
  20. package/dist/core/package-manager.js.map +1 -1
  21. package/dist/core/sdk.d.ts.map +1 -1
  22. package/dist/core/sdk.js +1 -1
  23. package/dist/core/sdk.js.map +1 -1
  24. package/dist/core/slash-commands.d.ts.map +1 -1
  25. package/dist/core/slash-commands.js +3 -3
  26. package/dist/core/slash-commands.js.map +1 -1
  27. package/dist/core/system-prompt.d.ts.map +1 -1
  28. package/dist/core/system-prompt.js +1 -1
  29. package/dist/core/system-prompt.js.map +1 -1
  30. package/dist/core/tools/index.d.ts +1 -1
  31. package/dist/core/tools/index.d.ts.map +1 -1
  32. package/dist/core/tools/index.js +5 -5
  33. package/dist/core/tools/index.js.map +1 -1
  34. package/dist/core/tools/websearch.d.ts.map +1 -1
  35. package/dist/core/tools/websearch.js +3 -3
  36. package/dist/core/tools/websearch.js.map +1 -1
  37. package/dist/extensions/gitnexus/gateway.d.ts +6 -6
  38. package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
  39. package/dist/extensions/gitnexus/gateway.js +4 -4
  40. package/dist/extensions/gitnexus/gateway.js.map +1 -1
  41. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  42. package/dist/extensions/gitnexus/index.js +4 -2
  43. package/dist/extensions/gitnexus/index.js.map +1 -1
  44. package/dist/extensions/mcp/abort.d.ts +3 -0
  45. package/dist/extensions/mcp/abort.d.ts.map +1 -0
  46. package/dist/extensions/mcp/abort.js +41 -0
  47. package/dist/extensions/mcp/abort.js.map +1 -0
  48. package/dist/extensions/mcp/agent-dir.d.ts +6 -0
  49. package/dist/extensions/mcp/agent-dir.d.ts.map +1 -0
  50. package/dist/extensions/mcp/agent-dir.js +18 -0
  51. package/dist/extensions/mcp/agent-dir.js.map +1 -0
  52. package/dist/extensions/mcp/agent-plugin-loader.d.ts +9 -0
  53. package/dist/extensions/mcp/agent-plugin-loader.d.ts.map +1 -0
  54. package/dist/extensions/mcp/agent-plugin-loader.js +384 -0
  55. package/dist/extensions/mcp/agent-plugin-loader.js.map +1 -0
  56. package/dist/extensions/mcp/app-bridge.bundle.js +67 -0
  57. package/dist/extensions/mcp/commands.d.ts +27 -0
  58. package/dist/extensions/mcp/commands.d.ts.map +1 -0
  59. package/dist/extensions/mcp/commands.js +629 -0
  60. package/dist/extensions/mcp/commands.js.map +1 -0
  61. package/dist/extensions/mcp/config.d.ts +121 -38
  62. package/dist/extensions/mcp/config.d.ts.map +1 -1
  63. package/dist/extensions/mcp/config.js +1030 -70
  64. package/dist/extensions/mcp/config.js.map +1 -1
  65. package/dist/extensions/mcp/consent-manager.d.ts +14 -0
  66. package/dist/extensions/mcp/consent-manager.d.ts.map +1 -0
  67. package/dist/extensions/mcp/consent-manager.js +60 -0
  68. package/dist/extensions/mcp/consent-manager.js.map +1 -0
  69. package/dist/extensions/mcp/direct-tools.d.ts +11 -0
  70. package/dist/extensions/mcp/direct-tools.d.ts.map +1 -0
  71. package/dist/extensions/mcp/direct-tools.js +562 -0
  72. package/dist/extensions/mcp/direct-tools.js.map +1 -0
  73. package/dist/extensions/mcp/elicitation-handler.d.ts +18 -0
  74. package/dist/extensions/mcp/elicitation-handler.d.ts.map +1 -0
  75. package/dist/extensions/mcp/elicitation-handler.js +320 -0
  76. package/dist/extensions/mcp/elicitation-handler.js.map +1 -0
  77. package/dist/extensions/mcp/error-signal.d.ts +16 -0
  78. package/dist/extensions/mcp/error-signal.d.ts.map +1 -0
  79. package/dist/extensions/mcp/error-signal.js +22 -0
  80. package/dist/extensions/mcp/error-signal.js.map +1 -0
  81. package/dist/extensions/mcp/errors.d.ts +104 -0
  82. package/dist/extensions/mcp/errors.d.ts.map +1 -0
  83. package/dist/extensions/mcp/errors.js +171 -0
  84. package/dist/extensions/mcp/errors.js.map +1 -0
  85. package/dist/extensions/mcp/glimpse-ui.d.ts +10 -0
  86. package/dist/extensions/mcp/glimpse-ui.d.ts.map +1 -0
  87. package/dist/extensions/mcp/glimpse-ui.js +67 -0
  88. package/dist/extensions/mcp/glimpse-ui.js.map +1 -0
  89. package/dist/extensions/mcp/host-html-template.d.ts +17 -0
  90. package/dist/extensions/mcp/host-html-template.d.ts.map +1 -0
  91. package/dist/extensions/mcp/host-html-template.js +442 -0
  92. package/dist/extensions/mcp/host-html-template.js.map +1 -0
  93. package/dist/extensions/mcp/index.d.ts +20 -15
  94. package/dist/extensions/mcp/index.d.ts.map +1 -1
  95. package/dist/extensions/mcp/index.js +1064 -82
  96. package/dist/extensions/mcp/index.js.map +1 -1
  97. package/dist/extensions/mcp/init.d.ts +28 -0
  98. package/dist/extensions/mcp/init.d.ts.map +1 -0
  99. package/dist/extensions/mcp/init.js +631 -0
  100. package/dist/extensions/mcp/init.js.map +1 -0
  101. package/dist/extensions/mcp/json-schema-validator.d.ts +3 -0
  102. package/dist/extensions/mcp/json-schema-validator.d.ts.map +1 -0
  103. package/dist/extensions/mcp/json-schema-validator.js +53 -0
  104. package/dist/extensions/mcp/json-schema-validator.js.map +1 -0
  105. package/dist/extensions/mcp/lifecycle.d.ts +56 -0
  106. package/dist/extensions/mcp/lifecycle.d.ts.map +1 -0
  107. package/dist/extensions/mcp/lifecycle.js +395 -0
  108. package/dist/extensions/mcp/lifecycle.js.map +1 -0
  109. package/dist/extensions/mcp/logger.d.ts +52 -0
  110. package/dist/extensions/mcp/logger.d.ts.map +1 -0
  111. package/dist/extensions/mcp/logger.js +134 -0
  112. package/dist/extensions/mcp/logger.js.map +1 -0
  113. package/dist/extensions/mcp/mcp-auth-flow.d.ts +117 -0
  114. package/dist/extensions/mcp/mcp-auth-flow.d.ts.map +1 -0
  115. package/dist/extensions/mcp/mcp-auth-flow.js +869 -0
  116. package/dist/extensions/mcp/mcp-auth-flow.js.map +1 -0
  117. package/dist/extensions/mcp/mcp-auth.d.ts +162 -0
  118. package/dist/extensions/mcp/mcp-auth.d.ts.map +1 -0
  119. package/dist/extensions/mcp/mcp-auth.js +897 -0
  120. package/dist/extensions/mcp/mcp-auth.js.map +1 -0
  121. package/dist/extensions/mcp/mcp-bearer-store.d.ts +25 -0
  122. package/dist/extensions/mcp/mcp-bearer-store.d.ts.map +1 -0
  123. package/dist/extensions/mcp/mcp-bearer-store.js +350 -0
  124. package/dist/extensions/mcp/mcp-bearer-store.js.map +1 -0
  125. package/dist/extensions/mcp/mcp-callback-server.d.ts +54 -0
  126. package/dist/extensions/mcp/mcp-callback-server.d.ts.map +1 -0
  127. package/dist/extensions/mcp/mcp-callback-server.js +454 -0
  128. package/dist/extensions/mcp/mcp-callback-server.js.map +1 -0
  129. package/dist/extensions/mcp/mcp-code.d.ts +48 -0
  130. package/dist/extensions/mcp/mcp-code.d.ts.map +1 -0
  131. package/dist/extensions/mcp/mcp-code.js +358 -0
  132. package/dist/extensions/mcp/mcp-code.js.map +1 -0
  133. package/dist/extensions/mcp/mcp-keyring-helper.cjs +89 -0
  134. package/dist/extensions/mcp/mcp-oauth-provider.d.ts +135 -0
  135. package/dist/extensions/mcp/mcp-oauth-provider.d.ts.map +1 -0
  136. package/dist/extensions/mcp/mcp-oauth-provider.js +491 -0
  137. package/dist/extensions/mcp/mcp-oauth-provider.js.map +1 -0
  138. package/dist/extensions/mcp/mcp-output-guard.d.ts +59 -0
  139. package/dist/extensions/mcp/mcp-output-guard.d.ts.map +1 -0
  140. package/dist/extensions/mcp/mcp-output-guard.js +343 -0
  141. package/dist/extensions/mcp/mcp-output-guard.js.map +1 -0
  142. package/dist/extensions/mcp/mcp-panel.d.ts +71 -0
  143. package/dist/extensions/mcp/mcp-panel.d.ts.map +1 -0
  144. package/dist/extensions/mcp/mcp-panel.js +973 -0
  145. package/dist/extensions/mcp/mcp-panel.js.map +1 -0
  146. package/dist/extensions/mcp/mcp-probe.d.ts +7 -0
  147. package/dist/extensions/mcp/mcp-probe.d.ts.map +1 -0
  148. package/dist/extensions/mcp/mcp-probe.js +155 -0
  149. package/dist/extensions/mcp/mcp-probe.js.map +1 -0
  150. package/dist/extensions/mcp/mcp-script-worker.mjs +130 -0
  151. package/dist/extensions/mcp/mcp-setup-panel.d.ts +85 -0
  152. package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -0
  153. package/dist/extensions/mcp/mcp-setup-panel.js +618 -0
  154. package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -0
  155. package/dist/extensions/mcp/mcp-status.d.ts +12 -0
  156. package/dist/extensions/mcp/mcp-status.d.ts.map +1 -0
  157. package/dist/extensions/mcp/mcp-status.js +98 -0
  158. package/dist/extensions/mcp/mcp-status.js.map +1 -0
  159. package/dist/extensions/mcp/mcp-trace.d.ts +95 -0
  160. package/dist/extensions/mcp/mcp-trace.d.ts.map +1 -0
  161. package/dist/extensions/mcp/mcp-trace.js +252 -0
  162. package/dist/extensions/mcp/mcp-trace.js.map +1 -0
  163. package/dist/extensions/mcp/metadata-cache.d.ts +20 -0
  164. package/dist/extensions/mcp/metadata-cache.d.ts.map +1 -0
  165. package/dist/extensions/mcp/metadata-cache.js +318 -0
  166. package/dist/extensions/mcp/metadata-cache.js.map +1 -0
  167. package/dist/extensions/mcp/npx-resolver.d.ts +7 -0
  168. package/dist/extensions/mcp/npx-resolver.d.ts.map +1 -0
  169. package/dist/extensions/mcp/npx-resolver.js +505 -0
  170. package/dist/extensions/mcp/npx-resolver.js.map +1 -0
  171. package/dist/extensions/mcp/oauth.d.ts +21 -0
  172. package/dist/extensions/mcp/oauth.d.ts.map +1 -0
  173. package/dist/extensions/mcp/oauth.js +15 -0
  174. package/dist/extensions/mcp/oauth.js.map +1 -0
  175. package/dist/extensions/mcp/onboarding-state.d.ts +13 -0
  176. package/dist/extensions/mcp/onboarding-state.d.ts.map +1 -0
  177. package/dist/extensions/mcp/onboarding-state.js +65 -0
  178. package/dist/extensions/mcp/onboarding-state.js.map +1 -0
  179. package/dist/extensions/mcp/package-mcp-loader.d.ts +3 -0
  180. package/dist/extensions/mcp/package-mcp-loader.d.ts.map +1 -0
  181. package/dist/extensions/mcp/package-mcp-loader.js +170 -0
  182. package/dist/extensions/mcp/package-mcp-loader.js.map +1 -0
  183. package/dist/extensions/mcp/panel-keys.d.ts +21 -0
  184. package/dist/extensions/mcp/panel-keys.d.ts.map +1 -0
  185. package/dist/extensions/mcp/panel-keys.js +29 -0
  186. package/dist/extensions/mcp/panel-keys.js.map +1 -0
  187. package/dist/extensions/mcp/prompts.d.ts +63 -0
  188. package/dist/extensions/mcp/prompts.d.ts.map +1 -0
  189. package/dist/extensions/mcp/prompts.js +297 -0
  190. package/dist/extensions/mcp/prompts.js.map +1 -0
  191. package/dist/extensions/mcp/proxy-modes.d.ts +15 -0
  192. package/dist/extensions/mcp/proxy-modes.d.ts.map +1 -0
  193. package/dist/extensions/mcp/proxy-modes.js +1421 -0
  194. package/dist/extensions/mcp/proxy-modes.js.map +1 -0
  195. package/dist/extensions/mcp/request-headers-command.d.ts +11 -0
  196. package/dist/extensions/mcp/request-headers-command.d.ts.map +1 -0
  197. package/dist/extensions/mcp/request-headers-command.js +334 -0
  198. package/dist/extensions/mcp/request-headers-command.js.map +1 -0
  199. package/dist/extensions/mcp/resource-tools.d.ts +2 -0
  200. package/dist/extensions/mcp/resource-tools.d.ts.map +1 -0
  201. package/dist/extensions/mcp/resource-tools.js +15 -0
  202. package/dist/extensions/mcp/resource-tools.js.map +1 -0
  203. package/dist/extensions/mcp/runtime-owner.d.ts +14 -0
  204. package/dist/extensions/mcp/runtime-owner.d.ts.map +1 -0
  205. package/dist/extensions/mcp/runtime-owner.js +84 -0
  206. package/dist/extensions/mcp/runtime-owner.js.map +1 -0
  207. package/dist/extensions/mcp/sampling-handler.d.ts +18 -0
  208. package/dist/extensions/mcp/sampling-handler.d.ts.map +1 -0
  209. package/dist/extensions/mcp/sampling-handler.js +229 -0
  210. package/dist/extensions/mcp/sampling-handler.js.map +1 -0
  211. package/dist/extensions/mcp/search-ranking.d.ts +25 -0
  212. package/dist/extensions/mcp/search-ranking.d.ts.map +1 -0
  213. package/dist/extensions/mcp/search-ranking.js +234 -0
  214. package/dist/extensions/mcp/search-ranking.js.map +1 -0
  215. package/dist/extensions/mcp/server-manager.d.ts +116 -0
  216. package/dist/extensions/mcp/server-manager.d.ts.map +1 -0
  217. package/dist/extensions/mcp/server-manager.js +1052 -0
  218. package/dist/extensions/mcp/server-manager.js.map +1 -0
  219. package/dist/extensions/mcp/session-recovery.d.ts +27 -0
  220. package/dist/extensions/mcp/session-recovery.d.ts.map +1 -0
  221. package/dist/extensions/mcp/session-recovery.js +140 -0
  222. package/dist/extensions/mcp/session-recovery.js.map +1 -0
  223. package/dist/extensions/mcp/skills/mcp-scripting/SKILL.md +38 -0
  224. package/dist/extensions/mcp/state.d.ts +62 -0
  225. package/dist/extensions/mcp/state.d.ts.map +1 -0
  226. package/dist/extensions/mcp/state.js +2 -0
  227. package/dist/extensions/mcp/state.js.map +1 -0
  228. package/dist/extensions/mcp/tool-approval.d.ts +11 -0
  229. package/dist/extensions/mcp/tool-approval.d.ts.map +1 -0
  230. package/dist/extensions/mcp/tool-approval.js +156 -0
  231. package/dist/extensions/mcp/tool-approval.js.map +1 -0
  232. package/dist/extensions/mcp/tool-metadata.d.ts +11 -0
  233. package/dist/extensions/mcp/tool-metadata.d.ts.map +1 -0
  234. package/dist/extensions/mcp/tool-metadata.js +264 -0
  235. package/dist/extensions/mcp/tool-metadata.js.map +1 -0
  236. package/dist/extensions/mcp/tool-registrar.d.ts +21 -0
  237. package/dist/extensions/mcp/tool-registrar.d.ts.map +1 -0
  238. package/dist/extensions/mcp/tool-registrar.js +229 -0
  239. package/dist/extensions/mcp/tool-registrar.js.map +1 -0
  240. package/dist/extensions/mcp/tool-result-renderer.d.ts +83 -0
  241. package/dist/extensions/mcp/tool-result-renderer.d.ts.map +1 -0
  242. package/dist/extensions/mcp/tool-result-renderer.js +345 -0
  243. package/dist/extensions/mcp/tool-result-renderer.js.map +1 -0
  244. package/dist/extensions/mcp/ts-shape.d.ts +3 -0
  245. package/dist/extensions/mcp/ts-shape.d.ts.map +1 -0
  246. package/dist/extensions/mcp/ts-shape.js +160 -0
  247. package/dist/extensions/mcp/ts-shape.js.map +1 -0
  248. package/dist/extensions/mcp/types.d.ts +521 -0
  249. package/dist/extensions/mcp/types.d.ts.map +1 -0
  250. package/dist/extensions/mcp/types.js +294 -0
  251. package/dist/extensions/mcp/types.js.map +1 -0
  252. package/dist/extensions/mcp/ui-app-bridge-helpers.d.ts +7 -0
  253. package/dist/extensions/mcp/ui-app-bridge-helpers.d.ts.map +1 -0
  254. package/dist/extensions/mcp/ui-app-bridge-helpers.js +37 -0
  255. package/dist/extensions/mcp/ui-app-bridge-helpers.js.map +1 -0
  256. package/dist/extensions/mcp/ui-resource-handler.d.ts +18 -0
  257. package/dist/extensions/mcp/ui-resource-handler.d.ts.map +1 -0
  258. package/dist/extensions/mcp/ui-resource-handler.js +210 -0
  259. package/dist/extensions/mcp/ui-resource-handler.js.map +1 -0
  260. package/dist/extensions/mcp/ui-server.d.ts +52 -0
  261. package/dist/extensions/mcp/ui-server.d.ts.map +1 -0
  262. package/dist/extensions/mcp/ui-server.js +740 -0
  263. package/dist/extensions/mcp/ui-server.js.map +1 -0
  264. package/dist/extensions/mcp/ui-session.d.ts +40 -0
  265. package/dist/extensions/mcp/ui-session.d.ts.map +1 -0
  266. package/dist/extensions/mcp/ui-session.js +477 -0
  267. package/dist/extensions/mcp/ui-session.js.map +1 -0
  268. package/dist/extensions/mcp/ui-stream-types.d.ts +105 -0
  269. package/dist/extensions/mcp/ui-stream-types.d.ts.map +1 -0
  270. package/dist/extensions/mcp/ui-stream-types.js +61 -0
  271. package/dist/extensions/mcp/ui-stream-types.js.map +1 -0
  272. package/dist/extensions/mcp/ui-tool-visibility.d.ts +5 -0
  273. package/dist/extensions/mcp/ui-tool-visibility.d.ts.map +1 -0
  274. package/dist/extensions/mcp/ui-tool-visibility.js +27 -0
  275. package/dist/extensions/mcp/ui-tool-visibility.js.map +1 -0
  276. package/dist/extensions/mcp/unix-socket-transport.d.ts +15 -0
  277. package/dist/extensions/mcp/unix-socket-transport.d.ts.map +1 -0
  278. package/dist/extensions/mcp/unix-socket-transport.js +85 -0
  279. package/dist/extensions/mcp/unix-socket-transport.js.map +1 -0
  280. package/dist/extensions/mcp/utils.d.ts +32 -0
  281. package/dist/extensions/mcp/utils.d.ts.map +1 -0
  282. package/dist/extensions/mcp/utils.js +336 -0
  283. package/dist/extensions/mcp/utils.js.map +1 -0
  284. package/dist/extensions/subagent/index.d.ts +61 -1
  285. package/dist/extensions/subagent/index.d.ts.map +1 -1
  286. package/dist/extensions/subagent/index.js +177 -4
  287. package/dist/extensions/subagent/index.js.map +1 -1
  288. package/dist/extensions/web-access.d.ts.map +1 -1
  289. package/dist/extensions/web-access.js +1 -1
  290. package/dist/extensions/web-access.js.map +1 -1
  291. package/dist/index.d.ts +2 -0
  292. package/dist/index.d.ts.map +1 -1
  293. package/dist/index.js +20057 -1947
  294. package/dist/index.js.map +1 -1
  295. package/dist/mcp-keyring-helper.cjs +89 -0
  296. package/dist/mcp-script-worker.mjs +130 -0
  297. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  298. package/dist/modes/interactive/components/model-selector.js +1 -1
  299. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  300. package/dist/modes/interactive/components/response-separator.d.ts +1 -1
  301. package/dist/modes/interactive/components/response-separator.d.ts.map +1 -1
  302. package/dist/modes/interactive/components/response-separator.js +1 -1
  303. package/dist/modes/interactive/components/response-separator.js.map +1 -1
  304. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  305. package/dist/modes/interactive/interactive-mode.js +7 -5
  306. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  307. package/dist/rpc-entry.js +20263 -2158
  308. package/dist/skills/mcp-scripting/SKILL.md +38 -0
  309. package/dist/vendor/ai.js +280 -205
  310. package/docs/mcp.md +178 -0
  311. package/docs/pi-subagents.md +75 -0
  312. package/docs/quickstart.md +1 -1
  313. package/docs/usage.md +2 -1
  314. package/examples/extensions/pi-subagents-worker-policy.ts +86 -0
  315. package/npm-shrinkwrap.json +763 -6
  316. package/package.json +15 -4
  317. package/dist/extensions/mcp/client.d.ts +0 -86
  318. package/dist/extensions/mcp/client.d.ts.map +0 -1
  319. package/dist/extensions/mcp/client.js +0 -275
  320. package/dist/extensions/mcp/client.js.map +0 -1
@@ -0,0 +1,210 @@
1
+ import { UrlElicitationRequiredError } from "@modelcontextprotocol/client";
2
+ import { ResourceFetchError, ResourceParseError } from "./errors.js";
3
+ import { logger } from "./logger.js";
4
+ import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
5
+ import { isServerDisabled, } from "./types.js";
6
+ import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.js";
7
+ export class UiResourceHandler {
8
+ log = logger.child({ component: "UiResourceHandler" });
9
+ manager;
10
+ config;
11
+ constructor(manager, config = undefined) {
12
+ this.manager = manager;
13
+ this.config = config;
14
+ }
15
+ async readUiResource(serverName, uri, options = {}) {
16
+ const log = this.log.child({ server: serverName, uri });
17
+ if (!uri.startsWith("ui://")) {
18
+ throw new ResourceParseError(uri, "URI must start with ui://", { server: serverName });
19
+ }
20
+ log.debug("Fetching UI resource");
21
+ let result;
22
+ try {
23
+ const config = options.config ?? this.config;
24
+ if (config && isServerDisabled(config.mcpServers[serverName])) {
25
+ throw new Error(`MCP server "${serverName}" is disabled`);
26
+ }
27
+ if (config) {
28
+ this.manager.touch(serverName);
29
+ this.manager.incrementInFlight(serverName);
30
+ try {
31
+ result = await withSessionRecovery({
32
+ manager: this.manager,
33
+ config,
34
+ ...(options.signal ? { signal: options.signal } : {}),
35
+ ...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
36
+ }, serverName, (connection) => connection.client.readResource({ uri }, this.manager.getRequestOptions(serverName, options.signal)));
37
+ }
38
+ finally {
39
+ this.manager.decrementInFlight(serverName);
40
+ this.manager.touch(serverName);
41
+ }
42
+ }
43
+ else {
44
+ result = await this.manager.readResource(serverName, uri, options.signal);
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError)
49
+ throw error;
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ log.error("Failed to read resource", error instanceof Error ? error : undefined);
52
+ throw new ResourceFetchError(uri, message, {
53
+ server: serverName,
54
+ ...(error instanceof Error ? { cause: error } : {}),
55
+ });
56
+ }
57
+ const content = selectContent(result, uri);
58
+ const mimeType = content.mimeType;
59
+ if (mimeType && !isHtmlMimeType(mimeType)) {
60
+ log.warn("Unsupported MIME type", { mimeType });
61
+ throw new ResourceParseError(uri, `unsupported MIME type "${mimeType}" (expected text/html or ${RESOURCE_MIME_TYPE})`, { server: serverName, mimeType });
62
+ }
63
+ const html = toHtml(content);
64
+ if (!html.trim()) {
65
+ log.warn("Resource content is empty");
66
+ throw new ResourceParseError(uri, "content is empty", { server: serverName });
67
+ }
68
+ const contentMeta = extractUiMeta(content._meta);
69
+ const listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));
70
+ log.debug("Resource loaded successfully", {
71
+ contentLength: html.length,
72
+ hasCsp: !!contentMeta.csp || !!listMeta.csp,
73
+ });
74
+ return {
75
+ uri: content.uri ?? uri,
76
+ html,
77
+ mimeType: mimeType ?? RESOURCE_MIME_TYPE,
78
+ meta: {
79
+ ...((contentMeta.csp ?? listMeta.csp) !== undefined ? { csp: contentMeta.csp ?? listMeta.csp } : {}),
80
+ ...((contentMeta.permissions ?? listMeta.permissions) !== undefined
81
+ ? { permissions: contentMeta.permissions ?? listMeta.permissions }
82
+ : {}),
83
+ ...((contentMeta.domain ?? listMeta.domain) !== undefined
84
+ ? { domain: contentMeta.domain ?? listMeta.domain }
85
+ : {}),
86
+ ...((contentMeta.prefersBorder ?? listMeta.prefersBorder) !== undefined
87
+ ? { prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder }
88
+ : {}),
89
+ },
90
+ };
91
+ }
92
+ getListResourceMeta(serverName, uri) {
93
+ const connection = this.manager.getConnection(serverName);
94
+ if (!connection?.resources?.length)
95
+ return undefined;
96
+ const resource = connection.resources.find((entry) => entry.uri === uri);
97
+ if (!resource || !resource._meta || typeof resource._meta !== "object")
98
+ return undefined;
99
+ return resource._meta;
100
+ }
101
+ }
102
+ function selectContent(result, preferredUri) {
103
+ const contents = (result.contents ?? []);
104
+ if (contents.length === 0) {
105
+ throw new Error(`No contents returned for UI resource: ${preferredUri}`);
106
+ }
107
+ const byUri = contents.find((content) => content.uri === preferredUri);
108
+ if (byUri)
109
+ return byUri;
110
+ const byHtmlMime = contents.find((content) => content.mimeType && isHtmlMimeType(content.mimeType));
111
+ if (byHtmlMime)
112
+ return byHtmlMime;
113
+ const firstContent = contents[0];
114
+ if (!firstContent) {
115
+ throw new Error(`No contents returned for UI resource: ${preferredUri}`);
116
+ }
117
+ return firstContent;
118
+ }
119
+ function isHtmlMimeType(mimeType) {
120
+ const normalized = mimeType.toLowerCase();
121
+ return normalized.startsWith("text/html") || normalized === RESOURCE_MIME_TYPE.toLowerCase();
122
+ }
123
+ function toHtml(content) {
124
+ if (typeof content.text === "string") {
125
+ return content.text;
126
+ }
127
+ if (typeof content.blob === "string") {
128
+ return Buffer.from(content.blob, "base64").toString("utf-8");
129
+ }
130
+ throw new Error(`UI resource ${content.uri ?? "(unknown)"} did not include text or blob content`);
131
+ }
132
+ const OPENAI_CSP_FIELD_MAPPINGS = [
133
+ ["resource_domains", "resourceDomains"],
134
+ ["connect_domains", "connectDomains"],
135
+ ["frame_domains", "frameDomains"],
136
+ ];
137
+ const UI_CSP_DOMAIN_FIELDS = [
138
+ "resourceDomains",
139
+ "connectDomains",
140
+ "frameDomains",
141
+ "baseUriDomains",
142
+ ];
143
+ function extractUiMeta(meta) {
144
+ if (!meta || typeof meta !== "object")
145
+ return {};
146
+ const ui = isRecord(meta.ui) ? meta.ui : undefined;
147
+ const out = {};
148
+ const openAiCsp = Object.hasOwn(meta, "openai/widgetCSP")
149
+ ? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"])
150
+ : undefined;
151
+ const hasStandardCsp = !!ui && Object.hasOwn(ui, "csp");
152
+ const standardCspValue = hasStandardCsp ? ui.csp : undefined;
153
+ if (hasStandardCsp && !isRecord(standardCspValue)) {
154
+ // A declared canonical container takes precedence even when malformed.
155
+ out.csp = {};
156
+ }
157
+ else {
158
+ const standardCsp = hasStandardCsp ? normalizeUiResourceCsp(standardCspValue) : undefined;
159
+ if (openAiCsp || standardCsp) {
160
+ out.csp = { ...openAiCsp, ...standardCsp };
161
+ if (isRecord(standardCspValue)) {
162
+ for (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {
163
+ if (Object.hasOwn(standardCspValue, standardField) &&
164
+ !copyStringArray(standardCspValue[standardField])) {
165
+ delete out.csp[standardField];
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ if (ui && isRecord(ui.permissions)) {
172
+ out.permissions = ui.permissions;
173
+ }
174
+ if (ui && typeof ui.domain === "string") {
175
+ out.domain = ui.domain;
176
+ }
177
+ if (ui && typeof ui.prefersBorder === "boolean") {
178
+ out.prefersBorder = ui.prefersBorder;
179
+ }
180
+ return out;
181
+ }
182
+ function normalizeUiResourceCsp(value) {
183
+ if (!isRecord(value))
184
+ return {};
185
+ const csp = {};
186
+ for (const field of UI_CSP_DOMAIN_FIELDS) {
187
+ const domains = copyStringArray(value[field]);
188
+ if (domains)
189
+ csp[field] = domains;
190
+ }
191
+ return csp;
192
+ }
193
+ function normalizeOpenAiWidgetCsp(value) {
194
+ if (!isRecord(value))
195
+ return {};
196
+ const csp = {};
197
+ for (const [sourceField, targetField] of OPENAI_CSP_FIELD_MAPPINGS) {
198
+ const domains = copyStringArray(value[sourceField]);
199
+ if (domains)
200
+ csp[targetField] = domains;
201
+ }
202
+ return csp;
203
+ }
204
+ function copyStringArray(value) {
205
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? [...value] : undefined;
206
+ }
207
+ function isRecord(value) {
208
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209
+ }
210
+ //# sourceMappingURL=ui-resource-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-resource-handler.js","sourceRoot":"","sources":["../../../src/extensions/mcp/ui-resource-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ReadResourceResult, UrlElicitationRequiredError } from \"@modelcontextprotocol/client\";\nimport { ResourceFetchError, ResourceParseError } from \"./errors.ts\";\nimport { logger } from \"./logger.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport { SessionRecoveryAuthRequiredError, type SessionRecoveryDeps, withSessionRecovery } from \"./session-recovery.ts\";\nimport {\n\tisServerDisabled,\n\ttype McpConfig,\n\ttype UiResourceContent,\n\ttype UiResourceCsp,\n\ttype UiResourceMeta,\n\ttype UiResourcePermissions,\n} from \"./types.ts\";\nimport { RESOURCE_MIME_TYPE } from \"./ui-app-bridge-helpers.ts\";\n\ninterface ResourceContentRecord {\n\turi?: string;\n\tmimeType?: string;\n\ttext?: string;\n\tblob?: string;\n\t_meta?: Record<string, unknown>;\n}\n\ninterface ReadUiResourceOptions {\n\tconfig?: McpConfig;\n\tsignal?: AbortSignal;\n\tonNeedsAuth?: SessionRecoveryDeps[\"onNeedsAuth\"];\n}\n\nexport class UiResourceHandler {\n\tprivate log = logger.child({ component: \"UiResourceHandler\" });\n\tprivate manager: McpServerManager;\n\tprivate config: McpConfig | undefined;\n\n\tconstructor(manager: McpServerManager, config: McpConfig | undefined = undefined) {\n\t\tthis.manager = manager;\n\t\tthis.config = config;\n\t}\n\n\tasync readUiResource(\n\t\tserverName: string,\n\t\turi: string,\n\t\toptions: ReadUiResourceOptions = {},\n\t): Promise<UiResourceContent> {\n\t\tconst log = this.log.child({ server: serverName, uri });\n\n\t\tif (!uri.startsWith(\"ui://\")) {\n\t\t\tthrow new ResourceParseError(uri, \"URI must start with ui://\", { server: serverName });\n\t\t}\n\n\t\tlog.debug(\"Fetching UI resource\");\n\n\t\tlet result: ReadResourceResult;\n\t\ttry {\n\t\t\tconst config = options.config ?? this.config;\n\t\t\tif (config && isServerDisabled(config.mcpServers[serverName])) {\n\t\t\t\tthrow new Error(`MCP server \"${serverName}\" is disabled`);\n\t\t\t}\n\t\t\tif (config) {\n\t\t\t\tthis.manager.touch(serverName);\n\t\t\t\tthis.manager.incrementInFlight(serverName);\n\t\t\t\ttry {\n\t\t\t\t\tresult = await withSessionRecovery(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmanager: this.manager,\n\t\t\t\t\t\t\tconfig,\n\t\t\t\t\t\t\t...(options.signal ? { signal: options.signal } : {}),\n\t\t\t\t\t\t\t...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tserverName,\n\t\t\t\t\t\t(connection) =>\n\t\t\t\t\t\t\tconnection.client.readResource(\n\t\t\t\t\t\t\t\t{ uri },\n\t\t\t\t\t\t\t\tthis.manager.getRequestOptions(serverName, options.signal),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t} finally {\n\t\t\t\t\tthis.manager.decrementInFlight(serverName);\n\t\t\t\t\tthis.manager.touch(serverName);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tresult = await this.manager.readResource(serverName, uri, options.signal);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError)\n\t\t\t\tthrow error;\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tlog.error(\"Failed to read resource\", error instanceof Error ? error : undefined);\n\t\t\tthrow new ResourceFetchError(uri, message, {\n\t\t\t\tserver: serverName,\n\t\t\t\t...(error instanceof Error ? { cause: error } : {}),\n\t\t\t});\n\t\t}\n\n\t\tconst content = selectContent(result, uri);\n\t\tconst mimeType = content.mimeType;\n\n\t\tif (mimeType && !isHtmlMimeType(mimeType)) {\n\t\t\tlog.warn(\"Unsupported MIME type\", { mimeType });\n\t\t\tthrow new ResourceParseError(\n\t\t\t\turi,\n\t\t\t\t`unsupported MIME type \"${mimeType}\" (expected text/html or ${RESOURCE_MIME_TYPE})`,\n\t\t\t\t{ server: serverName, mimeType },\n\t\t\t);\n\t\t}\n\n\t\tconst html = toHtml(content);\n\t\tif (!html.trim()) {\n\t\t\tlog.warn(\"Resource content is empty\");\n\t\t\tthrow new ResourceParseError(uri, \"content is empty\", { server: serverName });\n\t\t}\n\n\t\tconst contentMeta = extractUiMeta(content._meta);\n\t\tconst listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));\n\n\t\tlog.debug(\"Resource loaded successfully\", {\n\t\t\tcontentLength: html.length,\n\t\t\thasCsp: !!contentMeta.csp || !!listMeta.csp,\n\t\t});\n\n\t\treturn {\n\t\t\turi: content.uri ?? uri,\n\t\t\thtml,\n\t\t\tmimeType: mimeType ?? RESOURCE_MIME_TYPE,\n\t\t\tmeta: {\n\t\t\t\t...((contentMeta.csp ?? listMeta.csp) !== undefined ? { csp: contentMeta.csp ?? listMeta.csp } : {}),\n\t\t\t\t...((contentMeta.permissions ?? listMeta.permissions) !== undefined\n\t\t\t\t\t? { permissions: contentMeta.permissions ?? listMeta.permissions }\n\t\t\t\t\t: {}),\n\t\t\t\t...((contentMeta.domain ?? listMeta.domain) !== undefined\n\t\t\t\t\t? { domain: contentMeta.domain ?? listMeta.domain }\n\t\t\t\t\t: {}),\n\t\t\t\t...((contentMeta.prefersBorder ?? listMeta.prefersBorder) !== undefined\n\t\t\t\t\t? { prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate getListResourceMeta(serverName: string, uri: string): Record<string, unknown> | undefined {\n\t\tconst connection = this.manager.getConnection(serverName);\n\t\tif (!connection?.resources?.length) return undefined;\n\t\tconst resource = connection.resources.find((entry) => entry.uri === uri);\n\t\tif (!resource || !resource._meta || typeof resource._meta !== \"object\") return undefined;\n\t\treturn resource._meta;\n\t}\n}\n\nfunction selectContent(result: ReadResourceResult, preferredUri: string): ResourceContentRecord {\n\tconst contents = (result.contents ?? []) as ResourceContentRecord[];\n\tif (contents.length === 0) {\n\t\tthrow new Error(`No contents returned for UI resource: ${preferredUri}`);\n\t}\n\n\tconst byUri = contents.find((content) => content.uri === preferredUri);\n\tif (byUri) return byUri;\n\n\tconst byHtmlMime = contents.find((content) => content.mimeType && isHtmlMimeType(content.mimeType));\n\tif (byHtmlMime) return byHtmlMime;\n\n\tconst firstContent = contents[0];\n\tif (!firstContent) {\n\t\tthrow new Error(`No contents returned for UI resource: ${preferredUri}`);\n\t}\n\treturn firstContent;\n}\n\nfunction isHtmlMimeType(mimeType: string): boolean {\n\tconst normalized = mimeType.toLowerCase();\n\treturn normalized.startsWith(\"text/html\") || normalized === RESOURCE_MIME_TYPE.toLowerCase();\n}\n\nfunction toHtml(content: ResourceContentRecord): string {\n\tif (typeof content.text === \"string\") {\n\t\treturn content.text;\n\t}\n\n\tif (typeof content.blob === \"string\") {\n\t\treturn Buffer.from(content.blob, \"base64\").toString(\"utf-8\");\n\t}\n\n\tthrow new Error(`UI resource ${content.uri ?? \"(unknown)\"} did not include text or blob content`);\n}\n\nconst OPENAI_CSP_FIELD_MAPPINGS = [\n\t[\"resource_domains\", \"resourceDomains\"],\n\t[\"connect_domains\", \"connectDomains\"],\n\t[\"frame_domains\", \"frameDomains\"],\n] as const;\n\nconst UI_CSP_DOMAIN_FIELDS: readonly (keyof UiResourceCsp)[] = [\n\t\"resourceDomains\",\n\t\"connectDomains\",\n\t\"frameDomains\",\n\t\"baseUriDomains\",\n];\n\nfunction extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMeta {\n\tif (!meta || typeof meta !== \"object\") return {};\n\n\tconst ui = isRecord(meta.ui) ? meta.ui : undefined;\n\tconst out: UiResourceMeta = {};\n\tconst openAiCsp = Object.hasOwn(meta, \"openai/widgetCSP\")\n\t\t? normalizeOpenAiWidgetCsp(meta[\"openai/widgetCSP\"])\n\t\t: undefined;\n\tconst hasStandardCsp = !!ui && Object.hasOwn(ui, \"csp\");\n\tconst standardCspValue = hasStandardCsp ? ui.csp : undefined;\n\n\tif (hasStandardCsp && !isRecord(standardCspValue)) {\n\t\t// A declared canonical container takes precedence even when malformed.\n\t\tout.csp = {};\n\t} else {\n\t\tconst standardCsp = hasStandardCsp ? normalizeUiResourceCsp(standardCspValue) : undefined;\n\t\tif (openAiCsp || standardCsp) {\n\t\t\tout.csp = { ...openAiCsp, ...standardCsp };\n\t\t\tif (isRecord(standardCspValue)) {\n\t\t\t\tfor (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tObject.hasOwn(standardCspValue, standardField) &&\n\t\t\t\t\t\t!copyStringArray(standardCspValue[standardField])\n\t\t\t\t\t) {\n\t\t\t\t\t\tdelete out.csp[standardField];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (ui && isRecord(ui.permissions)) {\n\t\tout.permissions = ui.permissions as UiResourcePermissions;\n\t}\n\tif (ui && typeof ui.domain === \"string\") {\n\t\tout.domain = ui.domain;\n\t}\n\tif (ui && typeof ui.prefersBorder === \"boolean\") {\n\t\tout.prefersBorder = ui.prefersBorder;\n\t}\n\n\treturn out;\n}\n\nfunction normalizeUiResourceCsp(value: unknown): UiResourceCsp {\n\tif (!isRecord(value)) return {};\n\n\tconst csp: UiResourceCsp = {};\n\tfor (const field of UI_CSP_DOMAIN_FIELDS) {\n\t\tconst domains = copyStringArray(value[field]);\n\t\tif (domains) csp[field] = domains;\n\t}\n\treturn csp;\n}\n\nfunction normalizeOpenAiWidgetCsp(value: unknown): UiResourceCsp {\n\tif (!isRecord(value)) return {};\n\n\tconst csp: UiResourceCsp = {};\n\tfor (const [sourceField, targetField] of OPENAI_CSP_FIELD_MAPPINGS) {\n\t\tconst domains = copyStringArray(value[sourceField]);\n\t\tif (domains) csp[targetField] = domains;\n\t}\n\treturn csp;\n}\n\nfunction copyStringArray(value: unknown): string[] | undefined {\n\treturn Array.isArray(value) && value.every((entry) => typeof entry === \"string\") ? [...value] : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/client";
2
+ import type { ConsentManager } from "./consent-manager.ts";
3
+ import type { McpServerManager } from "./server-manager.ts";
4
+ import { type SessionRecoveryDeps } from "./session-recovery.ts";
5
+ import type { McpExtensionState } from "./state.ts";
6
+ import { type McpConfig, type UiHostContext, type UiMessageParams, type UiModelContextParams, type UiResourceContent, type UiSessionMessages, type UiStreamSummary } from "./types.ts";
7
+ export interface UiServerOptions {
8
+ serverName: string;
9
+ toolName: string;
10
+ toolArgs: Record<string, unknown>;
11
+ resource: UiResourceContent;
12
+ manager: McpServerManager;
13
+ /**
14
+ * Live extension config, used to re-read the server definition when
15
+ * recovering a terminated Streamable HTTP session (see
16
+ * session-recovery.ts). Optional so existing embedders/tests that don't
17
+ * need session recovery aren't forced to supply it; without it, proxied
18
+ * tool calls run without recovery (unchanged pre-existing behavior).
19
+ */
20
+ config?: McpConfig;
21
+ /** Live state enables TUI approval prompts for iframe-originated tool calls. */
22
+ state?: McpExtensionState;
23
+ onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
24
+ consentManager: ConsentManager;
25
+ hostContext?: UiHostContext;
26
+ initialResultPromise?: Promise<CallToolResult>;
27
+ sessionToken?: string;
28
+ port?: number;
29
+ onMessage?: (params: UiMessageParams) => Promise<void> | void;
30
+ onContextUpdate?: (params: UiModelContextParams) => Promise<void> | void;
31
+ onComplete?: (reason: string) => void;
32
+ }
33
+ export interface UiServerHandle {
34
+ url: string;
35
+ port: number;
36
+ sessionToken: string;
37
+ serverName: string;
38
+ toolName: string;
39
+ viewer?: "browser" | "glimpse" | "suppressed";
40
+ windowOpen?: boolean;
41
+ close: (reason?: string) => void;
42
+ sendToolInput: (args: Record<string, unknown>) => void;
43
+ sendToolResult: (result: CallToolResult) => void;
44
+ sendResultPatch: (result: CallToolResult) => void;
45
+ sendToolCancelled: (reason: string) => void;
46
+ sendHostContext: (context: UiHostContext) => void;
47
+ /** Get accumulated messages from this session */
48
+ getSessionMessages: () => UiSessionMessages;
49
+ getStreamSummary: () => UiStreamSummary | undefined;
50
+ }
51
+ export declare function startUiServer(options: UiServerOptions): Promise<UiServerHandle>;
52
+ //# sourceMappingURL=ui-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-server.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/ui-server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAoC,KAAK,mBAAmB,EAAuB,MAAM,uBAAuB,CAAC;AACxH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAKN,KAAK,SAAS,EAId,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAIzB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,MAAM,YAAY,CAAC;AAcpB,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,WAAW,CAAC,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IACjD,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9D,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;IAC9C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,eAAe,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAClD,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,eAAe,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAClD,iDAAiD;IACjD,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;IAC5C,gBAAgB,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;CACpD;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAmrBrF","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport http, { type IncomingMessage, type ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport type { CallToolRequest, CallToolResult } from \"@modelcontextprotocol/client\";\nimport { ContentBlockSchema } from \"@modelcontextprotocol/core\";\nimport type { ConsentManager } from \"./consent-manager.ts\";\nimport { ServerError, wrapError } from \"./errors.ts\";\nimport { buildCspMetaContent, buildHostHtmlTemplate } from \"./host-html-template.ts\";\nimport { logger } from \"./logger.ts\";\nimport { resourceNameToToolName } from \"./resource-tools.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport { SessionRecoveryAuthRequiredError, type SessionRecoveryDeps, withSessionRecovery } from \"./session-recovery.ts\";\nimport type { McpExtensionState } from \"./state.ts\";\nimport { ensureToolCallApproved, isToolCallApprovalRequired } from \"./tool-approval.ts\";\nimport {\n\tcreateUiModelContextUpdate,\n\textractUiPromptText,\n\tgetVisualizationStreamEnvelope,\n\tisServerDisabled,\n\ttype McpConfig,\n\ttype UiDisplayMode,\n\ttype UiDisplayModeRequest,\n\ttype UiDisplayModeResult,\n\ttype UiHostContext,\n\ttype UiMessageParams,\n\ttype UiModelContextParams,\n\ttype UiOpenLinkResult,\n\ttype UiProxyRequestBody,\n\ttype UiProxyResult,\n\ttype UiResourceContent,\n\ttype UiSessionMessages,\n\ttype UiStreamSummary,\n} from \"./types.ts\";\nimport { buildAllowAttribute } from \"./ui-app-bridge-helpers.ts\";\nimport { extractUiToolVisibility, isUiToolCallableByApp, isUiToolVisibleToModel } from \"./ui-tool-visibility.ts\";\nimport { formatAuthRequiredMessage, normalizeToolArguments } from \"./utils.ts\";\n\nconst MAX_BODY_SIZE = 2 * 1024 * 1024;\nconst ABANDONED_GRACE_MS = 60_000;\nconst WATCHDOG_INTERVAL_MS = 5_000;\nconst MAX_EVENT_LOG = 128;\nconst MAX_CONTEXT_UPDATES = 20;\nconst MOSHI_DISCOVERY_PORT_START = 8377;\nconst MOSHI_DISCOVERY_PORT_END = 8396;\nlet nextMoshiDiscoveryPort = MOSHI_DISCOVERY_PORT_START;\n\nexport interface UiServerOptions {\n\tserverName: string;\n\ttoolName: string;\n\ttoolArgs: Record<string, unknown>;\n\tresource: UiResourceContent;\n\tmanager: McpServerManager;\n\t/**\n\t * Live extension config, used to re-read the server definition when\n\t * recovering a terminated Streamable HTTP session (see\n\t * session-recovery.ts). Optional so existing embedders/tests that don't\n\t * need session recovery aren't forced to supply it; without it, proxied\n\t * tool calls run without recovery (unchanged pre-existing behavior).\n\t */\n\tconfig?: McpConfig;\n\t/** Live state enables TUI approval prompts for iframe-originated tool calls. */\n\tstate?: McpExtensionState;\n\tonNeedsAuth?: SessionRecoveryDeps[\"onNeedsAuth\"];\n\tconsentManager: ConsentManager;\n\thostContext?: UiHostContext;\n\tinitialResultPromise?: Promise<CallToolResult>;\n\tsessionToken?: string;\n\tport?: number;\n\tonMessage?: (params: UiMessageParams) => Promise<void> | void;\n\tonContextUpdate?: (params: UiModelContextParams) => Promise<void> | void;\n\tonComplete?: (reason: string) => void;\n}\n\nexport interface UiServerHandle {\n\turl: string;\n\tport: number;\n\tsessionToken: string;\n\tserverName: string;\n\ttoolName: string;\n\tviewer?: \"browser\" | \"glimpse\" | \"suppressed\";\n\twindowOpen?: boolean;\n\tclose: (reason?: string) => void;\n\tsendToolInput: (args: Record<string, unknown>) => void;\n\tsendToolResult: (result: CallToolResult) => void;\n\tsendResultPatch: (result: CallToolResult) => void;\n\tsendToolCancelled: (reason: string) => void;\n\tsendHostContext: (context: UiHostContext) => void;\n\t/** Get accumulated messages from this session */\n\tgetSessionMessages: () => UiSessionMessages;\n\tgetStreamSummary: () => UiStreamSummary | undefined;\n}\n\nexport async function startUiServer(options: UiServerOptions): Promise<UiServerHandle> {\n\tconst sessionToken = options.sessionToken ?? randomUUID();\n\tconst uiResourceToken = randomUUID();\n\tconst log = logger.child({\n\t\tcomponent: \"UiServer\",\n\t\tserver: options.serverName,\n\t\ttool: options.toolName,\n\t\tsession: sessionToken.slice(0, 8),\n\t});\n\n\tlog.debug(\"Starting UI server\");\n\n\tconst sseClients = new Set<ServerResponse>();\n\tlet completed = false;\n\tlet lastHeartbeatAt = Date.now();\n\tlet watchdog: NodeJS.Timeout | null = null;\n\tlet currentDisplayMode: UiDisplayMode = options.hostContext?.displayMode ?? \"inline\";\n\tlet nextEventId = 1;\n\tconst eventLog: Array<{ id: number; name: string; payload: unknown }> = [];\n\tlet latestCheckpointEventId: number | undefined;\n\tlet streamSummary: UiStreamSummary | undefined;\n\n\t// Track messages from UI for retrieval\n\tconst sessionMessages: UiSessionMessages = {\n\t\tprompts: [],\n\t\tnotifications: [],\n\t\tintents: [],\n\t\tcontexts: [],\n\t};\n\n\tconst hostContext: UiHostContext = {\n\t\tdisplayMode: currentDisplayMode,\n\t\tavailableDisplayModes: [\"inline\", \"fullscreen\", \"pip\"],\n\t\tplatform: \"desktop\",\n\t\t...options.hostContext,\n\t\t// Only include toolInfo if caller provides full tool definition with inputSchema\n\t\t// The App validates toolInfo.tool.inputSchema as required object\n\t};\n\n\tconst initialStreamContext = hostContext[\"nexus-mcp/stream\"];\n\tif (initialStreamContext && typeof initialStreamContext === \"object\") {\n\t\tconst streamId = (initialStreamContext as { streamId?: unknown }).streamId;\n\t\tconst mode = (initialStreamContext as { mode?: unknown }).mode;\n\t\tif (typeof streamId === \"string\" && (mode === \"eager\" || mode === \"stream-first\")) {\n\t\t\tstreamSummary = {\n\t\t\t\tstreamId,\n\t\t\t\tmode,\n\t\t\t\tframes: 0,\n\t\t\t\tphases: [],\n\t\t\t};\n\t\t}\n\t}\n\n\tconst isAppOnlyTool = (toolName: string): boolean => {\n\t\tconst toolDefinition = options.manager\n\t\t\t.getConnection(options.serverName)\n\t\t\t?.tools?.find((tool) => tool.name === toolName);\n\t\tif (!toolDefinition) return false;\n\t\tconst visibility = extractUiToolVisibility(toolDefinition._meta);\n\t\treturn isUiToolCallableByApp(visibility) && !isUiToolVisibleToModel(visibility);\n\t};\n\n\tconst recordUiMessage = async (msgParams: UiMessageParams): Promise<void> => {\n\t\tconst promptText = extractUiPromptText(msgParams);\n\n\t\t// Track messages by type (order: prompt → intent → notify)\n\t\t// Must match the order in index.ts onMessage handler\n\t\tif (promptText) {\n\t\t\tsessionMessages.prompts.push(promptText);\n\t\t\tlog.debug(\"UI prompt received\", { prompt: promptText.slice(0, 100) });\n\t\t} else if (msgParams.type === \"intent\" || msgParams.intent) {\n\t\t\tconst intentName = msgParams.intent ?? \"\";\n\t\t\tif (intentName) {\n\t\t\t\tsessionMessages.intents.push({\n\t\t\t\t\tintent: intentName,\n\t\t\t\t\t...(msgParams.params !== undefined ? { params: msgParams.params } : {}),\n\t\t\t\t});\n\t\t\t\tlog.debug(\"UI intent received\", { intent: intentName });\n\t\t\t}\n\t\t} else if (msgParams.type === \"notify\" || msgParams.message) {\n\t\t\tconst notifyText = msgParams.message ?? \"\";\n\t\t\tif (notifyText) {\n\t\t\t\tsessionMessages.notifications.push(notifyText);\n\t\t\t\tlog.debug(\"UI notification\", { message: notifyText.slice(0, 100) });\n\t\t\t}\n\t\t}\n\n\t\tawait options.onMessage?.(msgParams);\n\t};\n\n\tconst touchHeartbeat = () => {\n\t\tlastHeartbeatAt = Date.now();\n\t};\n\n\tconst updateStreamSummary = (eventId: number, payload: unknown) => {\n\t\tconst envelope = getVisualizationStreamEnvelope(\n\t\t\t(payload as { structuredContent?: unknown } | null)?.structuredContent,\n\t\t);\n\t\tif (!envelope) return;\n\t\tif (envelope.frameType === \"checkpoint\" || envelope.frameType === \"final\") {\n\t\t\tlatestCheckpointEventId = eventId;\n\t\t}\n\t\tif (!streamSummary) {\n\t\t\tstreamSummary = {\n\t\t\t\tstreamId: envelope.streamId,\n\t\t\t\tmode: \"eager\",\n\t\t\t\tframes: 0,\n\t\t\t\tphases: [],\n\t\t\t};\n\t\t}\n\t\tstreamSummary.frames += 1;\n\t\tif (!streamSummary.phases.includes(envelope.phase)) {\n\t\t\tstreamSummary.phases.push(envelope.phase);\n\t\t}\n\t\tstreamSummary.finalStatus = envelope.status;\n\t\tif (envelope.message !== undefined) streamSummary.lastMessage = envelope.message;\n\t\telse delete streamSummary.lastMessage;\n\t};\n\n\tconst serializeEvent = (eventId: number, name: string, payload: unknown): string => {\n\t\treturn `id: ${eventId}\\nevent: ${name}\\ndata: ${JSON.stringify(payload)}\\n\\n`;\n\t};\n\n\tconst getLatestCheckpointIndex = () => {\n\t\tconst firstEventId = eventLog[0]?.id;\n\t\tif (latestCheckpointEventId === undefined || firstEventId === undefined || latestCheckpointEventId < firstEventId)\n\t\t\treturn -1;\n\t\treturn latestCheckpointEventId - firstEventId;\n\t};\n\n\tconst pruneEventLog = () => {\n\t\tif (eventLog.length <= MAX_EVENT_LOG) return;\n\t\tconst latestCheckpointIndex = getLatestCheckpointIndex();\n\n\t\tif (latestCheckpointIndex > 0) {\n\t\t\teventLog.splice(0, latestCheckpointIndex);\n\t\t}\n\n\t\tif (eventLog.length > MAX_EVENT_LOG) {\n\t\t\teventLog.splice(0, eventLog.length - MAX_EVENT_LOG);\n\t\t}\n\t};\n\n\tconst pushEvent = (name: string, payload: unknown) => {\n\t\tif (completed) return;\n\t\tconst eventId = nextEventId++;\n\t\teventLog.push({ id: eventId, name, payload });\n\t\tupdateStreamSummary(eventId, payload);\n\t\tpruneEventLog();\n\t\tconst chunk = serializeEvent(eventId, name, payload);\n\t\tfor (const client of sseClients) {\n\t\t\ttry {\n\t\t\t\tclient.write(chunk);\n\t\t\t} catch {\n\t\t\t\tsseClients.delete(client);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst replayEvents = (res: ServerResponse, lastEventIdHeader?: string | null) => {\n\t\tconst parsedLastId = lastEventIdHeader ? Number(lastEventIdHeader) : Number.NaN;\n\t\tconst eventsToReplay = Number.isFinite(parsedLastId)\n\t\t\t? eventLog.filter((entry) => entry.id > parsedLastId)\n\t\t\t: (() => {\n\t\t\t\t\tconst latestCheckpointIndex = getLatestCheckpointIndex();\n\t\t\t\t\treturn latestCheckpointIndex >= 0 ? eventLog.slice(latestCheckpointIndex) : eventLog;\n\t\t\t\t})();\n\n\t\tfor (const entry of eventsToReplay) {\n\t\t\ttry {\n\t\t\t\tres.write(serializeEvent(entry.id, entry.name, entry.payload));\n\t\t\t} catch {\n\t\t\t\tsseClients.delete(res);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeSse = () => {\n\t\tfor (const client of sseClients) {\n\t\t\ttry {\n\t\t\t\tclient.end();\n\t\t\t} catch {}\n\t\t}\n\t\tsseClients.clear();\n\t};\n\n\tconst stopWatchdog = () => {\n\t\tif (!watchdog) return;\n\t\tclearInterval(watchdog);\n\t\twatchdog = null;\n\t};\n\n\tconst markCompleted = (reason: string) => {\n\t\tif (completed) return;\n\t\tlog.debug(\"Session completed\", { reason });\n\t\tpushEvent(\"session-complete\", { reason });\n\t\tcompleted = true;\n\t\tstopWatchdog();\n\t\toptions.onComplete?.(reason);\n\t};\n\n\tconst server = http.createServer(async (req, res) => {\n\t\ttry {\n\t\t\tconst method = req.method || \"GET\";\n\t\t\tconst hostHeader = req.headers.host;\n\t\t\tconst url = new URL(req.url || \"/\", `http://${hostHeader || \"127.0.0.1\"}`);\n\t\t\tif (hostHeader !== undefined && !isAllowedHost(url.hostname)) {\n\t\t\t\tsendText(res, 403, \"Invalid host\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"HEAD\" && url.pathname === \"/\") {\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"GET\" && url.pathname === \"/\") {\n\t\t\t\tif (!url.searchParams.has(\"session\")) {\n\t\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\", \"Cache-Control\": \"no-store\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\t'<!doctype html><html><head><meta charset=\"utf-8\"><title>MCP UI</title></head>' +\n\t\t\t\t\t\t\t\"<body><p>Open the authenticated MCP UI URL shown by Nexus.</p></body></html>\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!validateTokenQuery(url, sessionToken, res)) return;\n\t\t\t\ttouchHeartbeat();\n\n\t\t\t\tconst html = buildHostHtmlTemplate({\n\t\t\t\t\tsessionToken,\n\t\t\t\t\tuiResourceToken,\n\t\t\t\t\tserverName: options.serverName,\n\t\t\t\t\ttoolName: options.toolName,\n\t\t\t\t\ttoolArgs: options.toolArgs,\n\t\t\t\t\tresource: options.resource,\n\t\t\t\t\tallowAttribute: buildAllowAttribute(options.resource.meta.permissions),\n\t\t\t\t\trequireToolConsent: options.consentManager.requiresPrompt(options.serverName),\n\t\t\t\t\tcacheToolConsent: options.consentManager.shouldCacheConsent(),\n\t\t\t\t\thostContext,\n\t\t\t\t});\n\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(html);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"GET\" && url.pathname === \"/events\") {\n\t\t\t\tif (!validateTokenQuery(url, sessionToken, res)) return;\n\t\t\t\ttouchHeartbeat();\n\t\t\t\tlog.debug(\"SSE client connected\", { clientCount: sseClients.size + 1 });\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\t\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\t\t\tConnection: \"keep-alive\",\n\t\t\t\t\t\"X-Accel-Buffering\": \"no\",\n\t\t\t\t});\n\t\t\t\tres.write(\": connected\\n\\n\");\n\t\t\t\tsseClients.add(res);\n\t\t\t\treplayEvents(res, req.headers[\"last-event-id\"] ? String(req.headers[\"last-event-id\"]) : null);\n\t\t\t\treq.on(\"close\", () => {\n\t\t\t\t\tsseClients.delete(res);\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"GET\" && url.pathname === \"/health\") {\n\t\t\t\tif (!validateTokenQuery(url, sessionToken, res)) return;\n\t\t\t\tsendJson(res, 200, { ok: true, result: { healthy: true } });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"GET\" && url.pathname === \"/ui-app\") {\n\t\t\t\tif (!validateTokenQuery(url, uiResourceToken, res, \"resource\")) return;\n\t\t\t\ttouchHeartbeat();\n\t\t\t\t// Enforce host metadata independently of where app HTML places its document head.\n\t\t\t\tconst cspContent = buildCspMetaContent(options.resource.meta.csp);\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\t\"Content-Security-Policy\": cspContent,\n\t\t\t\t});\n\t\t\t\tres.end(options.resource.html);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method === \"GET\" && url.pathname === \"/app-bridge.bundle.js\") {\n\t\t\t\t// Serve the pre-bundled AppBridge module\n\t\t\t\tconst bundlePath = path.join(import.meta.dirname, \"app-bridge.bundle.js\");\n\t\t\t\ttry {\n\t\t\t\t\tconst content = await fs.readFile(bundlePath, \"utf-8\");\n\t\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\t\"Content-Type\": \"application/javascript\",\n\t\t\t\t\t\t\"Cache-Control\": \"public, max-age=31536000\",\n\t\t\t\t\t});\n\t\t\t\t\tres.end(content);\n\t\t\t\t} catch {\n\t\t\t\t\tsendJson(res, 500, { ok: false, error: \"Bundle not found\" });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (method !== \"POST\") {\n\t\t\t\tsendJson(res, 404, { ok: false, error: \"Not found\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst body = await parseBody(req, res);\n\t\t\tif (!body) return;\n\t\t\tif (!validateTokenBody(body, sessionToken, res)) return;\n\t\t\tconst params = body.params ?? {};\n\t\t\ttouchHeartbeat();\n\n\t\t\tif (url.pathname === \"/proxy/tools/call\") {\n\t\t\t\toptions.consentManager.ensureApproved(options.serverName);\n\t\t\t\tconst callParams = params as CallToolRequest[\"params\"];\n\t\t\t\tif (!callParams || typeof callParams.name !== \"string\" || !callParams.name.trim()) {\n\t\t\t\t\tsendJson(res, 400, { ok: false, error: \"Invalid tools/call params\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst connection = options.manager.getConnection(options.serverName);\n\t\t\t\tif (!connection || connection.status !== \"connected\") {\n\t\t\t\t\tsendJson(res, 503, { ok: false, error: `Server \"${options.serverName}\" is not connected` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tisServerDisabled(options.config?.mcpServers[options.serverName]) ||\n\t\t\t\t\tisServerDisabled(connection.definition)\n\t\t\t\t) {\n\t\t\t\t\tsendJson(res, 503, { ok: false, error: `Server \"${options.serverName}\" is disabled` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst toolDefinitions = Array.isArray(connection.tools) ? connection.tools : [];\n\t\t\t\tconst toolDefinition = toolDefinitions.find((tool) => tool.name === callParams.name);\n\t\t\t\tif (!toolDefinition) {\n\t\t\t\t\tsendJson(res, 403, { ok: false, error: `MCP tool \"${callParams.name}\" is not callable by apps` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst uiVisibility = extractUiToolVisibility(toolDefinition._meta);\n\t\t\t\tif (!isUiToolCallableByApp(uiVisibility)) {\n\t\t\t\t\tsendJson(res, 403, { ok: false, error: `MCP tool \"${callParams.name}\" is not callable by apps` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet normalizedArguments: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tnormalizedArguments = normalizeToolArguments(\n\t\t\t\t\t\tcallParams.arguments,\n\t\t\t\t\t\t`tool \"${callParams.name}\" arguments`,\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst callArgs = {\n\t\t\t\t\tname: callParams.name,\n\t\t\t\t\targuments: normalizedArguments,\n\t\t\t\t};\n\t\t\t\tconst toolMeta = {\n\t\t\t\t\tname: callParams.name,\n\t\t\t\t\toriginalName: callParams.name,\n\t\t\t\t\tdescription: toolDefinition?.description ?? \"\",\n\t\t\t\t\t...(toolDefinition?.inputSchema !== undefined ? { inputSchema: toolDefinition.inputSchema } : {}),\n\t\t\t\t\t...(uiVisibility !== undefined ? { uiVisibility } : {}),\n\t\t\t\t};\n\t\t\t\tconst approvalMetadata = new Map(options.state?.toolMetadata);\n\t\t\t\tconst definition =\n\t\t\t\t\toptions.config?.mcpServers[options.serverName] ?? options.state?.config.mcpServers[options.serverName];\n\t\t\t\tapprovalMetadata.set(options.serverName, [\n\t\t\t\t\t...connection.tools.map((tool) => ({\n\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\toriginalName: tool.name,\n\t\t\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\t\t})),\n\t\t\t\t\t...(definition?.exposeResources !== false\n\t\t\t\t\t\t? (connection.resources ?? []).map((resource) => {\n\t\t\t\t\t\t\t\tconst originalName = `read_${resourceNameToToolName(resource.name)}`;\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tname: originalName,\n\t\t\t\t\t\t\t\t\toriginalName,\n\t\t\t\t\t\t\t\t\tdescription: resource.description ?? `Read resource: ${resource.uri}`,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: []),\n\t\t\t\t]);\n\t\t\t\tconst approval = options.state\n\t\t\t\t\t? await ensureToolCallApproved(\n\t\t\t\t\t\t\toptions.state,\n\t\t\t\t\t\t\toptions.serverName,\n\t\t\t\t\t\t\ttoolMeta,\n\t\t\t\t\t\t\tcallArgs.arguments,\n\t\t\t\t\t\t\toptions.state.owner?.signal,\n\t\t\t\t\t\t\t\"iframe\",\n\t\t\t\t\t\t\tapprovalMetadata,\n\t\t\t\t\t\t)\n\t\t\t\t\t: options.config &&\n\t\t\t\t\t\t\tisToolCallApprovalRequired(options.config, options.serverName, toolMeta, approvalMetadata)\n\t\t\t\t\t\t? { ok: false as const, reason: \"approval_required_headless\" as const }\n\t\t\t\t\t\t: { ok: true as const };\n\t\t\t\tif (approval.ok === false) {\n\t\t\t\t\tconst denied = approval.reason === \"denied\";\n\t\t\t\t\tconst message = denied\n\t\t\t\t\t\t? `The user declined approval to run MCP tool \"${callParams.name}\" on server \"${options.serverName}\".`\n\t\t\t\t\t\t: `MCP tool \"${callParams.name}\" on server \"${options.serverName}\" is approval-gated and requires an interactive session.`;\n\t\t\t\t\tsendJson(res, 200, {\n\t\t\t\t\t\tok: true,\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: message }],\n\t\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\t\terror: denied ? \"approval_denied\" : \"approval_required\",\n\t\t\t\t\t\t\t\tserver: options.serverName,\n\t\t\t\t\t\t\t\ttool: callParams.name,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\toptions.manager.touch(options.serverName);\n\t\t\t\t\toptions.manager.incrementInFlight(options.serverName);\n\t\t\t\t\tconst result = options.config\n\t\t\t\t\t\t? await withSessionRecovery(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tmanager: options.manager,\n\t\t\t\t\t\t\t\t\tconfig: options.config,\n\t\t\t\t\t\t\t\t\t...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toptions.serverName,\n\t\t\t\t\t\t\t\t(conn) =>\n\t\t\t\t\t\t\t\t\tconn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName)),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: await connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));\n\t\t\t\t\tsendJson(res, 200, { ok: true, result });\n\t\t\t\t} finally {\n\t\t\t\t\toptions.manager.decrementInFlight(options.serverName);\n\t\t\t\t\toptions.manager.touch(options.serverName);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/consent\") {\n\t\t\t\tconst approved = !!(params as { approved?: boolean }).approved;\n\t\t\t\toptions.consentManager.registerDecision(options.serverName, approved);\n\t\t\t\tsendJson(res, 200, { ok: true, result: { approved } });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/generated-tool-call-intent\") {\n\t\t\t\tconst tool = typeof params.tool === \"string\" ? params.tool : undefined;\n\t\t\t\tif (tool && isAppOnlyTool(tool)) {\n\t\t\t\t\tlog.debug(\"Ignored generated app-only tool call intent\", { tool });\n\t\t\t\t} else {\n\t\t\t\t\tawait recordUiMessage({\n\t\t\t\t\t\ttype: \"intent\",\n\t\t\t\t\t\tintent: \"call_tool\",\n\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t...(tool !== undefined ? { tool } : {}),\n\t\t\t\t\t\t\t...(params.arguments !== undefined ? { arguments: params.arguments } : {}),\n\t\t\t\t\t\t\t...(params.isError !== undefined ? { isError: params.isError } : {}),\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsendJson(res, 200, { ok: true, result: {} });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/message\") {\n\t\t\t\tawait recordUiMessage(params as UiMessageParams);\n\t\t\t\tsendJson(res, 200, { ok: true, result: {} });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/context\") {\n\t\t\t\tconst content = params.content;\n\t\t\t\tconst structuredContent = params.structuredContent;\n\t\t\t\tif (\n\t\t\t\t\t(content !== undefined &&\n\t\t\t\t\t\t(!Array.isArray(content) || content.some((block) => !ContentBlockSchema.safeParse(block).success))) ||\n\t\t\t\t\t(structuredContent !== undefined &&\n\t\t\t\t\t\t(!structuredContent || typeof structuredContent !== \"object\" || Array.isArray(structuredContent)))\n\t\t\t\t) {\n\t\t\t\t\tsendJson(res, 400, { ok: false, error: \"Invalid update-model-context params\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst ctxParams: UiModelContextParams = {\n\t\t\t\t\t...(content !== undefined ? { content: content as NonNullable<UiModelContextParams[\"content\"]> } : {}),\n\t\t\t\t\t...(structuredContent !== undefined\n\t\t\t\t\t\t? { structuredContent: structuredContent as Record<string, unknown> }\n\t\t\t\t\t\t: {}),\n\t\t\t\t};\n\t\t\t\tconst update = createUiModelContextUpdate(ctxParams);\n\t\t\t\tif (update) {\n\t\t\t\t\tsessionMessages.contexts.push(update);\n\t\t\t\t\twhile (sessionMessages.contexts.length > MAX_CONTEXT_UPDATES) {\n\t\t\t\t\t\tsessionMessages.contexts.shift();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlog.debug(\"UI context update\", { hasContent: !!ctxParams.content, hasUpdate: !!update });\n\t\t\t\tawait options.onContextUpdate?.(ctxParams);\n\t\t\t\tsendJson(res, 200, { ok: true, result: {} });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/open-link\") {\n\t\t\t\tconst openParams = params as { url?: string };\n\t\t\t\tif (!openParams?.url || typeof openParams.url !== \"string\") {\n\t\t\t\t\tsendJson(res, 400, { ok: false, error: \"Invalid open-link params\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet result: UiOpenLinkResult = {};\n\t\t\t\ttry {\n\t\t\t\t\tconst externalUrl = new URL(openParams.url);\n\t\t\t\t\tif (\n\t\t\t\t\t\t(externalUrl.protocol !== \"http:\" && externalUrl.protocol !== \"https:\") ||\n\t\t\t\t\t\texternalUrl.username ||\n\t\t\t\t\t\texternalUrl.password\n\t\t\t\t\t) {\n\t\t\t\t\t\tresult = { isError: true };\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tresult = { isError: true };\n\t\t\t\t}\n\t\t\t\tsendJson(res, 200, { ok: true, result });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/download-file\") {\n\t\t\t\tsendJson(res, 200, { ok: true, result: { isError: true } });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/request-display-mode\") {\n\t\t\t\tconst displayParams = params as UiDisplayModeRequest;\n\t\t\t\tconst requested = displayParams?.mode;\n\t\t\t\tconst available = hostContext.availableDisplayModes ?? [\"inline\"];\n\t\t\t\tif (requested && available.includes(requested)) {\n\t\t\t\t\tcurrentDisplayMode = requested;\n\t\t\t\t}\n\t\t\t\thostContext.displayMode = currentDisplayMode;\n\t\t\t\tpushEvent(\"host-context\", { displayMode: currentDisplayMode });\n\t\t\t\tconst result: UiDisplayModeResult = { mode: currentDisplayMode };\n\t\t\t\tsendJson(res, 200, { ok: true, result });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/heartbeat\") {\n\t\t\t\tsendJson(res, 200, { ok: true, result: {} });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (url.pathname === \"/proxy/ui/complete\") {\n\t\t\t\tconst reason =\n\t\t\t\t\ttypeof (params as { reason?: string }).reason === \"string\"\n\t\t\t\t\t\t? (params as { reason?: string }).reason!\n\t\t\t\t\t\t: \"done\";\n\t\t\t\tmarkCompleted(reason);\n\t\t\t\tsendJson(res, 200, { ok: true, result: {} });\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tserver.close();\n\t\t\t\t\t} catch {}\n\t\t\t\t\tcloseSse();\n\t\t\t\t}, 20).unref();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsendJson(res, 404, { ok: false, error: \"Not found\" });\n\t\t} catch (error) {\n\t\t\tif (error instanceof SessionRecoveryAuthRequiredError) {\n\t\t\t\tconst fallback = `Server \"${options.serverName}\" requires OAuth authentication. Run mcp({ action: \"auth-start\", server: \"${options.serverName}\" }) to get a browser URL, or /mcp-auth ${options.serverName} in an interactive local session.`;\n\t\t\t\tconst message =\n\t\t\t\t\terror.authMessage ??\n\t\t\t\t\t(options.config ? formatAuthRequiredMessage(options.config, options.serverName, fallback) : fallback);\n\t\t\t\tsendJson(res, 401, { ok: false, error: message });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst wrapped = wrapError(error, { server: options.serverName, tool: options.toolName });\n\t\t\tconst status = /approval required|denied/i.test(wrapped.message) ? 403 : 500;\n\t\t\tif (status === 500) {\n\t\t\t\tlog.error(\"Request handler error\", error instanceof Error ? error : undefined);\n\t\t\t}\n\t\t\tsendJson(res, status, { ok: false, error: wrapped.message });\n\t\t}\n\t});\n\n\tif (options.initialResultPromise) {\n\t\toptions.initialResultPromise.then(\n\t\t\t(result) => pushEvent(\"tool-result\", result),\n\t\t\t(error) => {\n\t\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\t\tpushEvent(\"tool-cancelled\", { reason });\n\t\t\t},\n\t\t);\n\t}\n\n\twatchdog = setInterval(() => {\n\t\tif (completed) return;\n\t\tif (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) return;\n\t\tmarkCompleted(\"stale\");\n\t\ttry {\n\t\t\tserver.close();\n\t\t} catch {}\n\t\tcloseSse();\n\t}, WATCHDOG_INTERVAL_MS);\n\twatchdog.unref();\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst candidates = resolvePortCandidates(options.port);\n\t\tlet candidateIndex = 0;\n\n\t\tconst listen = () => {\n\t\t\tserver.once(\"error\", onError);\n\t\t\tserver.listen(candidates[candidateIndex], \"127.0.0.1\", onListening);\n\t\t};\n\n\t\tconst onError = (error: NodeJS.ErrnoException) => {\n\t\t\tserver.off(\"listening\", onListening);\n\t\t\tif (error.code === \"EADDRINUSE\" && candidateIndex < candidates.length - 1) {\n\t\t\t\tcandidateIndex += 1;\n\t\t\t\tlisten();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlog.error(\"Failed to start server\", error);\n\t\t\tconst port = candidates[candidateIndex];\n\t\t\treject(\n\t\t\t\tnew ServerError(error.message, {\n\t\t\t\t\t...(port !== undefined ? { port } : {}),\n\t\t\t\t\tcause: error,\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\n\t\tconst onListening = () => {\n\t\t\tserver.off(\"error\", onError);\n\t\t\tconst address = server.address();\n\t\t\tif (!address || typeof address === \"string\") {\n\t\t\t\tconst err = new ServerError(\"invalid address\");\n\t\t\t\tlog.error(\"Invalid server address\", err);\n\t\t\t\treject(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog.debug(\"Server started\", { port: address.port });\n\t\t\trememberMoshiDiscoveryPort(address.port);\n\n\t\t\tconst handle: UiServerHandle = {\n\t\t\t\turl: `http://localhost:${address.port}/?session=${sessionToken}`,\n\t\t\t\tport: address.port,\n\t\t\t\tsessionToken,\n\t\t\t\tserverName: options.serverName,\n\t\t\t\ttoolName: options.toolName,\n\t\t\t\tclose: (reason?: string) => {\n\t\t\t\t\tmarkCompleted(reason ?? \"closed\");\n\t\t\t\t\ttry {\n\t\t\t\t\t\tserver.close();\n\t\t\t\t\t} catch {}\n\t\t\t\t\tcloseSse();\n\t\t\t\t},\n\t\t\t\tsendToolInput: (args: Record<string, unknown>) => {\n\t\t\t\t\tpushEvent(\"tool-input\", { arguments: args });\n\t\t\t\t},\n\t\t\t\tsendToolResult: (result: CallToolResult) => {\n\t\t\t\t\tpushEvent(\"tool-result\", result);\n\t\t\t\t},\n\t\t\t\tsendResultPatch: (result: CallToolResult) => {\n\t\t\t\t\tpushEvent(\"result-patch\", result);\n\t\t\t\t},\n\t\t\t\tsendToolCancelled: (reason: string) => {\n\t\t\t\t\tpushEvent(\"tool-cancelled\", { reason });\n\t\t\t\t},\n\t\t\t\tsendHostContext: (context: UiHostContext) => {\n\t\t\t\t\tObject.assign(hostContext, context);\n\t\t\t\t\tpushEvent(\"host-context\", context);\n\t\t\t\t},\n\t\t\t\tgetSessionMessages: () => ({ ...sessionMessages }),\n\t\t\t\tgetStreamSummary: () =>\n\t\t\t\t\tstreamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : undefined,\n\t\t\t};\n\n\t\t\tresolve(handle);\n\t\t};\n\n\t\tlisten();\n\t});\n}\n\nasync function parseBody(\n\treq: IncomingMessage,\n\tres: ServerResponse,\n): Promise<UiProxyRequestBody<Record<string, unknown>> | null> {\n\ttry {\n\t\tconst body = await readBody(req);\n\t\tif (!body || typeof body !== \"object\") {\n\t\t\tsendJson(res, 400, { ok: false, error: \"Invalid request body\" });\n\t\t\treturn null;\n\t\t}\n\t\treturn body as UiProxyRequestBody<Record<string, unknown>>;\n\t} catch (error) {\n\t\tsendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : \"Invalid body\" });\n\t\treturn null;\n\t}\n}\n\nfunction readBody(req: IncomingMessage): Promise<unknown> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet size = 0;\n\t\tconst chunks: Buffer[] = [];\n\n\t\treq.on(\"data\", (chunk: Buffer) => {\n\t\t\tsize += chunk.length;\n\t\t\tif (size > MAX_BODY_SIZE) {\n\t\t\t\treq.destroy();\n\t\t\t\treject(new Error(\"Request body too large\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tchunks.push(chunk);\n\t\t});\n\n\t\treq.on(\"end\", () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(Buffer.concat(chunks).toString(\"utf-8\")));\n\t\t\t} catch (error) {\n\t\t\t\treject(error);\n\t\t\t}\n\t\t});\n\n\t\treq.on(\"error\", reject);\n\t});\n}\n\nfunction resolvePortCandidates(port: number | undefined): number[] {\n\tif (port !== undefined) return [port];\n\tconst candidates: number[] = [];\n\tconst count = MOSHI_DISCOVERY_PORT_END - MOSHI_DISCOVERY_PORT_START + 1;\n\tfor (let offset = 0; offset < count; offset += 1) {\n\t\tconst candidate =\n\t\t\tMOSHI_DISCOVERY_PORT_START + ((nextMoshiDiscoveryPort - MOSHI_DISCOVERY_PORT_START + offset) % count);\n\t\tcandidates.push(candidate);\n\t}\n\tcandidates.push(0);\n\treturn candidates;\n}\n\nfunction rememberMoshiDiscoveryPort(port: number): void {\n\tif (port < MOSHI_DISCOVERY_PORT_START || port > MOSHI_DISCOVERY_PORT_END) return;\n\tnextMoshiDiscoveryPort = port >= MOSHI_DISCOVERY_PORT_END ? MOSHI_DISCOVERY_PORT_START : port + 1;\n}\n\nfunction isAllowedHost(hostname: string): boolean {\n\treturn hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"[::1]\";\n}\n\nfunction validateTokenQuery(url: URL, expected: string, res: ServerResponse, parameter = \"session\"): boolean {\n\tconst token = url.searchParams.get(parameter);\n\tif (token !== expected) {\n\t\tsendJson(res, 403, { ok: false, error: \"Invalid session\" });\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction validateTokenBody(\n\tbody: UiProxyRequestBody<Record<string, unknown>>,\n\texpected: string,\n\tres: ServerResponse,\n): boolean {\n\tif (body.token !== expected) {\n\t\tsendJson(res, 403, { ok: false, error: \"Invalid session\" });\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction sendJson<T>(res: ServerResponse, status: number, payload: UiProxyResult<T>): void {\n\tres.writeHead(status, {\n\t\t\"Content-Type\": \"application/json; charset=utf-8\",\n\t\t\"Cache-Control\": \"no-store\",\n\t});\n\tres.end(JSON.stringify(payload));\n}\n\nfunction sendText(res: ServerResponse, status: number, text: string): void {\n\tres.writeHead(status, {\n\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t\t\"Cache-Control\": \"no-store\",\n\t});\n\tres.end(text);\n}\n"]}