@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
@@ -2,7 +2,7 @@ import type { ActionMcpAppResourceConfig } from "../action.js";
2
2
  import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
3
3
 
4
4
  const MCP_APP_IMPORT =
5
- "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps";
5
+ "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5/app-with-deps";
6
6
 
7
7
  export const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = "$requestOrigin";
8
8
  const MCP_APP_WRAPPER_CHROME_HEIGHT = 44;
@@ -0,0 +1,380 @@
1
+ import { ssrfSafeFetch } from "../extensions/url-safety.js";
2
+
3
+ const CLIENT_METADATA_MAX_BYTES = 64 * 1024;
4
+ const CLIENT_METADATA_TIMEOUT_MS = 5_000;
5
+ const CLIENT_METADATA_MAX_REDIRECTS = 2;
6
+ const CLIENT_METADATA_MAX_CACHE_ENTRIES = 128;
7
+ const CLIENT_METADATA_MAX_CACHE_TTL_MS = 5 * 60_000;
8
+
9
+ const DISALLOWED_REDIRECT_SCHEMES = new Set([
10
+ "javascript:",
11
+ "data:",
12
+ "vbscript:",
13
+ "file:",
14
+ "blob:",
15
+ "about:",
16
+ ]);
17
+
18
+ export interface OAuthClientMetadataDocument {
19
+ clientId: string;
20
+ clientName: string;
21
+ redirectUris: string[];
22
+ grantTypes: string[];
23
+ responseTypes: string[];
24
+ tokenEndpointAuthMethod: "none";
25
+ applicationType: "native" | "web";
26
+ }
27
+
28
+ interface CachedClientMetadata {
29
+ expiresAt: number;
30
+ metadata: OAuthClientMetadataDocument;
31
+ }
32
+
33
+ const clientMetadataCache = new Map<string, CachedClientMetadata>();
34
+
35
+ export function isUrlBasedOAuthClientId(clientId: string): boolean {
36
+ return /^[a-z][a-z0-9+.-]*:/i.test(clientId);
37
+ }
38
+
39
+ export function validateOAuthClientMetadataUrl(clientId: string): URL {
40
+ if (!clientId || clientId !== clientId.trim() || clientId.length > 2048) {
41
+ throw new Error("Client metadata URL is invalid");
42
+ }
43
+ let url: URL;
44
+ try {
45
+ url = new URL(clientId);
46
+ } catch {
47
+ throw new Error("Client metadata URL is invalid");
48
+ }
49
+ if (
50
+ url.protocol !== "https:" ||
51
+ url.pathname === "/" ||
52
+ !url.pathname ||
53
+ url.hash ||
54
+ url.username ||
55
+ url.password
56
+ ) {
57
+ throw new Error("Client metadata URL must be HTTPS with a non-root path");
58
+ }
59
+ const authorityEnd = clientId.indexOf("/", "https://".length);
60
+ const queryStart = clientId.indexOf("?", authorityEnd);
61
+ const rawPath = clientId.slice(
62
+ authorityEnd,
63
+ queryStart === -1 ? undefined : queryStart,
64
+ );
65
+ if (
66
+ rawPath.split("/").some((segment) => {
67
+ try {
68
+ const decoded = decodeURIComponent(segment);
69
+ return decoded === "." || decoded === "..";
70
+ } catch {
71
+ return true;
72
+ }
73
+ })
74
+ ) {
75
+ throw new Error("Client metadata URL must not contain dot path segments");
76
+ }
77
+ return url;
78
+ }
79
+
80
+ export function isAllowedOAuthRedirectUri(value: unknown): value is string {
81
+ if (typeof value !== "string" || value.length > 2048) return false;
82
+ try {
83
+ const url = new URL(value);
84
+ if (url.hash || url.username || url.password) return false;
85
+ if (url.protocol === "https:") return true;
86
+ if (url.protocol === "http:") {
87
+ return (
88
+ url.hostname === "localhost" ||
89
+ url.hostname === "127.0.0.1" ||
90
+ url.hostname === "::1" ||
91
+ url.hostname === "[::1]"
92
+ );
93
+ }
94
+ return (
95
+ !DISALLOWED_REDIRECT_SCHEMES.has(url.protocol) &&
96
+ /^[a-z][a-z0-9+.-]*:$/.test(url.protocol)
97
+ );
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ export function applicationTypeForRedirectUris(
104
+ redirectUris: string[],
105
+ ): "native" | "web" {
106
+ return redirectUris.some((value) => {
107
+ const url = new URL(value);
108
+ return (
109
+ url.protocol !== "https:" ||
110
+ url.hostname === "localhost" ||
111
+ url.hostname === "127.0.0.1" ||
112
+ url.hostname === "::1" ||
113
+ url.hostname === "[::1]"
114
+ );
115
+ })
116
+ ? "native"
117
+ : "web";
118
+ }
119
+
120
+ function parseStringArray(value: unknown): string[] {
121
+ return Array.isArray(value)
122
+ ? value.filter((item): item is string => typeof item === "string")
123
+ : [];
124
+ }
125
+
126
+ function validateClientMetadataDocument(
127
+ clientId: string,
128
+ value: unknown,
129
+ ): OAuthClientMetadataDocument {
130
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
131
+ throw new Error("Client metadata document must be a JSON object");
132
+ }
133
+ const document = value as Record<string, unknown>;
134
+ if (document.client_id !== clientId) {
135
+ throw new Error(
136
+ "Client metadata document client_id does not match its URL",
137
+ );
138
+ }
139
+ const clientName =
140
+ typeof document.client_name === "string" ? document.client_name.trim() : "";
141
+ if (
142
+ !clientName ||
143
+ clientName.length > 120 ||
144
+ /[\u0000-\u001f\u007f]/.test(clientName)
145
+ ) {
146
+ throw new Error("Client metadata document client_name is invalid");
147
+ }
148
+
149
+ const rawRedirectUris = document.redirect_uris;
150
+ const redirectUris = parseStringArray(rawRedirectUris);
151
+ if (
152
+ !Array.isArray(rawRedirectUris) ||
153
+ redirectUris.length === 0 ||
154
+ redirectUris.length > 20 ||
155
+ redirectUris.length !== rawRedirectUris.length ||
156
+ !redirectUris.every(isAllowedOAuthRedirectUri)
157
+ ) {
158
+ throw new Error("Client metadata document redirect_uris are invalid");
159
+ }
160
+
161
+ const tokenEndpointAuthMethod =
162
+ typeof document.token_endpoint_auth_method === "string"
163
+ ? document.token_endpoint_auth_method
164
+ : "none";
165
+ if (
166
+ document.token_endpoint_auth_method !== undefined &&
167
+ typeof document.token_endpoint_auth_method !== "string"
168
+ ) {
169
+ throw new Error(
170
+ "Client metadata document token_endpoint_auth_method is invalid",
171
+ );
172
+ }
173
+ if (tokenEndpointAuthMethod !== "none") {
174
+ throw new Error("Only public Client ID Metadata clients are supported");
175
+ }
176
+
177
+ const grantTypes = parseStringArray(document.grant_types);
178
+ if (
179
+ (document.grant_types !== undefined &&
180
+ (!Array.isArray(document.grant_types) ||
181
+ grantTypes.length !== document.grant_types.length)) ||
182
+ grantTypes.length > 10 ||
183
+ !grantTypes.every(
184
+ (grant) => grant === "authorization_code" || grant === "refresh_token",
185
+ )
186
+ ) {
187
+ throw new Error("Client metadata document grant_types are unsupported");
188
+ }
189
+
190
+ const responseTypes = parseStringArray(document.response_types);
191
+ if (
192
+ (document.response_types !== undefined &&
193
+ (!Array.isArray(document.response_types) ||
194
+ responseTypes.length !== document.response_types.length)) ||
195
+ responseTypes.length > 10 ||
196
+ !responseTypes.every((responseType) => responseType === "code")
197
+ ) {
198
+ throw new Error("Client metadata document response_types are unsupported");
199
+ }
200
+
201
+ const requestedApplicationType =
202
+ typeof document.application_type === "string"
203
+ ? document.application_type
204
+ : undefined;
205
+ if (
206
+ (document.application_type !== undefined &&
207
+ typeof document.application_type !== "string") ||
208
+ (requestedApplicationType &&
209
+ requestedApplicationType !== "native" &&
210
+ requestedApplicationType !== "web")
211
+ ) {
212
+ throw new Error("Client metadata document application_type is unsupported");
213
+ }
214
+ const applicationType: "native" | "web" =
215
+ requestedApplicationType === "native" || requestedApplicationType === "web"
216
+ ? requestedApplicationType
217
+ : applicationTypeForRedirectUris(redirectUris);
218
+
219
+ return {
220
+ clientId,
221
+ clientName,
222
+ redirectUris: [...new Set(redirectUris)],
223
+ grantTypes: grantTypes.length
224
+ ? [...new Set(grantTypes)]
225
+ : ["authorization_code", "refresh_token"],
226
+ responseTypes: responseTypes.length
227
+ ? [...new Set(responseTypes)]
228
+ : ["code"],
229
+ tokenEndpointAuthMethod: "none",
230
+ applicationType,
231
+ };
232
+ }
233
+
234
+ async function readBoundedResponseBody(response: Response): Promise<string> {
235
+ const contentLength = Number(response.headers.get("content-length"));
236
+ if (
237
+ Number.isFinite(contentLength) &&
238
+ contentLength > CLIENT_METADATA_MAX_BYTES
239
+ ) {
240
+ await response.body?.cancel().catch(() => undefined);
241
+ throw new Error("Client metadata document is too large");
242
+ }
243
+ if (!response.body) return "";
244
+
245
+ const reader = response.body.getReader();
246
+ const decoder = new TextDecoder();
247
+ let bytesRead = 0;
248
+ let text = "";
249
+ try {
250
+ for (;;) {
251
+ const { done, value } = await reader.read();
252
+ if (done) break;
253
+ bytesRead += value.byteLength;
254
+ if (bytesRead > CLIENT_METADATA_MAX_BYTES) {
255
+ await reader.cancel().catch(() => undefined);
256
+ throw new Error("Client metadata document is too large");
257
+ }
258
+ text += decoder.decode(value, { stream: true });
259
+ }
260
+ text += decoder.decode();
261
+ return text;
262
+ } finally {
263
+ reader.releaseLock();
264
+ }
265
+ }
266
+
267
+ function cacheTtlMs(headers: Headers, now: number): number {
268
+ const cacheControl = headers.get("cache-control")?.toLowerCase() ?? "";
269
+ if (/(?:^|,)\s*(?:no-store|no-cache|private)(?:\s|,|$)/.test(cacheControl)) {
270
+ return 0;
271
+ }
272
+ const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(\d+)/)?.[1];
273
+ if (maxAge) {
274
+ return Math.min(Number(maxAge) * 1_000, CLIENT_METADATA_MAX_CACHE_TTL_MS);
275
+ }
276
+ const expiresAt = Date.parse(headers.get("expires") ?? "");
277
+ if (Number.isFinite(expiresAt) && expiresAt > now) {
278
+ return Math.min(expiresAt - now, CLIENT_METADATA_MAX_CACHE_TTL_MS);
279
+ }
280
+ return 0;
281
+ }
282
+
283
+ function cacheClientMetadata(
284
+ clientId: string,
285
+ metadata: OAuthClientMetadataDocument,
286
+ response: Response,
287
+ now: number,
288
+ ): void {
289
+ const ttlMs = cacheTtlMs(response.headers, now);
290
+ if (ttlMs <= 0) return;
291
+ clientMetadataCache.delete(clientId);
292
+ clientMetadataCache.set(clientId, {
293
+ expiresAt: now + ttlMs,
294
+ metadata,
295
+ });
296
+ while (clientMetadataCache.size > CLIENT_METADATA_MAX_CACHE_ENTRIES) {
297
+ const oldest = clientMetadataCache.keys().next().value;
298
+ if (typeof oldest !== "string") break;
299
+ clientMetadataCache.delete(oldest);
300
+ }
301
+ }
302
+
303
+ async function fetchOAuthClientMetadataDocument(
304
+ clientId: string,
305
+ signal: AbortSignal,
306
+ ): Promise<{ metadata: OAuthClientMetadataDocument; response: Response }> {
307
+ const response = await ssrfSafeFetch(
308
+ clientId,
309
+ {
310
+ headers: {
311
+ Accept: "application/json, application/client-metadata+json",
312
+ },
313
+ signal,
314
+ },
315
+ {
316
+ maxRedirects: CLIENT_METADATA_MAX_REDIRECTS,
317
+ httpsOnly: true,
318
+ },
319
+ );
320
+ if (!response.ok) {
321
+ await response.body?.cancel().catch(() => undefined);
322
+ throw new Error(
323
+ `Client metadata document request failed with ${response.status}`,
324
+ );
325
+ }
326
+ const contentType =
327
+ response.headers.get("content-type")?.split(";", 1)[0]?.trim() ?? "";
328
+ if (
329
+ contentType !== "application/json" &&
330
+ contentType !== "application/client-metadata+json"
331
+ ) {
332
+ await response.body?.cancel().catch(() => undefined);
333
+ throw new Error("Client metadata document content type is invalid");
334
+ }
335
+
336
+ const body = await readBoundedResponseBody(response);
337
+ let parsed: unknown;
338
+ try {
339
+ parsed = JSON.parse(body);
340
+ } catch {
341
+ throw new Error("Client metadata document is not valid JSON");
342
+ }
343
+ return {
344
+ metadata: validateClientMetadataDocument(clientId, parsed),
345
+ response,
346
+ };
347
+ }
348
+
349
+ export async function resolveOAuthClientMetadataDocument(
350
+ clientId: string,
351
+ ): Promise<OAuthClientMetadataDocument> {
352
+ validateOAuthClientMetadataUrl(clientId);
353
+ const now = Date.now();
354
+ const cached = clientMetadataCache.get(clientId);
355
+ if (cached && cached.expiresAt > now) {
356
+ clientMetadataCache.delete(clientId);
357
+ clientMetadataCache.set(clientId, cached);
358
+ return cached.metadata;
359
+ }
360
+ if (cached) clientMetadataCache.delete(clientId);
361
+
362
+ const abort = new AbortController();
363
+ let timeout: ReturnType<typeof setTimeout> | undefined;
364
+ try {
365
+ const timedOut = new Promise<never>((_, reject) => {
366
+ timeout = setTimeout(() => {
367
+ abort.abort();
368
+ reject(new Error("Client metadata document request timed out"));
369
+ }, CLIENT_METADATA_TIMEOUT_MS);
370
+ });
371
+ const { metadata, response } = await Promise.race([
372
+ fetchOAuthClientMetadataDocument(clientId, abort.signal),
373
+ timedOut,
374
+ ]);
375
+ cacheClientMetadata(clientId, metadata, response, now);
376
+ return metadata;
377
+ } finally {
378
+ if (timeout) clearTimeout(timeout);
379
+ }
380
+ }
@@ -17,6 +17,12 @@ import { getOrgDomain } from "../org/context.js";
17
17
  import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
