@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 @@
1
+ {"version":3,"file":"mcp-output-guard.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/mcp-output-guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,eAAO,MAAM,4BAA4B,QAAY,CAAC;AACtD,eAAO,MAAM,4BAA4B,OAAO,CAAC;AACjD,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAQvD,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,IAAI,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,4BAA4B,CAC3C,QAAQ,CAAC,EAAE,WAAW,GACpB,IAAI,CAAC,qBAAqB,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAStF;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKpF;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,GAAE,qBAA0B,GACjC,OAAO,CAAC,gBAAgB,CAAC,CA6D3B","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { mkdtemp, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { ContentBlock, McpSettings } from \"./types.ts\";\n\nexport const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;\nexport const DEFAULT_MCP_OUTPUT_MAX_LINES = 2000;\nexport const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;\n\nconst CONTENT_SUMMARY_LIMIT = 20;\nconst KEY_PREVIEW_LIMIT = 20;\nconst KEY_MAX_CHARS = 120;\n\ntype Recordish = Record<string, unknown>;\n\nexport interface McpOutputGuardDetails {\n\ttruncated: true;\n\toriginalBytes: number;\n\treturnedBytes: number;\n\toriginalLines: number;\n\treturnedLines: number;\n\t/** Number of image content blocks returned untouched alongside the truncated text. */\n\timageBlocksPassedThrough?: number;\n\tfullOutputPath?: string;\n\twriteError?: string;\n}\n\nexport interface McpResultSummary {\n\tomitted: true;\n\treason: string;\n\tisError: boolean;\n\tcontentBlocks: number;\n\tcontentSummary: Array<Record<string, unknown>>;\n\tstructuredContent?: Record<string, unknown>;\n\tmeta?: Record<string, unknown>;\n\textraFields?: Array<Record<string, unknown>>;\n\trawResultBytes: number;\n\tfullResultPath?: string;\n\tresultWriteError?: string;\n}\n\nexport interface McpOutputGuardOptions {\n\tenabled?: boolean;\n\tprefix?: string;\n\tsuffix?: string;\n\temptyTextFallback?: string;\n\tmaxBytes?: number;\n\tmaxLines?: number;\n\tdetailsMaxBytes?: number;\n\t/**\n\t * Raw MCP result to expose as details.mcpResult. Kept raw when its JSON\n\t * fits detailsMaxBytes (or when the guard is disabled); otherwise replaced\n\t * with a compact summary and spilled to a temp file. Omit for call sites\n\t * whose details never carried the raw result (e.g. direct tools).\n\t */\n\trawMcpResult?: unknown;\n}\n\nexport interface GuardedMcpOutput {\n\tcontent: ContentBlock[];\n\toutputGuard?: McpOutputGuardDetails;\n\tmcpResult?: unknown;\n}\n\nexport function resolveMcpOutputGuardOptions(\n\tsettings?: McpSettings,\n): Pick<McpOutputGuardOptions, \"enabled\" | \"maxBytes\" | \"maxLines\" | \"detailsMaxBytes\"> {\n\tconst configured = settings?.outputGuard;\n\tconst tuning = typeof configured === \"object\" && configured !== null ? configured : undefined;\n\treturn {\n\t\tenabled: envKillSwitch(\"MCP_OUTPUT_GUARD\") ?? configured !== false,\n\t\tmaxBytes: positiveInt(tuning?.maxBytes) ?? DEFAULT_MCP_OUTPUT_MAX_BYTES,\n\t\tmaxLines: positiveInt(tuning?.maxLines) ?? DEFAULT_MCP_OUTPUT_MAX_LINES,\n\t\tdetailsMaxBytes: positiveInt(tuning?.detailsMaxBytes) ?? DEFAULT_MCP_DETAILS_MAX_BYTES,\n\t};\n}\n\n/** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */\nexport function guardedMcpDetails(guarded: GuardedMcpOutput): Record<string, unknown> {\n\treturn {\n\t\t...(guarded.mcpResult !== undefined ? { mcpResult: guarded.mcpResult } : {}),\n\t\t...(guarded.outputGuard ? { outputGuard: guarded.outputGuard } : {}),\n\t};\n}\n\n/**\n * Bound model-facing MCP output. Text output is capped at maxBytes/maxLines and\n * spilled to a temp file when oversized. Image blocks pass through untouched —\n * they are delivered to the provider as native image content, not text context.\n */\nexport async function guardMcpOutput(\n\tcontent: ContentBlock[],\n\toptions: McpOutputGuardOptions = {},\n): Promise<GuardedMcpOutput> {\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;\n\tconst maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;\n\tconst detailsMaxBytes = options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES;\n\tconst prefix = options.prefix ?? \"\";\n\tconst suffix = options.suffix ?? \"\";\n\n\tconst normalizedContent = withEmptyTextFallback(\n\t\tcontent.length > 0\n\t\t\t? sanitizeContent(content)\n\t\t\t: [{ type: \"text\" as const, text: options.emptyTextFallback ?? \"(empty result)\" }],\n\t\toptions.emptyTextFallback,\n\t);\n\n\tif (options.enabled === false) {\n\t\treturn {\n\t\t\tcontent: addAffixes(normalizedContent, prefix, suffix),\n\t\t\t...(options.rawMcpResult !== undefined ? { mcpResult: options.rawMcpResult } : {}),\n\t\t};\n\t}\n\n\tconst imageBlocks = normalizedContent.filter((block) => block.type === \"image\");\n\tconst textOutput = normalizedContent\n\t\t.filter((block) => block.type === \"text\")\n\t\t.map((block) => (block as { text: string }).text)\n\t\t.join(\"\\n\");\n\tconst composedOutput = `${prefix}${textOutput}${suffix}`;\n\tconst stats = textStats(composedOutput);\n\n\tlet guardedContent: ContentBlock[] = addAffixes(normalizedContent, prefix, suffix);\n\tlet outputGuard: McpOutputGuardDetails | undefined;\n\n\tif (stats.bytes > maxBytes || stats.lines > maxLines) {\n\t\tconst { path: fullOutputPath, error: writeError } = await saveArtifact(\"output\", composedOutput);\n\t\tconst notice = formatTruncationNotice(stats, fullOutputPath, writeError);\n\t\tconst previewBudget = reserveBudget(maxBytes, maxLines, notice);\n\t\tconst preview = truncateHead(composedOutput, previewBudget.maxBytes, previewBudget.maxLines);\n\t\tconst finalText = `${preview.content}\\n\\n${notice}`;\n\t\tconst finalStats = textStats(finalText);\n\n\t\tguardedContent = [{ type: \"text\" as const, text: finalText }, ...imageBlocks];\n\t\toutputGuard = {\n\t\t\ttruncated: true,\n\t\t\toriginalBytes: stats.bytes,\n\t\t\treturnedBytes: finalStats.bytes,\n\t\t\toriginalLines: stats.lines,\n\t\t\treturnedLines: finalStats.lines,\n\t\t\t...(imageBlocks.length > 0 ? { imageBlocksPassedThrough: imageBlocks.length } : {}),\n\t\t\t...(fullOutputPath !== undefined ? { fullOutputPath } : {}),\n\t\t\t...(writeError !== undefined ? { writeError } : {}),\n\t\t};\n\t}\n\n\tconst mcpResult =\n\t\toptions.rawMcpResult === undefined ? undefined : await boundMcpResult(options.rawMcpResult, detailsMaxBytes);\n\n\treturn {\n\t\tcontent: guardedContent,\n\t\t...(outputGuard ? { outputGuard } : {}),\n\t\t...(mcpResult !== undefined ? { mcpResult } : {}),\n\t};\n}\n\nfunction sanitizeContent(content: ContentBlock[]): ContentBlock[] {\n\treturn content.map((block) => {\n\t\tif (block.type !== \"image\") return block;\n\t\tconst mimeType =\n\t\t\ttypeof block.mimeType === \"string\" && block.mimeType.trim()\n\t\t\t\t? block.mimeType.trim().slice(0, 100)\n\t\t\t\t: \"image/png\";\n\t\treturn { ...block, mimeType };\n\t});\n}\n\nfunction withEmptyTextFallback(content: ContentBlock[], fallback: string | undefined): ContentBlock[] {\n\tif (!fallback) return content;\n\tconst textOutput = content\n\t\t.filter((block) => block.type === \"text\")\n\t\t.map((block) => (block as { text: string }).text)\n\t\t.join(\"\\n\");\n\tif (textOutput) return content;\n\treturn [{ type: \"text\", text: fallback }, ...content.filter((block) => block.type === \"image\")];\n}\n\nfunction addAffixes(content: ContentBlock[], prefix: string, suffix: string): ContentBlock[] {\n\tif (!prefix && !suffix) return content;\n\tconst next: ContentBlock[] = [...content];\n\n\tif (prefix) {\n\t\tconst index = next.findIndex((block) => block.type === \"text\");\n\t\tconst block = next[index];\n\t\tif (block?.type === \"text\") {\n\t\t\tnext[index] = { ...block, text: `${prefix}${block.text}` };\n\t\t} else {\n\t\t\tnext.unshift({ type: \"text\", text: prefix });\n\t\t}\n\t}\n\n\tif (suffix) {\n\t\tlet index = -1;\n\t\tfor (let i = next.length - 1; i >= 0; i--) {\n\t\t\tif (next[i]?.type === \"text\") {\n\t\t\t\tindex = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst block = next[index];\n\t\tif (block?.type === \"text\") {\n\t\t\tnext[index] = { ...block, text: `${block.text}${suffix}` };\n\t\t} else {\n\t\t\tnext.push({ type: \"text\", text: suffix });\n\t\t}\n\t}\n\n\treturn next;\n}\n\nfunction reserveBudget(maxBytes: number, maxLines: number, notice: string): { maxBytes: number; maxLines: number } {\n\tconst noticeStats = textStats(`\\n\\n${notice}`);\n\treturn {\n\t\tmaxBytes: Math.max(0, maxBytes - noticeStats.bytes),\n\t\tmaxLines: Math.max(0, maxLines - noticeStats.lines),\n\t};\n}\n\nfunction truncateHead(\n\ttext: string,\n\tmaxBytes: number,\n\tmaxLines: number,\n): { content: string; bytes: number; lines: number } {\n\tconst lines = text.split(\"\\n\");\n\tconst output: string[] = [];\n\tlet bytes = 0;\n\n\tfor (const line of lines) {\n\t\tif (output.length >= maxLines) break;\n\t\tconst separatorBytes = output.length > 0 ? 1 : 0;\n\t\tconst lineBytes = byteLength(line);\n\t\tif (bytes + separatorBytes + lineBytes > maxBytes) {\n\t\t\tconst remaining = maxBytes - bytes - separatorBytes;\n\t\t\tif (remaining > 0) {\n\t\t\t\toutput.push(truncateStringToBytes(line, remaining));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\toutput.push(line);\n\t\tbytes += separatorBytes + lineBytes;\n\t}\n\n\tconst content = output.join(\"\\n\");\n\tconst stats = textStats(content);\n\treturn { content, bytes: stats.bytes, lines: stats.lines };\n}\n\nfunction truncateStringToBytes(value: string, maxBytes: number): string {\n\tif (byteLength(value) <= maxBytes) return value;\n\tconst buffer = Buffer.from(value, \"utf8\");\n\tlet end = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;\n\twhile (end > 0 && (buffer.readUInt8(end) & 0xc0) === 0x80) end--;\n\treturn buffer.subarray(0, end).toString(\"utf8\");\n}\n\nfunction formatTruncationNotice(\n\tstats: { bytes: number; lines: number },\n\tfullOutputPath: string | undefined,\n\twriteError: string | undefined,\n): string {\n\tconst base = `[MCP text output truncated: original ${stats.lines.toLocaleString()} lines / ${formatSize(stats.bytes)}.`;\n\tif (fullOutputPath) {\n\t\treturn `${base} Full text saved to: ${fullOutputPath} — use read with offset/limit or grep to inspect.]`;\n\t}\n\treturn `${base} Full output could not be saved: ${writeError ?? \"unknown error\"}]`;\n}\n\n/**\n * Bound details.mcpResult: keep the raw result when its JSON fits within\n * detailsMaxBytes; otherwise replace it with a compact summary and spill the\n * raw JSON to a temp file.\n */\nasync function boundMcpResult(result: unknown, detailsMaxBytes: number): Promise<unknown> {\n\tconst raw = safeStringify(result);\n\tconst rawBytes = byteLength(raw);\n\tif (rawBytes <= detailsMaxBytes) return result;\n\treturn summarizeMcpResult(result, raw, rawBytes);\n}\n\nasync function summarizeMcpResult(result: unknown, raw: string, rawBytes: number): Promise<McpResultSummary> {\n\tconst { path: fullResultPath, error: resultWriteError } = await saveArtifact(\"mcp-result\", raw);\n\n\tconst record = asRecord(result);\n\tconst content = Array.isArray(record?.content) ? record.content : [];\n\tconst summary: McpResultSummary = {\n\t\tomitted: true,\n\t\treason:\n\t\t\t\"Raw MCP result exceeded the details size limit and was replaced with this summary to keep session context bounded.\",\n\t\tisError: record?.isError === true,\n\t\tcontentBlocks: content.length,\n\t\tcontentSummary: summarizeContent(content),\n\t\trawResultBytes: rawBytes,\n\t\t...(fullResultPath !== undefined ? { fullResultPath } : {}),\n\t\t...(resultWriteError !== undefined ? { resultWriteError } : {}),\n\t};\n\n\tif (record && \"structuredContent\" in record) {\n\t\tsummary.structuredContent = summarizeValue(record.structuredContent);\n\t}\n\tif (record && \"_meta\" in record) {\n\t\tsummary.meta = summarizeValue(record._meta);\n\t}\n\tif (record) {\n\t\tconst standard = new Set([\"content\", \"isError\", \"structuredContent\", \"_meta\"]);\n\t\tconst extraFields = Object.keys(record)\n\t\t\t.filter((key) => !standard.has(key))\n\t\t\t.slice(0, KEY_PREVIEW_LIMIT)\n\t\t\t.map((key) => ({\n\t\t\t\tkey: truncateKey(key),\n\t\t\t\ttype: typeof record[key],\n\t\t\t\testimatedBytes: estimateValueBytes(record[key]),\n\t\t\t\tomitted: true,\n\t\t\t}));\n\t\tif (extraFields.length > 0) summary.extraFields = extraFields;\n\t}\n\n\treturn summary;\n}\n\nfunction summarizeContent(content: unknown[]): Array<Record<string, unknown>> {\n\tconst summaries: Array<Record<string, unknown>> = content.slice(0, CONTENT_SUMMARY_LIMIT).map((block) => {\n\t\tconst record = asRecord(block);\n\t\tif (!record) return { type: typeof block, omitted: true };\n\t\tif (record.type === \"text\") {\n\t\t\tconst text = typeof record.text === \"string\" ? record.text : \"\";\n\t\t\treturn { type: \"text\", bytes: byteLength(text), lines: textStats(text).lines, textOmitted: true };\n\t\t}\n\t\tif (record.type === \"image\") {\n\t\t\tconst data = typeof record.data === \"string\" ? record.data : \"\";\n\t\t\treturn {\n\t\t\t\ttype: \"image\",\n\t\t\t\tmimeType: typeof record.mimeType === \"string\" ? record.mimeType : undefined,\n\t\t\t\tdataBytes: byteLength(data),\n\t\t\t\tdataOmitted: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: typeof record.type === \"string\" ? record.type : \"unknown\",\n\t\t\testimatedBytes: estimateValueBytes(record),\n\t\t\tomitted: true,\n\t\t};\n\t});\n\tif (content.length > CONTENT_SUMMARY_LIMIT) {\n\t\tsummaries.push({ type: \"omitted\", count: content.length - CONTENT_SUMMARY_LIMIT });\n\t}\n\treturn summaries;\n}\n\nfunction summarizeValue(value: unknown): Record<string, unknown> {\n\tconst record = asRecord(value);\n\tif (!record) {\n\t\treturn { type: value === null ? \"null\" : typeof value, estimatedBytes: estimateValueBytes(value), omitted: true };\n\t}\n\tconst keys = Object.keys(record);\n\treturn {\n\t\ttype: Array.isArray(value) ? \"array\" : \"object\",\n\t\testimatedBytes: estimateValueBytes(value),\n\t\tkeyCount: keys.length,\n\t\tkeysPreview: keys.slice(0, KEY_PREVIEW_LIMIT).map(truncateKey),\n\t\tomitted: true,\n\t};\n}\n\nfunction estimateValueBytes(value: unknown, depth = 0): number {\n\tif (value === null || value === undefined) return 0;\n\tif (typeof value === \"string\") return byteLength(value);\n\tif (typeof value === \"number\" || typeof value === \"boolean\" || typeof value === \"bigint\")\n\t\treturn byteLength(String(value));\n\tconst record = asRecord(value);\n\tif (!record || depth >= 2) return 0;\n\tconst values = Array.isArray(value)\n\t\t? value.slice(0, KEY_PREVIEW_LIMIT)\n\t\t: Object.values(record).slice(0, KEY_PREVIEW_LIMIT);\n\treturn values.reduce((total, item) => total + estimateValueBytes(item, depth + 1), 0);\n}\n\nfunction truncateKey(key: string): string {\n\treturn key.length <= KEY_MAX_CHARS ? key : `${key.slice(0, KEY_MAX_CHARS - 1)}…`;\n}\n\nasync function saveArtifact(kind: string, text: string): Promise<{ path?: string; error?: string }> {\n\ttry {\n\t\tconst dir = await mkdtemp(join(tmpdir(), \"nexus-mcp-output-\"));\n\t\tconst path = join(dir, `${kind}-${randomBytes(4).toString(\"hex\")}.txt`);\n\t\tawait writeFile(path, text, { encoding: \"utf8\", mode: 0o600 });\n\t\treturn { path };\n\t} catch (error) {\n\t\treturn { error: error instanceof Error ? error.message : String(error) };\n\t}\n}\n\nfunction asRecord(value: unknown): Recordish | undefined {\n\treturn typeof value === \"object\" && value !== null ? (value as Recordish) : undefined;\n}\n\nfunction safeStringify(value: unknown): string {\n\ttry {\n\t\t// The output guard measures and spills raw MCP results; it does not render this JSON for the model.\n\t\treturn JSON.stringify(value) ?? String(value);\n\t} catch {\n\t\treturn String(value);\n\t}\n}\n\nfunction textStats(text: string): { bytes: number; lines: number } {\n\treturn { bytes: byteLength(text), lines: text.length === 0 ? 0 : text.split(\"\\n\").length };\n}\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf8\");\n}\n\nfunction positiveInt(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n\tconst integer = Math.floor(value);\n\treturn integer > 0 ? integer : undefined;\n}\n\nfunction envKillSwitch(name: string): boolean | undefined {\n\tconst value = process.env[name]?.trim().toLowerCase();\n\tif (!value) return undefined;\n\tif ([\"0\", \"false\", \"no\", \"off\"].includes(value)) return false;\n\tif ([\"1\", \"true\", \"yes\", \"on\"].includes(value)) return true;\n\treturn undefined;\n}\n\nfunction formatSize(bytes: number): string {\n\tif (bytes < 1024) return `${bytes} B`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;\n}\n"]}
@@ -0,0 +1,343 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ export const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;
6
+ export const DEFAULT_MCP_OUTPUT_MAX_LINES = 2000;
7
+ export const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;
8
+ const CONTENT_SUMMARY_LIMIT = 20;
9
+ const KEY_PREVIEW_LIMIT = 20;
10
+ const KEY_MAX_CHARS = 120;
11
+ export function resolveMcpOutputGuardOptions(settings) {
12
+ const configured = settings?.outputGuard;
13
+ const tuning = typeof configured === "object" && configured !== null ? configured : undefined;
14
+ return {
15
+ enabled: envKillSwitch("MCP_OUTPUT_GUARD") ?? configured !== false,
16
+ maxBytes: positiveInt(tuning?.maxBytes) ?? DEFAULT_MCP_OUTPUT_MAX_BYTES,
17
+ maxLines: positiveInt(tuning?.maxLines) ?? DEFAULT_MCP_OUTPUT_MAX_LINES,
18
+ detailsMaxBytes: positiveInt(tuning?.detailsMaxBytes) ?? DEFAULT_MCP_DETAILS_MAX_BYTES,
19
+ };
20
+ }
21
+ /** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */
22
+ export function guardedMcpDetails(guarded) {
23
+ return {
24
+ ...(guarded.mcpResult !== undefined ? { mcpResult: guarded.mcpResult } : {}),
25
+ ...(guarded.outputGuard ? { outputGuard: guarded.outputGuard } : {}),
26
+ };
27
+ }
28
+ /**
29
+ * Bound model-facing MCP output. Text output is capped at maxBytes/maxLines and
30
+ * spilled to a temp file when oversized. Image blocks pass through untouched —
31
+ * they are delivered to the provider as native image content, not text context.
32
+ */
33
+ export async function guardMcpOutput(content, options = {}) {
34
+ const maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;
35
+ const maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;
36
+ const detailsMaxBytes = options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES;
37
+ const prefix = options.prefix ?? "";
38
+ const suffix = options.suffix ?? "";
39
+ const normalizedContent = withEmptyTextFallback(content.length > 0
40
+ ? sanitizeContent(content)
41
+ : [{ type: "text", text: options.emptyTextFallback ?? "(empty result)" }], options.emptyTextFallback);
42
+ if (options.enabled === false) {
43
+ return {
44
+ content: addAffixes(normalizedContent, prefix, suffix),
45
+ ...(options.rawMcpResult !== undefined ? { mcpResult: options.rawMcpResult } : {}),
46
+ };
47
+ }
48
+ const imageBlocks = normalizedContent.filter((block) => block.type === "image");
49
+ const textOutput = normalizedContent
50
+ .filter((block) => block.type === "text")
51
+ .map((block) => block.text)
52
+ .join("\n");
53
+ const composedOutput = `${prefix}${textOutput}${suffix}`;
54
+ const stats = textStats(composedOutput);
55
+ let guardedContent = addAffixes(normalizedContent, prefix, suffix);
56
+ let outputGuard;
57
+ if (stats.bytes > maxBytes || stats.lines > maxLines) {
58
+ const { path: fullOutputPath, error: writeError } = await saveArtifact("output", composedOutput);
59
+ const notice = formatTruncationNotice(stats, fullOutputPath, writeError);
60
+ const previewBudget = reserveBudget(maxBytes, maxLines, notice);
61
+ const preview = truncateHead(composedOutput, previewBudget.maxBytes, previewBudget.maxLines);
62
+ const finalText = `${preview.content}\n\n${notice}`;
63
+ const finalStats = textStats(finalText);
64
+ guardedContent = [{ type: "text", text: finalText }, ...imageBlocks];
65
+ outputGuard = {
66
+ truncated: true,
67
+ originalBytes: stats.bytes,
68
+ returnedBytes: finalStats.bytes,
69
+ originalLines: stats.lines,
70
+ returnedLines: finalStats.lines,
71
+ ...(imageBlocks.length > 0 ? { imageBlocksPassedThrough: imageBlocks.length } : {}),
72
+ ...(fullOutputPath !== undefined ? { fullOutputPath } : {}),
73
+ ...(writeError !== undefined ? { writeError } : {}),
74
+ };
75
+ }
76
+ const mcpResult = options.rawMcpResult === undefined ? undefined : await boundMcpResult(options.rawMcpResult, detailsMaxBytes);
77
+ return {
78
+ content: guardedContent,
79
+ ...(outputGuard ? { outputGuard } : {}),
80
+ ...(mcpResult !== undefined ? { mcpResult } : {}),
81
+ };
82
+ }
83
+ function sanitizeContent(content) {
84
+ return content.map((block) => {
85
+ if (block.type !== "image")
86
+ return block;
87
+ const mimeType = typeof block.mimeType === "string" && block.mimeType.trim()
88
+ ? block.mimeType.trim().slice(0, 100)
89
+ : "image/png";
90
+ return { ...block, mimeType };
91
+ });
92
+ }
93
+ function withEmptyTextFallback(content, fallback) {
94
+ if (!fallback)
95
+ return content;
96
+ const textOutput = content
97
+ .filter((block) => block.type === "text")
98
+ .map((block) => block.text)
99
+ .join("\n");
100
+ if (textOutput)
101
+ return content;
102
+ return [{ type: "text", text: fallback }, ...content.filter((block) => block.type === "image")];
103
+ }
104
+ function addAffixes(content, prefix, suffix) {
105
+ if (!prefix && !suffix)
106
+ return content;
107
+ const next = [...content];
108
+ if (prefix) {
109
+ const index = next.findIndex((block) => block.type === "text");
110
+ const block = next[index];
111
+ if (block?.type === "text") {
112
+ next[index] = { ...block, text: `${prefix}${block.text}` };
113
+ }
114
+ else {
115
+ next.unshift({ type: "text", text: prefix });
116
+ }
117
+ }
118
+ if (suffix) {
119
+ let index = -1;
120
+ for (let i = next.length - 1; i >= 0; i--) {
121
+ if (next[i]?.type === "text") {
122
+ index = i;
123
+ break;
124
+ }
125
+ }
126
+ const block = next[index];
127
+ if (block?.type === "text") {
128
+ next[index] = { ...block, text: `${block.text}${suffix}` };
129
+ }
130
+ else {
131
+ next.push({ type: "text", text: suffix });
132
+ }
133
+ }
134
+ return next;
135
+ }
136
+ function reserveBudget(maxBytes, maxLines, notice) {
137
+ const noticeStats = textStats(`\n\n${notice}`);
138
+ return {
139
+ maxBytes: Math.max(0, maxBytes - noticeStats.bytes),
140
+ maxLines: Math.max(0, maxLines - noticeStats.lines),
141
+ };
142
+ }
143
+ function truncateHead(text, maxBytes, maxLines) {
144
+ const lines = text.split("\n");
145
+ const output = [];
146
+ let bytes = 0;
147
+ for (const line of lines) {
148
+ if (output.length >= maxLines)
149
+ break;
150
+ const separatorBytes = output.length > 0 ? 1 : 0;
151
+ const lineBytes = byteLength(line);
152
+ if (bytes + separatorBytes + lineBytes > maxBytes) {
153
+ const remaining = maxBytes - bytes - separatorBytes;
154
+ if (remaining > 0) {
155
+ output.push(truncateStringToBytes(line, remaining));
156
+ }
157
+ break;
158
+ }
159
+ output.push(line);
160
+ bytes += separatorBytes + lineBytes;
161
+ }
162
+ const content = output.join("\n");
163
+ const stats = textStats(content);
164
+ return { content, bytes: stats.bytes, lines: stats.lines };
165
+ }
166
+ function truncateStringToBytes(value, maxBytes) {
167
+ if (byteLength(value) <= maxBytes)
168
+ return value;
169
+ const buffer = Buffer.from(value, "utf8");
170
+ let end = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;
171
+ while (end > 0 && (buffer.readUInt8(end) & 0xc0) === 0x80)
172
+ end--;
173
+ return buffer.subarray(0, end).toString("utf8");
174
+ }
175
+ function formatTruncationNotice(stats, fullOutputPath, writeError) {
176
+ const base = `[MCP text output truncated: original ${stats.lines.toLocaleString()} lines / ${formatSize(stats.bytes)}.`;
177
+ if (fullOutputPath) {
178
+ return `${base} Full text saved to: ${fullOutputPath} — use read with offset/limit or grep to inspect.]`;
179
+ }
180
+ return `${base} Full output could not be saved: ${writeError ?? "unknown error"}]`;
181
+ }
182
+ /**
183
+ * Bound details.mcpResult: keep the raw result when its JSON fits within
184
+ * detailsMaxBytes; otherwise replace it with a compact summary and spill the
185
+ * raw JSON to a temp file.
186
+ */
187
+ async function boundMcpResult(result, detailsMaxBytes) {
188
+ const raw = safeStringify(result);
189
+ const rawBytes = byteLength(raw);
190
+ if (rawBytes <= detailsMaxBytes)
191
+ return result;
192
+ return summarizeMcpResult(result, raw, rawBytes);
193
+ }
194
+ async function summarizeMcpResult(result, raw, rawBytes) {
195
+ const { path: fullResultPath, error: resultWriteError } = await saveArtifact("mcp-result", raw);
196
+ const record = asRecord(result);
197
+ const content = Array.isArray(record?.content) ? record.content : [];
198
+ const summary = {
199
+ omitted: true,
200
+ reason: "Raw MCP result exceeded the details size limit and was replaced with this summary to keep session context bounded.",
201
+ isError: record?.isError === true,
202
+ contentBlocks: content.length,
203
+ contentSummary: summarizeContent(content),
204
+ rawResultBytes: rawBytes,
205
+ ...(fullResultPath !== undefined ? { fullResultPath } : {}),
206
+ ...(resultWriteError !== undefined ? { resultWriteError } : {}),
207
+ };
208
+ if (record && "structuredContent" in record) {
209
+ summary.structuredContent = summarizeValue(record.structuredContent);
210
+ }
211
+ if (record && "_meta" in record) {
212
+ summary.meta = summarizeValue(record._meta);
213
+ }
214
+ if (record) {
215
+ const standard = new Set(["content", "isError", "structuredContent", "_meta"]);
216
+ const extraFields = Object.keys(record)
217
+ .filter((key) => !standard.has(key))
218
+ .slice(0, KEY_PREVIEW_LIMIT)
219
+ .map((key) => ({
220
+ key: truncateKey(key),
221
+ type: typeof record[key],
222
+ estimatedBytes: estimateValueBytes(record[key]),
223
+ omitted: true,
224
+ }));
225
+ if (extraFields.length > 0)
226
+ summary.extraFields = extraFields;
227
+ }
228
+ return summary;
229
+ }
230
+ function summarizeContent(content) {
231
+ const summaries = content.slice(0, CONTENT_SUMMARY_LIMIT).map((block) => {
232
+ const record = asRecord(block);
233
+ if (!record)
234
+ return { type: typeof block, omitted: true };
235
+ if (record.type === "text") {
236
+ const text = typeof record.text === "string" ? record.text : "";
237
+ return { type: "text", bytes: byteLength(text), lines: textStats(text).lines, textOmitted: true };
238
+ }
239
+ if (record.type === "image") {
240
+ const data = typeof record.data === "string" ? record.data : "";
241
+ return {
242
+ type: "image",
243
+ mimeType: typeof record.mimeType === "string" ? record.mimeType : undefined,
244
+ dataBytes: byteLength(data),
245
+ dataOmitted: true,
246
+ };
247
+ }
248
+ return {
249
+ type: typeof record.type === "string" ? record.type : "unknown",
250
+ estimatedBytes: estimateValueBytes(record),
251
+ omitted: true,
252
+ };
253
+ });
254
+ if (content.length > CONTENT_SUMMARY_LIMIT) {
255
+ summaries.push({ type: "omitted", count: content.length - CONTENT_SUMMARY_LIMIT });
256
+ }
257
+ return summaries;
258
+ }
259
+ function summarizeValue(value) {
260
+ const record = asRecord(value);
261
+ if (!record) {
262
+ return { type: value === null ? "null" : typeof value, estimatedBytes: estimateValueBytes(value), omitted: true };
263
+ }
264
+ const keys = Object.keys(record);
265
+ return {
266
+ type: Array.isArray(value) ? "array" : "object",
267
+ estimatedBytes: estimateValueBytes(value),
268
+ keyCount: keys.length,
269
+ keysPreview: keys.slice(0, KEY_PREVIEW_LIMIT).map(truncateKey),
270
+ omitted: true,
271
+ };
272
+ }
273
+ function estimateValueBytes(value, depth = 0) {
274
+ if (value === null || value === undefined)
275
+ return 0;
276
+ if (typeof value === "string")
277
+ return byteLength(value);
278
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
279
+ return byteLength(String(value));
280
+ const record = asRecord(value);
281
+ if (!record || depth >= 2)
282
+ return 0;
283
+ const values = Array.isArray(value)
284
+ ? value.slice(0, KEY_PREVIEW_LIMIT)
285
+ : Object.values(record).slice(0, KEY_PREVIEW_LIMIT);
286
+ return values.reduce((total, item) => total + estimateValueBytes(item, depth + 1), 0);
287
+ }
288
+ function truncateKey(key) {
289
+ return key.length <= KEY_MAX_CHARS ? key : `${key.slice(0, KEY_MAX_CHARS - 1)}…`;
290
+ }
291
+ async function saveArtifact(kind, text) {
292
+ try {
293
+ const dir = await mkdtemp(join(tmpdir(), "nexus-mcp-output-"));
294
+ const path = join(dir, `${kind}-${randomBytes(4).toString("hex")}.txt`);
295
+ await writeFile(path, text, { encoding: "utf8", mode: 0o600 });
296
+ return { path };
297
+ }
298
+ catch (error) {
299
+ return { error: error instanceof Error ? error.message : String(error) };
300
+ }
301
+ }
302
+ function asRecord(value) {
303
+ return typeof value === "object" && value !== null ? value : undefined;
304
+ }
305
+ function safeStringify(value) {
306
+ try {
307
+ // The output guard measures and spills raw MCP results; it does not render this JSON for the model.
308
+ return JSON.stringify(value) ?? String(value);
309
+ }
310
+ catch {
311
+ return String(value);
312
+ }
313
+ }
314
+ function textStats(text) {
315
+ return { bytes: byteLength(text), lines: text.length === 0 ? 0 : text.split("\n").length };
316
+ }
317
+ function byteLength(text) {
318
+ return Buffer.byteLength(text, "utf8");
319
+ }
320
+ function positiveInt(value) {
321
+ if (typeof value !== "number" || !Number.isFinite(value))
322
+ return undefined;
323
+ const integer = Math.floor(value);
324
+ return integer > 0 ? integer : undefined;
325
+ }
326
+ function envKillSwitch(name) {
327
+ const value = process.env[name]?.trim().toLowerCase();
328
+ if (!value)
329
+ return undefined;
330
+ if (["0", "false", "no", "off"].includes(value))
331
+ return false;
332
+ if (["1", "true", "yes", "on"].includes(value))
333
+ return true;
334
+ return undefined;
335
+ }
336
+ function formatSize(bytes) {
337
+ if (bytes < 1024)
338
+ return `${bytes} B`;
339
+ if (bytes < 1024 * 1024)
340
+ return `${(bytes / 1024).toFixed(1)} KiB`;
341
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
342
+ }
343
+ //# sourceMappingURL=mcp-output-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-output-guard.js","sourceRoot":"","sources":["../../../src/extensions/mcp/mcp-output-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { mkdtemp, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { ContentBlock, McpSettings } from \"./types.ts\";\n\nexport const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;\nexport const DEFAULT_MCP_OUTPUT_MAX_LINES = 2000;\nexport const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;\n\nconst CONTENT_SUMMARY_LIMIT = 20;\nconst KEY_PREVIEW_LIMIT = 20;\nconst KEY_MAX_CHARS = 120;\n\ntype Recordish = Record<string, unknown>;\n\nexport interface McpOutputGuardDetails {\n\ttruncated: true;\n\toriginalBytes: number;\n\treturnedBytes: number;\n\toriginalLines: number;\n\treturnedLines: number;\n\t/** Number of image content blocks returned untouched alongside the truncated text. */\n\timageBlocksPassedThrough?: number;\n\tfullOutputPath?: string;\n\twriteError?: string;\n}\n\nexport interface McpResultSummary {\n\tomitted: true;\n\treason: string;\n\tisError: boolean;\n\tcontentBlocks: number;\n\tcontentSummary: Array<Record<string, unknown>>;\n\tstructuredContent?: Record<string, unknown>;\n\tmeta?: Record<string, unknown>;\n\textraFields?: Array<Record<string, unknown>>;\n\trawResultBytes: number;\n\tfullResultPath?: string;\n\tresultWriteError?: string;\n}\n\nexport interface McpOutputGuardOptions {\n\tenabled?: boolean;\n\tprefix?: string;\n\tsuffix?: string;\n\temptyTextFallback?: string;\n\tmaxBytes?: number;\n\tmaxLines?: number;\n\tdetailsMaxBytes?: number;\n\t/**\n\t * Raw MCP result to expose as details.mcpResult. Kept raw when its JSON\n\t * fits detailsMaxBytes (or when the guard is disabled); otherwise replaced\n\t * with a compact summary and spilled to a temp file. Omit for call sites\n\t * whose details never carried the raw result (e.g. direct tools).\n\t */\n\trawMcpResult?: unknown;\n}\n\nexport interface GuardedMcpOutput {\n\tcontent: ContentBlock[];\n\toutputGuard?: McpOutputGuardDetails;\n\tmcpResult?: unknown;\n}\n\nexport function resolveMcpOutputGuardOptions(\n\tsettings?: McpSettings,\n): Pick<McpOutputGuardOptions, \"enabled\" | \"maxBytes\" | \"maxLines\" | \"detailsMaxBytes\"> {\n\tconst configured = settings?.outputGuard;\n\tconst tuning = typeof configured === \"object\" && configured !== null ? configured : undefined;\n\treturn {\n\t\tenabled: envKillSwitch(\"MCP_OUTPUT_GUARD\") ?? configured !== false,\n\t\tmaxBytes: positiveInt(tuning?.maxBytes) ?? DEFAULT_MCP_OUTPUT_MAX_BYTES,\n\t\tmaxLines: positiveInt(tuning?.maxLines) ?? DEFAULT_MCP_OUTPUT_MAX_LINES,\n\t\tdetailsMaxBytes: positiveInt(tuning?.detailsMaxBytes) ?? DEFAULT_MCP_DETAILS_MAX_BYTES,\n\t};\n}\n\n/** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */\nexport function guardedMcpDetails(guarded: GuardedMcpOutput): Record<string, unknown> {\n\treturn {\n\t\t...(guarded.mcpResult !== undefined ? { mcpResult: guarded.mcpResult } : {}),\n\t\t...(guarded.outputGuard ? { outputGuard: guarded.outputGuard } : {}),\n\t};\n}\n\n/**\n * Bound model-facing MCP output. Text output is capped at maxBytes/maxLines and\n * spilled to a temp file when oversized. Image blocks pass through untouched —\n * they are delivered to the provider as native image content, not text context.\n */\nexport async function guardMcpOutput(\n\tcontent: ContentBlock[],\n\toptions: McpOutputGuardOptions = {},\n): Promise<GuardedMcpOutput> {\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;\n\tconst maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;\n\tconst detailsMaxBytes = options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES;\n\tconst prefix = options.prefix ?? \"\";\n\tconst suffix = options.suffix ?? \"\";\n\n\tconst normalizedContent = withEmptyTextFallback(\n\t\tcontent.length > 0\n\t\t\t? sanitizeContent(content)\n\t\t\t: [{ type: \"text\" as const, text: options.emptyTextFallback ?? \"(empty result)\" }],\n\t\toptions.emptyTextFallback,\n\t);\n\n\tif (options.enabled === false) {\n\t\treturn {\n\t\t\tcontent: addAffixes(normalizedContent, prefix, suffix),\n\t\t\t...(options.rawMcpResult !== undefined ? { mcpResult: options.rawMcpResult } : {}),\n\t\t};\n\t}\n\n\tconst imageBlocks = normalizedContent.filter((block) => block.type === \"image\");\n\tconst textOutput = normalizedContent\n\t\t.filter((block) => block.type === \"text\")\n\t\t.map((block) => (block as { text: string }).text)\n\t\t.join(\"\\n\");\n\tconst composedOutput = `${prefix}${textOutput}${suffix}`;\n\tconst stats = textStats(composedOutput);\n\n\tlet guardedContent: ContentBlock[] = addAffixes(normalizedContent, prefix, suffix);\n\tlet outputGuard: McpOutputGuardDetails | undefined;\n\n\tif (stats.bytes > maxBytes || stats.lines > maxLines) {\n\t\tconst { path: fullOutputPath, error: writeError } = await saveArtifact(\"output\", composedOutput);\n\t\tconst notice = formatTruncationNotice(stats, fullOutputPath, writeError);\n\t\tconst previewBudget = reserveBudget(maxBytes, maxLines, notice);\n\t\tconst preview = truncateHead(composedOutput, previewBudget.maxBytes, previewBudget.maxLines);\n\t\tconst finalText = `${preview.content}\\n\\n${notice}`;\n\t\tconst finalStats = textStats(finalText);\n\n\t\tguardedContent = [{ type: \"text\" as const, text: finalText }, ...imageBlocks];\n\t\toutputGuard = {\n\t\t\ttruncated: true,\n\t\t\toriginalBytes: stats.bytes,\n\t\t\treturnedBytes: finalStats.bytes,\n\t\t\toriginalLines: stats.lines,\n\t\t\treturnedLines: finalStats.lines,\n\t\t\t...(imageBlocks.length > 0 ? { imageBlocksPassedThrough: imageBlocks.length } : {}),\n\t\t\t...(fullOutputPath !== undefined ? { fullOutputPath } : {}),\n\t\t\t...(writeError !== undefined ? { writeError } : {}),\n\t\t};\n\t}\n\n\tconst mcpResult =\n\t\toptions.rawMcpResult === undefined ? undefined : await boundMcpResult(options.rawMcpResult, detailsMaxBytes);\n\n\treturn {\n\t\tcontent: guardedContent,\n\t\t...(outputGuard ? { outputGuard } : {}),\n\t\t...(mcpResult !== undefined ? { mcpResult } : {}),\n\t};\n}\n\nfunction sanitizeContent(content: ContentBlock[]): ContentBlock[] {\n\treturn content.map((block) => {\n\t\tif (block.type !== \"image\") return block;\n\t\tconst mimeType =\n\t\t\ttypeof block.mimeType === \"string\" && block.mimeType.trim()\n\t\t\t\t? block.mimeType.trim().slice(0, 100)\n\t\t\t\t: \"image/png\";\n\t\treturn { ...block, mimeType };\n\t});\n}\n\nfunction withEmptyTextFallback(content: ContentBlock[], fallback: string | undefined): ContentBlock[] {\n\tif (!fallback) return content;\n\tconst textOutput = content\n\t\t.filter((block) => block.type === \"text\")\n\t\t.map((block) => (block as { text: string }).text)\n\t\t.join(\"\\n\");\n\tif (textOutput) return content;\n\treturn [{ type: \"text\", text: fallback }, ...content.filter((block) => block.type === \"image\")];\n}\n\nfunction addAffixes(content: ContentBlock[], prefix: string, suffix: string): ContentBlock[] {\n\tif (!prefix && !suffix) return content;\n\tconst next: ContentBlock[] = [...content];\n\n\tif (prefix) {\n\t\tconst index = next.findIndex((block) => block.type === \"text\");\n\t\tconst block = next[index];\n\t\tif (block?.type === \"text\") {\n\t\t\tnext[index] = { ...block, text: `${prefix}${block.text}` };\n\t\t} else {\n\t\t\tnext.unshift({ type: \"text\", text: prefix });\n\t\t}\n\t}\n\n\tif (suffix) {\n\t\tlet index = -1;\n\t\tfor (let i = next.length - 1; i >= 0; i--) {\n\t\t\tif (next[i]?.type === \"text\") {\n\t\t\t\tindex = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst block = next[index];\n\t\tif (block?.type === \"text\") {\n\t\t\tnext[index] = { ...block, text: `${block.text}${suffix}` };\n\t\t} else {\n\t\t\tnext.push({ type: \"text\", text: suffix });\n\t\t}\n\t}\n\n\treturn next;\n}\n\nfunction reserveBudget(maxBytes: number, maxLines: number, notice: string): { maxBytes: number; maxLines: number } {\n\tconst noticeStats = textStats(`\\n\\n${notice}`);\n\treturn {\n\t\tmaxBytes: Math.max(0, maxBytes - noticeStats.bytes),\n\t\tmaxLines: Math.max(0, maxLines - noticeStats.lines),\n\t};\n}\n\nfunction truncateHead(\n\ttext: string,\n\tmaxBytes: number,\n\tmaxLines: number,\n): { content: string; bytes: number; lines: number } {\n\tconst lines = text.split(\"\\n\");\n\tconst output: string[] = [];\n\tlet bytes = 0;\n\n\tfor (const line of lines) {\n\t\tif (output.length >= maxLines) break;\n\t\tconst separatorBytes = output.length > 0 ? 1 : 0;\n\t\tconst lineBytes = byteLength(line);\n\t\tif (bytes + separatorBytes + lineBytes > maxBytes) {\n\t\t\tconst remaining = maxBytes - bytes - separatorBytes;\n\t\t\tif (remaining > 0) {\n\t\t\t\toutput.push(truncateStringToBytes(line, remaining));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\toutput.push(line);\n\t\tbytes += separatorBytes + lineBytes;\n\t}\n\n\tconst content = output.join(\"\\n\");\n\tconst stats = textStats(content);\n\treturn { content, bytes: stats.bytes, lines: stats.lines };\n}\n\nfunction truncateStringToBytes(value: string, maxBytes: number): string {\n\tif (byteLength(value) <= maxBytes) return value;\n\tconst buffer = Buffer.from(value, \"utf8\");\n\tlet end = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;\n\twhile (end > 0 && (buffer.readUInt8(end) & 0xc0) === 0x80) end--;\n\treturn buffer.subarray(0, end).toString(\"utf8\");\n}\n\nfunction formatTruncationNotice(\n\tstats: { bytes: number; lines: number },\n\tfullOutputPath: string | undefined,\n\twriteError: string | undefined,\n): string {\n\tconst base = `[MCP text output truncated: original ${stats.lines.toLocaleString()} lines / ${formatSize(stats.bytes)}.`;\n\tif (fullOutputPath) {\n\t\treturn `${base} Full text saved to: ${fullOutputPath} — use read with offset/limit or grep to inspect.]`;\n\t}\n\treturn `${base} Full output could not be saved: ${writeError ?? \"unknown error\"}]`;\n}\n\n/**\n * Bound details.mcpResult: keep the raw result when its JSON fits within\n * detailsMaxBytes; otherwise replace it with a compact summary and spill the\n * raw JSON to a temp file.\n */\nasync function boundMcpResult(result: unknown, detailsMaxBytes: number): Promise<unknown> {\n\tconst raw = safeStringify(result);\n\tconst rawBytes = byteLength(raw);\n\tif (rawBytes <= detailsMaxBytes) return result;\n\treturn summarizeMcpResult(result, raw, rawBytes);\n}\n\nasync function summarizeMcpResult(result: unknown, raw: string, rawBytes: number): Promise<McpResultSummary> {\n\tconst { path: fullResultPath, error: resultWriteError } = await saveArtifact(\"mcp-result\", raw);\n\n\tconst record = asRecord(result);\n\tconst content = Array.isArray(record?.content) ? record.content : [];\n\tconst summary: McpResultSummary = {\n\t\tomitted: true,\n\t\treason:\n\t\t\t\"Raw MCP result exceeded the details size limit and was replaced with this summary to keep session context bounded.\",\n\t\tisError: record?.isError === true,\n\t\tcontentBlocks: content.length,\n\t\tcontentSummary: summarizeContent(content),\n\t\trawResultBytes: rawBytes,\n\t\t...(fullResultPath !== undefined ? { fullResultPath } : {}),\n\t\t...(resultWriteError !== undefined ? { resultWriteError } : {}),\n\t};\n\n\tif (record && \"structuredContent\" in record) {\n\t\tsummary.structuredContent = summarizeValue(record.structuredContent);\n\t}\n\tif (record && \"_meta\" in record) {\n\t\tsummary.meta = summarizeValue(record._meta);\n\t}\n\tif (record) {\n\t\tconst standard = new Set([\"content\", \"isError\", \"structuredContent\", \"_meta\"]);\n\t\tconst extraFields = Object.keys(record)\n\t\t\t.filter((key) => !standard.has(key))\n\t\t\t.slice(0, KEY_PREVIEW_LIMIT)\n\t\t\t.map((key) => ({\n\t\t\t\tkey: truncateKey(key),\n\t\t\t\ttype: typeof record[key],\n\t\t\t\testimatedBytes: estimateValueBytes(record[key]),\n\t\t\t\tomitted: true,\n\t\t\t}));\n\t\tif (extraFields.length > 0) summary.extraFields = extraFields;\n\t}\n\n\treturn summary;\n}\n\nfunction summarizeContent(content: unknown[]): Array<Record<string, unknown>> {\n\tconst summaries: Array<Record<string, unknown>> = content.slice(0, CONTENT_SUMMARY_LIMIT).map((block) => {\n\t\tconst record = asRecord(block);\n\t\tif (!record) return { type: typeof block, omitted: true };\n\t\tif (record.type === \"text\") {\n\t\t\tconst text = typeof record.text === \"string\" ? record.text : \"\";\n\t\t\treturn { type: \"text\", bytes: byteLength(text), lines: textStats(text).lines, textOmitted: true };\n\t\t}\n\t\tif (record.type === \"image\") {\n\t\t\tconst data = typeof record.data === \"string\" ? record.data : \"\";\n\t\t\treturn {\n\t\t\t\ttype: \"image\",\n\t\t\t\tmimeType: typeof record.mimeType === \"string\" ? record.mimeType : undefined,\n\t\t\t\tdataBytes: byteLength(data),\n\t\t\t\tdataOmitted: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: typeof record.type === \"string\" ? record.type : \"unknown\",\n\t\t\testimatedBytes: estimateValueBytes(record),\n\t\t\tomitted: true,\n\t\t};\n\t});\n\tif (content.length > CONTENT_SUMMARY_LIMIT) {\n\t\tsummaries.push({ type: \"omitted\", count: content.length - CONTENT_SUMMARY_LIMIT });\n\t}\n\treturn summaries;\n}\n\nfunction summarizeValue(value: unknown): Record<string, unknown> {\n\tconst record = asRecord(value);\n\tif (!record) {\n\t\treturn { type: value === null ? \"null\" : typeof value, estimatedBytes: estimateValueBytes(value), omitted: true };\n\t}\n\tconst keys = Object.keys(record);\n\treturn {\n\t\ttype: Array.isArray(value) ? \"array\" : \"object\",\n\t\testimatedBytes: estimateValueBytes(value),\n\t\tkeyCount: keys.length,\n\t\tkeysPreview: keys.slice(0, KEY_PREVIEW_LIMIT).map(truncateKey),\n\t\tomitted: true,\n\t};\n}\n\nfunction estimateValueBytes(value: unknown, depth = 0): number {\n\tif (value === null || value === undefined) return 0;\n\tif (typeof value === \"string\") return byteLength(value);\n\tif (typeof value === \"number\" || typeof value === \"boolean\" || typeof value === \"bigint\")\n\t\treturn byteLength(String(value));\n\tconst record = asRecord(value);\n\tif (!record || depth >= 2) return 0;\n\tconst values = Array.isArray(value)\n\t\t? value.slice(0, KEY_PREVIEW_LIMIT)\n\t\t: Object.values(record).slice(0, KEY_PREVIEW_LIMIT);\n\treturn values.reduce((total, item) => total + estimateValueBytes(item, depth + 1), 0);\n}\n\nfunction truncateKey(key: string): string {\n\treturn key.length <= KEY_MAX_CHARS ? key : `${key.slice(0, KEY_MAX_CHARS - 1)}…`;\n}\n\nasync function saveArtifact(kind: string, text: string): Promise<{ path?: string; error?: string }> {\n\ttry {\n\t\tconst dir = await mkdtemp(join(tmpdir(), \"nexus-mcp-output-\"));\n\t\tconst path = join(dir, `${kind}-${randomBytes(4).toString(\"hex\")}.txt`);\n\t\tawait writeFile(path, text, { encoding: \"utf8\", mode: 0o600 });\n\t\treturn { path };\n\t} catch (error) {\n\t\treturn { error: error instanceof Error ? error.message : String(error) };\n\t}\n}\n\nfunction asRecord(value: unknown): Recordish | undefined {\n\treturn typeof value === \"object\" && value !== null ? (value as Recordish) : undefined;\n}\n\nfunction safeStringify(value: unknown): string {\n\ttry {\n\t\t// The output guard measures and spills raw MCP results; it does not render this JSON for the model.\n\t\treturn JSON.stringify(value) ?? String(value);\n\t} catch {\n\t\treturn String(value);\n\t}\n}\n\nfunction textStats(text: string): { bytes: number; lines: number } {\n\treturn { bytes: byteLength(text), lines: text.length === 0 ? 0 : text.split(\"\\n\").length };\n}\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf8\");\n}\n\nfunction positiveInt(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n\tconst integer = Math.floor(value);\n\treturn integer > 0 ? integer : undefined;\n}\n\nfunction envKillSwitch(name: string): boolean | undefined {\n\tconst value = process.env[name]?.trim().toLowerCase();\n\tif (!value) return undefined;\n\tif ([\"0\", \"false\", \"no\", \"off\"].includes(value)) return false;\n\tif ([\"1\", \"true\", \"yes\", \"on\"].includes(value)) return true;\n\treturn undefined;\n}\n\nfunction formatSize(bytes: number): string {\n\tif (bytes < 1024) return `${bytes} B`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;\n}\n"]}
@@ -0,0 +1,71 @@
1
+ import { type MetadataCache } from "./metadata-cache.ts";
2
+ import { type PanelKeybindings } from "./panel-keys.ts";
3
+ import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
4
+ declare class McpPanel {
5
+ private noticeLines;
6
+ private prefix;
7
+ private servers;
8
+ private cursorIndex;
9
+ private nameQuery;
10
+ private descSearchActive;
11
+ private descQuery;
12
+ private dirty;
13
+ private confirmingDiscard;
14
+ private discardSelected;
15
+ private importNotice;
16
+ private authNotice;
17
+ private authInFlight;
18
+ private inactivityTimeout;
19
+ private visibleItems;
20
+ private tui;
21
+ private t;
22
+ private authOnly;
23
+ private keys;
24
+ private config;
25
+ private cache;
26
+ private callbacks;
27
+ private done;
28
+ private static readonly MAX_VISIBLE;
29
+ private static readonly INACTIVITY_MS;
30
+ constructor(config: McpConfig, cache: MetadataCache | null, provenance: Map<string, ServerProvenance>, callbacks: McpPanelCallbacks, tui: {
31
+ requestRender(): void;
32
+ }, done: (result: McpPanelResult) => void, options?: {
33
+ noticeLines?: string[];
34
+ authOnly?: boolean;
35
+ keybindings?: PanelKeybindings;
36
+ });
37
+ private resetInactivityTimeout;
38
+ private cleanup;
39
+ private rebuildVisibleItems;
40
+ private updateDirty;
41
+ private buildResult;
42
+ handleInput(data: string): void;
43
+ private authenticateSelectedServer;
44
+ private authenticateServer;
45
+ private reconnectServer;
46
+ private toggleItem;
47
+ private toggleToolDirect;
48
+ private handleDiscardInput;
49
+ private moveCursor;
50
+ private getOtherCurrentCandidates;
51
+ private rebuildServerTools;
52
+ render(width: number): string[];
53
+ private renderServerRow;
54
+ private selectedServerHasFailureMessage;
55
+ private wrapText;
56
+ private renderConnectionStatus;
57
+ private renderToolRow;
58
+ invalidate(): void;
59
+ dispose(): void;
60
+ }
61
+ export declare function createMcpPanel(config: McpConfig, cache: MetadataCache | null, provenance: Map<string, ServerProvenance>, callbacks: McpPanelCallbacks, tui: {
62
+ requestRender(): void;
63
+ }, done: (result: McpPanelResult) => void, options?: {
64
+ noticeLines?: string[];
65
+ authOnly?: boolean;
66
+ keybindings?: PanelKeybindings;
67
+ }): McpPanel & {
68
+ dispose(): void;
69
+ };
70
+ export {};
71
+ //# sourceMappingURL=mcp-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-panel.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/mcp-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { matchesKey, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { copyToClipboard } from \"../../utils/clipboard.ts\";\nimport { type CachedTool, isServerCacheValid, type MetadataCache, type ServerCacheEntry } from \"./metadata-cache.ts\";\nimport { createPanelKeys, type PanelKeybindings, type PanelKeys } from \"./panel-keys.ts\";\nimport { resourceNameToToolName } from \"./resource-tools.ts\";\nimport type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance, ToolPrefix } from \"./types.ts\";\nimport { getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from \"./types.ts\";\nimport { isUiToolVisibleToModel } from \"./ui-tool-visibility.ts\";\nimport { sanitizeTerminalText, stripOscSequences } from \"./utils.ts\";\n\ninterface PanelTheme {\n\tborder: string;\n\ttitle: string;\n\tselected: string;\n\tdirect: string;\n\tneedsAuth: string;\n\tplaceholder: string;\n\tdescription: string;\n\thint: string;\n\tconfirm: string;\n\tcancel: string;\n}\n\nconst DEFAULT_THEME: PanelTheme = {\n\tborder: \"2\",\n\ttitle: \"2\",\n\tselected: \"36\",\n\tdirect: \"32\",\n\tneedsAuth: \"33\",\n\tplaceholder: \"2;3\",\n\tdescription: \"2\",\n\thint: \"2\",\n\tconfirm: \"32\",\n\tcancel: \"31\",\n};\n\nfunction fg(code: string, text: string): string {\n\tif (!code) return text;\n\treturn `\\x1b[${code}m${text}\\x1b[0m`;\n}\n\nconst RAINBOW_COLORS = [\n\t\"38;2;178;129;214\",\n\t\"38;2;215;135;175\",\n\t\"38;2;254;188;56\",\n\t\"38;2;228;192;15\",\n\t\"38;2;137;210;129\",\n\t\"38;2;0;175;175\",\n\t\"38;2;23;143;185\",\n];\n\nfunction rainbowProgress(filled: number, total: number): string {\n\tconst dots: string[] = [];\n\tfor (let i = 0; i < total; i++) {\n\t\tconst color = RAINBOW_COLORS[i % RAINBOW_COLORS.length];\n\t\tif (!color) continue;\n\t\tdots.push(fg(color, i < filled ? \"●\" : \"○\"));\n\t}\n\treturn dots.join(\" \");\n}\n\nfunction fuzzyScore(query: string, text: string): number {\n\tconst lq = query.toLowerCase();\n\tconst lt = text.toLowerCase();\n\tif (lt.includes(lq)) return 100 + (lq.length / lt.length) * 50;\n\tlet score = 0;\n\tlet qi = 0;\n\tlet consecutive = 0;\n\tfor (let i = 0; i < lt.length && qi < lq.length; i++) {\n\t\tif (lt[i] === lq[qi]) {\n\t\t\tscore += 10 + consecutive;\n\t\t\tconsecutive += 5;\n\t\t\tqi++;\n\t\t} else {\n\t\t\tconsecutive = 0;\n\t\t}\n\t}\n\treturn qi === lq.length ? score : 0;\n}\n\nfunction sanitizeDisplayText(text: string | null | undefined): string {\n\treturn sanitizeTerminalText(text ?? \"\");\n}\n\nfunction sanitizeRowContent(content: string): string {\n\tconst withoutOsc = stripOscSequences(content);\n\tlet result = \"\";\n\tlet pendingSpace = false;\n\tfor (let i = 0; i < withoutOsc.length; i++) {\n\t\tconst rest = withoutOsc.slice(i);\n\t\tconst ansi = rest.match(/^(?:\\x1b\\[[0-?]*[ -/]*[@-~]|\\x1b[@-Z\\\\-_])/);\n\t\tif (ansi) {\n\t\t\tresult += ansi[0];\n\t\t\ti += ansi[0].length - 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst code = withoutOsc.charCodeAt(i);\n\t\tif (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {\n\t\t\tpendingSpace = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (pendingSpace && result && !result.endsWith(\" \")) {\n\t\t\tresult += \" \";\n\t\t}\n\t\tpendingSpace = false;\n\t\tresult += withoutOsc[i];\n\t}\n\treturn result;\n}\n\nfunction estimateTokens(tool: CachedTool): number {\n\tconst schemaLen = JSON.stringify(tool.inputSchema ?? {}).length;\n\tconst descLen = tool.description?.length ?? 0;\n\treturn Math.ceil((tool.name.length + descLen + schemaLen) / 4) + 10;\n}\n\ntype ConnectionStatus = \"connected\" | \"idle\" | \"failed\" | \"needs-auth\" | \"connecting\" | \"disabled\";\n\ninterface ToolState {\n\tname: string;\n\tdescription: string;\n\tisDirect: boolean;\n\twasDirect: boolean;\n\testimatedTokens: number;\n}\n\ninterface ServerState {\n\tname: string;\n\texpanded: boolean;\n\tsource: \"user\" | \"project\" | \"import\";\n\timportKind?: string;\n\tincludeTools?: string[];\n\texcludeTools?: string[];\n\texposeResources: boolean;\n\tconnectionStatus: ConnectionStatus;\n\tfailureMessage?: string | null;\n\ttools: ToolState[];\n\tdirectCount: number;\n\tdirectTokens: number;\n\thasCachedData: boolean;\n}\n\ninterface VisibleItem {\n\ttype: \"server\" | \"tool\";\n\tserverIndex: number;\n\ttoolIndex?: number;\n}\n\nclass McpPanel {\n\tprivate noticeLines: string[];\n\tprivate prefix: ToolPrefix;\n\tprivate servers: ServerState[] = [];\n\tprivate cursorIndex = 0;\n\tprivate nameQuery = \"\";\n\tprivate descSearchActive = false;\n\tprivate descQuery = \"\";\n\tprivate dirty = false;\n\tprivate confirmingDiscard = false;\n\tprivate discardSelected = 1;\n\tprivate importNotice: string | null = null;\n\tprivate authNotice: string | null = null;\n\tprivate authInFlight: string | null = null;\n\tprivate inactivityTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate visibleItems: VisibleItem[] = [];\n\tprivate tui: { requestRender(): void };\n\tprivate t = DEFAULT_THEME;\n\tprivate authOnly: boolean;\n\tprivate keys: PanelKeys;\n\tprivate config: McpConfig;\n\tprivate cache: MetadataCache | null;\n\tprivate callbacks: McpPanelCallbacks;\n\tprivate done: (result: McpPanelResult) => void;\n\n\tprivate static readonly MAX_VISIBLE = 12;\n\tprivate static readonly INACTIVITY_MS = 60_000;\n\n\tconstructor(\n\t\tconfig: McpConfig,\n\t\tcache: MetadataCache | null,\n\t\tprovenance: Map<string, ServerProvenance>,\n\t\tcallbacks: McpPanelCallbacks,\n\t\ttui: { requestRender(): void },\n\t\tdone: (result: McpPanelResult) => void,\n\t\toptions: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings } = {},\n\t) {\n\t\tthis.config = config;\n\t\tthis.cache = cache;\n\t\tthis.callbacks = callbacks;\n\t\tthis.done = done;\n\t\tthis.tui = tui;\n\t\tthis.noticeLines = options.noticeLines ?? [];\n\t\tthis.authOnly = options.authOnly === true;\n\t\tthis.keys = createPanelKeys(options.keybindings);\n\t\tthis.prefix = config.settings?.toolPrefix ?? \"server\";\n\n\t\tfor (const [serverName, definition] of Object.entries(config.mcpServers)) {\n\t\t\tif (this.authOnly && !callbacks.canAuthenticate(serverName)) continue;\n\t\t\tconst prov = provenance.get(serverName);\n\t\t\tconst cachedEntry = this.cache?.servers?.[serverName];\n\t\t\tconst serverCache = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;\n\n\t\t\tconst globalDirect = config.settings?.directTools;\n\t\t\tlet toolFilter: true | string[] | false = false;\n\t\t\tif (definition.directTools !== undefined) {\n\t\t\t\ttoolFilter = definition.directTools;\n\t\t\t} else if (globalDirect) {\n\t\t\t\ttoolFilter = globalDirect;\n\t\t\t}\n\n\t\t\tconst tools: ToolState[] = [];\n\t\t\tif (serverCache && !this.authOnly && !isServerDisabled(definition)) {\n\t\t\t\tfor (const tool of serverCache.tools ?? []) {\n\t\t\t\t\tif (!isUiToolVisibleToModel(tool.uiVisibility)) continue;\n\t\t\t\t\tif (\n\t\t\t\t\t\t!isToolAllowed(\n\t\t\t\t\t\t\ttool.name,\n\t\t\t\t\t\t\tserverName,\n\t\t\t\t\t\t\tthis.prefix,\n\t\t\t\t\t\t\tdefinition.includeTools,\n\t\t\t\t\t\t\tdefinition.excludeTools,\n\t\t\t\t\t\t\tthis.getOtherCurrentCandidates(serverName, definition, serverCache, tool.name),\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(tool.name));\n\t\t\t\t\ttools.push({\n\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\t\t\tisDirect,\n\t\t\t\t\t\twasDirect: isDirect,\n\t\t\t\t\t\testimatedTokens: estimateTokens(tool),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (definition.exposeResources !== false) {\n\t\t\t\t\tfor (const resource of serverCache.resources ?? []) {\n\t\t\t\t\t\tconst baseName = `read_${resourceNameToToolName(resource.name)}`;\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t!isToolAllowed(\n\t\t\t\t\t\t\t\tbaseName,\n\t\t\t\t\t\t\t\tserverName,\n\t\t\t\t\t\t\t\tthis.prefix,\n\t\t\t\t\t\t\t\tdefinition.includeTools,\n\t\t\t\t\t\t\t\tdefinition.excludeTools,\n\t\t\t\t\t\t\t\tthis.getOtherCurrentCandidates(serverName, definition, serverCache, baseName),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(baseName));\n\t\t\t\t\t\tconst ct: CachedTool = {\n\t\t\t\t\t\t\tname: baseName,\n\t\t\t\t\t\t\t...(resource.description !== undefined ? { description: resource.description } : {}),\n\t\t\t\t\t\t};\n\t\t\t\t\t\ttools.push({\n\t\t\t\t\t\t\tname: baseName,\n\t\t\t\t\t\t\tdescription: resource.description ?? `Read resource: ${resource.uri}`,\n\t\t\t\t\t\t\tisDirect,\n\t\t\t\t\t\t\twasDirect: isDirect,\n\t\t\t\t\t\t\testimatedTokens: estimateTokens(ct),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst status = callbacks.getConnectionStatus(serverName);\n\t\t\tconst failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;\n\t\t\tlet directCount = 0;\n\t\t\tlet directTokens = 0;\n\t\t\tfor (const tool of tools) {\n\t\t\t\tif (!tool.isDirect) continue;\n\t\t\t\tdirectCount++;\n\t\t\t\tdirectTokens += tool.estimatedTokens;\n\t\t\t}\n\t\t\tthis.servers.push({\n\t\t\t\tname: serverName,\n\t\t\t\texpanded: false,\n\t\t\t\tsource: prov?.kind ?? \"user\",\n\t\t\t\t...(prov?.importKind !== undefined ? { importKind: prov.importKind } : {}),\n\t\t\t\t...(definition.includeTools !== undefined ? { includeTools: definition.includeTools } : {}),\n\t\t\t\t...(definition.excludeTools !== undefined ? { excludeTools: definition.excludeTools } : {}),\n\t\t\t\texposeResources: definition.exposeResources !== false,\n\t\t\t\tconnectionStatus: status,\n\t\t\t\tfailureMessage,\n\t\t\t\ttools,\n\t\t\t\tdirectCount,\n\t\t\t\tdirectTokens,\n\t\t\t\thasCachedData: !!serverCache,\n\t\t\t});\n\t\t}\n\n\t\tthis.rebuildVisibleItems();\n\t\tthis.resetInactivityTimeout();\n\t}\n\n\tprivate resetInactivityTimeout(): void {\n\t\tif (this.inactivityTimeout) clearTimeout(this.inactivityTimeout);\n\t\tthis.inactivityTimeout = setTimeout(() => {\n\t\t\tthis.cleanup();\n\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t}, McpPanel.INACTIVITY_MS);\n\t}\n\n\tprivate cleanup(): void {\n\t\tif (this.inactivityTimeout) {\n\t\t\tclearTimeout(this.inactivityTimeout);\n\t\t\tthis.inactivityTimeout = null;\n\t\t}\n\t}\n\n\tprivate rebuildVisibleItems(): void {\n\t\tconst query = this.descSearchActive ? this.descQuery : this.nameQuery;\n\t\tconst mode = this.descSearchActive ? \"desc\" : \"name\";\n\n\t\tthis.visibleItems = [];\n\t\tfor (let si = 0; si < this.servers.length; si++) {\n\t\t\tconst server = this.servers[si];\n\t\t\tif (!server) continue;\n\t\t\tif (query && this.authOnly) {\n\t\t\t\tconst score = mode === \"name\" ? fuzzyScore(query, server.name) : 0;\n\t\t\t\tif (score > 0) {\n\t\t\t\t\tthis.visibleItems.push({ type: \"server\", serverIndex: si });\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tthis.visibleItems.push({ type: \"server\", serverIndex: si });\n\t\t\tif (server.expanded || query) {\n\t\t\t\tfor (let ti = 0; ti < server.tools.length; ti++) {\n\t\t\t\t\tconst tool = server.tools[ti];\n\t\t\t\t\tif (!tool) continue;\n\t\t\t\t\tif (query) {\n\t\t\t\t\t\tconst score =\n\t\t\t\t\t\t\tmode === \"name\"\n\t\t\t\t\t\t\t\t? Math.max(fuzzyScore(query, tool.name), fuzzyScore(query, server.name) * 0.6)\n\t\t\t\t\t\t\t\t: fuzzyScore(query, tool.description);\n\t\t\t\t\t\tif (score === 0) continue;\n\t\t\t\t\t}\n\t\t\t\t\tthis.visibleItems.push({ type: \"tool\", serverIndex: si, toolIndex: ti });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (query && !this.authOnly) {\n\t\t\tthis.visibleItems = this.visibleItems.filter((item) => {\n\t\t\t\tif (item.type === \"server\") {\n\t\t\t\t\treturn this.visibleItems.some(\n\t\t\t\t\t\t(other) => other.type === \"tool\" && other.serverIndex === item.serverIndex,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate updateDirty(): void {\n\t\tthis.dirty = this.servers.some((s) => s.tools.some((t) => t.isDirect !== t.wasDirect));\n\t}\n\n\tprivate buildResult(): McpPanelResult {\n\t\tconst changes = new Map<string, true | string[] | false>();\n\t\tfor (const server of this.servers) {\n\t\t\tconst changed = server.tools.some((t) => t.isDirect !== t.wasDirect);\n\t\t\tif (!changed) continue;\n\t\t\tconst directTools = server.tools.filter((t) => t.isDirect);\n\t\t\tif (directTools.length === server.tools.length && server.tools.length > 0) {\n\t\t\t\tchanges.set(server.name, true);\n\t\t\t} else if (directTools.length === 0) {\n\t\t\t\tchanges.set(server.name, false);\n\t\t\t} else {\n\t\t\t\tchanges.set(\n\t\t\t\t\tserver.name,\n\t\t\t\t\tdirectTools.map((t) => t.name),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\treturn { changes, cancelled: false };\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.resetInactivityTimeout();\n\t\tthis.importNotice = null;\n\t\tif (!this.authInFlight) this.authNotice = null;\n\n\t\tif (this.confirmingDiscard) {\n\t\t\tthis.handleDiscardInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Global shortcuts — always work, even during desc search\n\t\tif (matchesKey(data, \"ctrl+c\")) {\n\t\t\tthis.cleanup();\n\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.keys.save(data)) {\n\t\t\tthis.cleanup();\n\t\t\tthis.done(this.buildResult());\n\t\t\treturn;\n\t\t}\n\n\t\t// Modal description search mode\n\t\tif (this.descSearchActive) {\n\t\t\tif (matchesKey(data, \"escape\") || this.keys.selectConfirm(data)) {\n\t\t\t\tthis.descSearchActive = false;\n\t\t\t\tthis.descQuery = \"\";\n\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matchesKey(data, \"backspace\")) {\n\t\t\t\tif (this.descQuery.length > 0) {\n\t\t\t\t\tthis.descQuery = this.descQuery.slice(0, -1);\n\t\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.keys.selectUp(data)) {\n\t\t\t\tthis.moveCursor(-1);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.keys.selectDown(data)) {\n\t\t\t\tthis.moveCursor(1);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matchesKey(data, \"space\")) {\n\t\t\t\t// Toggle even while in desc search\n\t\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\t\tif (item) this.toggleItem(item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data.length === 1 && data.charCodeAt(0) >= 32) {\n\t\t\t\tthis.descQuery += data;\n\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"escape\")) {\n\t\t\tif (this.nameQuery) {\n\t\t\t\tthis.nameQuery = \"\";\n\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.dirty) {\n\t\t\t\tthis.confirmingDiscard = true;\n\t\t\t\tthis.discardSelected = 1;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cleanup();\n\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.keys.selectUp(data)) {\n\t\t\tthis.moveCursor(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectDown(data)) {\n\t\t\tthis.moveCursor(1);\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"space\")) {\n\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\tif (item && !this.authOnly) this.toggleItem(item);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\tif (!item) return;\n\t\t\tconst server = this.servers[item.serverIndex];\n\t\t\tif (!server) return;\n\t\t\tif (item.type === \"server\") {\n\t\t\t\tif (server.connectionStatus === \"disabled\") return;\n\t\t\t\tif (this.authOnly || server.connectionStatus === \"needs-auth\") {\n\t\t\t\t\tthis.authenticateServer(server);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tserver.expanded = !server.expanded;\n\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t} else if (item.toolIndex !== undefined) {\n\t\t\t\tconst tool = server.tools[item.toolIndex];\n\t\t\t\tif (!tool) return;\n\t\t\t\tthis.toggleToolDirect(server, tool);\n\t\t\t\tif (tool.isDirect && server.source === \"import\") {\n\t\t\t\t\tthis.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? \"external\")} — will copy to user config on save`;\n\t\t\t\t}\n\t\t\t\tthis.updateDirty();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"ctrl+a\")) {\n\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\tif (item) this.authenticateSelectedServer(item);\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"ctrl+r\")) {\n\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\tif (!item) return;\n\t\t\tconst server = this.servers[item.serverIndex];\n\t\t\tif (server) this.reconnectServer(server);\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, \"ctrl+y\")) {\n\t\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\t\tif (!item) return;\n\t\t\tconst server = this.servers[item.serverIndex];\n\t\t\tif (!server || server.connectionStatus !== \"failed\" || !server.failureMessage) return;\n\t\t\tconst serverName = sanitizeDisplayText(server.name);\n\t\t\tconst failureMessage = sanitizeDisplayText(server.failureMessage);\n\t\t\tcopyToClipboard(failureMessage)\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.authNotice = `Copied error for ${serverName} to clipboard`;\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tconst message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));\n\t\t\t\t\tthis.authNotice = `Failed to copy error for ${serverName}: ${message}`;\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (data === \"?\") {\n\t\t\tif (this.authOnly) return;\n\t\t\tthis.descSearchActive = true;\n\t\t\tthis.descQuery = \"\";\n\t\t\tthis.rebuildVisibleItems();\n\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\treturn;\n\t\t}\n\n\t\t// Backspace removes from name query\n\t\tif (matchesKey(data, \"backspace\")) {\n\t\t\tif (this.nameQuery.length > 0) {\n\t\t\t\tthis.nameQuery = this.nameQuery.slice(0, -1);\n\t\t\t\tthis.rebuildVisibleItems();\n\t\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// All other printable chars → always-on name search\n\t\tif (data.length === 1 && data.charCodeAt(0) >= 32) {\n\t\t\tthis.nameQuery += data;\n\t\t\tthis.rebuildVisibleItems();\n\t\t\tthis.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate authenticateSelectedServer(item: VisibleItem): void {\n\t\tconst server = this.servers[item.serverIndex];\n\t\tif (server) this.authenticateServer(server);\n\t}\n\n\tprivate authenticateServer(server: ServerState): void {\n\t\tif (this.authInFlight) return;\n\t\tif (server.connectionStatus === \"connecting\" || server.connectionStatus === \"disabled\") return;\n\t\tconst serverName = sanitizeDisplayText(server.name);\n\t\tif (!this.callbacks.canAuthenticate(server.name)) {\n\t\t\tthis.authNotice = `${serverName} does not use OAuth authentication.`;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.authInFlight = server.name;\n\t\tthis.authNotice = `Authenticating ${serverName}...`;\n\t\tthis.tui.requestRender();\n\n\t\tthis.callbacks\n\t\t\t.authenticate(server.name)\n\t\t\t.then((result) => {\n\t\t\t\tserver.connectionStatus = this.callbacks.getConnectionStatus(server.name);\n\t\t\t\tif (result.ok) {\n\t\t\t\t\tthis.authNotice = `OAuth finished for ${serverName}. Reconnecting...`;\n\t\t\t\t\tthis.authInFlight = null;\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\tthis.reconnectServer(server, { afterAuth: true });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst message = sanitizeDisplayText(result.message);\n\t\t\t\tthis.authNotice = `OAuth failed for ${serverName}${message ? `: ${message}` : \". Check the notification for details.\"}`;\n\t\t\t\tthis.authInFlight = null;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tconst message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));\n\t\t\t\tserver.connectionStatus = this.callbacks.getConnectionStatus(server.name);\n\t\t\t\tthis.authNotice = `OAuth failed for ${serverName}: ${message}`;\n\t\t\t\tthis.authInFlight = null;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t});\n\t}\n\n\tprivate reconnectServer(server: ServerState, options: { afterAuth?: boolean } = {}): void {\n\t\tif (server.connectionStatus === \"connecting\" || server.connectionStatus === \"disabled\") return;\n\t\tconst serverName = sanitizeDisplayText(server.name);\n\t\tserver.connectionStatus = \"connecting\";\n\t\tthis.tui.requestRender();\n\n\t\tthis.callbacks\n\t\t\t.reconnect(server.name)\n\t\t\t.then((connected) => {\n\t\t\t\tserver.connectionStatus = this.callbacks.getConnectionStatus(server.name);\n\t\t\t\tserver.failureMessage = this.callbacks.getFailureMessage?.(server.name) ?? null;\n\t\t\t\tif (server.connectionStatus === \"connected\") {\n\t\t\t\t\tconst entry = this.callbacks.refreshCacheAfterReconnect(server.name);\n\t\t\t\t\tif (entry) {\n\t\t\t\t\t\tthis.cache ??= { version: 1, servers: {} };\n\t\t\t\t\t\tthis.cache.servers[server.name] = entry;\n\t\t\t\t\t\tthis.rebuildServerTools(server, entry);\n\t\t\t\t\t}\n\t\t\t\t\tserver.hasCachedData = true;\n\t\t\t\t}\n\t\t\t\tif (options.afterAuth) {\n\t\t\t\t\tthis.authNotice =\n\t\t\t\t\t\tconnected && server.connectionStatus === \"connected\"\n\t\t\t\t\t\t\t? `OAuth finished for ${serverName}. Reconnected.`\n\t\t\t\t\t\t\t: `OAuth finished for ${serverName}, but reconnect did not complete. Press ctrl+r to retry.`;\n\t\t\t\t}\n\t\t\t\tthis.tui.requestRender();\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tserver.connectionStatus = \"failed\";\n\t\t\t\tconst message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));\n\t\t\t\tthis.authNotice = `Reconnect failed for ${serverName}: ${message}`;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t});\n\t}\n\n\tprivate toggleItem(item: VisibleItem): void {\n\t\tif (this.authOnly) return;\n\t\tconst server = this.servers[item.serverIndex];\n\t\tif (!server) return;\n\t\tif (item.type === \"server\") {\n\t\t\tconst newState = !server.tools.every((t) => t.isDirect);\n\t\t\tif (server.source === \"import\" && newState) {\n\t\t\t\tthis.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? \"external\")} — will copy to user config on save`;\n\t\t\t}\n\t\t\tlet directTokens = 0;\n\t\t\tfor (const tool of server.tools) {\n\t\t\t\ttool.isDirect = newState;\n\t\t\t\tif (newState) directTokens += tool.estimatedTokens;\n\t\t\t}\n\t\t\tserver.directCount = newState ? server.tools.length : 0;\n\t\t\tserver.directTokens = directTokens;\n\t\t} else if (item.toolIndex !== undefined) {\n\t\t\tconst tool = server.tools[item.toolIndex];\n\t\t\tif (!tool) return;\n\t\t\tthis.toggleToolDirect(server, tool);\n\t\t\tif (tool.isDirect && server.source === \"import\") {\n\t\t\t\tthis.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? \"external\")} — will copy to user config on save`;\n\t\t\t}\n\t\t}\n\t\tthis.updateDirty();\n\t}\n\n\tprivate toggleToolDirect(server: ServerState, tool: ToolState): void {\n\t\ttool.isDirect = !tool.isDirect;\n\t\tserver.directCount += tool.isDirect ? 1 : -1;\n\t\tserver.directTokens += tool.isDirect ? tool.estimatedTokens : -tool.estimatedTokens;\n\t}\n\n\tprivate handleDiscardInput(data: string): void {\n\t\tif (matchesKey(data, \"ctrl+c\")) {\n\t\t\tthis.cleanup();\n\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"escape\") || data === \"n\" || data === \"N\") {\n\t\t\tthis.confirmingDiscard = false;\n\t\t\treturn;\n\t\t}\n\t\tif (this.keys.selectConfirm(data)) {\n\t\t\tthis.cleanup();\n\t\t\tif (this.discardSelected === 0) {\n\t\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t\t} else {\n\t\t\t\tthis.done(this.buildResult());\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (data === \"y\" || data === \"Y\") {\n\t\t\tthis.cleanup();\n\t\t\tthis.done({ cancelled: true, changes: new Map() });\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"left\") || matchesKey(data, \"right\") || matchesKey(data, \"tab\")) {\n\t\t\tthis.discardSelected = this.discardSelected === 0 ? 1 : 0;\n\t\t}\n\t}\n\n\tprivate moveCursor(delta: number): void {\n\t\tif (this.visibleItems.length === 0) return;\n\t\tthis.cursorIndex = Math.max(0, Math.min(this.visibleItems.length - 1, this.cursorIndex + delta));\n\t}\n\n\tprivate getOtherCurrentCandidates(\n\t\tserverName: string,\n\t\tdefinition: McpConfig[\"mcpServers\"][string],\n\t\tcurrentEntry: ServerCacheEntry,\n\t\ttoolName: string,\n\t): Set<string> {\n\t\tconst candidates = new Set<string>();\n\t\tfor (const [otherServerName, otherDefinition] of Object.entries(this.config.mcpServers)) {\n\t\t\tif (isServerDisabled(otherDefinition)) continue;\n\t\t\tconst cachedEntry = this.cache?.servers?.[otherServerName];\n\t\t\tconst entry =\n\t\t\t\totherServerName === serverName\n\t\t\t\t\t? currentEntry\n\t\t\t\t\t: cachedEntry && isServerCacheValid(cachedEntry, otherDefinition)\n\t\t\t\t\t\t? cachedEntry\n\t\t\t\t\t\t: undefined;\n\t\t\tif (!entry) continue;\n\t\t\tconst otherPrefix = resolveToolPrefix(otherDefinition, this.prefix);\n\t\t\tfor (const tool of entry.tools ?? []) {\n\t\t\t\tif (!isUiToolVisibleToModel(tool.uiVisibility)) continue;\n\t\t\t\tfor (const candidate of getToolNameCandidates(tool.name, otherServerName, otherPrefix, false))\n\t\t\t\t\tcandidates.add(candidate);\n\t\t\t}\n\t\t\tif (otherDefinition.exposeResources !== false) {\n\t\t\t\tfor (const resource of entry.resources ?? []) {\n\t\t\t\t\tconst baseName = `read_${resourceNameToToolName(resource.name)}`;\n\t\t\t\t\tfor (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false))\n\t\t\t\t\t\tcandidates.add(candidate);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const candidate of getToolNameCandidates(\n\t\t\ttoolName,\n\t\t\tserverName,\n\t\t\tresolveToolPrefix(definition, this.prefix),\n\t\t\tfalse,\n\t\t))\n\t\t\tcandidates.delete(candidate);\n\t\treturn candidates;\n\t}\n\n\tprivate rebuildServerTools(server: ServerState, entry: ServerCacheEntry): void {\n\t\tconst existingState = new Map<string, boolean>();\n\t\tfor (const t of server.tools) existingState.set(t.name, t.isDirect);\n\n\t\tconst newTools: ToolState[] = [];\n\t\tfor (const tool of entry.tools ?? []) {\n\t\t\tif (!isUiToolVisibleToModel(tool.uiVisibility)) continue;\n\t\t\tif (\n\t\t\t\t!isToolAllowed(\n\t\t\t\t\ttool.name,\n\t\t\t\t\tserver.name,\n\t\t\t\t\tthis.prefix,\n\t\t\t\t\tserver.includeTools,\n\t\t\t\t\tserver.excludeTools,\n\t\t\t\t\tthis.getOtherCurrentCandidates(server.name, server, entry, tool.name),\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst prev = existingState.get(tool.name);\n\t\t\tconst isDirect = prev !== undefined ? prev : false;\n\t\t\tnewTools.push({\n\t\t\t\tname: tool.name,\n\t\t\t\tdescription: tool.description ?? \"\",\n\t\t\t\tisDirect,\n\t\t\t\twasDirect:\n\t\t\t\t\tprev !== undefined ? (server.tools.find((t) => t.name === tool.name)?.wasDirect ?? false) : false,\n\t\t\t\testimatedTokens: estimateTokens(tool),\n\t\t\t});\n\t\t}\n\n\t\tif (server.exposeResources) {\n\t\t\tfor (const resource of entry.resources ?? []) {\n\t\t\t\tconst baseName = `read_${resourceNameToToolName(resource.name)}`;\n\t\t\t\tif (\n\t\t\t\t\t!isToolAllowed(\n\t\t\t\t\t\tbaseName,\n\t\t\t\t\t\tserver.name,\n\t\t\t\t\t\tthis.prefix,\n\t\t\t\t\t\tserver.includeTools,\n\t\t\t\t\t\tserver.excludeTools,\n\t\t\t\t\t\tthis.getOtherCurrentCandidates(server.name, server, entry, baseName),\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst prev = existingState.get(baseName);\n\t\t\t\tconst isDirect = prev !== undefined ? prev : false;\n\t\t\t\tconst ct: CachedTool = {\n\t\t\t\t\tname: baseName,\n\t\t\t\t\t...(resource.description !== undefined ? { description: resource.description } : {}),\n\t\t\t\t};\n\t\t\t\tnewTools.push({\n\t\t\t\t\tname: baseName,\n\t\t\t\t\tdescription: resource.description ?? `Read resource: ${resource.uri}`,\n\t\t\t\t\tisDirect,\n\t\t\t\t\twasDirect:\n\t\t\t\t\t\tprev !== undefined ? (server.tools.find((t) => t.name === baseName)?.wasDirect ?? false) : false,\n\t\t\t\t\testimatedTokens: estimateTokens(ct),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tserver.tools = newTools;\n\t\tserver.directCount = 0;\n\t\tserver.directTokens = 0;\n\t\tfor (const tool of newTools) {\n\t\t\tif (!tool.isDirect) continue;\n\t\t\tserver.directCount++;\n\t\t\tserver.directTokens += tool.estimatedTokens;\n\t\t}\n\t\tthis.rebuildVisibleItems();\n\t\tthis.updateDirty();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst innerW = width - 2;\n\t\tconst lines: string[] = [];\n\t\tconst t = this.t;\n\t\tconst bold = (s: string) => `\\x1b[1m${s}\\x1b[22m`;\n\t\tconst italic = (s: string) => `\\x1b[3m${s}\\x1b[23m`;\n\t\tconst inverse = (s: string) => `\\x1b[7m${s}\\x1b[27m`;\n\n\t\tconst row = (content: string) =>\n\t\t\tfg(t.border, \"│\") + truncateToWidth(` ${sanitizeRowContent(content)}`, innerW, \"…\", true) + fg(t.border, \"│\");\n\t\tconst emptyRow = () => fg(t.border, \"│\") + \" \".repeat(innerW) + fg(t.border, \"│\");\n\t\tconst divider = () => fg(t.border, `├${\"─\".repeat(innerW)}┤`);\n\n\t\tconst titleText = this.authOnly ? \" MCP OAuth \" : \" MCP Servers \";\n\t\tconst borderLen = innerW - visibleWidth(titleText);\n\t\tconst leftB = Math.floor(borderLen / 2);\n\t\tconst rightB = borderLen - leftB;\n\t\tlines.push(\n\t\t\tfg(t.border, `╭${\"─\".repeat(leftB)}`) + fg(t.title, titleText) + fg(t.border, `${\"─\".repeat(rightB)}╮`),\n\t\t);\n\n\t\tlines.push(emptyRow());\n\n\t\tconst cursor = fg(t.selected, \"│\");\n\t\tconst searchIcon = fg(t.border, \"◎\");\n\t\tif (this.descSearchActive) {\n\t\t\tlines.push(row(`${searchIcon} ${fg(t.needsAuth, \"desc:\")} ${this.descQuery}${cursor}`));\n\t\t} else if (this.nameQuery) {\n\t\t\tlines.push(row(`${searchIcon} ${this.nameQuery}${cursor}`));\n\t\t} else {\n\t\t\tlines.push(row(`${searchIcon} ${fg(t.placeholder, italic(\"search...\"))}`));\n\t\t}\n\n\t\tlines.push(emptyRow());\n\t\tif (this.noticeLines.length > 0) {\n\t\t\tfor (const notice of this.noticeLines) {\n\t\t\t\tlines.push(row(fg(t.hint, italic(sanitizeDisplayText(notice)))));\n\t\t\t}\n\t\t\tlines.push(emptyRow());\n\t\t}\n\t\tlines.push(divider());\n\n\t\tif (this.servers.length === 0) {\n\t\t\tlines.push(emptyRow());\n\t\t\tlines.push(\n\t\t\t\trow(\n\t\t\t\t\tfg(\n\t\t\t\t\t\tt.hint,\n\t\t\t\t\t\titalic(this.authOnly ? \"No OAuth-capable MCP servers configured.\" : \"No MCP servers configured.\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t\tlines.push(emptyRow());\n\t\t} else {\n\t\t\tconst maxVis = McpPanel.MAX_VISIBLE;\n\t\t\tconst total = this.visibleItems.length;\n\t\t\tconst startIdx = Math.max(0, Math.min(this.cursorIndex - Math.floor(maxVis / 2), total - maxVis));\n\t\t\tconst endIdx = Math.min(startIdx + maxVis, total);\n\n\t\t\tlines.push(emptyRow());\n\n\t\t\tfor (let i = startIdx; i < endIdx; i++) {\n\t\t\t\tconst item = this.visibleItems[i];\n\t\t\t\tif (!item) continue;\n\t\t\t\tconst isCursor = i === this.cursorIndex;\n\t\t\t\tconst server = this.servers[item.serverIndex];\n\t\t\t\tif (!server) continue;\n\n\t\t\t\tif (item.type === \"server\") {\n\t\t\t\t\tlines.push(row(this.renderServerRow(server, isCursor)));\n\t\t\t\t\tif (isCursor && server.connectionStatus === \"failed\" && server.failureMessage) {\n\t\t\t\t\t\tfor (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerW - 6)) {\n\t\t\t\t\t\t\tlines.push(row(` ${fg(t.cancel, line)}`));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (item.toolIndex !== undefined) {\n\t\t\t\t\tconst tool = server.tools[item.toolIndex];\n\t\t\t\t\tif (tool) lines.push(row(this.renderToolRow(tool, isCursor, innerW)));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlines.push(emptyRow());\n\n\t\t\tif (total > maxVis) {\n\t\t\t\tconst prog = Math.round(((this.cursorIndex + 1) / total) * 10);\n\t\t\t\tlines.push(row(`${rainbowProgress(prog, 10)} ${fg(t.hint, `${this.cursorIndex + 1}/${total}`)}`));\n\t\t\t\tlines.push(emptyRow());\n\t\t\t}\n\n\t\t\tif (this.importNotice) {\n\t\t\t\tlines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.importNotice)))));\n\t\t\t\tlines.push(emptyRow());\n\t\t\t}\n\t\t\tif (this.authNotice) {\n\t\t\t\tlines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.authNotice)))));\n\t\t\t\tlines.push(emptyRow());\n\t\t\t}\n\t\t}\n\n\t\tlines.push(divider());\n\t\tlines.push(emptyRow());\n\n\t\tif (this.confirmingDiscard) {\n\t\t\tconst discardBtn =\n\t\t\t\tthis.discardSelected === 0 ? inverse(bold(fg(t.cancel, \" Discard \"))) : fg(t.hint, \" Discard \");\n\t\t\tconst keepBtn =\n\t\t\t\tthis.discardSelected === 1\n\t\t\t\t\t? inverse(bold(fg(t.confirm, \" Keep & Close \")))\n\t\t\t\t\t: fg(t.hint, \" Keep & Close \");\n\t\t\tlines.push(row(`Discard unsaved changes? ${discardBtn} ${keepBtn}`));\n\t\t} else {\n\t\t\tif (this.authOnly) {\n\t\t\t\tlines.push(row(fg(t.description, \"select a server to authenticate\")));\n\t\t\t} else {\n\t\t\t\tlet directCount = 0;\n\t\t\t\tlet directTokens = 0;\n\t\t\t\tfor (const server of this.servers) {\n\t\t\t\t\tdirectCount += server.directCount;\n\t\t\t\t\tdirectTokens += server.directTokens;\n\t\t\t\t}\n\t\t\t\tconst stats =\n\t\t\t\t\tdirectCount > 0 ? `${directCount} direct ~${directTokens.toLocaleString()} tokens` : \"no direct tools\";\n\t\t\t\tlines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, \" (unsaved)\") : \"\"))));\n\t\t\t}\n\t\t}\n\n\t\tlines.push(emptyRow());\n\t\tconst saveLabel = this.keys.saveLabel();\n\t\tconst hints = this.authOnly\n\t\t\t? [\n\t\t\t\t\t`${italic(\"↑↓\")} navigate`,\n\t\t\t\t\t`${italic(\"⏎\")} auth`,\n\t\t\t\t\t`${italic(\"ctrl+a\")} auth`,\n\t\t\t\t\t`${italic(\"esc\")} clear/close`,\n\t\t\t\t\t`${italic(\"ctrl+c\")} quit`,\n\t\t\t\t]\n\t\t\t: [\n\t\t\t\t\t`${italic(\"↑↓\")} navigate`,\n\t\t\t\t\t`${italic(\"space\")} toggle`,\n\t\t\t\t\t`${italic(\"⏎\")} expand/auth`,\n\t\t\t\t\t`${italic(\"ctrl+a\")} auth`,\n\t\t\t\t\t`${italic(\"ctrl+r\")} reconnect`,\n\t\t\t\t\t...(this.selectedServerHasFailureMessage() ? [`${italic(\"ctrl+y\")} copy error`] : []),\n\t\t\t\t\t`${italic(\"?\")} desc search`,\n\t\t\t\t\t...(saveLabel ? [`${italic(saveLabel)} save`] : []),\n\t\t\t\t\t`${italic(\"esc\")} clear/close`,\n\t\t\t\t\t`${italic(\"ctrl+c\")} quit`,\n\t\t\t\t];\n\t\tconst gap = \" \";\n\t\tconst gapW = 2;\n\t\tconst maxW = innerW - 2;\n\t\tlet curLine = \"\";\n\t\tlet curW = 0;\n\t\tfor (const hint of hints) {\n\t\t\tconst hw = visibleWidth(hint);\n\t\t\tconst needed = curW === 0 ? hw : gapW + hw;\n\t\t\tif (curW > 0 && curW + needed > maxW) {\n\t\t\t\tlines.push(row(fg(t.hint, curLine)));\n\t\t\t\tcurLine = hint;\n\t\t\t\tcurW = hw;\n\t\t\t} else {\n\t\t\t\tcurLine += (curW > 0 ? gap : \"\") + hint;\n\t\t\t\tcurW += needed;\n\t\t\t}\n\t\t}\n\t\tif (curLine) lines.push(row(fg(t.hint, curLine)));\n\n\t\tlines.push(fg(t.border, `╰${\"─\".repeat(innerW)}╯`));\n\n\t\treturn lines;\n\t}\n\n\tprivate renderServerRow(server: ServerState, isCursor: boolean): string {\n\t\tconst t = this.t;\n\t\tconst bold = (s: string) => `\\x1b[1m${s}\\x1b[22m`;\n\n\t\tconst expandIcon = server.expanded ? \"▾\" : \"▸\";\n\t\tconst prefix = isCursor ? fg(t.selected, expandIcon) : fg(t.border, server.expanded ? expandIcon : \"·\");\n\n\t\tconst serverName = sanitizeDisplayText(server.name);\n\t\tconst importKind = sanitizeDisplayText(server.importKind ?? \"import\");\n\t\tconst nameStr = isCursor ? bold(fg(t.selected, serverName)) : serverName;\n\t\tconst importLabel = server.source === \"import\" ? fg(t.description, ` (${importKind})`) : \"\";\n\t\tconst statusLabel = this.renderConnectionStatus(server);\n\n\t\tif (!server.hasCachedData && !this.authOnly) {\n\t\t\treturn `${prefix} ${nameStr}${importLabel} ${fg(t.description, \"(not cached)\")}${statusLabel}`;\n\t\t}\n\n\t\tconst directCount = server.directCount;\n\t\tconst totalCount = server.tools.length;\n\t\tlet toggleIcon = fg(t.description, \"○\");\n\t\tif (directCount === totalCount && totalCount > 0) {\n\t\t\ttoggleIcon = fg(t.direct, \"●\");\n\t\t} else if (directCount > 0) {\n\t\t\ttoggleIcon = fg(t.needsAuth, \"◐\");\n\t\t}\n\n\t\tlet toolInfo = \"\";\n\t\tif (totalCount > 0) {\n\t\t\ttoolInfo = `${directCount}/${totalCount}`;\n\t\t\tif (directCount > 0) {\n\t\t\t\ttoolInfo += ` ~${server.directTokens.toLocaleString()}`;\n\t\t\t}\n\t\t\ttoolInfo = fg(t.description, toolInfo);\n\t\t}\n\n\t\treturn `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;\n\t}\n\n\tprivate selectedServerHasFailureMessage(): boolean {\n\t\tconst item = this.visibleItems[this.cursorIndex];\n\t\tif (!item) return false;\n\t\tconst server = this.servers[item.serverIndex];\n\t\treturn server?.connectionStatus === \"failed\" && !!server.failureMessage;\n\t}\n\n\tprivate wrapText(text: string, width: number): string[] {\n\t\tconst max = Math.max(8, width);\n\t\tconst words = text.split(/\\s+/).filter(Boolean);\n\t\tconst lines: string[] = [];\n\t\tlet current = \"\";\n\t\tconst splitLongWord = (word: string): string => {\n\t\t\tlet rest = word;\n\t\t\twhile (visibleWidth(rest) > max) {\n\t\t\t\tlet take = \"\";\n\t\t\t\tlet index = 0;\n\t\t\t\twhile (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {\n\t\t\t\t\ttake += rest.charAt(index);\n\t\t\t\t\tindex++;\n\t\t\t\t}\n\t\t\t\tif (!take) take = rest.charAt(0);\n\t\t\t\tlines.push(take);\n\t\t\t\trest = rest.slice(take.length);\n\t\t\t}\n\t\t\treturn rest;\n\t\t};\n\n\t\tfor (const word of words) {\n\t\t\tconst candidate = current ? `${current} ${word}` : word;\n\t\t\tif (visibleWidth(candidate) <= max) {\n\t\t\t\tcurrent = candidate;\n\t\t\t} else {\n\t\t\t\tif (current) lines.push(current);\n\t\t\t\tcurrent = splitLongWord(word);\n\t\t\t}\n\t\t}\n\t\tif (current) lines.push(current);\n\t\treturn lines.length > 0 ? lines : [text];\n\t}\n\n\tprivate renderConnectionStatus(server: ServerState): string {\n\t\tconst t = this.t;\n\t\tif (this.authInFlight === server.name) return ` ${fg(t.needsAuth, \"authenticating\")}`;\n\t\tif (server.connectionStatus === \"disabled\") return ` ${fg(t.description, \"disabled\")}`;\n\t\tif (server.connectionStatus === \"needs-auth\") return ` ${fg(t.needsAuth, \"needs auth\")}`;\n\t\tif (server.connectionStatus === \"connecting\") return ` ${fg(t.needsAuth, \"connecting\")}`;\n\t\tif (server.connectionStatus === \"failed\") return ` ${fg(t.cancel, \"failed\")}`;\n\t\tif (this.authOnly && server.connectionStatus === \"connected\") return ` ${fg(t.direct, \"connected\")}`;\n\t\tif (this.authOnly) return ` ${fg(t.description, \"idle\")}`;\n\t\treturn \"\";\n\t}\n\n\tprivate renderToolRow(tool: ToolState, isCursor: boolean, innerW: number): string {\n\t\tconst t = this.t;\n\t\tconst bold = (s: string) => `\\x1b[1m${s}\\x1b[22m`;\n\n\t\tconst toggleIcon = tool.isDirect ? fg(t.direct, \"●\") : fg(t.description, \"○\");\n\t\tconst cursor = isCursor ? fg(t.selected, \"▸\") : \" \";\n\t\tconst toolName = sanitizeDisplayText(tool.name);\n\t\tconst description = sanitizeDisplayText(tool.description);\n\t\tconst nameStr = isCursor ? bold(fg(t.selected, toolName)) : toolName;\n\n\t\tconst prefixLen = 7 + visibleWidth(toolName);\n\t\tconst maxDescLen = Math.max(0, innerW - prefixLen - 8);\n\t\tconst descStr =\n\t\t\tmaxDescLen > 5 && description ? fg(t.description, `— ${truncateToWidth(description, maxDescLen, \"…\")}`) : \"\";\n\n\t\treturn ` ${cursor} ${toggleIcon} ${nameStr} ${descStr}`;\n\t}\n\n\tinvalidate(): void {}\n\n\tdispose(): void {\n\t\tthis.cleanup();\n\t}\n}\n\nexport function createMcpPanel(\n\tconfig: McpConfig,\n\tcache: MetadataCache | null,\n\tprovenance: Map<string, ServerProvenance>,\n\tcallbacks: McpPanelCallbacks,\n\ttui: { requestRender(): void },\n\tdone: (result: McpPanelResult) => void,\n\toptions?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings },\n): McpPanel & { dispose(): void } {\n\treturn new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});\n}\n"]}