create-nextblock 0.15.10 → 0.16.2

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 (266) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
@@ -1,415 +1,415 @@
1
- import { revalidatePath } from 'next/cache';
2
-
3
- import {
4
- createClient,
5
- getServiceRoleSupabaseClient,
6
- verifyPackageOnline,
7
- } from '@nextblock-cms/db/server';
8
- import {
9
- CORTEX_AI_PACKAGE_ID,
10
- handleCortexMcpMessage,
11
- isLocalhostHost,
12
- parseBearerToken,
13
- resolveCortexAiMcpSettings,
14
- shouldTrustLocalMcpRequest,
15
- touchCortexAiMcpToken,
16
- verifyCortexAiMcpToken,
17
- type CortexAiMcpScope,
18
- type CortexMcpToolContext,
19
- type JsonRpcMessage,
20
- } from '@nextblock-cms/cortex';
21
-
22
- import { validateBlockContent } from '../../../lib/blocks/blockRegistry';
23
- import { importExternalImageToMedia } from '../../cms/media/import-external-image';
24
- import { captureRevisionBaseline, commitRevisionFromBaseline } from '../../cms/revisions/service';
25
- import type { AnyFullContent } from '../../cms/revisions/utils';
26
-
27
- /**
28
- * Model Context Protocol server endpoint.
29
- *
30
- * Exposes the Cortex AI tool registry over MCP Streamable HTTP so external clients
31
- * (Claude Code, Claude Desktop, Cursor, VS Code) can operate this CMS with the same
32
- * typed, validated tools the in-app dashboard agent uses. The protocol itself lives
33
- * in `@nextblock-cms/cortex` (`mcp-server.ts`); this file is the HTTP shim plus auth.
34
- *
35
- * Node runtime, not Edge: the tool executors reach `node:crypto`, `sharp` (via the
36
- * media importer) and the service-role Supabase client.
37
- */
38
- export const runtime = 'nodejs';
39
- export const dynamic = 'force-dynamic';
40
-
41
- const SERVER_VERSION = '1.0.0';
42
-
43
- /**
44
- * Confirmation is skipped for MCP callers, deliberately.
45
- *
46
- * The in-app agent's two-phase confirm works by matching a phrase in the user's *next
47
- * chat message*, which has no analogue in MCP — the model calls a tool and gets a
48
- * result, with no channel to carry a human phrase back. Every MCP host already gates
49
- * tool calls behind its own approval UI, so the confirmation would be a second prompt
50
- * the protocol cannot satisfy, and leaving it on would simply make every mutating
51
- * tool return a preview forever. The real control for MCP is the token scope: a
52
- * read-only token never sees a mutating tool at all.
53
- */
54
- const MCP_SKIP_CONFIRMATION = true;
55
-
56
- type McpAuth = {
57
- actorUserId: string | null;
58
- /**
59
- * True when this token outlived the account that minted it (`created_by` is
60
- * `ON DELETE SET NULL`), so `actorUserId` below is a stand-in rather than the
61
- * principal that actually holds the credential.
62
- *
63
- * A stand-in is fine for *attribution* — a revision needs some author — but it
64
- * must never be the basis for *authorization*, or deleting an administrator would
65
- * silently promote their leftover token to whichever admin happens to sort first.
66
- * Offboarding someone is exactly when their credentials should lose power, not
67
- * inherit someone else's.
68
- */
69
- actorFromOrphanedToken: boolean;
70
- scopes: CortexAiMcpScope[];
71
- source: 'admin-session' | 'localhost' | 'token';
72
- };
73
-
74
- /**
75
- * MCP has no cookie session, so the importer is handed the actor this request already
76
- * authenticated. Without it every image import fails with "You must be signed in to
77
- * import an image" — which silently strips the imagery out of any page or product
78
- * built over MCP, since executors treat an import failure as non-fatal.
79
- */
80
- function createMcpImageImporter(actorUserId: string | null) {
81
- return async function importExternalImageForMcp(input: {
82
- url: string;
83
- altText?: string;
84
- }): Promise<{ id: string } | { error: string }> {
85
- const result = await importExternalImageToMedia({
86
- ...(actorUserId ? { actorUserId } : {}),
87
- altText: input.altText,
88
- url: input.url,
89
- });
90
-
91
- if ('error' in result) {
92
- return { error: result.error };
93
- }
94
-
95
- return { id: result.media.id };
96
- };
97
- }
98
-
99
- /** Mirrors the global-agent route so MCP writes land in Revision History like any other edit. */
100
- function createMcpRevisionRecorder(authorId: string | null) {
101
- return async function recordRevision(input: {
102
- baseline?: unknown;
103
- contentType: 'page' | 'post' | 'product';
104
- entityId: number | string;
105
- phase: 'capture' | 'commit';
106
- }): Promise<unknown> {
107
- if (input.phase === 'capture') {
108
- return captureRevisionBaseline(input.contentType, input.entityId);
109
- }
110
-
111
- const result = await commitRevisionFromBaseline(
112
- input.contentType,
113
- input.entityId,
114
- authorId,
115
- (input.baseline ?? null) as AnyFullContent | null
116
- );
117
-
118
- if ('error' in result) {
119
- console.error('Cortex AI MCP: revision not recorded —', result.error);
120
- }
121
-
122
- return undefined;
123
- };
124
- }
125
-
126
- /**
127
- * Reject cross-origin browser calls (DNS-rebinding defence, required by the spec).
128
- *
129
- * Only enforced when an `Origin` header is present: native MCP clients are not
130
- * browsers and send none, so requiring one would lock out every real caller.
131
- */
132
- function isOriginAllowed(request: Request): boolean {
133
- const origin = request.headers.get('origin');
134
-
135
- if (!origin) {
136
- return true;
137
- }
138
-
139
- let originHost: string;
140
-
141
- try {
142
- originHost = new URL(origin).host;
143
- } catch {
144
- return false;
145
- }
146
-
147
- if (isLocalhostHost(originHost)) {
148
- return true;
149
- }
150
-
151
- const host = request.headers.get('host');
152
-
153
- if (host && originHost.toLowerCase() === host.toLowerCase()) {
154
- return true;
155
- }
156
-
157
- const configuredUrl = process.env.NEXT_PUBLIC_URL;
158
-
159
- if (configuredUrl) {
160
- try {
161
- return new URL(configuredUrl).host.toLowerCase() === originHost.toLowerCase();
162
- } catch {
163
- return false;
164
- }
165
- }
166
-
167
- return false;
168
- }
169
-
170
- /**
171
- * Establish who is calling.
172
- *
173
- * Three accepted paths, in priority order:
174
- * 1. A bearer token from `mcp_access_tokens` — the path every external client uses.
175
- * 2. An authenticated ADMIN cookie session — lets the dashboard's own "Test
176
- * connection" button reach the endpoint without minting a token first.
177
- * 3. Loopback in development, when the operator has left that setting on.
178
- */
179
- async function authenticateMcpRequest(request: Request): Promise<McpAuth | null> {
180
- const serviceClient = getServiceRoleSupabaseClient();
181
- const settings = await resolveCortexAiMcpSettings(serviceClient);
182
-
183
- if (!settings.enabled) {
184
- return null;
185
- }
186
-
187
- const bearer = parseBearerToken(request.headers.get('authorization'));
188
-
189
- if (bearer) {
190
- const verification = await verifyCortexAiMcpToken(serviceClient, bearer);
191
-
192
- if (!verification.valid) {
193
- return null;
194
- }
195
-
196
- // Bookkeeping only — never block the call on it.
197
- void touchCortexAiMcpToken(serviceClient, verification.token.id);
198
-
199
- return {
200
- actorFromOrphanedToken: !verification.token.created_by,
201
- actorUserId: verification.token.created_by ?? (await resolveFallbackAdminUserId()),
202
- scopes: verification.scopes,
203
- source: 'token',
204
- };
205
- }
206
-
207
- const adminUserId = await resolveAdminSessionUserId();
208
-
209
- if (adminUserId) {
210
- return {
211
- actorFromOrphanedToken: false,
212
- actorUserId: adminUserId,
213
- scopes: ['read', 'write'],
214
- source: 'admin-session',
215
- };
216
- }
217
-
218
- if (shouldTrustLocalMcpRequest({ hostHeader: request.headers.get('host'), settings })) {
219
- // Loopback trust is an explicit opt-in on a development machine, where anyone
220
- // who can reach this endpoint can already read the service-role key out of
221
- // .env.local. Not treated as orphaned: it grants nothing new.
222
- return {
223
- actorFromOrphanedToken: false,
224
- actorUserId: await resolveFallbackAdminUserId(),
225
- scopes: ['read', 'write'],
226
- source: 'localhost',
227
- };
228
- }
229
-
230
- return null;
231
- }
232
-
233
- /**
234
- * Every mutating Cortex executor calls `getActorUserId()` and throws without one, so a
235
- * connection with no identity behind it can read but never write. Two connections have
236
- * that problem: localhost trust (nobody signed in) and a token whose creator was since
237
- * deleted (`created_by` is `ON DELETE SET NULL`).
238
- *
239
- * Rather than advertise a `write` scope those connections cannot actually use, fall back
240
- * to an ADMIN profile so the write is attributed to a real person in Revision History.
241
- * This grants no new authority — reaching here already required either loopback in
242
- * development or a valid admin-minted token — it only supplies the author field.
243
- *
244
- * `id` ordering is arbitrary but stable, which is what matters: the same fallback admin
245
- * every time, so revision authorship does not jump between people run to run.
246
- */
247
- async function resolveFallbackAdminUserId(): Promise<string | null> {
248
- try {
249
- const { data } = await getServiceRoleSupabaseClient()
250
- .from('profiles')
251
- .select('id')
252
- .eq('role', 'ADMIN')
253
- .order('id', { ascending: true })
254
- .limit(1)
255
- .maybeSingle();
256
-
257
- return data?.id ?? null;
258
- } catch {
259
- return null;
260
- }
261
- }
262
-
263
- async function resolveAdminSessionUserId(): Promise<string | null> {
264
- try {
265
- const supabase = createClient();
266
- const {
267
- data: { user },
268
- } = await supabase.auth.getUser();
269
-
270
- if (!user) {
271
- return null;
272
- }
273
-
274
- const { data: profile } = await supabase
275
- .from('profiles')
276
- .select('role')
277
- .eq('id', user.id)
278
- .single();
279
-
280
- return profile?.role === 'ADMIN' ? user.id : null;
281
- } catch {
282
- return null;
283
- }
284
- }
285
-
286
- function buildToolContext(auth: McpAuth): CortexMcpToolContext {
287
- return {
288
- actorFromOrphanedToken: auth.actorFromOrphanedToken,
289
- actorUserId: auth.actorUserId,
290
- importExternalImage: createMcpImageImporter(auth.actorUserId),
291
- // No open editor over MCP: tools that need a target take it in their arguments
292
- // (`cmsTarget`, `slug`, `entityId`) rather than inheriting one from a UI.
293
- pageContext: null,
294
- recordRevision: createMcpRevisionRecorder(auth.actorUserId),
295
- revalidatePath,
296
- skipConfirmation: MCP_SKIP_CONFIRMATION,
297
- supabase: getServiceRoleSupabaseClient(),
298
- validateBlockContent,
299
- };
300
- }
301
-
302
- const JSON_HEADERS = {
303
- 'Cache-Control': 'no-store',
304
- 'Content-Type': 'application/json; charset=utf-8',
305
- } as const;
306
-
307
- /**
308
- * 401 for an unauthenticated caller.
309
- *
310
- * The `WWW-Authenticate` value is intentionally bare. Adding a `resource_metadata`
311
- * parameter would advertise RFC 9728 OAuth discovery, and Claude Code responds to
312
- * that by starting an OAuth flow — which dead-ends against a static-token server.
313
- * A plain challenge tells the client "send a bearer token" and nothing more.
314
- */
315
- function unauthorized(message: string): Response {
316
- return new Response(JSON.stringify({ error: message }), {
317
- headers: {
318
- ...JSON_HEADERS,
319
- 'WWW-Authenticate': 'Bearer realm="NextBlock Cortex AI MCP"',
320
- },
321
- status: 401,
322
- });
323
- }
324
-
325
- export async function POST(request: Request): Promise<Response> {
326
- if (!isOriginAllowed(request)) {
327
- return new Response(JSON.stringify({ error: 'Origin not allowed.' }), {
328
- headers: JSON_HEADERS,
329
- status: 403,
330
- });
331
- }
332
-
333
- const isCortexAiActive = await verifyPackageOnline(CORTEX_AI_PACKAGE_ID);
334
-
335
- if (!isCortexAiActive) {
336
- return new Response(
337
- JSON.stringify({ error: 'NextBlock Cortex AI is not active for this workspace.' }),
338
- { headers: JSON_HEADERS, status: 403 }
339
- );
340
- }
341
-
342
- const auth = await authenticateMcpRequest(request);
343
-
344
- if (!auth) {
345
- return unauthorized(
346
- 'A valid NextBlock MCP access token is required. Generate one in CMS Settings → Cortex AI, and confirm the MCP server is enabled there.'
347
- );
348
- }
349
-
350
- let message: JsonRpcMessage;
351
-
352
- try {
353
- message = (await request.json()) as JsonRpcMessage;
354
- } catch {
355
- return new Response(
356
- JSON.stringify({
357
- error: { code: -32700, message: 'Parse error: request body is not valid JSON.' },
358
- id: null,
359
- jsonrpc: '2.0',
360
- }),
361
- { headers: JSON_HEADERS, status: 400 }
362
- );
363
- }
364
-
365
- const response = await handleCortexMcpMessage(message, {
366
- context: buildToolContext(auth),
367
- scopes: auth.scopes,
368
- serverVersion: SERVER_VERSION,
369
- });
370
-
371
- // Notifications and responses: 202 Accepted with no body. Returning a JSON-RPC
372
- // envelope for a message that carried no `id` desyncs strict clients.
373
- if (response.body === null) {
374
- return new Response(null, { status: response.status });
375
- }
376
-
377
- return new Response(JSON.stringify(response.body), {
378
- headers: JSON_HEADERS,
379
- status: response.status,
380
- });
381
- }
382
-
383
- /**
384
- * The optional server→client SSE stream.
385
- *
386
- * This server never initiates requests or pushes unsolicited notifications — every
387
- * response is returned inline on the POST — so there is nothing to stream. The spec
388
- * explicitly permits answering the GET with 405 in that case.
389
- */
390
- export function GET(): Response {
391
- return new Response(
392
- JSON.stringify({
393
- error:
394
- 'This MCP endpoint does not offer a server-initiated SSE stream. Send JSON-RPC messages via POST.',
395
- }),
396
- { headers: { ...JSON_HEADERS, Allow: 'POST, DELETE, OPTIONS' }, status: 405 }
397
- );
398
- }
399
-
400
- /** Session termination. The server is stateless, so there is no session to tear down. */
401
- export function DELETE(): Response {
402
- return new Response(null, { status: 204 });
403
- }
404
-
405
- export function OPTIONS(): Response {
406
- return new Response(null, {
407
- headers: {
408
- 'Access-Control-Allow-Headers':
409
- 'Authorization, Content-Type, MCP-Protocol-Version, Mcp-Session-Id, Mcp-Method, Mcp-Name',
410
- 'Access-Control-Allow-Methods': 'POST, DELETE, OPTIONS',
411
- Allow: 'POST, DELETE, OPTIONS',
412
- },
413
- status: 204,
414
- });
415
- }
1
+ import { revalidatePath } from 'next/cache';
2
+
3
+ import {
4
+ createClient,
5
+ getServiceRoleSupabaseClient,
6
+ verifyPackageOnline,
7
+ } from '@nextblock-cms/db/server';
8
+ import {
9
+ CORTEX_AI_PACKAGE_ID,
10
+ handleCortexMcpMessage,
11
+ isLocalhostHost,
12
+ parseBearerToken,
13
+ resolveCortexAiMcpSettings,
14
+ shouldTrustLocalMcpRequest,
15
+ touchCortexAiMcpToken,
16
+ verifyCortexAiMcpToken,
17
+ type CortexAiMcpScope,
18
+ type CortexMcpToolContext,
19
+ type JsonRpcMessage,
20
+ } from '@nextblock-cms/cortex';
21
+
22
+ import { validateBlockContent } from '../../../lib/blocks/blockRegistry';
23
+ import { importExternalImageToMedia } from '../../cms/media/import-external-image';
24
+ import { captureRevisionBaseline, commitRevisionFromBaseline } from '../../cms/revisions/service';
25
+ import type { AnyFullContent } from '../../cms/revisions/utils';
26
+
27
+ /**
28
+ * Model Context Protocol server endpoint.
29
+ *
30
+ * Exposes the Cortex AI tool registry over MCP Streamable HTTP so external clients
31
+ * (Claude Code, Claude Desktop, Cursor, VS Code) can operate this CMS with the same
32
+ * typed, validated tools the in-app dashboard agent uses. The protocol itself lives
33
+ * in `@nextblock-cms/cortex` (`mcp-server.ts`); this file is the HTTP shim plus auth.
34
+ *
35
+ * Node runtime, not Edge: the tool executors reach `node:crypto`, `sharp` (via the
36
+ * media importer) and the service-role Supabase client.
37
+ */
38
+ export const runtime = 'nodejs';
39
+ export const dynamic = 'force-dynamic';
40
+
41
+ const SERVER_VERSION = '1.0.0';
42
+
43
+ /**
44
+ * Confirmation is skipped for MCP callers, deliberately.
45
+ *
46
+ * The in-app agent's two-phase confirm works by matching a phrase in the user's *next
47
+ * chat message*, which has no analogue in MCP — the model calls a tool and gets a
48
+ * result, with no channel to carry a human phrase back. Every MCP host already gates
49
+ * tool calls behind its own approval UI, so the confirmation would be a second prompt
50
+ * the protocol cannot satisfy, and leaving it on would simply make every mutating
51
+ * tool return a preview forever. The real control for MCP is the token scope: a
52
+ * read-only token never sees a mutating tool at all.
53
+ */
54
+ const MCP_SKIP_CONFIRMATION = true;
55
+
56
+ type McpAuth = {
57
+ actorUserId: string | null;
58
+ /**
59
+ * True when this token outlived the account that minted it (`created_by` is
60
+ * `ON DELETE SET NULL`), so `actorUserId` below is a stand-in rather than the
61
+ * principal that actually holds the credential.
62
+ *
63
+ * A stand-in is fine for *attribution* — a revision needs some author — but it
64
+ * must never be the basis for *authorization*, or deleting an administrator would
65
+ * silently promote their leftover token to whichever admin happens to sort first.
66
+ * Offboarding someone is exactly when their credentials should lose power, not
67
+ * inherit someone else's.
68
+ */
69
+ actorFromOrphanedToken: boolean;
70
+ scopes: CortexAiMcpScope[];
71
+ source: 'admin-session' | 'localhost' | 'token';
72
+ };
73
+
74
+ /**
75
+ * MCP has no cookie session, so the importer is handed the actor this request already
76
+ * authenticated. Without it every image import fails with "You must be signed in to
77
+ * import an image" — which silently strips the imagery out of any page or product
78
+ * built over MCP, since executors treat an import failure as non-fatal.
79
+ */
80
+ function createMcpImageImporter(actorUserId: string | null) {
81
+ return async function importExternalImageForMcp(input: {
82
+ url: string;
83
+ altText?: string;
84
+ }): Promise<{ id: string } | { error: string }> {
85
+ const result = await importExternalImageToMedia({
86
+ ...(actorUserId ? { actorUserId } : {}),
87
+ altText: input.altText,
88
+ url: input.url,
89
+ });
90
+
91
+ if ('error' in result) {
92
+ return { error: result.error };
93
+ }
94
+
95
+ return { id: result.media.id };
96
+ };
97
+ }
98
+
99
+ /** Mirrors the global-agent route so MCP writes land in Revision History like any other edit. */
100
+ function createMcpRevisionRecorder(authorId: string | null) {
101
+ return async function recordRevision(input: {
102
+ baseline?: unknown;
103
+ contentType: 'page' | 'post' | 'product';
104
+ entityId: number | string;
105
+ phase: 'capture' | 'commit';
106
+ }): Promise<unknown> {
107
+ if (input.phase === 'capture') {
108
+ return captureRevisionBaseline(input.contentType, input.entityId);
109
+ }
110
+
111
+ const result = await commitRevisionFromBaseline(
112
+ input.contentType,
113
+ input.entityId,
114
+ authorId,
115
+ (input.baseline ?? null) as AnyFullContent | null
116
+ );
117
+
118
+ if ('error' in result) {
119
+ console.error('Cortex AI MCP: revision not recorded —', result.error);
120
+ }
121
+
122
+ return undefined;
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Reject cross-origin browser calls (DNS-rebinding defence, required by the spec).
128
+ *
129
+ * Only enforced when an `Origin` header is present: native MCP clients are not
130
+ * browsers and send none, so requiring one would lock out every real caller.
131
+ */
132
+ function isOriginAllowed(request: Request): boolean {
133
+ const origin = request.headers.get('origin');
134
+
135
+ if (!origin) {
136
+ return true;
137
+ }
138
+
139
+ let originHost: string;
140
+
141
+ try {
142
+ originHost = new URL(origin).host;
143
+ } catch {
144
+ return false;
145
+ }
146
+
147
+ if (isLocalhostHost(originHost)) {
148
+ return true;
149
+ }
150
+
151
+ const host = request.headers.get('host');
152
+
153
+ if (host && originHost.toLowerCase() === host.toLowerCase()) {
154
+ return true;
155
+ }
156
+
157
+ const configuredUrl = process.env.NEXT_PUBLIC_URL;
158
+
159
+ if (configuredUrl) {
160
+ try {
161
+ return new URL(configuredUrl).host.toLowerCase() === originHost.toLowerCase();
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ return false;
168
+ }
169
+
170
+ /**
171
+ * Establish who is calling.
172
+ *
173
+ * Three accepted paths, in priority order:
174
+ * 1. A bearer token from `mcp_access_tokens` — the path every external client uses.
175
+ * 2. An authenticated ADMIN cookie session — lets the dashboard's own "Test
176
+ * connection" button reach the endpoint without minting a token first.
177
+ * 3. Loopback in development, when the operator has left that setting on.
178
+ */
179
+ async function authenticateMcpRequest(request: Request): Promise<McpAuth | null> {
180
+ const serviceClient = getServiceRoleSupabaseClient();
181
+ const settings = await resolveCortexAiMcpSettings(serviceClient);
182
+
183
+ if (!settings.enabled) {
184
+ return null;
185
+ }
186
+
187
+ const bearer = parseBearerToken(request.headers.get('authorization'));
188
+
189
+ if (bearer) {
190
+ const verification = await verifyCortexAiMcpToken(serviceClient, bearer);
191
+
192
+ if (!verification.valid) {
193
+ return null;
194
+ }
195
+
196
+ // Bookkeeping only — never block the call on it.
197
+ void touchCortexAiMcpToken(serviceClient, verification.token.id);
198
+
199
+ return {
200
+ actorFromOrphanedToken: !verification.token.created_by,
201
+ actorUserId: verification.token.created_by ?? (await resolveFallbackAdminUserId()),
202
+ scopes: verification.scopes,
203
+ source: 'token',
204
+ };
205
+ }
206
+
207
+ const adminUserId = await resolveAdminSessionUserId();
208
+
209
+ if (adminUserId) {
210
+ return {
211
+ actorFromOrphanedToken: false,
212
+ actorUserId: adminUserId,
213
+ scopes: ['read', 'write'],
214
+ source: 'admin-session',
215
+ };
216
+ }
217
+
218
+ if (shouldTrustLocalMcpRequest({ hostHeader: request.headers.get('host'), settings })) {
219
+ // Loopback trust is an explicit opt-in on a development machine, where anyone
220
+ // who can reach this endpoint can already read the service-role key out of
221
+ // .env.local. Not treated as orphaned: it grants nothing new.
222
+ return {
223
+ actorFromOrphanedToken: false,
224
+ actorUserId: await resolveFallbackAdminUserId(),
225
+ scopes: ['read', 'write'],
226
+ source: 'localhost',
227
+ };
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ /**
234
+ * Every mutating Cortex executor calls `getActorUserId()` and throws without one, so a
235
+ * connection with no identity behind it can read but never write. Two connections have
236
+ * that problem: localhost trust (nobody signed in) and a token whose creator was since
237
+ * deleted (`created_by` is `ON DELETE SET NULL`).
238
+ *
239
+ * Rather than advertise a `write` scope those connections cannot actually use, fall back
240
+ * to an ADMIN profile so the write is attributed to a real person in Revision History.
241
+ * This grants no new authority — reaching here already required either loopback in
242
+ * development or a valid admin-minted token — it only supplies the author field.
243
+ *
244
+ * `id` ordering is arbitrary but stable, which is what matters: the same fallback admin
245
+ * every time, so revision authorship does not jump between people run to run.
246
+ */
247
+ async function resolveFallbackAdminUserId(): Promise<string | null> {
248
+ try {
249
+ const { data } = await getServiceRoleSupabaseClient()
250
+ .from('profiles')
251
+ .select('id')
252
+ .eq('role', 'ADMIN')
253
+ .order('id', { ascending: true })
254
+ .limit(1)
255
+ .maybeSingle();
256
+
257
+ return data?.id ?? null;
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ async function resolveAdminSessionUserId(): Promise<string | null> {
264
+ try {
265
+ const supabase = createClient();
266
+ const {
267
+ data: { user },
268
+ } = await supabase.auth.getUser();
269
+
270
+ if (!user) {
271
+ return null;
272
+ }
273
+
274
+ const { data: profile } = await supabase
275
+ .from('profiles')
276
+ .select('role')
277
+ .eq('id', user.id)
278
+ .single();
279
+
280
+ return profile?.role === 'ADMIN' ? user.id : null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+
286
+ function buildToolContext(auth: McpAuth): CortexMcpToolContext {
287
+ return {
288
+ actorFromOrphanedToken: auth.actorFromOrphanedToken,
289
+ actorUserId: auth.actorUserId,
290
+ importExternalImage: createMcpImageImporter(auth.actorUserId),
291
+ // No open editor over MCP: tools that need a target take it in their arguments
292
+ // (`cmsTarget`, `slug`, `entityId`) rather than inheriting one from a UI.
293
+ pageContext: null,
294
+ recordRevision: createMcpRevisionRecorder(auth.actorUserId),
295
+ revalidatePath,
296
+ skipConfirmation: MCP_SKIP_CONFIRMATION,
297
+ supabase: getServiceRoleSupabaseClient(),
298
+ validateBlockContent,
299
+ };
300
+ }
301
+
302
+ const JSON_HEADERS = {
303
+ 'Cache-Control': 'no-store',
304
+ 'Content-Type': 'application/json; charset=utf-8',
305
+ } as const;
306
+
307
+ /**
308
+ * 401 for an unauthenticated caller.
309
+ *
310
+ * The `WWW-Authenticate` value is intentionally bare. Adding a `resource_metadata`
311
+ * parameter would advertise RFC 9728 OAuth discovery, and Claude Code responds to
312
+ * that by starting an OAuth flow — which dead-ends against a static-token server.
313
+ * A plain challenge tells the client "send a bearer token" and nothing more.
314
+ */
315
+ function unauthorized(message: string): Response {
316
+ return new Response(JSON.stringify({ error: message }), {
317
+ headers: {
318
+ ...JSON_HEADERS,
319
+ 'WWW-Authenticate': 'Bearer realm="NextBlock Cortex AI MCP"',
320
+ },
321
+ status: 401,
322
+ });
323
+ }
324
+
325
+ export async function POST(request: Request): Promise<Response> {
326
+ if (!isOriginAllowed(request)) {
327
+ return new Response(JSON.stringify({ error: 'Origin not allowed.' }), {
328
+ headers: JSON_HEADERS,
329
+ status: 403,
330
+ });
331
+ }
332
+
333
+ const isCortexAiActive = await verifyPackageOnline(CORTEX_AI_PACKAGE_ID);
334
+
335
+ if (!isCortexAiActive) {
336
+ return new Response(
337
+ JSON.stringify({ error: 'NextBlock Cortex AI is not active for this workspace.' }),
338
+ { headers: JSON_HEADERS, status: 403 }
339
+ );
340
+ }
341
+
342
+ const auth = await authenticateMcpRequest(request);
343
+
344
+ if (!auth) {
345
+ return unauthorized(
346
+ 'A valid NextBlock MCP access token is required. Generate one in CMS Settings → Cortex AI, and confirm the MCP server is enabled there.'
347
+ );
348
+ }
349
+
350
+ let message: JsonRpcMessage;
351
+
352
+ try {
353
+ message = (await request.json()) as JsonRpcMessage;
354
+ } catch {
355
+ return new Response(
356
+ JSON.stringify({
357
+ error: { code: -32700, message: 'Parse error: request body is not valid JSON.' },
358
+ id: null,
359
+ jsonrpc: '2.0',
360
+ }),
361
+ { headers: JSON_HEADERS, status: 400 }
362
+ );
363
+ }
364
+
365
+ const response = await handleCortexMcpMessage(message, {
366
+ context: buildToolContext(auth),
367
+ scopes: auth.scopes,
368
+ serverVersion: SERVER_VERSION,
369
+ });
370
+
371
+ // Notifications and responses: 202 Accepted with no body. Returning a JSON-RPC
372
+ // envelope for a message that carried no `id` desyncs strict clients.
373
+ if (response.body === null) {
374
+ return new Response(null, { status: response.status });
375
+ }
376
+
377
+ return new Response(JSON.stringify(response.body), {
378
+ headers: JSON_HEADERS,
379
+ status: response.status,
380
+ });
381
+ }
382
+
383
+ /**
384
+ * The optional server→client SSE stream.
385
+ *
386
+ * This server never initiates requests or pushes unsolicited notifications — every
387
+ * response is returned inline on the POST — so there is nothing to stream. The spec
388
+ * explicitly permits answering the GET with 405 in that case.
389
+ */
390
+ export function GET(): Response {
391
+ return new Response(
392
+ JSON.stringify({
393
+ error:
394
+ 'This MCP endpoint does not offer a server-initiated SSE stream. Send JSON-RPC messages via POST.',
395
+ }),
396
+ { headers: { ...JSON_HEADERS, Allow: 'POST, DELETE, OPTIONS' }, status: 405 }
397
+ );
398
+ }
399
+
400
+ /** Session termination. The server is stateless, so there is no session to tear down. */
401
+ export function DELETE(): Response {
402
+ return new Response(null, { status: 204 });
403
+ }
404
+
405
+ export function OPTIONS(): Response {
406
+ return new Response(null, {
407
+ headers: {
408
+ 'Access-Control-Allow-Headers':
409
+ 'Authorization, Content-Type, MCP-Protocol-Version, Mcp-Session-Id, Mcp-Method, Mcp-Name',
410
+ 'Access-Control-Allow-Methods': 'POST, DELETE, OPTIONS',
411
+ Allow: 'POST, DELETE, OPTIONS',
412
+ },
413
+ status: 204,
414
+ });
415
+ }