@agent-native/core 0.128.3 → 0.129.0

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 (350) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +91 -0
  3. package/corpus/core/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
  4. package/corpus/core/package.json +6 -3
  5. package/corpus/core/src/a2a/client.ts +18 -4
  6. package/corpus/core/src/a2a/server.ts +19 -3
  7. package/corpus/core/src/a2a/types.ts +7 -0
  8. package/corpus/core/src/action.ts +43 -16
  9. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -10
  10. package/corpus/core/src/agent/production-agent.ts +41 -3
  11. package/corpus/core/src/agent/thread-data-builder.ts +5 -0
  12. package/corpus/core/src/chat-threads/migrations.ts +53 -0
  13. package/corpus/core/src/cli/create.ts +4 -0
  14. package/corpus/core/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
  15. package/corpus/core/src/cli/skills.ts +16 -0
  16. package/corpus/core/src/client/AgentPanel.tsx +9 -5
  17. package/corpus/core/src/client/AssistantChat.tsx +138 -46
  18. package/corpus/core/src/client/MultiTabAssistantChat.tsx +16 -10
  19. package/corpus/core/src/client/agent-chat-adapter.ts +21 -3
  20. package/corpus/core/src/client/analytics.ts +159 -102
  21. package/corpus/core/src/client/chat/legacy-chart-shorthand.tsx +479 -0
  22. package/corpus/core/src/client/chat/markdown-renderer.tsx +23 -3
  23. package/corpus/core/src/client/chat/message-components.tsx +12 -5
  24. package/corpus/core/src/client/chat/runtime.ts +2 -1
  25. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -2
  26. package/corpus/core/src/client/chat-model-groups.ts +3 -0
  27. package/corpus/core/src/client/client-status-requests.ts +161 -0
  28. package/corpus/core/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
  29. package/corpus/core/src/client/sse-event-processor.ts +15 -3
  30. package/corpus/core/src/client/use-agent-engine-configured.ts +54 -50
  31. package/corpus/core/src/client/use-chat-models.ts +16 -11
  32. package/corpus/core/src/client/use-chat-threads.ts +32 -9
  33. package/corpus/core/src/client/use-session.ts +4 -5
  34. package/corpus/core/src/db/client.ts +83 -8
  35. package/corpus/core/src/db/ensure-additive-columns.ts +31 -20
  36. package/corpus/core/src/db/migrations.ts +22 -2
  37. package/corpus/core/src/extensions/actions.ts +1 -1
  38. package/corpus/core/src/mcp/approval-store.ts +99 -0
  39. package/corpus/core/src/mcp/build-server.ts +579 -303
  40. package/corpus/core/src/mcp/embed-app.ts +1 -1
  41. package/corpus/core/src/mcp/oauth-client-metadata.ts +380 -0
  42. package/corpus/core/src/mcp/oauth-route.ts +57 -52
  43. package/corpus/core/src/mcp/screen-memory-stdio.ts +400 -378
  44. package/corpus/core/src/mcp/server.ts +29 -145
  45. package/corpus/core/src/mcp/stdio.ts +72 -80
  46. package/corpus/core/src/mcp-client/manager.ts +6 -10
  47. package/corpus/core/src/mcp-client/oauth-client.ts +127 -36
  48. package/corpus/core/src/mcp-client/oauth-routes.ts +16 -6
  49. package/corpus/core/src/mcp-client/routes.ts +6 -5
  50. package/corpus/core/src/server/agent-capabilities.ts +20 -0
  51. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +27 -0
  52. package/corpus/core/src/server/agent-chat-plugin.ts +18 -0
  53. package/corpus/core/src/server/transcribe-voice.ts +14 -5
  54. package/corpus/core/src/templates/default/.agents/skills/security/SKILL.md +13 -2
  55. package/corpus/core/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  56. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  57. package/corpus/core/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
  58. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +12 -0
  59. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +13 -2
  60. package/corpus/templates/analytics/actions/generate-chart.ts +2 -2
  61. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +1 -10
  62. package/corpus/templates/analytics/app/global.css +0 -31
  63. package/corpus/templates/analytics/app/lib/sql-query.ts +46 -20
  64. package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboard-loading-states-now-use-a-simpler-standar.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboards-now-load-first-party-charts-reliably-in.md +6 -0
  66. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +42 -16
  67. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -2
  68. package/corpus/templates/analytics/shared/sql-query-limits.ts +1 -0
  69. package/corpus/templates/assets/.agents/skills/security/SKILL.md +13 -2
  70. package/corpus/templates/brain/.agents/skills/security/SKILL.md +13 -2
  71. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +13 -2
  72. package/corpus/templates/chat/.agents/skills/security/SKILL.md +13 -2
  73. package/corpus/templates/clips/.agents/skills/security/SKILL.md +13 -2
  74. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -6
  75. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -2
  76. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -1
  77. package/corpus/templates/clips/changelog/2026-07-28-copied-share-links-no-longer-leave-an-unnecessary-confirmati.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-07-28-playback-skip-controls-no-longer-show-overlapping-numbers.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-07-28-public-clips-share-buttons-now-use-a-clean-text-only-label.md +6 -0
  80. package/corpus/templates/clips/desktop/src/app.tsx +14 -17
  81. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +3 -1
  82. package/corpus/templates/clips/desktop/src/styles.css +13 -10
  83. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +2 -2
  84. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +36 -0
  85. package/corpus/templates/content/.agents/skills/security/SKILL.md +13 -2
  86. package/corpus/templates/content/actions/_builder-cms-read-client.ts +155 -34
  87. package/corpus/templates/content/actions/_builder-docs-client.ts +151 -30
  88. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +5 -2
  89. package/corpus/templates/content/app/root.tsx +35 -14
  90. package/corpus/templates/content/changelog/2026-07-28-builder-backed-files-media-fields-now-accept-direct-file-url.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-28-builder-content-reads-now-negotiate-the-newest-mcp-protocol-.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-28-public-content-pages-load-faster-before-the-optional-ask-sid.md +6 -0
  93. package/corpus/templates/design/.agents/skills/security/SKILL.md +13 -2
  94. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +36 -16
  95. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +42 -3
  96. package/corpus/templates/design/actions/open-visual-edit.ts +2 -1
  97. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  98. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -15
  99. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +314 -0
  100. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +88 -3
  101. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
  102. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +7 -6
  103. package/corpus/templates/design/app/i18n-data.ts +56 -11
  104. package/corpus/templates/design/app/i18n-responsive-interact.ts +183 -0
  105. package/corpus/templates/design/app/lib/agent-chat.ts +60 -0
  106. package/corpus/templates/design/app/pages/design-editor/responsive-interact.ts +89 -0
  107. package/corpus/templates/design/changelog/2026-07-28-design-edits-can-be-applied-through-the-host-coding-agent-fr.md +6 -0
  108. package/corpus/templates/design/changelog/2026-07-28-interact-mode-can-preview-designs-in-responsive-device-sizes.md +6 -0
  109. package/corpus/templates/design/changelog/2026-07-28-interact-mode-now-opens-a-full-screen-responsive-preview-wit.md +6 -0
  110. package/corpus/templates/design/changelog/2026-07-28-show-or-hide-design-editing-chrome-with-cmd-on-apple-devices.md +2 -2
  111. package/corpus/templates/design/changelog/2026-07-28-visual-edit-now-always-shows-the-live-running-app-in-the-can.md +6 -0
  112. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +13 -2
  113. package/corpus/templates/forms/.agents/skills/security/SKILL.md +13 -2
  114. package/corpus/templates/macros/.agents/skills/security/SKILL.md +13 -2
  115. package/corpus/templates/mail/.agents/skills/security/SKILL.md +13 -2
  116. package/corpus/templates/plan/.agents/skills/security/SKILL.md +13 -2
  117. package/corpus/templates/slides/.agents/skills/security/SKILL.md +13 -2
  118. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +13 -2
  119. package/corpus/toolkit/CHANGELOG.md +13 -0
  120. package/corpus/toolkit/package.json +1 -1
  121. package/corpus/toolkit/src/chat-history.css +1 -2
  122. package/corpus/toolkit/src/composer/TiptapComposer.tsx +45 -0
  123. package/dist/a2a/client.d.ts +7 -0
  124. package/dist/a2a/client.d.ts.map +1 -1
  125. package/dist/a2a/client.js +8 -3
  126. package/dist/a2a/client.js.map +1 -1
  127. package/dist/a2a/server.d.ts.map +1 -1
  128. package/dist/a2a/server.js +18 -3
  129. package/dist/a2a/server.js.map +1 -1
  130. package/dist/a2a/types.d.ts +7 -0
  131. package/dist/a2a/types.d.ts.map +1 -1
  132. package/dist/a2a/types.js.map +1 -1
  133. package/dist/action.d.ts +15 -1
  134. package/dist/action.d.ts.map +1 -1
  135. package/dist/action.js +39 -16
  136. package/dist/action.js.map +1 -1
  137. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  138. package/dist/agent/engine/ai-sdk-engine.js +14 -10
  139. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  140. package/dist/agent/production-agent.d.ts.map +1 -1
  141. package/dist/agent/production-agent.js +35 -7
  142. package/dist/agent/production-agent.js.map +1 -1
  143. package/dist/agent/thread-data-builder.d.ts +2 -0
  144. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  145. package/dist/agent/thread-data-builder.js +3 -0
  146. package/dist/agent/thread-data-builder.js.map +1 -1
  147. package/dist/chat-threads/migrations.d.ts +21 -0
  148. package/dist/chat-threads/migrations.d.ts.map +1 -0
  149. package/dist/chat-threads/migrations.js +45 -0
  150. package/dist/chat-threads/migrations.js.map +1 -0
  151. package/dist/cli/create.d.ts.map +1 -1
  152. package/dist/cli/create.js +4 -0
  153. package/dist/cli/create.js.map +1 -1
  154. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  155. package/dist/cli/skills-content/design-visual-edit-skill.d.ts.map +1 -1
  156. package/dist/cli/skills-content/design-visual-edit-skill.js +36 -16
  157. package/dist/cli/skills-content/design-visual-edit-skill.js.map +1 -1
  158. package/dist/cli/skills.d.ts.map +1 -1
  159. package/dist/cli/skills.js +15 -0
  160. package/dist/cli/skills.js.map +1 -1
  161. package/dist/client/AgentPanel.d.ts.map +1 -1
  162. package/dist/client/AgentPanel.js +6 -5
  163. package/dist/client/AgentPanel.js.map +1 -1
  164. package/dist/client/AssistantChat.d.ts +37 -0
  165. package/dist/client/AssistantChat.d.ts.map +1 -1
  166. package/dist/client/AssistantChat.js +41 -8
  167. package/dist/client/AssistantChat.js.map +1 -1
  168. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  169. package/dist/client/MultiTabAssistantChat.js +11 -10
  170. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  171. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  172. package/dist/client/agent-chat-adapter.js +19 -3
  173. package/dist/client/agent-chat-adapter.js.map +1 -1
  174. package/dist/client/analytics.d.ts.map +1 -1
  175. package/dist/client/analytics.js +150 -101
  176. package/dist/client/analytics.js.map +1 -1
  177. package/dist/client/chat/legacy-chart-shorthand.d.ts +29 -0
  178. package/dist/client/chat/legacy-chart-shorthand.d.ts.map +1 -0
  179. package/dist/client/chat/legacy-chart-shorthand.js +330 -0
  180. package/dist/client/chat/legacy-chart-shorthand.js.map +1 -0
  181. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  182. package/dist/client/chat/markdown-renderer.js +8 -2
  183. package/dist/client/chat/markdown-renderer.js.map +1 -1
  184. package/dist/client/chat/message-components.d.ts +2 -1
  185. package/dist/client/chat/message-components.d.ts.map +1 -1
  186. package/dist/client/chat/message-components.js +11 -4
  187. package/dist/client/chat/message-components.js.map +1 -1
  188. package/dist/client/chat/runtime.d.ts.map +1 -1
  189. package/dist/client/chat/runtime.js +2 -1
  190. package/dist/client/chat/runtime.js.map +1 -1
  191. package/dist/client/chat/tool-call-display.d.ts +4 -1
  192. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  193. package/dist/client/chat/tool-call-display.js +14 -8
  194. package/dist/client/chat/tool-call-display.js.map +1 -1
  195. package/dist/client/chat-model-groups.d.ts.map +1 -1
  196. package/dist/client/chat-model-groups.js +3 -0
  197. package/dist/client/chat-model-groups.js.map +1 -1
  198. package/dist/client/client-status-requests.d.ts +14 -0
  199. package/dist/client/client-status-requests.d.ts.map +1 -0
  200. package/dist/client/client-status-requests.js +126 -0
  201. package/dist/client/client-status-requests.js.map +1 -0
  202. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  203. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
  204. package/dist/client/sse-event-processor.d.ts +7 -0
  205. package/dist/client/sse-event-processor.d.ts.map +1 -1
  206. package/dist/client/sse-event-processor.js +8 -3
  207. package/dist/client/sse-event-processor.js.map +1 -1
  208. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  209. package/dist/client/use-agent-engine-configured.js +29 -43
  210. package/dist/client/use-agent-engine-configured.js.map +1 -1
  211. package/dist/client/use-chat-models.d.ts.map +1 -1
  212. package/dist/client/use-chat-models.js +11 -11
  213. package/dist/client/use-chat-models.js.map +1 -1
  214. package/dist/client/use-chat-threads.d.ts.map +1 -1
  215. package/dist/client/use-chat-threads.js +23 -7
  216. package/dist/client/use-chat-threads.js.map +1 -1
  217. package/dist/client/use-session.d.ts.map +1 -1
  218. package/dist/client/use-session.js +4 -4
  219. package/dist/client/use-session.js.map +1 -1
  220. package/dist/collab/awareness.d.ts +2 -2
  221. package/dist/collab/awareness.d.ts.map +1 -1
  222. package/dist/db/client.d.ts.map +1 -1
  223. package/dist/db/client.js +48 -4
  224. package/dist/db/client.js.map +1 -1
  225. package/dist/db/ensure-additive-columns.d.ts.map +1 -1
  226. package/dist/db/ensure-additive-columns.js +32 -21
  227. package/dist/db/ensure-additive-columns.js.map +1 -1
  228. package/dist/db/migrations.d.ts +9 -0
  229. package/dist/db/migrations.d.ts.map +1 -1
  230. package/dist/db/migrations.js +14 -3
  231. package/dist/db/migrations.js.map +1 -1
  232. package/dist/extensions/actions.js +1 -1
  233. package/dist/extensions/actions.js.map +1 -1
  234. package/dist/mcp/approval-store.d.ts +16 -0
  235. package/dist/mcp/approval-store.d.ts.map +1 -0
  236. package/dist/mcp/approval-store.js +85 -0
  237. package/dist/mcp/approval-store.js.map +1 -0
  238. package/dist/mcp/build-server.d.ts +1 -34
  239. package/dist/mcp/build-server.d.ts.map +1 -1
  240. package/dist/mcp/build-server.js +220 -23
  241. package/dist/mcp/build-server.js.map +1 -1
  242. package/dist/mcp/embed-app.js +1 -1
  243. package/dist/mcp/embed-app.js.map +1 -1
  244. package/dist/mcp/oauth-client-metadata.d.ts +15 -0
  245. package/dist/mcp/oauth-client-metadata.d.ts.map +1 -0
  246. package/dist/mcp/oauth-client-metadata.js +297 -0
  247. package/dist/mcp/oauth-client-metadata.js.map +1 -0
  248. package/dist/mcp/oauth-route.d.ts.map +1 -1
  249. package/dist/mcp/oauth-route.js +39 -53
  250. package/dist/mcp/oauth-route.js.map +1 -1
  251. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  252. package/dist/mcp/screen-memory-stdio.js +304 -296
  253. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  254. package/dist/mcp/server.d.ts +12 -25
  255. package/dist/mcp/server.d.ts.map +1 -1
  256. package/dist/mcp/server.js +24 -133
  257. package/dist/mcp/server.js.map +1 -1
  258. package/dist/mcp/stdio.d.ts.map +1 -1
  259. package/dist/mcp/stdio.js +36 -35
  260. package/dist/mcp/stdio.js.map +1 -1
  261. package/dist/mcp-client/manager.d.ts.map +1 -1
  262. package/dist/mcp-client/manager.js +6 -6
  263. package/dist/mcp-client/manager.js.map +1 -1
  264. package/dist/mcp-client/oauth-client.d.ts +13 -17
  265. package/dist/mcp-client/oauth-client.d.ts.map +1 -1
  266. package/dist/mcp-client/oauth-client.js +74 -16
  267. package/dist/mcp-client/oauth-client.js.map +1 -1
  268. package/dist/mcp-client/oauth-routes.d.ts +2 -2
  269. package/dist/mcp-client/oauth-routes.d.ts.map +1 -1
  270. package/dist/mcp-client/oauth-routes.js +16 -6
  271. package/dist/mcp-client/oauth-routes.js.map +1 -1
  272. package/dist/mcp-client/routes.js +5 -5
  273. package/dist/mcp-client/routes.js.map +1 -1
  274. package/dist/notifications/routes.d.ts +1 -1
  275. package/dist/progress/routes.d.ts +1 -1
  276. package/dist/resources/handlers.d.ts +1 -1
  277. package/dist/secrets/routes.d.ts +9 -9
  278. package/dist/server/agent-capabilities.d.ts.map +1 -1
  279. package/dist/server/agent-capabilities.js +21 -0
  280. package/dist/server/agent-capabilities.js.map +1 -1
  281. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -0
  282. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  283. package/dist/server/agent-chat/action-filters-a2a.js +16 -0
  284. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  285. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  286. package/dist/server/agent-chat-plugin.js +11 -1
  287. package/dist/server/agent-chat-plugin.js.map +1 -1
  288. package/dist/server/transcribe-voice.js +12 -6
  289. package/dist/server/transcribe-voice.js.map +1 -1
  290. package/dist/templates/chat/.agents/skills/security/SKILL.md +13 -2
  291. package/dist/templates/default/.agents/skills/security/SKILL.md +13 -2
  292. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  293. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  294. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
  295. package/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
  296. package/package.json +7 -4
  297. package/src/a2a/client.ts +18 -4
  298. package/src/a2a/server.ts +19 -3
  299. package/src/a2a/types.ts +7 -0
  300. package/src/action.ts +43 -16
  301. package/src/agent/engine/ai-sdk-engine.ts +14 -10
  302. package/src/agent/production-agent.ts +41 -3
  303. package/src/agent/thread-data-builder.ts +5 -0
  304. package/src/chat-threads/migrations.ts +53 -0
  305. package/src/cli/create.ts +4 -0
  306. package/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
  307. package/src/cli/skills.ts +16 -0
  308. package/src/client/AgentPanel.tsx +9 -5
  309. package/src/client/AssistantChat.tsx +138 -46
  310. package/src/client/MultiTabAssistantChat.tsx +16 -10
  311. package/src/client/agent-chat-adapter.ts +21 -3
  312. package/src/client/analytics.ts +159 -102
  313. package/src/client/chat/legacy-chart-shorthand.tsx +479 -0
  314. package/src/client/chat/markdown-renderer.tsx +23 -3
  315. package/src/client/chat/message-components.tsx +12 -5
  316. package/src/client/chat/runtime.ts +2 -1
  317. package/src/client/chat/tool-call-display.tsx +25 -2
  318. package/src/client/chat-model-groups.ts +3 -0
  319. package/src/client/client-status-requests.ts +161 -0
  320. package/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
  321. package/src/client/sse-event-processor.ts +15 -3
  322. package/src/client/use-agent-engine-configured.ts +54 -50
  323. package/src/client/use-chat-models.ts +16 -11
  324. package/src/client/use-chat-threads.ts +32 -9
  325. package/src/client/use-session.ts +4 -5
  326. package/src/db/client.ts +83 -8
  327. package/src/db/ensure-additive-columns.ts +31 -20
  328. package/src/db/migrations.ts +22 -2
  329. package/src/extensions/actions.ts +1 -1
  330. package/src/mcp/approval-store.ts +99 -0
  331. package/src/mcp/build-server.ts +579 -303
  332. package/src/mcp/embed-app.ts +1 -1
  333. package/src/mcp/oauth-client-metadata.ts +380 -0
  334. package/src/mcp/oauth-route.ts +57 -52
  335. package/src/mcp/screen-memory-stdio.ts +400 -378
  336. package/src/mcp/server.ts +29 -145
  337. package/src/mcp/stdio.ts +72 -80
  338. package/src/mcp-client/manager.ts +6 -10
  339. package/src/mcp-client/oauth-client.ts +127 -36
  340. package/src/mcp-client/oauth-routes.ts +16 -6
  341. package/src/mcp-client/routes.ts +6 -5
  342. package/src/server/agent-capabilities.ts +20 -0
  343. package/src/server/agent-chat/action-filters-a2a.ts +27 -0
  344. package/src/server/agent-chat-plugin.ts +18 -0
  345. package/src/server/transcribe-voice.ts +14 -5
  346. package/src/templates/chat/.agents/skills/security/SKILL.md +13 -2
  347. package/src/templates/default/.agents/skills/security/SKILL.md +13 -2
  348. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  349. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  350. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
