@herbertgao/pi-extensions 2026.9.16 → 2026.9.18

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 (267) hide show
  1. package/README.md +14 -15
  2. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +4 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +4 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/package.json +75 -75
  6. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  7. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  8. package/node_modules/@herbertgao/resume-from/package.json +4 -2
  9. package/node_modules/@herbertgao/sol-pi/package.json +6 -6
  10. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
  11. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +99 -99
  12. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  13. package/node_modules/@tifan/pi-handoff/package.json +1 -1
  14. package/node_modules/@tifan/pi-handoff/src/index.ts +56 -26
  15. package/node_modules/@tifan/pi-mermaid-open/package.json +1 -1
  16. package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
  17. package/node_modules/@tifan/pi-recap/package.json +1 -1
  18. package/node_modules/@tifan/pi-rename/package.json +1 -1
  19. package/node_modules/pi-antigravity/package.json +2 -2
  20. package/node_modules/pi-lens/package.json +158 -158
  21. package/node_modules/pi-multi-account/package.json +2 -2
  22. package/node_modules/pi-typesafe/README.md +1 -1
  23. package/node_modules/pi-typesafe/dist/auth.d.ts +9 -5
  24. package/node_modules/pi-typesafe/dist/auth.js +7 -3
  25. package/node_modules/pi-typesafe/dist/backends.d.ts +33 -3
  26. package/node_modules/pi-typesafe/dist/backends.js +95 -1
  27. package/node_modules/pi-typesafe/dist/client.d.ts +5 -5
  28. package/node_modules/pi-typesafe/dist/client.js +12 -9
  29. package/node_modules/pi-typesafe/dist/credentials.d.ts +14 -8
  30. package/node_modules/pi-typesafe/dist/credentials.js +10 -7
  31. package/node_modules/pi-typesafe/dist/errors.d.ts +2 -2
  32. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  33. package/node_modules/pi-typesafe/dist/index.js +1 -1
  34. package/node_modules/pi-typesafe/dist/login.d.ts +2 -2
  35. package/node_modules/pi-typesafe/dist/login.js +2 -2
  36. package/node_modules/pi-typesafe/package.json +5 -5
  37. package/node_modules/pi-web-access/package.json +2 -2
  38. package/node_modules/remote-pi/package.json +5 -1
  39. package/package.json +16 -28
  40. package/node_modules/pi-mcp-adapter/CHANGELOG.md +0 -1013
  41. package/node_modules/pi-mcp-adapter/LICENSE +0 -21
  42. package/node_modules/pi-mcp-adapter/OAUTH.md +0 -369
  43. package/node_modules/pi-mcp-adapter/README.md +0 -1021
  44. package/node_modules/pi-mcp-adapter/abort.ts +0 -34
  45. package/node_modules/pi-mcp-adapter/agent-dir.ts +0 -68
  46. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +0 -416
  47. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +0 -36
  48. package/node_modules/pi-mcp-adapter/app-bridge.bundle.js +0 -67
  49. package/node_modules/pi-mcp-adapter/banner.png +0 -0
  50. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +0 -193
  51. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +0 -365
  52. package/node_modules/pi-mcp-adapter/cli.js +0 -454
  53. package/node_modules/pi-mcp-adapter/commands.ts +0 -861
  54. package/node_modules/pi-mcp-adapter/config.ts +0 -1565
  55. package/node_modules/pi-mcp-adapter/consent-manager.ts +0 -95
  56. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +0 -244
  57. package/node_modules/pi-mcp-adapter/direct-tools.ts +0 -428
  58. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +0 -2
  59. package/node_modules/pi-mcp-adapter/dist/abort.js +0 -36
  60. package/node_modules/pi-mcp-adapter/dist/abort.js.map +0 -1
  61. package/node_modules/pi-mcp-adapter/dist/agent-dir.d.ts +0 -10
  62. package/node_modules/pi-mcp-adapter/dist/agent-dir.js +0 -63
  63. package/node_modules/pi-mcp-adapter/dist/agent-dir.js.map +0 -1
  64. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.d.ts +0 -8
  65. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +0 -398
  66. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +0 -1
  67. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +0 -1
  68. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +0 -31
  69. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +0 -1
  70. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +0 -6
  71. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +0 -194
  72. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +0 -1
  73. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +0 -9
  74. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +0 -362
  75. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +0 -1
  76. package/node_modules/pi-mcp-adapter/dist/config.d.ts +0 -134
  77. package/node_modules/pi-mcp-adapter/dist/config.js +0 -1361
  78. package/node_modules/pi-mcp-adapter/dist/config.js.map +0 -1
  79. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +0 -18
  80. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +0 -88
  81. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +0 -1
  82. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +0 -17
  83. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +0 -316
  84. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +0 -1
  85. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +0 -131
  86. package/node_modules/pi-mcp-adapter/dist/errors.js +0 -278
  87. package/node_modules/pi-mcp-adapter/dist/errors.js.map +0 -1
  88. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +0 -8
  89. package/node_modules/pi-mcp-adapter/dist/http-ca.js +0 -88
  90. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +0 -1
  91. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +0 -14
  92. package/node_modules/pi-mcp-adapter/dist/jev-client.js +0 -367
  93. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +0 -1
  94. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +0 -77
  95. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +0 -2
  96. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +0 -1
  97. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +0 -51
  98. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +0 -205
  99. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +0 -1
  100. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +0 -2
  101. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +0 -68
  102. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +0 -1
  103. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +0 -56
  104. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +0 -410
  105. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +0 -1
  106. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +0 -51
  107. package/node_modules/pi-mcp-adapter/dist/logger.js +0 -131
  108. package/node_modules/pi-mcp-adapter/dist/logger.js.map +0 -1
  109. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +0 -26
  110. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +0 -103
  111. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +0 -1
  112. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +0 -118
  113. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +0 -1097
  114. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +0 -1
  115. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +0 -170
  116. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +0 -1142
  117. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +0 -1
  118. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +0 -24
  119. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +0 -203
  120. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +0 -1
  121. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +0 -53
  122. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +0 -441
  123. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +0 -1
  124. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +0 -148
  125. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +0 -689
  126. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +0 -1
  127. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +0 -6
  128. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +0 -166
  129. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +0 -1
  130. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +0 -102
  131. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +0 -369
  132. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +0 -1
  133. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +0 -95
  134. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +0 -242
  135. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +0 -1
  136. package/node_modules/pi-mcp-adapter/dist/metadata-cache.d.ts +0 -19
  137. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +0 -301
  138. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +0 -1
  139. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +0 -6
  140. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +0 -505
  141. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +0 -1
  142. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.d.ts +0 -2
  143. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +0 -144
  144. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +0 -1
  145. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +0 -10
  146. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +0 -312
  147. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +0 -1
  148. package/node_modules/pi-mcp-adapter/dist/resource-tools.d.ts +0 -1
  149. package/node_modules/pi-mcp-adapter/dist/resource-tools.js +0 -15
  150. package/node_modules/pi-mcp-adapter/dist/resource-tools.js.map +0 -1
  151. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +0 -13
  152. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +0 -97
  153. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +0 -1
  154. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +0 -17
  155. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +0 -203
  156. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +0 -1
  157. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +0 -11
  158. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +0 -172
  159. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +0 -1
  160. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +0 -157
  161. package/node_modules/pi-mcp-adapter/dist/server-manager.js +0 -1745
  162. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +0 -1
  163. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +0 -34
  164. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +0 -140
  165. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +0 -1
  166. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +0 -26
  167. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +0 -142
  168. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +0 -1
  169. package/node_modules/pi-mcp-adapter/dist/state.d.ts +0 -73
  170. package/node_modules/pi-mcp-adapter/dist/state.js +0 -2
  171. package/node_modules/pi-mcp-adapter/dist/state.js.map +0 -1
  172. package/node_modules/pi-mcp-adapter/dist/types.d.ts +0 -642
  173. package/node_modules/pi-mcp-adapter/dist/types.js +0 -331
  174. package/node_modules/pi-mcp-adapter/dist/types.js.map +0 -1
  175. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.d.ts +0 -6
  176. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js +0 -37
  177. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js.map +0 -1
  178. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +0 -17
  179. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +0 -219
  180. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +0 -1
  181. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.d.ts +0 -104
  182. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js +0 -59
  183. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js.map +0 -1
  184. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.d.ts +0 -4
  185. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js +0 -27
  186. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js.map +0 -1
  187. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +0 -15
  188. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +0 -85
  189. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +0 -1
  190. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +0 -41
  191. package/node_modules/pi-mcp-adapter/dist/utils.js +0 -417
  192. package/node_modules/pi-mcp-adapter/dist/utils.js.map +0 -1
  193. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +0 -358
  194. package/node_modules/pi-mcp-adapter/error-signal.ts +0 -32
  195. package/node_modules/pi-mcp-adapter/errors.ts +0 -364
  196. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +0 -85
  197. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +0 -34
  198. package/node_modules/pi-mcp-adapter/failure-backoff.ts +0 -23
  199. package/node_modules/pi-mcp-adapter/glimpse-ui.ts +0 -80
  200. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -515
  201. package/node_modules/pi-mcp-adapter/http-ca.ts +0 -84
  202. package/node_modules/pi-mcp-adapter/index.ts +0 -2106
  203. package/node_modules/pi-mcp-adapter/init.ts +0 -721
  204. package/node_modules/pi-mcp-adapter/jev-client.ts +0 -300
  205. package/node_modules/pi-mcp-adapter/jev-contracts.ts +0 -53
  206. package/node_modules/pi-mcp-adapter/jev-key-store.ts +0 -192
  207. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +0 -83
  208. package/node_modules/pi-mcp-adapter/lazy-loader.ts +0 -7
  209. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -457
  210. package/node_modules/pi-mcp-adapter/logger.ts +0 -169
  211. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +0 -113
  212. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +0 -1296
  213. package/node_modules/pi-mcp-adapter/mcp-auth.ts +0 -1287
  214. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -250
  215. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +0 -534
  216. package/node_modules/pi-mcp-adapter/mcp-code.ts +0 -495
  217. package/node_modules/pi-mcp-adapter/mcp-install.ts +0 -60
  218. package/node_modules/pi-mcp-adapter/mcp-keyring-helper.cjs +0 -89
  219. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +0 -819
  220. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +0 -511
  221. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +0 -104
  222. package/node_modules/pi-mcp-adapter/mcp-panel.ts +0 -1099
  223. package/node_modules/pi-mcp-adapter/mcp-probe.ts +0 -201
  224. package/node_modules/pi-mcp-adapter/mcp-references.ts +0 -373
  225. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +0 -137
  226. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +0 -747
  227. package/node_modules/pi-mcp-adapter/mcp-status.ts +0 -105
  228. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +0 -467
  229. package/node_modules/pi-mcp-adapter/mcp-trace.ts +0 -306
  230. package/node_modules/pi-mcp-adapter/metadata-cache.ts +0 -362
  231. package/node_modules/pi-mcp-adapter/namespace-tools.ts +0 -334
  232. package/node_modules/pi-mcp-adapter/npx-resolver.ts +0 -543
  233. package/node_modules/pi-mcp-adapter/oauth-handler.ts +0 -30
  234. package/node_modules/pi-mcp-adapter/oauth.ts +0 -52
  235. package/node_modules/pi-mcp-adapter/onboarding-state.ts +0 -76
  236. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +0 -156
  237. package/node_modules/pi-mcp-adapter/package.json +0 -219
  238. package/node_modules/pi-mcp-adapter/panel-keys.ts +0 -53
  239. package/node_modules/pi-mcp-adapter/prompts.ts +0 -385
  240. package/node_modules/pi-mcp-adapter/proxy-modes.ts +0 -1735
  241. package/node_modules/pi-mcp-adapter/request-headers-command.ts +0 -333
  242. package/node_modules/pi-mcp-adapter/resource-tools.ts +0 -17
  243. package/node_modules/pi-mcp-adapter/runtime-owner.ts +0 -106
  244. package/node_modules/pi-mcp-adapter/sampling-handler.ts +0 -261
  245. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +0 -144
  246. package/node_modules/pi-mcp-adapter/search-ranking.ts +0 -309
  247. package/node_modules/pi-mcp-adapter/secure-keyring.ts +0 -170
  248. package/node_modules/pi-mcp-adapter/semantic-search.ts +0 -186
  249. package/node_modules/pi-mcp-adapter/server-manager.ts +0 -2088
  250. package/node_modules/pi-mcp-adapter/session-approvals.ts +0 -201
  251. package/node_modules/pi-mcp-adapter/session-recovery.ts +0 -158
  252. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +0 -49
  253. package/node_modules/pi-mcp-adapter/state.ts +0 -71
  254. package/node_modules/pi-mcp-adapter/tool-approval.ts +0 -205
  255. package/node_modules/pi-mcp-adapter/tool-metadata.ts +0 -302
  256. package/node_modules/pi-mcp-adapter/tool-registrar.ts +0 -255
  257. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +0 -573
  258. package/node_modules/pi-mcp-adapter/ts-shape.ts +0 -149
  259. package/node_modules/pi-mcp-adapter/types.ts +0 -1120
  260. package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +0 -40
  261. package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +0 -266
  262. package/node_modules/pi-mcp-adapter/ui-server.ts +0 -993
  263. package/node_modules/pi-mcp-adapter/ui-session.ts +0 -624
  264. package/node_modules/pi-mcp-adapter/ui-stream-types.ts +0 -89
  265. package/node_modules/pi-mcp-adapter/ui-tool-visibility.ts +0 -25
  266. package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +0 -85
  267. package/node_modules/pi-mcp-adapter/utils.ts +0 -464