18
18
  import { getAuthSecret } from "../server/better-auth-instance.js";
19
19
  import { readBody } from "../server/h3-helpers.js";
20
+ import {
21
+ applicationTypeForRedirectUris,
22
+ isAllowedOAuthRedirectUri,
23
+ isUrlBasedOAuthClientId,
24
+ resolveOAuthClientMetadataDocument,
25
+ } from "./oauth-client-metadata.js";
20
26
  import {
21
27
  createOAuthCode,
22
28
  createOAuthRefreshToken,
@@ -30,7 +36,6 @@ import {
30
36
  } from "./oauth-store.js";
31
37
  import {
32
38
  MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
33
- MCP_OAUTH_DEFAULT_SCOPE,
34
39
  MCP_OAUTH_SCOPES,
35
40
  normalizeOAuthScope,
36
41
  signMcpOAuthAccessToken,
@@ -42,6 +47,11 @@ export interface McpOAuthRouteOptions {
42
47
  appName?: string;
43
48
  }
44
49
 
50
+ const MCP_OAUTH_RESOURCE_SCOPES = MCP_OAUTH_SCOPES.filter(
51
+ (scope) => scope !== "offline_access",
52
+ );
53
+ const MCP_OAUTH_RESOURCE_SCOPE = MCP_OAUTH_RESOURCE_SCOPES.join(" ");
54
+
45
55
  function json(body: unknown, status = 200): Response {
46
56
  return new Response(JSON.stringify(body), {
47
57
  status,
@@ -229,7 +239,7 @@ export function getMcpOAuthProtectedResourceMetadataUrl(
229
239
 
230
240
  export function buildMcpOAuthChallenge(event: H3Event): string {
231
241
  const metadata = getMcpOAuthProtectedResourceMetadataUrl(event);
232
- const scope = MCP_OAUTH_DEFAULT_SCOPE;
242
+ const scope = MCP_OAUTH_RESOURCE_SCOPE;
233
243
  return metadata
234
244
  ? `Bearer resource_metadata="${metadata}", scope="${scope}"`
235
245
  : `Bearer scope="${scope}"`;
@@ -268,7 +278,7 @@ export function handleMcpOAuthProtectedResourceMetadata(
268
278
  return json({
269
279
  resource,
270
280
  authorization_servers: [issuer],
271
- scopes_supported: MCP_OAUTH_SCOPES,
281
+ scopes_supported: MCP_OAUTH_RESOURCE_SCOPES,
272
282
  resource_documentation: issuer,
273
283
  });
274
284
  }
@@ -296,54 +306,11 @@ export function handleMcpOAuthAuthorizationServerMetadata(
296
306
  code_challenge_methods_supported: ["S256"],
297
307
  token_endpoint_auth_methods_supported: ["none"],
298
308
  scopes_supported: MCP_OAUTH_SCOPES,
309
+ authorization_response_iss_parameter_supported: true,
310
+ client_id_metadata_document_supported: true,
299
311
  });
300
312
  }
301
313
 
302
- // Schemes that must never be accepted as a redirect target: they can execute
303
- // script or read local resources if a redirect is ever rendered in a browser
304
- // or webview context.
305
- const DISALLOWED_REDIRECT_SCHEMES = new Set([
306
- "javascript:",
307
- "data:",
308
- "vbscript:",
309
- "file:",
310
- "blob:",
311
- "about:",
312
- ]);
313
-
314
- // Native/desktop IDE clients (Cursor, VS Code, …) register a private-use URI
315
- // scheme callback such as `cursor://` or `vscode://` (RFC 8252 §7.1) instead of
316
- // an https/loopback URL. The authorization code is bound by PKCE (S256), so
317
- // delivering it through a client-registered app scheme is safe.
318
- function isPrivateUseRedirectScheme(protocol: string): boolean {
319
- if (DISALLOWED_REDIRECT_SCHEMES.has(protocol)) return false;
320
- // RFC 3986 scheme grammar: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ), with a
321
- // trailing ":" from URL.protocol. Require a non-http(s) custom scheme here;
322
- // http/https are handled explicitly above.
323
- return /^[a-z][a-z0-9+.-]*:$/.test(protocol);
324
- }
325
-
326
- function isAllowedRedirectUri(value: unknown): value is string {
327
- if (typeof value !== "string" || value.length > 2048) return false;
328
- try {
329
- const url = new URL(value);
330
- if (url.hash) return false;
331
- if (url.username || url.password) return false;
332
- if (url.protocol === "https:") return true;
333
- if (url.protocol === "http:") {
334
- return (
335
- url.hostname === "localhost" ||
336
- url.hostname === "127.0.0.1" ||
337
- url.hostname === "::1" ||
338
- url.hostname === "[::1]"
339
- );
340
- }
341
- return isPrivateUseRedirectScheme(url.protocol);
342
- } catch {
343
- return false;
344
- }
345
- }
346
-
347
314
  function parseStringArray(value: unknown): string[] {
348
315
  return Array.isArray(value)
349
316
  ? value.filter((item): item is string => typeof item === "string")
@@ -362,7 +329,7 @@ async function handleRegister(event: H3Event): Promise<Response> {
362
329
  if (
363
330
  redirectUris.length === 0 ||
364
331
  redirectUris.length > 20 ||
365
- !redirectUris.every(isAllowedRedirectUri)
332
+ !redirectUris.every(isAllowedOAuthRedirectUri)
366
333
  ) {
367
334
  return oauthError(
368
335
  "invalid_client_metadata",
@@ -393,6 +360,22 @@ async function handleRegister(event: H3Event): Promise<Response> {
393
360
  "Only public OAuth clients are supported",
394
361
  );
395
362
  }
363
+ const requestedApplicationType =
364
+ typeof body.application_type === "string"
365
+ ? body.application_type
366
+ : undefined;
367
+ if (
368
+ requestedApplicationType &&
369
+ requestedApplicationType !== "native" &&
370
+ requestedApplicationType !== "web"
371
+ ) {
372
+ return oauthError(
373
+ "invalid_client_metadata",
374
+ "application_type must be native or web",
375
+ );
376
+ }
377
+ const applicationType =
378
+ requestedApplicationType ?? applicationTypeForRedirectUris(redirectUris);
396
379
 
397
380
  const clientName =
398
381
  typeof body.client_name === "string"
@@ -422,6 +405,7 @@ async function handleRegister(event: H3Event): Promise<Response> {
422
405
  grant_types: client.grantTypes,
423
406
  response_types: client.responseTypes,
424
407
  token_endpoint_auth_method: client.tokenEndpointAuthMethod,
408
+ application_type: applicationType,
425
409
  },
426
410
  201,
427
411
  );
@@ -429,12 +413,14 @@ async function handleRegister(event: H3Event): Promise<Response> {
429
413
 
430
414
  function redirectWithOAuthError(params: {
431
415
  redirectUri: string;
416
+ issuer: string;
432
417
  state?: string;
433
418
  error: string;
434
419
  description?: string;
435
420
  }): Response {
436
421
  const url = new URL(params.redirectUri);
437
422
  url.searchParams.set("error", params.error);
423
+ url.searchParams.set("iss", params.issuer);
438
424
  if (params.description) {
439
425
  url.searchParams.set("error_description", params.description);
440
426
  }
@@ -444,17 +430,20 @@ function redirectWithOAuthError(params: {
444
430
 
445
431
  function buildCodeRedirectUrl(params: {
446
432
  redirectUri: string;
433
+ issuer: string;
447
434
  code: string;
448
435
  state?: string;
449
436
  }): string {
450
437
  const url = new URL(params.redirectUri);
451
438
  url.searchParams.set("code", params.code);
439
+ url.searchParams.set("iss", params.issuer);
452
440
  if (params.state) url.searchParams.set("state", params.state);
453
441
  return url.toString();
454
442
  }
455
443
 
456
444
  function redirectWithCode(params: {
457
445
  redirectUri: string;
446
+ issuer: string;
458
447
  code: string;
459
448
  state?: string;
460
449
  }): Response {
@@ -574,6 +563,7 @@ function renderConsentPage(params: {
574
563
  appName: string;
575
564
  email: string;
576
565
  clientName: string;
566
+ redirectUri: string;
577
567
  scopes: string[];
578
568
  fields: Record<string, string>;
579
569
  }): string {
@@ -602,6 +592,7 @@ function renderConsentPage(params: {
602
592
  <h1>Authorize ${escapeHtml(params.clientName)}</h1>
603
593
  <p>${escapeHtml(params.appName)} will let this MCP client act as ${escapeHtml(params.email)} for these scopes:</p>
604
594
  <ul>${scopes}</ul>
595
+ <p>After authorization, your browser will return to <code>${escapeHtml(params.redirectUri)}</code>.</p>
605
596
  <form method="post">
606
597
  ${hidden}
607
598
  <div class="actions">
@@ -721,6 +712,7 @@ async function handleAuthorize(
721
712
  );
722
713
  }
723
714
  const params = await readOAuthParams(event);
715
+ const issuer = getMcpOAuthIssuer(event);
724
716
  const state = params.state;
725
717
  const clientId = params.client_id;
726
718
  const redirectUri = params.redirect_uri;
@@ -739,6 +731,7 @@ async function handleAuthorize(
739
731
  if (
740
732
  !clientId ||
741
733
  !redirectUri ||
734
+ !issuer ||
742
735
  !resource ||
743
736
  !expectedResources.includes(resource)
744
737
  ) {
@@ -748,7 +741,9 @@ async function handleAuthorize(
748
741
  return oauthError("invalid_request", "PKCE S256 is required");
749
742
  }
750
743
 
751
- const client = await getOAuthClient(clientId);
744
+ const client = isUrlBasedOAuthClientId(clientId)
745
+ ? await resolveOAuthClientMetadataDocument(clientId).catch(() => null)
746
+ : await getOAuthClient(clientId);
752
747
  if (!client || !client.redirectUris.includes(redirectUri)) {
753
748
  return oauthError("invalid_client", "Unknown client or redirect_uri");
754
749
  }
@@ -758,6 +753,7 @@ async function handleAuthorize(
758
753
  if (params.prompt === "none") {
759
754
  return redirectWithOAuthError({
760
755
  redirectUri,
756
+ issuer,
761
757
  state,
762
758
  error: "login_required",
763
759
  });
@@ -772,6 +768,7 @@ async function handleAuthorize(
772
768
  if (!scope) {
773
769
  return redirectWithOAuthError({
774
770
  redirectUri,
771
+ issuer,
775
772
  state,
776
773
  error: "invalid_scope",
777
774
  });
@@ -782,6 +779,7 @@ async function handleAuthorize(
782
779
  appName: options.appName || options.appId || "Agent Native",
783
780
  email: session.email,
784
781
  clientName: client.clientName || client.clientId,
782
+ redirectUri,
785
783
  scopes: scope.split(/\s+/),
786
784
  fields: {
787
785
  response_type: "code",
@@ -821,6 +819,7 @@ async function handleAuthorize(
821
819
  if (params.decision !== "approve") {
822
820
  return redirectWithOAuthError({
823
821
  redirectUri,
822
+ issuer,
824
823
  state,
825
824
  error: "access_denied",
826
825
  });
@@ -850,7 +849,7 @@ async function handleAuthorize(
850
849
  isDeepLinkRedirect =
851
850
  protocol !== "http:" &&
852
851
  protocol !== "https:" &&
853
- isPrivateUseRedirectScheme(protocol);
852
+ /^[a-z][a-z0-9+.-]*:$/.test(protocol);
854
853
  } catch {
855
854
  isDeepLinkRedirect = false;
856
855
  }
@@ -861,13 +860,19 @@ async function handleAuthorize(
861
860
  clientName: client.clientName ?? null,
862
861
  redirectUrl: buildCodeRedirectUrl({
863
862
  redirectUri,
863
+ issuer,
864
864
  state,
865
865
  code: code.code,
866
866
  }),
867
867
  }),
868
868
  );
869
869
  }
870
- return redirectWithCode({ redirectUri, state, code: code.code });
870
+ return redirectWithCode({
871
+ redirectUri,
872
+ issuer,
873
+ state,
874
+ code: code.code,
875
+ });
871
876
  }
872
877
 
873
878
  async function issueTokenSet(params: {