@@ -18,6 +18,14 @@
18
18
  * — it can be bundled into the serverless function alongside `mountMCP`.
19
19
  */
20
20
 
21
+ import type {
22
+ CallToolResult,
23
+ InputRequiredResult,
24
+ RequestStateCodec,
25
+ ServerContext,
26
+ Tool,
27
+ } from "@modelcontextprotocol/server";
28
+
21
29
  import {
22
30
  MCP_APP_EXTENSION_ID,
23
31
  MCP_APP_MIME_TYPE,
@@ -45,6 +53,10 @@ import {
45
53
  withCollapsedAgentSidebarParam,
46
54
  } from "../shared/agent-sidebar-url.js";
47
55
  import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
56
+ import {
57
+ consumeMcpApprovalGrant,
58
+ createMcpApprovalGrant,
59
+ } from "./approval-store.js";
48
60
  import { getBuiltinCrossAppTools } from "./builtin-tools.js";
49
61
  import {
50
62
  MCP_CONNECT_OAUTH_CLIENT_ID,
@@ -163,6 +175,81 @@ export interface MCPCallerIdentity {
163
175
  firstPartyMcp?: boolean;
164
176
  }
165
177
 
178
+ const MCP_ACTION_APPROVAL_TTL_SECONDS = 10 * 60;
179
+ const MCP_ACTION_APPROVAL_INPUT_KEY = "actionApproval";
180
+
181
+ interface McpActionApprovalState {
182
+ version: 1;
183
+ nonce: string;
184
+ actionName: string;
185
+ argumentsHash: string;
186
+ expiresAt: number;
187
+ }
188
+
189
+ function canonicalJson(value: unknown): string {
190
+ if (
191
+ value === null ||
192
+ typeof value === "boolean" ||
193
+ typeof value === "string"
194
+ ) {
195
+ return JSON.stringify(value);
196
+ }
197
+ if (typeof value === "number") {
198
+ if (!Number.isFinite(value)) {
199
+ throw new TypeError("MCP action arguments must contain finite numbers");
200
+ }
201
+ return JSON.stringify(value);
202
+ }
203
+ if (Array.isArray(value)) {
204
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
205
+ }
206
+ if (value && typeof value === "object") {
207
+ const record = value as Record<string, unknown>;
208
+ return `{${Object.keys(record)
209
+ .sort()
210
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
211
+ .join(",")}}`;
212
+ }
213
+ throw new TypeError("MCP action arguments must be JSON values");
214
+ }
215
+
216
+ async function sha256Base64Url(value: string): Promise<string> {
217
+ const digest = await globalThis.crypto.subtle.digest(
218
+ "SHA-256",
219
+ new TextEncoder().encode(value),
220
+ );
221
+ const bytes = new Uint8Array(digest);
222
+ let binary = "";
223
+ for (const byte of bytes) binary += String.fromCharCode(byte);
224
+ return btoa(binary)
225
+ .replace(/\+/g, "-")
226
+ .replace(/\//g, "_")
227
+ .replace(/=+$/, "");
228
+ }
229
+
230
+ function mcpApprovalPrincipal(
231
+ identity: MCPCallerIdentity | undefined,
232
+ orgId: string | undefined,
233
+ requestMeta: MCPRequestMeta | undefined,
234
+ ): string {
235
+ return canonicalJson({
236
+ caller: "mcp",
237
+ userEmail: identity?.userEmail?.trim().toLowerCase() ?? "",
238
+ orgId: orgId ?? identity?.orgId ?? "",
239
+ orgDomain: identity?.orgDomain?.trim().toLowerCase() ?? "",
240
+ oauthClientId: identity?.oauthClientId ?? "",
241
+ firstPartyMcp: identity?.firstPartyMcp === true,
242
+ origin: requestMeta?.origin ?? "",
243
+ });
244
+ }
245
+
246
+ function actionApprovalError(message: string): CallToolResult {
247
+ return {
248
+ content: [{ type: "text" as const, text: message }],
249
+ isError: true,
250
+ };
251
+ }
252
+
166
253
  /** Per-request context used to turn an action's relative deep link into the
167
254
  * absolute web URL (and desktop `agentnative://` URL) the external agent
168
255
  * surfaces. Derived from the inbound request headers in `mountMCP`, or from
@@ -935,6 +1022,10 @@ function mcpServerInfo(config: MCPConfig, requestMeta?: MCPRequestMeta) {
935
1022
  };
936
1023
  }
937
1024
 
1025
+ function compareMcpCatalogValues(a: string, b: string): number {
1026
+ return a < b ? -1 : a > b ? 1 : 0;
1027
+ }
1028
+
938
1029
  function safeUiSegment(value: string | undefined, fallback: string): string {
939
1030
  const normalized = (value || fallback)
940
1031
  .trim()
@@ -1419,14 +1510,13 @@ export async function createMCPServerForRequest(
1419
1510
  identity: MCPCallerIdentity | undefined,
1420
1511
  requestMeta?: MCPRequestMeta,
1421
1512
  ) {
1422
- const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
1423
1513
  const {
1424
- ListToolsRequestSchema,
1425
- CallToolRequestSchema,
1426
- ListResourcesRequestSchema,
1427
- ReadResourceRequestSchema,
1428
- ListResourceTemplatesRequestSchema,
1429
- } = await import("@modelcontextprotocol/sdk/types.js");
1514
+ ResourceNotFoundError,
1515
+ Server,
1516
+ acceptedContent,
1517
+ createRequestStateCodec,
1518
+ inputRequired,
1519
+ } = await import("@modelcontextprotocol/server");
1430
1520
 
1431
1521
  // Resolve the effective caller identity. JWT / header-derived identity
1432
1522
  // (passed by `mountMCP` via `verifyAuth`) wins. When the caller passed no
@@ -1527,6 +1617,39 @@ export async function createMCPServerForRequest(
1527
1617
  if (fullCatalogRequested) {
1528
1618
  warnFullCatalogServed(Object.keys(advertisedActions).length);
1529
1619
  }
1620
+ // Resolve orgId once per request (DB lookup) so approval state and every
1621
+ // downstream action see the same authenticated principal.
1622
+ const orgIdPromise = resolveMcpIdentityOrgId(effectiveIdentity);
1623
+ const hasApprovalActions = Object.values(actions).some(
1624
+ (entry) => entry.needsApproval !== undefined,
1625
+ );
1626
+ const approvalOrgId = hasApprovalActions ? await orgIdPromise : undefined;
1627
+ const approvalPrincipal = hasApprovalActions
1628
+ ? mcpApprovalPrincipal(effectiveIdentity, approvalOrgId, requestMeta)
1629
+ : undefined;
1630
+ const approvalCallerKey = approvalPrincipal
1631
+ ? await sha256Base64Url(approvalPrincipal)
1632
+ : undefined;
1633
+ let approvalCodec: RequestStateCodec<McpActionApprovalState> | undefined;
1634
+ let approvalConfigurationError = false;
1635
+ if (hasApprovalActions && approvalPrincipal) {
1636
+ try {
1637
+ const { getAuthSecret } =
1638
+ await import("../server/better-auth-instance.js");
1639
+ approvalCodec = createRequestStateCodec<McpActionApprovalState>({
1640
+ key: getAuthSecret(),
1641
+ ttlSeconds: MCP_ACTION_APPROVAL_TTL_SECONDS,
1642
+ bind: (ctx) => `${ctx.mcpReq.method}\0${approvalPrincipal}`,
1643
+ });
1644
+ } catch {
1645
+ // Production secret resolution deliberately throws when neither the
1646
+ // explicit Better Auth secret nor the workspace-derived A2A secret is
1647
+ // stable. Keep the MCP server available for reads, but refuse every
1648
+ // approval-gated action rather than minting replayable/ephemeral hosted
1649
+ // authorization state.
1650
+ approvalConfigurationError = true;
1651
+ }
1652
+ }
1530
1653
  const supportsMcpApps =
1531
1654
  compactMcpAppCatalog ||
1532
1655
  Object.values(advertisedActions).some((entry) =>
@@ -1546,15 +1669,28 @@ export async function createMCPServerForRequest(
1546
1669
  }
1547
1670
  : {}),
1548
1671
  },
1672
+ // Every catalog and resource is identity-scoped and can change at runtime
1673
+ // through app configuration/HMR. Explicitly mark modern cacheable results
1674
+ // private and immediately stale; the 2026 codec adds the required
1675
+ // ttlMs/cacheScope fields while 2025 responses remain byte-compatible.
1676
+ cacheHints: {
1677
+ "server/discover": { ttlMs: 0, cacheScope: "private" },
1678
+ "tools/list": { ttlMs: 0, cacheScope: "private" },
1679
+ "resources/list": { ttlMs: 0, cacheScope: "private" },
1680
+ "resources/templates/list": { ttlMs: 0, cacheScope: "private" },
1681
+ "resources/read": { ttlMs: 0, cacheScope: "private" },
1682
+ },
1683
+ ...(approvalCodec
1684
+ ? {
1685
+ inputRequired: {
1686
+ maxRounds: 2,
1687
+ roundTimeoutMs: 10 * 60_000,
1688
+ },
1689
+ requestState: { verify: approvalCodec.verify },
1690
+ }
1691
+ : {}),
1549
1692
  });
1550
1693
 
1551
- // Resolve orgId once per request (DB lookup) so subsequent wraps are
1552
- // synchronous. The caller identity may be undefined for true dev-open —
1553
- // in that case we run with no userEmail/orgId, which makes downstream
1554
- // tools that require per-user scope return empty results rather than
1555
- // cross-tenant data (the safe default).
1556
- const orgIdPromise = resolveMcpIdentityOrgId(effectiveIdentity);
1557
-
1558
1694
  /**
1559
1695
  * Wrap a callback in
1560
1696
  * `runWithRequestContext({ userEmail, orgId, requestOrigin }, fn)`.
@@ -1578,63 +1714,178 @@ export async function createMCPServerForRequest(
1578
1714
  ) as Promise<T>;
1579
1715
  }
1580
1716
 
1717
+ async function requireMcpActionApproval(
1718
+ entry: ActionEntry,
1719
+ name: string,
1720
+ args: Record<string, unknown>,
1721
+ ctx: ServerContext,
1722
+ ): Promise<CallToolResult | InputRequiredResult | undefined> {
1723
+ const verifiedState =
1724
+ ctx.mcpReq.requestState<McpActionApprovalState>() ?? undefined;
1725
+ const argumentsHash = await sha256Base64Url(canonicalJson(args));
1726
+
1727
+ if (verifiedState !== undefined) {
1728
+ if (
1729
+ verifiedState.version !== 1 ||
1730
+ typeof verifiedState.nonce !== "string" ||
1731
+ verifiedState.actionName !== name ||
1732
+ verifiedState.argumentsHash !== argumentsHash ||
1733
+ !Number.isFinite(verifiedState.expiresAt) ||
1734
+ verifiedState.expiresAt < Date.now() ||
1735
+ !approvalCallerKey
1736
+ ) {
1737
+ return actionApprovalError(
1738
+ `Approval for ${name} is invalid or does not match this exact call.`,
1739
+ );
1740
+ }
1741
+
1742
+ const consumed = await consumeMcpApprovalGrant({
1743
+ nonce: verifiedState.nonce,
1744
+ callerKey: approvalCallerKey,
1745
+ actionName: name,
1746
+ argumentsHash,
1747
+ expiresAt: verifiedState.expiresAt,
1748
+ });
1749
+ if (!consumed) {
1750
+ return actionApprovalError(
1751
+ `Approval for ${name} is invalid, expired, or already used.`,
1752
+ );
1753
+ }
1754
+
1755
+ const approval = acceptedContent(
1756
+ ctx.mcpReq.inputResponses,
1757
+ MCP_ACTION_APPROVAL_INPUT_KEY,
1758
+ ) as Record<string, unknown> | undefined;
1759
+ if (approval?.decision !== "approve") {
1760
+ return actionApprovalError(`${name} was not approved.`);
1761
+ }
1762
+ return undefined;
1763
+ }
1764
+
1765
+ if (entry.needsApproval === undefined) return undefined;
1766
+ let mustApprove = false;
1767
+ try {
1768
+ mustApprove =
1769
+ typeof entry.needsApproval === "function"
1770
+ ? Boolean(
1771
+ await entry.needsApproval(args, {
1772
+ userEmail: getRequestUserEmail(),
1773
+ orgId: getRequestOrgId() ?? null,
1774
+ caller: "mcp",
1775
+ actionName: name,
1776
+ }),
1777
+ )
1778
+ : entry.needsApproval === true;
1779
+ } catch {
1780
+ mustApprove = true;
1781
+ }
1782
+ if (!mustApprove) return undefined;
1783
+
1784
+ if (approvalConfigurationError || !approvalCodec || !approvalCallerKey) {
1785
+ return actionApprovalError(
1786
+ `${name} requires approval, but secure MCP approval is not configured on this server.`,
1787
+ );
1788
+ }
1789
+
1790
+ const now = Date.now();
1791
+ const approvalState: McpActionApprovalState = {
1792
+ version: 1,
1793
+ nonce: globalThis.crypto.randomUUID(),
1794
+ actionName: name,
1795
+ argumentsHash,
1796
+ expiresAt: now + MCP_ACTION_APPROVAL_TTL_SECONDS * 1000,
1797
+ };
1798
+ await createMcpApprovalGrant({
1799
+ ...approvalState,
1800
+ callerKey: approvalCallerKey,
1801
+ });
1802
+ const requestState = await approvalCodec.mint(approvalState, ctx);
1803
+ return inputRequired({
1804
+ inputRequests: {
1805
+ [MCP_ACTION_APPROVAL_INPUT_KEY]: inputRequired.elicit({
1806
+ message:
1807
+ `Action "${name}" requires your approval before it can run. ` +
1808
+ "Review the exact tool arguments in your MCP client, then explicitly choose Approve or Deny.",
1809
+ requestedSchema: {
1810
+ type: "object",
1811
+ properties: {
1812
+ decision: {
1813
+ type: "string",
1814
+ title: `Run ${name}?`,
1815
+ description:
1816
+ "Approve runs this exact call once. Deny leaves it unexecuted.",
1817
+ enum: ["approve", "deny"],
1818
+ },
1819
+ },
1820
+ required: ["decision"],
1821
+ },
1822
+ }),
1823
+ },
1824
+ requestState,
1825
+ });
1826
+ }
1827
+
1581
1828
  // tools/list — return all actions + ask-agent meta-tool. Wrapped in the
1582
1829
  // request context so per-user MCP visibility (mcp-client/visibility.ts)
1583
1830
  // applies to the listing too.
1584
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1831
+ server.setRequestHandler("tools/list", async () => {
1585
1832
  return withCallerContext(async () => {
1586
- const tools = await Promise.all(
1587
- Object.entries(advertisedActions).map(async ([name, entry]) => {
1588
- const hasLink = typeof entry.link === "function";
1589
- const mcpAppResource = await resolveMcpAppResourceSafely(
1590
- config,
1591
- name,
1592
- entry,
1593
- requestMeta,
1594
- );
1595
- const rawToolMeta =
1596
- (entry.tool as any)._meta &&
1597
- typeof (entry.tool as any)._meta === "object" &&
1598
- !Array.isArray((entry.tool as any)._meta)
1599
- ? { ...((entry.tool as any)._meta as Record<string, unknown>) }
1600
- : {};
1601
- const toolMeta = {
1602
- ...rawToolMeta,
1603
- // Advertisement path: only tag the tool with its inline-embed
1604
- // descriptor when the kill switch is on, so disabled embeds never
1605
- // prompt a host to render/read the `ui://` resource.
1606
- ...(mcpAppResource && requestMeta?.inlineMcpApps
1607
- ? {
1608
- ...openAiToolDescriptorMeta(mcpAppResource),
1609
- [MCP_APP_RESOURCE_URI_META_KEY]: mcpAppResource.uri,
1610
- ui: mcpAppToolUiMeta(
1611
- mcpAppResource,
1612
- entry.mcpApp?.visibility ??
1613
- metadataObject(rawToolMeta.ui).visibility,
1614
- ),
1615
- }
1616
- : {}),
1617
- };
1618
- const baseDescription = entry.tool.description ?? name;
1619
- const annotations: Record<string, unknown> = {
1620
- readOnlyHint: entry.readOnly === true,
1621
- destructiveHint: entry.publicAgent?.isConsequential === true,
1622
- openWorldHint: false,
1623
- };
1624
- if (hasLink) annotations["agent-native/producesOpenLink"] = true;
1625
- return {
1626
- name,
1627
- description: hasLink
1628
- ? `${baseDescription} After calling, surface the returned "Open in … →" link to the user.`
1629
- : baseDescription,
1630
- inputSchema: entry.tool.parameters ?? {
1631
- type: "object" as const,
1632
- properties: {},
1633
- },
1634
- ...(Object.keys(toolMeta).length > 0 ? { _meta: toolMeta } : {}),
1635
- annotations,
1636
- };
1637
- }),
1833
+ const tools: Tool[] = await Promise.all(
1834
+ Object.entries(advertisedActions)
1835
+ .sort(([a], [b]) => compareMcpCatalogValues(a, b))
1836
+ .map(async ([name, entry]) => {
1837
+ const hasLink = typeof entry.link === "function";
1838
+ const mcpAppResource = await resolveMcpAppResourceSafely(
1839
+ config,
1840
+ name,
1841
+ entry,
1842
+ requestMeta,
1843
+ );
1844
+ const rawToolMeta =
1845
+ (entry.tool as any)._meta &&
1846
+ typeof (entry.tool as any)._meta === "object" &&
1847
+ !Array.isArray((entry.tool as any)._meta)
1848
+ ? { ...((entry.tool as any)._meta as Record<string, unknown>) }
1849
+ : {};
1850
+ const toolMeta = {
1851
+ ...rawToolMeta,
1852
+ // Advertisement path: only tag the tool with its inline-embed
1853
+ // descriptor when the kill switch is on, so disabled embeds
1854
+ // never prompt a host to render/read the `ui://` resource.
1855
+ ...(mcpAppResource && requestMeta?.inlineMcpApps
1856
+ ? {
1857
+ ...openAiToolDescriptorMeta(mcpAppResource),
1858
+ [MCP_APP_RESOURCE_URI_META_KEY]: mcpAppResource.uri,
1859
+ ui: mcpAppToolUiMeta(
1860
+ mcpAppResource,
1861
+ entry.mcpApp?.visibility ??
1862
+ metadataObject(rawToolMeta.ui).visibility,
1863
+ ),
1864
+ }
1865
+ : {}),
1866
+ };
1867
+ const baseDescription = entry.tool.description ?? name;
1868
+ const annotations: Record<string, unknown> = {
1869
+ readOnlyHint: entry.readOnly === true,
1870
+ destructiveHint:
1871
+ entry.publicAgent?.isConsequential === true ||
1872
+ entry.needsApproval !== undefined,
1873
+ openWorldHint: false,
1874
+ };
1875
+ if (hasLink) annotations["agent-native/producesOpenLink"] = true;
1876
+ return {
1877
+ name,
1878
+ description: hasLink
1879
+ ? `${baseDescription} After calling, surface the returned "Open in … →" link to the user.`
1880
+ : baseDescription,
1881
+ inputSchema: entry.tool.parameters ?? {
1882
+ type: "object" as const,
1883
+ properties: {},
1884
+ },
1885
+ ...(Object.keys(toolMeta).length > 0 ? { _meta: toolMeta } : {}),
1886
+ annotations,
1887
+ } as Tool;
1888
+ }),
1638
1889
  );
1639
1890
 
1640
1891
  if (
@@ -1679,6 +1930,7 @@ export async function createMCPServerForRequest(
1679
1930
  });
1680
1931
  }
1681
1932
 
1933
+ tools.sort((a, b) => compareMcpCatalogValues(a.name, b.name));
1682
1934
  return { tools };
1683
1935
  });
1684
1936
  });
@@ -1686,52 +1938,208 @@ export async function createMCPServerForRequest(
1686
1938
  // tools/call — dispatch to action registry or ask-agent. Wrapped in the
1687
1939
  // request context so the action's `run(args)` and `askAgent()` execute
1688
1940
  // with the verified caller's identity, not the platform default.
1689
- server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
1690
- return withCallerContext(async () => {
1691
- const { name, arguments: args } = request.params;
1941
+ server.setRequestHandler(
1942
+ "tools/call",
1943
+ async (request: any, ctx: ServerContext) => {
1944
+ return withCallerContext(async () => {
1945
+ const { name, arguments: args } = request.params;
1946
+
1947
+ if (name === "ask-agent" && config.askAgent) {
1948
+ if (compactMcpAppCatalog || connectorCatalogActive) {
1949
+ return {
1950
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
1951
+ isError: true,
1952
+ };
1953
+ }
1954
+ if (!hasMcpOAuthScope(effectiveIdentity?.oauthScopes, "mcp:write")) {
1955
+ return {
1956
+ content: [
1957
+ {
1958
+ type: "text",
1959
+ text: "Forbidden: OAuth scope does not allow ask-agent",
1960
+ },
1961
+ ],
1962
+ isError: true,
1963
+ };
1964
+ }
1965
+ const message = args?.message ?? "";
1966
+ try {
1967
+ // Keep the legacy meta-tool compatible for local callers, but use
1968
+ // the same durable A2A submission path as ask_app whenever this is
1969
+ // an HTTP/hosted request. A full agent loop must never be held open
1970
+ // for minutes behind one MCP tools/call request. Always route
1971
+ // through ask_app's own run() — even with no request origin, since
1972
+ // it now bounds that case too via its process-local inline-task
1973
+ // fallback (ask-app-inline-tasks.ts) instead of us awaiting
1974
+ // config.askAgent() here unbounded. This is the shared helper: no
1975
+ // second task map to keep in sync.
1976
+ const hostedAskApp = getBuiltinCrossAppTools(
1977
+ config,
1978
+ requestMeta,
1979
+ ).ask_app;
1980
+ const result = await hostedAskApp.run({
1981
+ message,
1982
+ async: isExplicitAsyncAskAgent(args?.async),
1983
+ maxWaitMs: isExplicitAsyncAskAgent(args?.async)
1984
+ ? 0
1985
+ : boundedAskAgentWaitMs(args?.maxWaitMs),
1986
+ });
1987
+ return {
1988
+ content: [{ type: "text", text: formatAskAgentResult(result) }],
1989
+ };
1990
+ } catch (err: any) {
1991
+ return {
1992
+ content: [{ type: "text", text: `Error: ${err.message}` }],
1993
+ isError: true,
1994
+ };
1995
+ }
1996
+ }
1692
1997
 
1693
- if (name === "ask-agent" && config.askAgent) {
1694
- if (compactMcpAppCatalog || connectorCatalogActive) {
1998
+ // Connector-catalog tier: when active, callableActions === advertisedActions
1999
+ // (the filtered set). Non-listed tools are not callable — mirroring how
2000
+ // compactMcpAppCatalog gates calls on advertisedActions.
2001
+ const callableActions =
2002
+ compactMcpAppCatalog || connectorCatalogActive
2003
+ ? advertisedActions
2004
+ : actions;
2005
+ const entry = callableActions[name];
2006
+ if (!entry) {
1695
2007
  return {
1696
2008
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
1697
2009
  isError: true,
1698
2010
  };
1699
2011
  }
1700
- if (!hasMcpOAuthScope(effectiveIdentity?.oauthScopes, "mcp:write")) {
2012
+ if (
2013
+ !isActionVisibleForOAuthScope(entry, effectiveIdentity?.oauthScopes)
2014
+ ) {
1701
2015
  return {
1702
2016
  content: [
1703
2017
  {
1704
2018
  type: "text",
1705
- text: "Forbidden: OAuth scope does not allow ask-agent",
2019
+ text: `Forbidden: OAuth scope does not allow tool ${name}`,
1706
2020
  },
1707
2021
  ],
1708
2022
  isError: true,
1709
2023
  };
1710
2024
  }
1711
- const message = args?.message ?? "";
2025
+
1712
2026
  try {
1713
- // Keep the legacy meta-tool compatible for local callers, but use
1714
- // the same durable A2A submission path as ask_app whenever this is
1715
- // an HTTP/hosted request. A full agent loop must never be held open
1716
- // for minutes behind one MCP tools/call request. Always route
1717
- // through ask_app's own run() — even with no request origin, since
1718
- // it now bounds that case too via its process-local inline-task
1719
- // fallback (ask-app-inline-tasks.ts) instead of us awaiting
1720
- // config.askAgent() here unbounded. This is the shared helper: no
1721
- // second task map to keep in sync.
1722
- const hostedAskApp = getBuiltinCrossAppTools(
1723
- config,
2027
+ const approvalResult = await requireMcpActionApproval(
2028
+ entry,
2029
+ name,
2030
+ (args as Record<string, unknown>) ?? {},
2031
+ ctx,
2032
+ );
2033
+ if (approvalResult !== undefined) return approvalResult;
2034
+
2035
+ // We're inside `withCallerContext`, so the request-context getters
2036
+ // resolve the verified MCP caller's identity (do NOT inject a dev
2037
+ // fallback). Tag the call as an external-agent MCP dispatch.
2038
+ const result = await entry.run(
2039
+ (args as Record<string, string>) ?? {},
2040
+ {
2041
+ userEmail: getRequestUserEmail(),
2042
+ orgId: getRequestOrgId() ?? null,
2043
+ caller: "mcp",
2044
+ actionName: name,
2045
+ },
2046
+ );
2047
+ const mcpResult = isMcpActionResult(result) ? result : null;
2048
+ const rawResult = mcpResult ? mcpResult.raw : result;
2049
+ const resultForClient = mcpResult ? mcpResult.text : result;
2050
+ const mcpResultIsError =
2051
+ !!mcpResult &&
2052
+ !!mcpResult.raw &&
2053
+ typeof mcpResult.raw === "object" &&
2054
+ (mcpResult.raw as Record<string, unknown>).isError === true;
2055
+ // Render path: only treat the result as an inline embed when the kill
2056
+ // switch is on. When off, `mcpAppResource` is null so every embed
2057
+ // branch below degrades to the plain deep-link artifacts the tool would
2058
+ // otherwise return — no `openai/outputTemplate`, no minted embed-start,
2059
+ // no embed structuredContent — so the host shows a link, not an iframe.
2060
+ const mcpAppResourceCandidate = requestMeta?.inlineMcpApps
2061
+ ? await resolveMcpAppResourceSafely(
2062
+ config,
2063
+ name,
2064
+ entry,
2065
+ requestMeta,
2066
+ )
2067
+ : null;
2068
+ const rawResultForClient = mcpAppResourceCandidate
2069
+ ? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
2070
+ : rawResult;
2071
+ // Only attach the embed widget for a non-error result that has content.
2072
+ const embedHasContent = mcpResultHasContent(rawResultForClient);
2073
+ const mcpAppResource =
2074
+ mcpAppResourceCandidate && !mcpResultIsError && embedHasContent
2075
+ ? mcpAppResourceCandidate
2076
+ : null;
2077
+ // `openai/outputTemplate` is declared at the tool level, so the host
2078
+ // renders a widget for every call regardless of result _meta. The only
2079
+ // per-result signal it honors is `isError` (shows error text, no widget),
2080
+ // so treat an embed tool that produced nothing as an error.
2081
+ const embedProducedNothing =
2082
+ !!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
2083
+ const { block, _meta } = buildLinkArtifacts(
2084
+ entry,
2085
+ (args as Record<string, any>) ?? {},
2086
+ rawResultForClient,
1724
2087
  requestMeta,
1725
- ).ask_app;
1726
- const result = await hostedAskApp.run({
1727
- message,
1728
- async: isExplicitAsyncAskAgent(args?.async),
1729
- maxWaitMs: isExplicitAsyncAskAgent(args?.async)
1730
- ? 0
1731
- : boundedAskAgentWaitMs(args?.maxWaitMs),
1732
- });
2088
+ );
2089
+ const responseMeta: Record<string, unknown> = {
2090
+ ...(_meta ?? {}),
2091
+ ...(mcpAppResource
2092
+ ? mcpAppEmbedOpenLinkMeta(
2093
+ rawResultForClient,
2094
+ mcpAppResource,
2095
+ requestMeta,
2096
+ )
2097
+ : {}),
2098
+ ...(mcpAppResource ? openAiToolResultMeta(mcpAppResource) : {}),
2099
+ };
2100
+ const toolUiMeta = metadataObject((entry.tool as any)._meta?.ui);
2101
+ const toolVisibility = toolUiMeta.visibility;
2102
+ const isAppOnlyVisibility =
2103
+ Array.isArray(toolVisibility) &&
2104
+ toolVisibility.length > 0 &&
2105
+ toolVisibility.every((v) => v === "app");
2106
+ const readOnlyStructuredResult =
2107
+ entry.readOnly === true &&
2108
+ rawResultForClient &&
2109
+ typeof rawResultForClient === "object"
2110
+ ? Array.isArray(rawResultForClient)
2111
+ ? { items: rawResultForClient }
2112
+ : rawResultForClient
2113
+ : undefined;
2114
+ const structuredContent = mcpAppResource
2115
+ ? mcpAppStructuredContent(rawResultForClient, responseMeta)
2116
+ : isAppOnlyVisibility &&
2117
+ rawResult &&
2118
+ typeof rawResult === "object" &&
2119
+ !Array.isArray(rawResult)
2120
+ ? (rawResult as Record<string, unknown>)
2121
+ : readOnlyStructuredResult
2122
+ ? mcpAppStructuredContent(
2123
+ readOnlyStructuredResult,
2124
+ responseMeta,
2125
+ )
2126
+ : undefined;
2127
+ const text = mcpAppResource
2128
+ ? conciseMcpAppToolText(name, resultForClient, structuredContent!)
2129
+ : conciseToolResultText(name, resultForClient, {
2130
+ preserveObjectResult: entry.readOnly === true,
2131
+ });
2132
+ const content: any[] = [{ type: "text", text }];
2133
+ if (block) content.push(block);
1733
2134
  return {
1734
- content: [{ type: "text", text: formatAskAgentResult(result) }],
2135
+ content,
2136
+ ...(mcpResultIsError || embedProducedNothing
2137
+ ? { isError: true }
2138
+ : {}),
2139
+ ...(structuredContent ? { structuredContent } : {}),
2140
+ ...(Object.keys(responseMeta).length > 0
2141
+ ? { _meta: responseMeta }
2142
+ : {}),
1735
2143
  };
1736
2144
  } catch (err: any) {
1737
2145
  return {
@@ -1739,146 +2147,12 @@ export async function createMCPServerForRequest(
1739
2147
  isError: true,
1740
2148
  };
1741
2149
  }
1742
- }
1743
-
1744
- // Connector-catalog tier: when active, callableActions === advertisedActions
1745
- // (the filtered set). Non-listed tools are not callable — mirroring how
1746
- // compactMcpAppCatalog gates calls on advertisedActions.
1747
- const callableActions =
1748
- compactMcpAppCatalog || connectorCatalogActive
1749
- ? advertisedActions
1750
- : actions;
1751
- const entry = callableActions[name];
1752
- if (!entry) {
1753
- return {
1754
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
1755
- isError: true,
1756
- };
1757
- }
1758
- if (
1759
- !isActionVisibleForOAuthScope(entry, effectiveIdentity?.oauthScopes)
1760
- ) {
1761
- return {
1762
- content: [
1763
- {
1764
- type: "text",
1765
- text: `Forbidden: OAuth scope does not allow tool ${name}`,
1766
- },
1767
- ],
1768
- isError: true,
1769
- };
1770
- }
1771
-
1772
- try {
1773
- // We're inside `withCallerContext`, so the request-context getters
1774
- // resolve the verified MCP caller's identity (do NOT inject a dev
1775
- // fallback). Tag the call as an external-agent MCP dispatch.
1776
- const result = await entry.run((args as Record<string, string>) ?? {}, {
1777
- userEmail: getRequestUserEmail(),
1778
- orgId: getRequestOrgId() ?? null,
1779
- caller: "mcp",
1780
- actionName: name,
1781
- });
1782
- const mcpResult = isMcpActionResult(result) ? result : null;
1783
- const rawResult = mcpResult ? mcpResult.raw : result;
1784
- const resultForClient = mcpResult ? mcpResult.text : result;
1785
- const mcpResultIsError =
1786
- !!mcpResult &&
1787
- !!mcpResult.raw &&
1788
- typeof mcpResult.raw === "object" &&
1789
- (mcpResult.raw as Record<string, unknown>).isError === true;
1790
- // Render path: only treat the result as an inline embed when the kill
1791
- // switch is on. When off, `mcpAppResource` is null so every embed
1792
- // branch below degrades to the plain deep-link artifacts the tool would
1793
- // otherwise return — no `openai/outputTemplate`, no minted embed-start,
1794
- // no embed structuredContent — so the host shows a link, not an iframe.
1795
- const mcpAppResourceCandidate = requestMeta?.inlineMcpApps
1796
- ? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
1797
- : null;
1798
- const rawResultForClient = mcpAppResourceCandidate
1799
- ? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
1800
- : rawResult;
1801
- // Only attach the embed widget for a non-error result that has content.
1802
- const embedHasContent = mcpResultHasContent(rawResultForClient);
1803
- const mcpAppResource =
1804
- mcpAppResourceCandidate && !mcpResultIsError && embedHasContent
1805
- ? mcpAppResourceCandidate
1806
- : null;
1807
- // `openai/outputTemplate` is declared at the tool level, so the host
1808
- // renders a widget for every call regardless of result _meta. The only
1809
- // per-result signal it honors is `isError` (shows error text, no widget),
1810
- // so treat an embed tool that produced nothing as an error.
1811
- const embedProducedNothing =
1812
- !!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
1813
- const { block, _meta } = buildLinkArtifacts(
1814
- entry,
1815
- (args as Record<string, any>) ?? {},
1816
- rawResultForClient,
1817
- requestMeta,
1818
- );
1819
- const responseMeta: Record<string, unknown> = {
1820
- ...(_meta ?? {}),
1821
- ...(mcpAppResource
1822
- ? mcpAppEmbedOpenLinkMeta(
1823
- rawResultForClient,
1824
- mcpAppResource,
1825
- requestMeta,
1826
- )
1827
- : {}),
1828
- ...(mcpAppResource ? openAiToolResultMeta(mcpAppResource) : {}),
1829
- };
1830
- const toolUiMeta = metadataObject((entry.tool as any)._meta?.ui);
1831
- const toolVisibility = toolUiMeta.visibility;
1832
- const isAppOnlyVisibility =
1833
- Array.isArray(toolVisibility) &&
1834
- toolVisibility.length > 0 &&
1835
- toolVisibility.every((v) => v === "app");
1836
- const readOnlyStructuredResult =
1837
- entry.readOnly === true &&
1838
- rawResultForClient &&
1839
- typeof rawResultForClient === "object"
1840
- ? Array.isArray(rawResultForClient)
1841
- ? { items: rawResultForClient }
1842
- : rawResultForClient
1843
- : undefined;
1844
- const structuredContent = mcpAppResource
1845
- ? mcpAppStructuredContent(rawResultForClient, responseMeta)
1846
- : isAppOnlyVisibility &&
1847
- rawResult &&
1848
- typeof rawResult === "object" &&
1849
- !Array.isArray(rawResult)
1850
- ? (rawResult as Record<string, unknown>)
1851
- : readOnlyStructuredResult
1852
- ? mcpAppStructuredContent(readOnlyStructuredResult, responseMeta)
1853
- : undefined;
1854
- const text = mcpAppResource
1855
- ? conciseMcpAppToolText(name, resultForClient, structuredContent!)
1856
- : conciseToolResultText(name, resultForClient, {
1857
- preserveObjectResult: entry.readOnly === true,
1858
- });
1859
- const content: any[] = [{ type: "text", text }];
1860
- if (block) content.push(block);
1861
- return {
1862
- content,
1863
- ...(mcpResultIsError || embedProducedNothing
1864
- ? { isError: true }
1865
- : {}),
1866
- ...(structuredContent ? { structuredContent } : {}),
1867
- ...(Object.keys(responseMeta).length > 0
1868
- ? { _meta: responseMeta }
1869
- : {}),
1870
- };
1871
- } catch (err: any) {
1872
- return {
1873
- content: [{ type: "text", text: `Error: ${err.message}` }],
1874
- isError: true,
1875
- };
1876
- }
1877
- });
1878
- });
2150
+ });
2151
+ },
2152
+ );
1879
2153
 
1880
2154
  if (supportsMcpApps) {
1881
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
2155
+ server.setRequestHandler("resources/list", async () => {
1882
2156
  return withCallerContext(async () => {
1883
2157
  const mcpAppResources = await getMcpAppResources(
1884
2158
  config,
@@ -1886,21 +2160,23 @@ export async function createMCPServerForRequest(
1886
2160
  requestMeta,
1887
2161
  );
1888
2162
  return {
1889
- resources: mcpAppResources.map((resource) => ({
1890
- uri: resource.uri,
1891
- name: resource.name,
1892
- ...(resource.title ? { title: resource.title } : {}),
1893
- ...(resource.description
1894
- ? { description: resource.description }
1895
- : {}),
1896
- mimeType: resource.mimeType,
1897
- ...(resource._meta ? { _meta: resource._meta } : {}),
1898
- })),
2163
+ resources: mcpAppResources
2164
+ .sort((a, b) => compareMcpCatalogValues(a.uri, b.uri))
2165
+ .map((resource) => ({
2166
+ uri: resource.uri,
2167
+ name: resource.name,
2168
+ ...(resource.title ? { title: resource.title } : {}),
2169
+ ...(resource.description
2170
+ ? { description: resource.description }
2171
+ : {}),
2172
+ mimeType: resource.mimeType,
2173
+ ...(resource._meta ? { _meta: resource._meta } : {}),
2174
+ })),
1899
2175
  };
1900
2176
  });
1901
2177
  });
1902
2178
 
1903
- server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
2179
+ server.setRequestHandler("resources/templates/list", async () => {
1904
2180
  return withCallerContext(async () => {
1905
2181
  const mcpAppResources = await getMcpAppResources(
1906
2182
  config,
@@ -1908,71 +2184,71 @@ export async function createMCPServerForRequest(
1908
2184
  requestMeta,
1909
2185
  );
1910
2186
  return {
1911
- resourceTemplates: mcpAppResources.map((resource) => ({
1912
- uriTemplate: resource.uri,
1913
- name: resource.name,
1914
- ...(resource.title ? { title: resource.title } : {}),
1915
- ...(resource.description
1916
- ? { description: resource.description }
1917
- : {}),
1918
- mimeType: resource.mimeType,
1919
- ...(resource._meta ? { _meta: resource._meta } : {}),
1920
- })),
2187
+ resourceTemplates: mcpAppResources
2188
+ .sort((a, b) => compareMcpCatalogValues(a.uri, b.uri))
2189
+ .map((resource) => ({
2190
+ uriTemplate: resource.uri,
2191
+ name: resource.name,
2192
+ ...(resource.title ? { title: resource.title } : {}),
2193
+ ...(resource.description
2194
+ ? { description: resource.description }
2195
+ : {}),
2196
+ mimeType: resource.mimeType,
2197
+ ...(resource._meta ? { _meta: resource._meta } : {}),
2198
+ })),
1921
2199
  };
1922
2200
  });
1923
2201
  });
1924
2202
 
1925
- server.setRequestHandler(
1926
- ReadResourceRequestSchema,
1927
- async (request: any) => {
1928
- return withCallerContext(async () => {
1929
- const uri = request.params?.uri;
1930
- let found: {
1931
- actionName: string;
1932
- resource: ResolvedMcpAppResource;
1933
- } | null = null;
1934
- for (const [name, entry] of Object.entries(advertisedActions)) {
1935
- const resourceUri = getMcpAppResourceUri(config, name, entry);
1936
- if (!resourceUri || !matchesMcpAppResourceUri(resourceUri, uri)) {
1937
- continue;
1938
- }
1939
- const resource = await resolveMcpAppResourceSafely(
1940
- config,
1941
- name,
1942
- entry,
1943
- requestMeta,
1944
- );
1945
- if (resource) {
1946
- found = { actionName: name, resource };
1947
- break;
1948
- }
1949
- // resolveMcpAppResourceSafely returned null (e.g. an async resolver
1950
- // threw) — keep scanning the remaining candidates rather than
1951
- // aborting and reporting the resource as missing.
2203
+ server.setRequestHandler("resources/read", async (request: any) => {
2204
+ return withCallerContext(async () => {
2205
+ const uri = request.params?.uri;
2206
+ let found: {
2207
+ actionName: string;
2208
+ resource: ResolvedMcpAppResource;
2209
+ } | null = null;
2210
+ for (const [name, entry] of Object.entries(advertisedActions)) {
2211
+ const resourceUri = getMcpAppResourceUri(config, name, entry);
2212
+ if (!resourceUri || !matchesMcpAppResourceUri(resourceUri, uri)) {
2213
+ continue;
1952
2214
  }
1953
- if (!found) {
1954
- throw new Error(`MCP App resource not found: ${uri}`);
2215
+ const resource = await resolveMcpAppResourceSafely(
2216
+ config,
2217
+ name,
2218
+ entry,
2219
+ requestMeta,
2220
+ );
2221
+ if (resource) {
2222
+ found = { actionName: name, resource };
2223
+ break;
1955
2224
  }
1956
- return {
1957
- contents: [
1958
- {
1959
- uri,
1960
- mimeType: found.resource.mimeType,
1961
- text: renderMcpAppHtml(
1962
- found.resource,
1963
- found.actionName,
1964
- config,
1965
- requestMeta,
1966
- ),
1967
- ...(found.resource._meta
1968
- ? { _meta: found.resource._meta }
1969
- : {}),
1970
- },
1971
- ],
1972
- };
1973
- });
1974
- },
1975
- );
2225
+ // resolveMcpAppResourceSafely returned null (e.g. an async resolver
2226
+ // threw) — keep scanning the remaining candidates rather than
2227
+ // aborting and reporting the resource as missing.
2228
+ }
2229
+ if (!found) {
2230
+ throw new ResourceNotFoundError(
2231
+ String(uri ?? ""),
2232
+ `MCP App resource not found: ${uri}`,
2233
+ );
2234
+ }
2235
+ return {
2236
+ contents: [
2237
+ {
2238
+ uri,
2239
+ mimeType: found.resource.mimeType,
2240
+ text: renderMcpAppHtml(
2241
+ found.resource,
2242
+ found.actionName,
2243
+ config,
2244
+ requestMeta,
2245
+ ),
2246
+ ...(found.resource._meta ? { _meta: found.resource._meta } : {}),
2247
+ },
2248
+ ],
2249
+ };
2250
+ });
2251
+ });
1976
2252
  }
1977
2253
 
1978
2254
  return server;