@@ -1,993 +0,0 @@
1
- import http, { type IncomingMessage, type ServerResponse } from "node:http";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { randomUUID } from "node:crypto";
5
- import { buildAllowAttribute, getToolUiResourceUri } from "./ui-app-bridge-helpers.ts";
6
- import {
7
- type CallToolRequest,
8
- type CallToolResult,
9
- } from "@modelcontextprotocol/client";
10
- import { ContentBlockSchema } from "@modelcontextprotocol/core";
11
- import type { ConsentManager } from "./consent-manager.ts";
12
- import { ServerError, wrapError } from "./errors.ts";
13
- import { formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
14
- import { buildHostHtmlTemplate, buildSandboxResourceCsp } from "./host-html-template.ts";
15
- import {
16
- buildSandboxProxyCsp,
17
- buildSandboxProxyHtml,
18
- SANDBOX_PROXY_PATH,
19
- SANDBOX_RESOURCE_PATH_PREFIX,
20
- } from "./sandbox-proxy-template.ts";
21
- import { logger } from "./logger.ts";
22
- import type { McpServerManager, ServerConnection } from "./server-manager.ts";
23
- import type { McpExtensionState } from "./state.ts";
24
- import { SessionRecoveryAuthRequiredError, withSessionRecovery, type SessionRecoveryDeps } from "./session-recovery.ts";
25
- import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.ts";
26
- import { callToolViaTaskSession } from "./mcp-tasks.ts";
27
- import { extractUiToolVisibility, isUiToolCallableByApp, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
28
- import { resourceNameToToolName } from "./resource-tools.ts";
29
- import {
30
- createUiModelContextUpdate,
31
- extractUiPromptText,
32
- getVisualizationStreamEnvelope,
33
- isServerDisabled,
34
- type McpConfig,
35
- type UiDisplayMode,
36
- type UiDisplayModeRequest,
37
- type UiDisplayModeResult,
38
- type UiHostContext,
39
- type UiMessageParams,
40
- type UiModelContextParams,
41
- type UiOpenLinkResult,
42
- type UiProxyRequestBody,
43
- type UiProxyResult,
44
- type UiResourceContent,
45
- type UiServerHandle,
46
- type UiSessionMessages,
47
- type UiStreamSummary,
48
- } from "./types.ts";
49
-
50
- export type { UiServerHandle };
51
-
52
- const MAX_BODY_SIZE = 2 * 1024 * 1024;
53
- const ABANDONED_GRACE_MS = 60_000;
54
- const WATCHDOG_INTERVAL_MS = 5_000;
55
- const MAX_EVENT_LOG = 128;
56
- const MAX_CONTEXT_UPDATES = 20;
57
- const MOSHI_DISCOVERY_PORT_START = 8377;
58
- const MOSHI_DISCOVERY_PORT_END = 8396;
59
- let nextMoshiDiscoveryPort = MOSHI_DISCOVERY_PORT_START;
60
-
61
- export interface UiServerOptions {
62
- serverName: string;
63
- toolName: string;
64
- toolArgs: Record<string, unknown>;
65
- resource: UiResourceContent;
66
- manager: McpServerManager;
67
- /**
68
- * Live extension config, used to re-read the server definition when
69
- * recovering a terminated Streamable HTTP session (see
70
- * session-recovery.ts). Optional so existing embedders/tests that don't
71
- * need session recovery aren't forced to supply it; without it, proxied
72
- * tool calls run without recovery (unchanged pre-existing behavior).
73
- */
74
- config?: McpConfig;
75
- /** Live state enables TUI approval prompts for iframe-originated tool calls. */
76
- state?: McpExtensionState;
77
- onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
78
- consentManager: ConsentManager;
79
- hostContext?: UiHostContext;
80
- initialResultPromise?: Promise<CallToolResult>;
81
- sessionToken?: string;
82
- port?: number;
83
- onMessage?: (params: UiMessageParams) => Promise<void> | void;
84
- onContextUpdate?: (params: UiModelContextParams) => Promise<void> | void;
85
- onComplete?: (reason: string) => void;
86
- }
87
-
88
- export async function startUiServer(options: UiServerOptions): Promise<UiServerHandle> {
89
- const sessionToken = options.sessionToken ?? randomUUID();
90
- const sandboxResourcePath = `${SANDBOX_RESOURCE_PATH_PREFIX}${randomUUID()}`;
91
- const resourceAllow = buildAllowAttribute(options.resource.meta.permissions);
92
- const log = logger.child({
93
- component: "UiServer",
94
- server: options.serverName,
95
- tool: options.toolName,
96
- session: sessionToken.slice(0, 8),
97
- });
98
-
99
- log.debug("Starting UI server");
100
-
101
- const sseClients = new Set<ServerResponse>();
102
- let completed = false;
103
- let lastHeartbeatAt = Date.now();
104
- let watchdog: NodeJS.Timeout | null = null;
105
- let currentDisplayMode: UiDisplayMode = options.hostContext?.displayMode ?? "inline";
106
- let nextEventId = 1;
107
- const eventLog: Array<{ id: number; name: string; payload: unknown }> = [];
108
- let latestCheckpointEventId: number | undefined;
109
- let streamSummary: UiStreamSummary | undefined;
110
- let server: http.Server | null = null;
111
- let sandboxProxyServer: http.Server | null = null;
112
- let sandboxProxyUrl: string | null = null;
113
- let closeTimer: NodeJS.Timeout | null = null;
114
- let listenersClosed = false;
115
-
116
- // Track messages from UI for retrieval
117
- const sessionMessages: UiSessionMessages = {
118
- prompts: [],
119
- notifications: [],
120
- intents: [],
121
- contexts: [],
122
- };
123
-
124
- const hostContext: UiHostContext = {
125
- displayMode: currentDisplayMode,
126
- availableDisplayModes: ["inline", "fullscreen", "pip"],
127
- platform: "desktop",
128
- ...options.hostContext,
129
- // Only include toolInfo if caller provides full tool definition with inputSchema
130
- // The App validates toolInfo.tool.inputSchema as required object
131
- };
132
-
133
- const initialStreamContext = hostContext["pi-mcp-adapter/stream"];
134
- if (initialStreamContext && typeof initialStreamContext === "object") {
135
- const streamId = (initialStreamContext as { streamId?: unknown }).streamId;
136
- const mode = (initialStreamContext as { mode?: unknown }).mode;
137
- if (typeof streamId === "string" && (mode === "eager" || mode === "stream-first")) {
138
- streamSummary = {
139
- streamId,
140
- mode,
141
- frames: 0,
142
- phases: [],
143
- };
144
- }
145
- }
146
-
147
- const isAppOnlyTool = (toolName: string): boolean => {
148
- const toolDefinition = options.manager.getConnection(options.serverName)?.tools?.find((tool) => tool.name === toolName);
149
- if (!toolDefinition) return false;
150
- const visibility = extractUiToolVisibility(toolDefinition._meta);
151
- return isUiToolCallableByApp(visibility) && !isUiToolVisibleToModel(visibility);
152
- };
153
-
154
- const recordUiMessage = async (msgParams: UiMessageParams): Promise<void> => {
155
- const promptText = extractUiPromptText(msgParams);
156
-
157
- // Track messages by type (order: prompt → intent → notify)
158
- // Must match the order in index.ts onMessage handler
159
- if (promptText) {
160
- sessionMessages.prompts.push(promptText);
161
- log.debug("UI prompt received", { prompt: promptText.slice(0, 100) });
162
- } else if (msgParams.type === "intent" || msgParams.intent) {
163
- const intentName = msgParams.intent ?? "";
164
- if (intentName) {
165
- sessionMessages.intents.push({
166
- intent: intentName,
167
- ...(msgParams.params !== undefined ? { params: msgParams.params } : {}),
168
- });
169
- log.debug("UI intent received", { intent: intentName });
170
- }
171
- } else if (msgParams.type === "notify" || msgParams.message) {
172
- const notifyText = msgParams.message ?? "";
173
- if (notifyText) {
174
- sessionMessages.notifications.push(notifyText);
175
- log.debug("UI notification", { message: notifyText.slice(0, 100) });
176
- }
177
- }
178
-
179
- await options.onMessage?.(msgParams);
180
- };
181
-
182
- const touchHeartbeat = () => {
183
- lastHeartbeatAt = Date.now();
184
- };
185
-
186
- const updateStreamSummary = (eventId: number, payload: unknown) => {
187
- const envelope = getVisualizationStreamEnvelope((payload as { structuredContent?: unknown } | null)?.structuredContent);
188
- if (!envelope) return;
189
- if (envelope.frameType === "checkpoint" || envelope.frameType === "final") {
190
- latestCheckpointEventId = eventId;
191
- }
192
- if (!streamSummary) {
193
- streamSummary = {
194
- streamId: envelope.streamId,
195
- mode: "eager",
196
- frames: 0,
197
- phases: [],
198
- };
199
- }
200
- streamSummary.frames += 1;
201
- if (!streamSummary.phases.includes(envelope.phase)) {
202
- streamSummary.phases.push(envelope.phase);
203
- }
204
- streamSummary.finalStatus = envelope.status;
205
- if (envelope.message !== undefined) streamSummary.lastMessage = envelope.message;
206
- else delete streamSummary.lastMessage;
207
- };
208
-
209
- const serializeEvent = (eventId: number, name: string, payload: unknown): string => {
210
- return `id: ${eventId}\nevent: ${name}\ndata: ${JSON.stringify(payload)}\n\n`;
211
- };
212
-
213
- const getLatestCheckpointIndex = () => {
214
- const firstEventId = eventLog[0]?.id;
215
- if (latestCheckpointEventId === undefined || firstEventId === undefined || latestCheckpointEventId < firstEventId) return -1;
216
- return latestCheckpointEventId - firstEventId;
217
- };
218
-
219
- const pruneEventLog = () => {
220
- if (eventLog.length <= MAX_EVENT_LOG) return;
221
- const latestCheckpointIndex = getLatestCheckpointIndex();
222
-
223
- if (latestCheckpointIndex > 0) {
224
- eventLog.splice(0, latestCheckpointIndex);
225
- }
226
-
227
- if (eventLog.length > MAX_EVENT_LOG) {
228
- eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
229
- }
230
- };
231
-
232
- const pushEvent = (name: string, payload: unknown) => {
233
- if (completed) return;
234
- const eventId = nextEventId++;
235
- eventLog.push({ id: eventId, name, payload });
236
- updateStreamSummary(eventId, payload);
237
- pruneEventLog();
238
- const chunk = serializeEvent(eventId, name, payload);
239
- for (const client of sseClients) {
240
- try {
241
- client.write(chunk);
242
- } catch {
243
- sseClients.delete(client);
244
- }
245
- }
246
- };
247
-
248
- const replayEvents = (res: ServerResponse, lastEventIdHeader?: string | null) => {
249
- const parsedLastId = lastEventIdHeader ? Number(lastEventIdHeader) : Number.NaN;
250
- const eventsToReplay = Number.isFinite(parsedLastId)
251
- ? eventLog.filter((entry) => entry.id > parsedLastId)
252
- : (() => {
253
- const latestCheckpointIndex = getLatestCheckpointIndex();
254
- return latestCheckpointIndex >= 0 ? eventLog.slice(latestCheckpointIndex) : eventLog;
255
- })();
256
-
257
- for (const entry of eventsToReplay) {
258
- try {
259
- res.write(serializeEvent(entry.id, entry.name, entry.payload));
260
- } catch {
261
- sseClients.delete(res);
262
- return;
263
- }
264
- }
265
- };
266
-
267
- const closeSse = () => {
268
- for (const client of sseClients) {
269
- try {
270
- client.end();
271
- } catch {}
272
- }
273
- sseClients.clear();
274
- };
275
-
276
- const stopWatchdog = () => {
277
- if (!watchdog) return;
278
- clearInterval(watchdog);
279
- watchdog = null;
280
- };
281
-
282
- const closeListeners = () => {
283
- if (listenersClosed) return;
284
- listenersClosed = true;
285
- stopWatchdog();
286
- if (closeTimer) {
287
- clearTimeout(closeTimer);
288
- closeTimer = null;
289
- }
290
- try {
291
- server?.close();
292
- } catch {}
293
- try {
294
- sandboxProxyServer?.close();
295
- } catch {}
296
- closeSse();
297
- };
298
-
299
- const scheduleListenerClose = () => {
300
- if (closeTimer || listenersClosed) return;
301
- closeTimer = setTimeout(() => {
302
- closeTimer = null;
303
- closeListeners();
304
- }, 20);
305
- closeTimer.unref();
306
- };
307
-
308
- const markCompleted = (reason: string) => {
309
- if (completed) return;
310
- log.debug("Session completed", { reason });
311
- pushEvent("session-complete", { reason });
312
- completed = true;
313
- stopWatchdog();
314
- options.onComplete?.(reason);
315
- scheduleListenerClose();
316
- };
317
-
318
- const hostServer = http.createServer(async (req, res) => {
319
- try {
320
- const method = req.method || "GET";
321
- const hostHeader = req.headers.host;
322
- if (!hostHeader) {
323
- sendText(res, 400, "Missing host");
324
- return;
325
- }
326
- const url = new URL(req.url || "/", `http://${hostHeader}`);
327
- if (!isAllowedHost(url.hostname)) {
328
- sendText(res, 403, "Invalid host");
329
- return;
330
- }
331
-
332
- if (method === "HEAD" && url.pathname === "/") {
333
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
334
- res.end();
335
- return;
336
- }
337
-
338
- if (method === "GET" && url.pathname === "/") {
339
- if (!url.searchParams.has("session")) {
340
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
341
- res.end(
342
- "<!doctype html><html><head><meta charset=\"utf-8\"><title>MCP UI</title></head>" +
343
- "<body><p>Open the authenticated MCP UI URL shown by Pi.</p></body></html>",
344
- );
345
- return;
346
- }
347
- if (!validateTokenQuery(url, sessionToken, res)) return;
348
- touchHeartbeat();
349
- if (!sandboxProxyUrl) {
350
- sendText(res, 503, "Sandbox proxy is not ready");
351
- return;
352
- }
353
-
354
- const html = buildHostHtmlTemplate({
355
- sessionToken,
356
- serverName: options.serverName,
357
- toolName: options.toolName,
358
- toolArgs: options.toolArgs,
359
- resource: options.resource,
360
- allowAttribute: resourceAllow,
361
- requireToolConsent: options.consentManager.requiresPrompt(options.serverName),
362
- cacheToolConsent: options.consentManager.shouldCacheConsent(),
363
- hostContext,
364
- sandboxProxyUrl,
365
- });
366
-
367
- res.writeHead(200, {
368
- "Content-Type": "text/html; charset=utf-8",
369
- "Cache-Control": "no-store",
370
- });
371
- res.end(html);
372
- return;
373
- }
374
-
375
- if (method === "GET" && url.pathname === "/events") {
376
- if (!validateTokenQuery(url, sessionToken, res)) return;
377
- touchHeartbeat();
378
- log.debug("SSE client connected", { clientCount: sseClients.size + 1 });
379
- res.writeHead(200, {
380
- "Content-Type": "text/event-stream",
381
- "Cache-Control": "no-cache, no-transform",
382
- Connection: "keep-alive",
383
- "X-Accel-Buffering": "no",
384
- });
385
- res.write(": connected\n\n");
386
- sseClients.add(res);
387
- replayEvents(res, req.headers["last-event-id"] ? String(req.headers["last-event-id"]) : null);
388
- req.on("close", () => {
389
- sseClients.delete(res);
390
- });
391
- return;
392
- }
393
-
394
- if (method === "GET" && url.pathname === "/health") {
395
- if (!validateTokenQuery(url, sessionToken, res)) return;
396
- sendJson(res, 200, { ok: true, result: { healthy: true } });
397
- return;
398
- }
399
-
400
- if (method === "GET" && url.pathname === "/app-bridge.bundle.js") {
401
- // Serve the pre-bundled AppBridge module
402
- const bundlePath = path.join(import.meta.dirname, "app-bridge.bundle.js");
403
- try {
404
- const content = await fs.readFile(bundlePath, "utf-8");
405
- res.writeHead(200, {
406
- "Content-Type": "application/javascript",
407
- "Cache-Control": "public, max-age=31536000",
408
- });
409
- res.end(content);
410
- } catch {
411
- sendJson(res, 500, { ok: false, error: "Bundle not found" });
412
- }
413
- return;
414
- }
415
-
416
- if (method !== "POST") {
417
- sendJson(res, 404, { ok: false, error: "Not found" });
418
- return;
419
- }
420
-
421
- const body = await parseBody(req, res);
422
- if (!body) return;
423
- if (!validateTokenBody(body, sessionToken, res)) return;
424
- const params = body.params ?? {};
425
- touchHeartbeat();
426
-
427
- if (url.pathname === "/proxy/tools/call") {
428
- options.consentManager.ensureApproved(options.serverName);
429
- const callParams = params as CallToolRequest["params"];
430
- if (!callParams || typeof callParams.name !== "string" || !callParams.name.trim()) {
431
- sendJson(res, 400, { ok: false, error: "Invalid tools/call params" });
432
- return;
433
- }
434
-
435
- const connection = options.manager.getConnection(options.serverName);
436
- if (!connection || connection.status !== "connected") {
437
- sendJson(res, 503, { ok: false, error: `Server "${options.serverName}" is not connected` });
438
- return;
439
- }
440
- if (isServerDisabled(options.config?.mcpServers[options.serverName]) || isServerDisabled(connection.definition)) {
441
- sendJson(res, 503, { ok: false, error: `Server "${options.serverName}" is disabled` });
442
- return;
443
- }
444
-
445
- const toolDefinitions = Array.isArray(connection.tools) ? connection.tools : [];
446
- const toolDefinition = toolDefinitions.find((tool) => tool.name === callParams.name);
447
- if (!toolDefinition) {
448
- sendJson(res, 403, { ok: false, error: `MCP tool "${callParams.name}" is not callable by apps` });
449
- return;
450
- }
451
- const uiVisibility = extractUiToolVisibility(toolDefinition._meta);
452
- if (!isUiToolCallableByApp(uiVisibility)) {
453
- sendJson(res, 403, { ok: false, error: `MCP tool "${callParams.name}" is not callable by apps` });
454
- return;
455
- }
456
-
457
- let normalizedArguments: Record<string, unknown>;
458
- try {
459
- normalizedArguments = normalizeToolArguments(callParams.arguments, `tool "${callParams.name}" arguments`);
460
- } catch (error) {
461
- sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
462
- return;
463
- }
464
- const callArgs = {
465
- name: callParams.name,
466
- arguments: normalizedArguments,
467
- };
468
- let uiResourceUri: string | undefined;
469
- try {
470
- uiResourceUri = getToolUiResourceUri({ _meta: toolDefinition._meta });
471
- } catch {
472
- // Preserve the endpoint's existing behavior for malformed declarations.
473
- }
474
- const toolMeta = {
475
- name: callParams.name,
476
- originalName: callParams.name,
477
- description: toolDefinition?.description ?? "",
478
- ...(toolDefinition?.inputSchema !== undefined ? { inputSchema: toolDefinition.inputSchema } : {}),
479
- ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
480
- ...(uiVisibility !== undefined ? { uiVisibility } : {}),
481
- };
482
- const approvalMetadata = new Map(options.state?.toolMetadata);
483
- const definition = options.config?.mcpServers[options.serverName] ?? options.state?.config.mcpServers[options.serverName];
484
- approvalMetadata.set(options.serverName, [
485
- ...connection.tools.map(tool => ({
486
- name: tool.name,
487
- originalName: tool.name,
488
- description: tool.description ?? "",
489
- })),
490
- ...(definition?.exposeResources !== false ? (connection.resources ?? []).map(resource => {
491
- const originalName = `read_${resourceNameToToolName(resource.name)}`;
492
- return {
493
- name: originalName,
494
- originalName,
495
- description: resource.description ?? `Read resource: ${resource.uri}`,
496
- };
497
- }) : []),
498
- ]);
499
- const approval = options.state
500
- ? await ensureToolCallApproved(
501
- options.state,
502
- options.serverName,
503
- toolMeta,
504
- callArgs.arguments,
505
- options.state.owner?.signal,
506
- "iframe",
507
- approvalMetadata,
508
- )
509
- : options.config && isToolCallApprovalRequired(options.config, options.serverName, toolMeta, approvalMetadata)
510
- ? { ok: false as const, reason: "approval_required_headless" as const }
511
- : { ok: true as const };
512
- if (approval.ok === false) {
513
- const denied = approval.reason === "denied";
514
- const message = denied
515
- ? `The user declined approval to run MCP tool "${callParams.name}" on server "${options.serverName}".`
516
- : `MCP tool "${callParams.name}" on server "${options.serverName}" is approval-gated and requires an interactive session.`;
517
- sendJson(res, 200, {
518
- ok: true,
519
- result: {
520
- content: [{ type: "text" as const, text: message }],
521
- details: {
522
- error: denied ? "approval_denied" : "approval_required",
523
- server: options.serverName,
524
- tool: callParams.name,
525
- },
526
- },
527
- });
528
- return;
529
- }
530
-
531
- try {
532
- options.manager.touch(options.serverName);
533
- options.manager.incrementInFlight(options.serverName);
534
- const callTool = async (conn: ServerConnection) => {
535
- await options.manager.ensureListen?.(options.serverName, conn);
536
- const requestOptions = options.manager.getRequestOptions?.(options.serverName);
537
- if (conn.taskSession) {
538
- return callToolViaTaskSession(conn.taskSession, {
539
- name: callArgs.name,
540
- args: callArgs.arguments,
541
- requestTimeoutMs: requestOptions?.timeout,
542
- signal: requestOptions?.signal,
543
- });
544
- }
545
- return conn.client.callTool(callArgs, requestOptions);
546
- };
547
- const result = options.config
548
- ? await withSessionRecovery(
549
- {
550
- manager: options.manager,
551
- config: options.config,
552
- ...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
553
- },
554
- options.serverName,
555
- callTool,
556
- )
557
- : await callTool(connection);
558
- sendJson(res, 200, { ok: true, result });
559
- } finally {
560
- options.manager.decrementInFlight(options.serverName);
561
- options.manager.touch(options.serverName);
562
- }
563
- return;
564
- }
565
-
566
- if (url.pathname === "/proxy/ui/consent") {
567
- const approved = !!(params as { approved?: boolean }).approved;
568
- options.consentManager.registerDecision(options.serverName, approved);
569
- sendJson(res, 200, { ok: true, result: { approved } });
570
- return;
571
- }
572
-
573
- if (url.pathname === "/proxy/ui/generated-tool-call-intent") {
574
- const tool = typeof params.tool === "string" ? params.tool : undefined;
575
- if (tool && isAppOnlyTool(tool)) {
576
- log.debug("Ignored generated app-only tool call intent", { tool });
577
- } else {
578
- await recordUiMessage({
579
- type: "intent",
580
- intent: "call_tool",
581
- params: {
582
- ...(tool !== undefined ? { tool } : {}),
583
- ...(params.arguments !== undefined ? { arguments: params.arguments } : {}),
584
- ...(params.isError !== undefined ? { isError: params.isError } : {}),
585
- },
586
- });
587
- }
588
- sendJson(res, 200, { ok: true, result: {} });
589
- return;
590
- }
591
-
592
- if (url.pathname === "/proxy/ui/message") {
593
- await recordUiMessage(params as UiMessageParams);
594
- sendJson(res, 200, { ok: true, result: {} });
595
- return;
596
- }
597
-
598
- if (url.pathname === "/proxy/ui/context") {
599
- const content = params.content;
600
- const structuredContent = params.structuredContent;
601
- if (
602
- (content !== undefined && (!Array.isArray(content) || content.some((block) => !ContentBlockSchema.safeParse(block).success))) ||
603
- (structuredContent !== undefined && (!structuredContent || typeof structuredContent !== "object" || Array.isArray(structuredContent)))
604
- ) {
605
- sendJson(res, 400, { ok: false, error: "Invalid update-model-context params" });
606
- return;
607
- }
608
- const ctxParams: UiModelContextParams = {
609
- ...(content !== undefined ? { content: content as NonNullable<UiModelContextParams["content"]> } : {}),
610
- ...(structuredContent !== undefined ? { structuredContent: structuredContent as Record<string, unknown> } : {}),
611
- };
612
- const update = createUiModelContextUpdate(ctxParams);
613
- if (update) {
614
- sessionMessages.contexts.push(update);
615
- while (sessionMessages.contexts.length > MAX_CONTEXT_UPDATES) {
616
- sessionMessages.contexts.shift();
617
- }
618
- }
619
- log.debug("UI context update", { hasContent: !!ctxParams.content, hasUpdate: !!update });
620
- await options.onContextUpdate?.(ctxParams);
621
- sendJson(res, 200, { ok: true, result: {} });
622
- return;
623
- }
624
-
625
- if (url.pathname === "/proxy/ui/open-link") {
626
- const openParams = params as { url?: string };
627
- if (!openParams?.url || typeof openParams.url !== "string") {
628
- sendJson(res, 400, { ok: false, error: "Invalid open-link params" });
629
- return;
630
- }
631
- let result: UiOpenLinkResult = {};
632
- try {
633
- new URL(openParams.url);
634
- } catch {
635
- result = { isError: true };
636
- }
637
- sendJson(res, 200, { ok: true, result });
638
- return;
639
- }
640
-
641
- if (url.pathname === "/proxy/ui/download-file") {
642
- sendJson(res, 200, { ok: true, result: { isError: true } });
643
- return;
644
- }
645
-
646
- if (url.pathname === "/proxy/ui/request-display-mode") {
647
- const displayParams = params as UiDisplayModeRequest;
648
- const requested = displayParams?.mode;
649
- const available = hostContext.availableDisplayModes ?? ["inline"];
650
- if (requested && available.includes(requested)) {
651
- currentDisplayMode = requested;
652
- }
653
- hostContext.displayMode = currentDisplayMode;
654
- pushEvent("host-context", { displayMode: currentDisplayMode });
655
- const result: UiDisplayModeResult = { mode: currentDisplayMode };
656
- sendJson(res, 200, { ok: true, result });
657
- return;
658
- }
659
-
660
- if (url.pathname === "/proxy/ui/heartbeat") {
661
- sendJson(res, 200, { ok: true, result: {} });
662
- return;
663
- }
664
-
665
- if (url.pathname === "/proxy/ui/complete") {
666
- const reason = typeof (params as { reason?: string }).reason === "string"
667
- ? (params as { reason?: string }).reason!
668
- : "done";
669
- markCompleted(reason);
670
- sendJson(res, 200, { ok: true, result: {} });
671
- return;
672
- }
673
-
674
- sendJson(res, 404, { ok: false, error: "Not found" });
675
- } catch (error) {
676
- if (error instanceof SessionRecoveryAuthRequiredError) {
677
- const fallback = `Server "${options.serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${options.serverName}" }) to get a browser URL, or /mcp-auth ${options.serverName} in an interactive local session.`;
678
- const message = error.authMessage ?? (options.config
679
- ? formatAuthRequiredMessage(options.config, options.serverName, fallback)
680
- : fallback);
681
- sendJson(res, 401, { ok: false, error: message });
682
- return;
683
- }
684
- const wrapped = wrapError(error, { server: options.serverName, tool: options.toolName });
685
- const status = /approval required|denied/i.test(wrapped.message) ? 403 : 500;
686
- if (status === 500) {
687
- log.error("Request handler error", error instanceof Error ? error : undefined);
688
- }
689
- sendJson(res, status, { ok: false, error: wrapped.message });
690
- }
691
- });
692
- server = hostServer;
693
-
694
- if (options.initialResultPromise) {
695
- options.initialResultPromise.then(
696
- (result) => pushEvent("tool-result", result),
697
- (error) => {
698
- const reason = error instanceof Error ? error.message : String(error);
699
- pushEvent("tool-cancelled", { reason });
700
- }
701
- );
702
- }
703
-
704
- watchdog = setInterval(() => {
705
- if (completed) return;
706
- if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) return;
707
- markCompleted("stale");
708
- }, WATCHDOG_INTERVAL_MS);
709
- watchdog.unref();
710
-
711
- return new Promise((resolve, reject) => {
712
- const candidates = resolvePortCandidates(options.port);
713
- let candidateIndex = 0;
714
-
715
- const startSandboxProxy = (parentOrigin: string): Promise<number> => {
716
- const proxy = http.createServer((req, res) => {
717
- try {
718
- const method = req.method || "GET";
719
- const hostHeader = req.headers.host;
720
- if (!hostHeader) {
721
- sendText(res, 400, "Missing host");
722
- return;
723
- }
724
- const url = new URL(req.url || "/", `http://${hostHeader}`);
725
- if (!isAllowedHost(url.hostname)) {
726
- sendText(res, 403, "Invalid host");
727
- return;
728
- }
729
-
730
- if ((method === "GET" || method === "HEAD") && url.pathname === SANDBOX_PROXY_PATH) {
731
- res.writeHead(200, {
732
- "Content-Type": "text/html; charset=utf-8",
733
- "Cache-Control": "no-store",
734
- "Content-Security-Policy": buildSandboxProxyCsp(),
735
- "Referrer-Policy": "no-referrer",
736
- "X-Content-Type-Options": "nosniff",
737
- });
738
- res.end(method === "HEAD" ? undefined : buildSandboxProxyHtml({
739
- parentOrigin,
740
- resourcePath: sandboxResourcePath,
741
- allowAttribute: resourceAllow,
742
- }));
743
- return;
744
- }
745
-
746
- if ((method === "GET" || method === "HEAD") && url.pathname === sandboxResourcePath) {
747
- res.writeHead(200, {
748
- "Content-Type": "text/html; charset=utf-8",
749
- "Cache-Control": "no-store",
750
- "Content-Security-Policy": buildSandboxResourceCsp(options.resource.meta.csp),
751
- "Referrer-Policy": "no-referrer",
752
- "X-Content-Type-Options": "nosniff",
753
- });
754
- res.end(method === "HEAD" ? undefined : options.resource.html);
755
- return;
756
- }
757
-
758
- sendJson(res, 404, { ok: false, error: "Not found" });
759
- } catch (error) {
760
- sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
761
- }
762
- });
763
- sandboxProxyServer = proxy;
764
-
765
- return new Promise((resolveProxy, rejectProxy) => {
766
- const onError = (error: NodeJS.ErrnoException) => {
767
- proxy.off("listening", onListening);
768
- rejectProxy(error);
769
- };
770
- const onListening = () => {
771
- proxy.off("error", onError);
772
- const address = proxy.address();
773
- if (!address || typeof address === "string") {
774
- rejectProxy(new ServerError("invalid sandbox proxy address"));
775
- return;
776
- }
777
- resolveProxy(address.port);
778
- };
779
- proxy.once("error", onError);
780
- proxy.listen(0, "127.0.0.1", onListening);
781
- });
782
- };
783
-
784
- const listen = () => {
785
- const candidate = candidates[candidateIndex];
786
- if (candidate === undefined) {
787
- const error = new ServerError("no UI server port candidates available");
788
- closeListeners();
789
- reject(error);
790
- return;
791
- }
792
- hostServer.once("error", onError);
793
- hostServer.listen(candidate, "127.0.0.1", onListening);
794
- };
795
-
796
- const onError = (error: NodeJS.ErrnoException) => {
797
- hostServer.off("listening", onListening);
798
- if (error.code === "EADDRINUSE" && candidateIndex < candidates.length - 1) {
799
- candidateIndex += 1;
800
- listen();
801
- return;
802
- }
803
- log.error("Failed to start server", error);
804
- const port = candidates[candidateIndex];
805
- closeListeners();
806
- reject(new ServerError(error.message, {
807
- ...(port !== undefined ? { port } : {}),
808
- cause: error,
809
- }));
810
- };
811
-
812
- const onListening = () => {
813
- hostServer.off("error", onError);
814
- const address = hostServer.address();
815
- if (!address || typeof address === "string") {
816
- const err = new ServerError("invalid address");
817
- log.error("Invalid server address", err);
818
- closeListeners();
819
- reject(err);
820
- return;
821
- }
822
-
823
- const parentOrigin = `http://localhost:${address.port}`;
824
- void startSandboxProxy(parentOrigin).then((proxyPort) => {
825
- if (completed || listenersClosed) {
826
- closeListeners();
827
- reject(new ServerError("UI session completed before sandbox proxy was ready"));
828
- return;
829
- }
830
- sandboxProxyUrl = `http://localhost:${proxyPort}${SANDBOX_PROXY_PATH}`;
831
- log.debug("Servers started", { port: address.port, proxyPort });
832
- rememberMoshiDiscoveryPort(address.port);
833
-
834
- const handle: UiServerHandle = {
835
- url: `http://localhost:${address.port}/?session=${sessionToken}`,
836
- port: address.port,
837
- proxyUrl: sandboxProxyUrl,
838
- proxyPort,
839
- sessionToken,
840
- serverName: options.serverName,
841
- toolName: options.toolName,
842
- close: (reason?: string) => {
843
- markCompleted(reason ?? "closed");
844
- closeListeners();
845
- },
846
- sendToolInput: (args: Record<string, unknown>) => {
847
- pushEvent("tool-input", { arguments: args });
848
- },
849
- sendToolResult: (result: CallToolResult) => {
850
- pushEvent("tool-result", result);
851
- },
852
- sendResultPatch: (result: CallToolResult) => {
853
- pushEvent("result-patch", result);
854
- },
855
- sendToolCancelled: (reason: string) => {
856
- pushEvent("tool-cancelled", { reason });
857
- },
858
- sendResourceUpdated: (uri: string) => {
859
- pushEvent("resource-updated", { uri });
860
- },
861
- sendHostContext: (context: UiHostContext) => {
862
- Object.assign(hostContext, context);
863
- pushEvent("host-context", context);
864
- },
865
- getSessionMessages: () => ({ ...sessionMessages }),
866
- getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : undefined,
867
- };
868
-
869
- resolve(handle);
870
- }).catch((error) => {
871
- log.error("Failed to start sandbox proxy", error instanceof Error ? error : undefined);
872
- closeListeners();
873
- const wrapped = error instanceof ServerError
874
- ? error
875
- : new ServerError(error instanceof Error ? error.message : String(error), { cause: error });
876
- reject(wrapped);
877
- });
878
- };
879
-
880
- listen();
881
- });
882
- }
883
-
884
- async function parseBody(
885
- req: IncomingMessage,
886
- res: ServerResponse,
887
- ): Promise<UiProxyRequestBody<Record<string, unknown>> | null> {
888
- try {
889
- const body = await readBody(req);
890
- if (!body || typeof body !== "object") {
891
- sendJson(res, 400, { ok: false, error: "Invalid request body" });
892
- return null;
893
- }
894
- return body as UiProxyRequestBody<Record<string, unknown>>;
895
- } catch (error) {
896
- sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : "Invalid body" });
897
- return null;
898
- }
899
- }
900
-
901
- function readBody(req: IncomingMessage): Promise<unknown> {
902
- return new Promise((resolve, reject) => {
903
- let size = 0;
904
- const chunks: Buffer[] = [];
905
-
906
- req.on("data", (chunk: Buffer) => {
907
- size += chunk.length;
908
- if (size > MAX_BODY_SIZE) {
909
- req.destroy();
910
- reject(new Error("Request body too large"));
911
- return;
912
- }
913
- chunks.push(chunk);
914
- });
915
-
916
- req.on("end", () => {
917
- try {
918
- resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
919
- } catch (error) {
920
- reject(error);
921
- }
922
- });
923
-
924
- req.on("error", reject);
925
- });
926
- }
927
-
928
- function resolvePortCandidates(port: number | undefined): number[] {
929
- if (port !== undefined) return [port];
930
- const candidates: number[] = [];
931
- const count = MOSHI_DISCOVERY_PORT_END - MOSHI_DISCOVERY_PORT_START + 1;
932
- for (let offset = 0; offset < count; offset += 1) {
933
- const candidate = MOSHI_DISCOVERY_PORT_START + ((nextMoshiDiscoveryPort - MOSHI_DISCOVERY_PORT_START + offset) % count);
934
- candidates.push(candidate);
935
- }
936
- candidates.push(0);
937
- return candidates;
938
- }
939
-
940
- function rememberMoshiDiscoveryPort(port: number): void {
941
- if (port < MOSHI_DISCOVERY_PORT_START || port > MOSHI_DISCOVERY_PORT_END) return;
942
- nextMoshiDiscoveryPort = port >= MOSHI_DISCOVERY_PORT_END ? MOSHI_DISCOVERY_PORT_START : port + 1;
943
- }
944
-
945
- function isAllowedHost(hostname: string): boolean {
946
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
947
- }
948
-
949
- function validateTokenQuery(
950
- url: URL,
951
- expected: string,
952
- res: ServerResponse,
953
- parameter = "session",
954
- ): boolean {
955
- const token = url.searchParams.get(parameter);
956
- if (token !== expected) {
957
- sendJson(res, 403, { ok: false, error: "Invalid session" });
958
- return false;
959
- }
960
- return true;
961
- }
962
-
963
- function validateTokenBody(
964
- body: UiProxyRequestBody<Record<string, unknown>>,
965
- expected: string,
966
- res: ServerResponse,
967
- ): boolean {
968
- if (body.token !== expected) {
969
- sendJson(res, 403, { ok: false, error: "Invalid session" });
970
- return false;
971
- }
972
- return true;
973
- }
974
-
975
- function sendJson<T>(
976
- res: ServerResponse,
977
- status: number,
978
- payload: UiProxyResult<T>,
979
- ): void {
980
- res.writeHead(status, {
981
- "Content-Type": "application/json; charset=utf-8",
982
- "Cache-Control": "no-store",
983
- });
984
- res.end(JSON.stringify(payload));
985
- }
986
-
987
- function sendText(res: ServerResponse, status: number, text: string): void {
988
- res.writeHead(status, {
989
- "Content-Type": "text/plain; charset=utf-8",
990
- "Cache-Control": "no-store",
991
- });
992
- res.end(text);
993
- }