create-nextblock 0.16.1 → 0.16.3

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 (264) 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 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  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/route.ts +4 -4
  30. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
  31. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  32. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  33. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  34. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  35. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  37. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  38. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  39. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  40. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  41. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  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 +406 -406
  46. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  47. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  48. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  49. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
  53. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
  54. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  55. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  56. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  58. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  59. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  60. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  61. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  64. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  65. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  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/components/PageForm.tsx +649 -649
  74. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
  77. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
  78. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  79. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  82. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  83. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  84. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  85. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  86. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  90. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  91. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  92. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  93. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  98. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  99. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  100. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  101. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  102. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  105. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  106. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  107. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  108. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  109. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  110. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  111. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +671 -671
  116. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  117. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  118. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  119. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  120. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  121. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  122. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  123. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  124. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  125. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  126. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  127. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  128. package/templates/nextblock-template/app/providers.tsx +96 -96
  129. package/templates/nextblock-template/app/robots.ts +123 -123
  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 -137
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  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 -86
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  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 -154
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
  231. package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
  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 +1 -2
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/scripts/backup.js +115 -115
  256. package/templates/nextblock-template/scripts/restore.js +385 -385
  257. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  262. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  263. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  264. package/tsconfig.base.json +3 -3
@@ -1,628 +1,628 @@
1
- 'use client';
2
-
3
- import React, { useMemo, useState, useTransition } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import {
6
- Alert,
7
- AlertDescription,
8
- AlertTitle,
9
- Badge,
10
- Button,
11
- Card,
12
- CardContent,
13
- CardDescription,
14
- CardHeader,
15
- CardTitle,
16
- Checkbox,
17
- Input,
18
- Label,
19
- } from '@nextblock-cms/ui';
20
- import {
21
- AlertTriangle,
22
- Check,
23
- Copy,
24
- Info,
25
- KeyRound,
26
- Lock,
27
- Plug,
28
- Plus,
29
- Trash2,
30
- } from 'lucide-react';
31
-
32
- import {
33
- createMcpAccessTokenAction,
34
- revokeMcpAccessTokenAction,
35
- saveMcpSettingsAction,
36
- type McpAccessTokenSummary,
37
- } from './mcp-actions';
38
-
39
- type McpScope = 'read' | 'write';
40
-
41
- type McpServerSettingsCardProps = {
42
- allowLocalhostWithoutToken: boolean;
43
- enabled: boolean;
44
- /** Loopback URL for a locally-running dev server. */
45
- localMcpUrl: string;
46
- /** The publicly reachable endpoint, derived from NEXT_PUBLIC_URL. */
47
- mcpUrl: string;
48
- /**
49
- * Locks every control that writes (the toggles, minting, revoking) while leaving
50
- * the endpoint, the client snippets, and the copy buttons fully usable.
51
- *
52
- * Used by the shared sandbox: a visitor must still be able to see that MCP access
53
- * exists and what connecting looks like, but the settings row and the token table
54
- * belong to every other visitor too. Hiding the card instead — which is what the
55
- * sandbox did before this — taught evaluators that NextBlock had no MCP support.
56
- */
57
- readOnly?: boolean;
58
- /** Explains the lock. Required in spirit whenever `readOnly` is set. */
59
- readOnlyNotice?: string;
60
- tokens: McpAccessTokenSummary[];
61
- };
62
-
63
- const TOKEN_PLACEHOLDER = 'YOUR_TOKEN';
64
-
65
- function CopyButton({ label = 'Copy', value }: { label?: string; value: string }) {
66
- const [copied, setCopied] = useState(false);
67
-
68
- return (
69
- <Button
70
- type="button"
71
- variant="ghost"
72
- size="sm"
73
- className="h-7 shrink-0"
74
- onClick={() => {
75
- void navigator.clipboard.writeText(value).then(() => {
76
- setCopied(true);
77
- setTimeout(() => setCopied(false), 1800);
78
- });
79
- }}
80
- >
81
- {copied ? <Check className="mr-1.5 h-3.5 w-3.5" /> : <Copy className="mr-1.5 h-3.5 w-3.5" />}
82
- {copied ? 'Copied' : label}
83
- </Button>
84
- );
85
- }
86
-
87
- function Snippet({ code, title }: { code: string; title: string }) {
88
- return (
89
- <div className="space-y-1.5">
90
- <div className="flex items-center justify-between gap-2">
91
- <p className="text-xs font-medium">{title}</p>
92
- <CopyButton value={code} />
93
- </div>
94
- <pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 text-[11px] leading-relaxed">
95
- <code>{code}</code>
96
- </pre>
97
- </div>
98
- );
99
- }
100
-
101
- function formatDate(value: string | null) {
102
- if (!value) return null;
103
- return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).format(
104
- new Date(value)
105
- );
106
- }
107
-
108
- export function McpServerSettingsCard({
109
- allowLocalhostWithoutToken,
110
- enabled,
111
- localMcpUrl,
112
- mcpUrl,
113
- readOnly = false,
114
- readOnlyNotice,
115
- tokens,
116
- }: McpServerSettingsCardProps) {
117
- const router = useRouter();
118
- const [isPending, startTransition] = useTransition();
119
-
120
- const [isEnabled, setIsEnabled] = useState(enabled);
121
- const [allowLocalhost, setAllowLocalhost] = useState(allowLocalhostWithoutToken);
122
- const [tokenName, setTokenName] = useState('');
123
- const [allowWrites, setAllowWrites] = useState(true);
124
- const [expiresInDays, setExpiresInDays] = useState('');
125
- const [mintedToken, setMintedToken] = useState<string | null>(null);
126
- const [error, setError] = useState<string | null>(null);
127
- const [activeClient, setActiveClient] = useState<'claude-code' | 'claude-desktop' | 'cursor' | 'vscode'>(
128
- 'claude-code'
129
- );
130
- const [useLocalUrl, setUseLocalUrl] = useState(false);
131
-
132
- const url = useLocalUrl ? localMcpUrl : mcpUrl;
133
-
134
- // Once a token has been minted in this session, bake it into the snippets so the
135
- // admin can copy a config that actually works instead of hand-substituting.
136
- const tokenForSnippet = mintedToken ?? TOKEN_PLACEHOLDER;
137
-
138
- /**
139
- * Whether the generated snippets should carry an Authorization header at all.
140
- *
141
- * A loopback connection covered by localhost trust needs none — and must not send
142
- * one: the route verifies any bearer it receives and rejects an invalid value
143
- * outright rather than falling back to localhost trust, so pasting the literal
144
- * placeholder would 401 instead of silently working. Once a real token exists we
145
- * include it either way, since it works on both origins.
146
- */
147
- const usesLocalhostTrust = useLocalUrl && allowLocalhost && !mintedToken;
148
-
149
- const snippets = useMemo(() => {
150
- const authHeader = usesLocalhostTrust
151
- ? undefined
152
- : { Authorization: `Bearer ${tokenForSnippet}` };
153
-
154
- const claudeCode = JSON.stringify(
155
- {
156
- mcpServers: {
157
- nextblock: {
158
- ...(authHeader ? { headers: authHeader } : {}),
159
- type: 'http',
160
- url,
161
- },
162
- },
163
- },
164
- null,
165
- 2
166
- );
167
-
168
- const cursor = JSON.stringify(
169
- {
170
- mcpServers: {
171
- nextblock: {
172
- ...(authHeader ? { headers: authHeader } : {}),
173
- url,
174
- },
175
- },
176
- },
177
- null,
178
- 2
179
- );
180
-
181
- const vscode = JSON.stringify(
182
- usesLocalhostTrust
183
- ? { servers: { nextblock: { type: 'http', url } } }
184
- : {
185
- inputs: [
186
- {
187
- description: 'NextBlock MCP access token',
188
- id: 'nextblockToken',
189
- password: true,
190
- type: 'promptString',
191
- },
192
- ],
193
- servers: {
194
- nextblock: {
195
- headers: { Authorization: 'Bearer ${input:nextblockToken}' },
196
- type: 'http',
197
- url,
198
- },
199
- },
200
- },
201
- null,
202
- 2
203
- );
204
-
205
- const claudeDesktop = JSON.stringify(
206
- {
207
- mcpServers: {
208
- nextblock: {
209
- args: [
210
- '-y',
211
- 'mcp-remote',
212
- url,
213
- ...(usesLocalhostTrust
214
- ? []
215
- : ['--header', `Authorization: Bearer ${tokenForSnippet}`]),
216
- ],
217
- command: 'npx',
218
- },
219
- },
220
- },
221
- null,
222
- 2
223
- );
224
-
225
- return { claudeCode, claudeDesktop, cursor, vscode };
226
- }, [tokenForSnippet, url, usesLocalhostTrust]);
227
-
228
- const claudeCodeCli = usesLocalhostTrust
229
- ? `claude mcp add --transport http nextblock ${url}`
230
- : `claude mcp add --transport http nextblock ${url} --header "Authorization: Bearer ${tokenForSnippet}"`;
231
-
232
- // Belt-and-braces: the controls below are disabled in read-only mode, and the
233
- // server actions refuse sandbox writes on their own. These early returns just
234
- // mean a stray call from here can never even reach the network.
235
- function persistSettings(next: { allowLocalhostWithoutToken: boolean; enabled: boolean }) {
236
- if (readOnly) return;
237
-
238
- setError(null);
239
- startTransition(async () => {
240
- const result = await saveMcpSettingsAction(next);
241
-
242
- if (!result.success) {
243
- setError(result.error ?? 'Failed to save MCP settings.');
244
- // Snap the optimistic toggle back so the UI never claims a state the DB rejected.
245
- setIsEnabled(enabled);
246
- setAllowLocalhost(allowLocalhostWithoutToken);
247
- return;
248
- }
249
-
250
- router.refresh();
251
- });
252
- }
253
-
254
- function handleCreateToken() {
255
- if (readOnly) return;
256
-
257
- setError(null);
258
- setMintedToken(null);
259
-
260
- startTransition(async () => {
261
- const parsedDays = Number.parseInt(expiresInDays, 10);
262
- const result = await createMcpAccessTokenAction({
263
- expiresInDays: Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : null,
264
- name: tokenName,
265
- scopes: allowWrites ? (['read', 'write'] as McpScope[]) : (['read'] as McpScope[]),
266
- });
267
-
268
- if (!result.success || !result.token) {
269
- setError(result.error ?? 'Failed to create the token.');
270
- return;
271
- }
272
-
273
- setMintedToken(result.token);
274
- setTokenName('');
275
- setExpiresInDays('');
276
- router.refresh();
277
- });
278
- }
279
-
280
- function handleRevoke(id: string) {
281
- if (readOnly) return;
282
-
283
- setError(null);
284
- startTransition(async () => {
285
- const result = await revokeMcpAccessTokenAction({ id });
286
-
287
- if (!result.success) {
288
- setError(result.error ?? 'Failed to revoke the token.');
289
- return;
290
- }
291
-
292
- router.refresh();
293
- });
294
- }
295
-
296
- return (
297
- <Card>
298
- <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
299
- <div>
300
- <CardTitle className="flex flex-wrap items-center gap-2 text-base">
301
- <Plug className="h-4 w-4" />
302
- MCP server access
303
- <Badge variant={isEnabled ? 'default' : 'outline'} className="ml-0.5 font-normal">
304
- {isEnabled ? 'Enabled' : 'Disabled'}
305
- </Badge>
306
- {isEnabled && tokens.length > 0 && (
307
- <Badge variant="secondary" className="font-normal">
308
- {tokens.length} active {tokens.length === 1 ? 'token' : 'tokens'}
309
- </Badge>
310
- )}
311
- {readOnly && (
312
- <Badge variant="outline" className="gap-1 font-normal">
313
- <Lock className="h-3 w-3" />
314
- Read-only
315
- </Badge>
316
- )}
317
- </CardTitle>
318
- <CardDescription className="text-xs">
319
- Expose this CMS to external AI clients over the Model Context Protocol. Claude Code,
320
- Claude Desktop, Cursor, and VS Code get the same typed Cortex tools the dashboard agent
321
- uses — building pages, editing blocks, querying analytics — from inside your editor.
322
- </CardDescription>
323
- </div>
324
- </CardHeader>
325
-
326
- <CardContent className="space-y-4 pt-0">
327
- {readOnly && readOnlyNotice && (
328
- <Alert>
329
- <Info className="h-4 w-4" />
330
- <AlertTitle>Configured per install</AlertTitle>
331
- <AlertDescription className="text-xs">{readOnlyNotice}</AlertDescription>
332
- </Alert>
333
- )}
334
-
335
- {error && (
336
- <Alert variant="destructive">
337
- <AlertTriangle className="h-4 w-4" />
338
- <AlertTitle>Could not save</AlertTitle>
339
- <AlertDescription className="text-xs">{error}</AlertDescription>
340
- </Alert>
341
- )}
342
-
343
- {/* Toggles */}
344
- <div className="space-y-3 rounded-md border bg-muted/20 p-3">
345
- <div className="flex items-start gap-2.5">
346
- <Checkbox
347
- id="mcp_enabled"
348
- checked={isEnabled}
349
- disabled={isPending || readOnly}
350
- onCheckedChange={(checked) => {
351
- const next = checked === true;
352
- setIsEnabled(next);
353
- persistSettings({ allowLocalhostWithoutToken: allowLocalhost, enabled: next });
354
- }}
355
- />
356
- <div className="space-y-0.5">
357
- <Label htmlFor="mcp_enabled" className="text-sm font-medium">
358
- Enable the MCP server
359
- </Label>
360
- <p className="text-xs text-muted-foreground">
361
- While off, <span className="font-mono">/api/mcp</span> rejects every request. Off by
362
- default because this is a remote write surface onto your live content.
363
- </p>
364
- </div>
365
- </div>
366
-
367
- <div className="flex items-start gap-2.5">
368
- <Checkbox
369
- id="mcp_allow_localhost"
370
- checked={allowLocalhost}
371
- disabled={isPending || !isEnabled || readOnly}
372
- onCheckedChange={(checked) => {
373
- const next = checked === true;
374
- setAllowLocalhost(next);
375
- persistSettings({ allowLocalhostWithoutToken: next, enabled: isEnabled });
376
- }}
377
- />
378
- <div className="space-y-0.5">
379
- <Label htmlFor="mcp_allow_localhost" className="text-sm font-medium">
380
- Trust localhost without a token
381
- </Label>
382
- <p className="text-xs text-muted-foreground">
383
- Convenience for local development only — it is ignored whenever{' '}
384
- <span className="font-mono">NODE_ENV=production</span>, so your deployed site always
385
- requires a token.
386
- </p>
387
- </div>
388
- </div>
389
- </div>
390
-
391
- {/* Endpoint */}
392
- <div className="space-y-1.5">
393
- <Label className="text-xs">Endpoint URL</Label>
394
- <div className="flex items-center gap-2">
395
- <Input readOnly value={url} className="font-mono text-xs" />
396
- <CopyButton value={url} />
397
- </div>
398
- <div className="flex gap-1.5 pt-0.5">
399
- <Button
400
- type="button"
401
- size="sm"
402
- variant={useLocalUrl ? 'ghost' : 'secondary'}
403
- className="h-6 text-[11px]"
404
- onClick={() => setUseLocalUrl(false)}
405
- >
406
- Live site
407
- </Button>
408
- <Button
409
- type="button"
410
- size="sm"
411
- variant={useLocalUrl ? 'secondary' : 'ghost'}
412
- className="h-6 text-[11px]"
413
- onClick={() => setUseLocalUrl(true)}
414
- >
415
- Localhost
416
- </Button>
417
- </div>
418
- </div>
419
-
420
- {/* Token minting */}
421
- <div className="space-y-3 rounded-md border p-3">
422
- <div className="space-y-0.5">
423
- <p className="text-sm font-medium">Access tokens</p>
424
- <p className="text-xs text-muted-foreground">
425
- Only the token&rsquo;s hash is stored, so it is shown once and cannot be recovered
426
- afterwards. A read-only token cannot even list the mutating tools.
427
- </p>
428
- </div>
429
-
430
- <div className="flex flex-wrap items-end gap-2">
431
- <div className="min-w-[180px] flex-1 space-y-1.5">
432
- <Label htmlFor="mcp_token_name" className="text-xs">
433
- Name
434
- </Label>
435
- <Input
436
- id="mcp_token_name"
437
- value={tokenName}
438
- maxLength={80}
439
- disabled={readOnly}
440
- placeholder="My laptop — Claude Code"
441
- onChange={(event) => setTokenName(event.target.value)}
442
- />
443
- </div>
444
- <div className="w-28 space-y-1.5">
445
- <Label htmlFor="mcp_token_expiry" className="text-xs">
446
- Expires (days)
447
- </Label>
448
- <Input
449
- id="mcp_token_expiry"
450
- type="number"
451
- min={1}
452
- max={3650}
453
- value={expiresInDays}
454
- disabled={readOnly}
455
- placeholder="Never"
456
- onChange={(event) => setExpiresInDays(event.target.value)}
457
- />
458
- </div>
459
- <div className="flex h-9 items-center gap-2">
460
- <Checkbox
461
- id="mcp_token_write"
462
- checked={allowWrites}
463
- disabled={readOnly}
464
- onCheckedChange={(checked) => setAllowWrites(checked === true)}
465
- />
466
- <Label htmlFor="mcp_token_write" className="text-xs">
467
- Allow writes
468
- </Label>
469
- </div>
470
- <Button
471
- type="button"
472
- size="sm"
473
- disabled={isPending || readOnly || !tokenName.trim()}
474
- onClick={handleCreateToken}
475
- >
476
- <Plus className="mr-1.5 h-3.5 w-3.5" />
477
- Create
478
- </Button>
479
- </div>
480
-
481
- {mintedToken && (
482
- <Alert>
483
- <KeyRound className="h-4 w-4" />
484
- <AlertTitle>Copy this token now</AlertTitle>
485
- <AlertDescription className="space-y-2">
486
- <p className="text-xs">
487
- This is the only time it will be shown. The snippets below already include it.
488
- </p>
489
- <div className="flex items-center gap-2">
490
- <Input readOnly value={mintedToken} className="font-mono text-xs" />
491
- <CopyButton value={mintedToken} />
492
- </div>
493
- </AlertDescription>
494
- </Alert>
495
- )}
496
-
497
- {tokens.length > 0 && (
498
- <div className="divide-y rounded-md border">
499
- {tokens.map((token) => {
500
- const lastUsed = formatDate(token.lastUsedAt);
501
- const expires = formatDate(token.expiresAt);
502
-
503
- return (
504
- <div key={token.id} className="flex items-center justify-between gap-2 p-2.5">
505
- <div className="min-w-0 space-y-0.5">
506
- <div className="flex flex-wrap items-center gap-1.5">
507
- <span className="truncate text-sm font-medium">{token.name}</span>
508
- <Badge
509
- variant={token.scopes.includes('write') ? 'secondary' : 'outline'}
510
- className="font-normal"
511
- >
512
- {token.scopes.includes('write') ? 'read + write' : 'read only'}
513
- </Badge>
514
- </div>
515
- <p className="text-[11px] text-muted-foreground">
516
- <span className="font-mono">{token.tokenPrefix}…</span>
517
- {' · '}
518
- {lastUsed ? `last used ${lastUsed}` : 'never used'}
519
- {expires ? ` · expires ${expires}` : ''}
520
- </p>
521
- </div>
522
- <Button
523
- type="button"
524
- variant="ghost"
525
- size="sm"
526
- disabled={isPending || readOnly}
527
- className="h-7 shrink-0 text-destructive hover:text-destructive"
528
- onClick={() => handleRevoke(token.id)}
529
- >
530
- <Trash2 className="mr-1.5 h-3.5 w-3.5" />
531
- Revoke
532
- </Button>
533
- </div>
534
- );
535
- })}
536
- </div>
537
- )}
538
- </div>
539
-
540
- {/* Client configuration */}
541
- <div className="space-y-3 rounded-md border p-3">
542
- <div className="space-y-0.5">
543
- <p className="text-sm font-medium">Connect a client</p>
544
- <p className="text-xs text-muted-foreground">
545
- {usesLocalhostTrust
546
- ? 'No token needed: localhost trust covers this connection while the dev server runs. These snippets deliberately send no Authorization header — an invalid one would be rejected rather than falling back to localhost trust.'
547
- : mintedToken
548
- ? 'These snippets include the token you just created.'
549
- : readOnly
550
- ? `This is the exact configuration you would paste on your own install, with ${TOKEN_PLACEHOLDER} standing in for the token you mint there.`
551
- : `Create a token above and these snippets will fill it in; otherwise replace ${TOKEN_PLACEHOLDER}.`}
552
- </p>
553
- </div>
554
-
555
- <div className="flex flex-wrap gap-1.5">
556
- {(
557
- [
558
- ['claude-code', 'Claude Code'],
559
- ['claude-desktop', 'Claude Desktop'],
560
- ['cursor', 'Cursor'],
561
- ['vscode', 'VS Code'],
562
- ] as const
563
- ).map(([key, label]) => (
564
- <Button
565
- key={key}
566
- type="button"
567
- size="sm"
568
- variant={activeClient === key ? 'secondary' : 'ghost'}
569
- className="h-7 text-xs"
570
- onClick={() => setActiveClient(key)}
571
- >
572
- {label}
573
- </Button>
574
- ))}
575
- </div>
576
-
577
- {activeClient === 'claude-code' && (
578
- <div className="space-y-3">
579
- <Snippet code={claudeCodeCli} title="One-line CLI setup" />
580
- <Snippet
581
- code={snippets.claudeCode}
582
- title="…or add to .mcp.json in your project root"
583
- />
584
- <p className="text-[11px] text-muted-foreground">
585
- The <span className="font-mono">type</span> field is required — Claude Code skips a
586
- server entry that has a <span className="font-mono">url</span> but no{' '}
587
- <span className="font-mono">type</span>.
588
- </p>
589
- </div>
590
- )}
591
-
592
- {activeClient === 'claude-desktop' && (
593
- <div className="space-y-3">
594
- <Alert>
595
- <AlertTriangle className="h-4 w-4" />
596
- <AlertTitle>Two options, and the easy one has a catch</AlertTitle>
597
- <AlertDescription className="text-xs">
598
- Settings &rarr; Connectors &rarr; Add custom connector accepts{' '}
599
- <span className="font-mono">{url}</span> directly (type{' '}
600
- <span className="font-mono">Bearer YOUR_TOKEN</span>, including the space, in the
601
- auth field) — but custom connectors dial out from Anthropic&rsquo;s cloud, so a
602
- localhost or firewalled site will not connect that way. Use the config below
603
- instead in that case; it bridges over stdio from your own machine.
604
- </AlertDescription>
605
- </Alert>
606
- <Snippet code={snippets.claudeDesktop} title="claude_desktop_config.json" />
607
- </div>
608
- )}
609
-
610
- {activeClient === 'cursor' && (
611
- <Snippet code={snippets.cursor} title=".cursor/mcp.json" />
612
- )}
613
-
614
- {activeClient === 'vscode' && (
615
- <div className="space-y-3">
616
- <Snippet code={snippets.vscode} title=".vscode/mcp.json" />
617
- <p className="text-[11px] text-muted-foreground">
618
- VS Code uses <span className="font-mono">servers</span> at the top level, not{' '}
619
- <span className="font-mono">mcpServers</span>, and prompts for the token rather than
620
- storing it in the file.
621
- </p>
622
- </div>
623
- )}
624
- </div>
625
- </CardContent>
626
- </Card>
627
- );
628
- }
1
+ 'use client';
2
+
3
+ import React, { useMemo, useState, useTransition } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ Alert,
7
+ AlertDescription,
8
+ AlertTitle,
9
+ Badge,
10
+ Button,
11
+ Card,
12
+ CardContent,
13
+ CardDescription,
14
+ CardHeader,
15
+ CardTitle,
16
+ Checkbox,
17
+ Input,
18
+ Label,
19
+ } from '@nextblock-cms/ui';
20
+ import {
21
+ AlertTriangle,
22
+ Check,
23
+ Copy,
24
+ Info,
25
+ KeyRound,
26
+ Lock,
27
+ Plug,
28
+ Plus,
29
+ Trash2,
30
+ } from 'lucide-react';
31
+
32
+ import {
33
+ createMcpAccessTokenAction,
34
+ revokeMcpAccessTokenAction,
35
+ saveMcpSettingsAction,
36
+ type McpAccessTokenSummary,
37
+ } from './mcp-actions';
38
+
39
+ type McpScope = 'read' | 'write';
40
+
41
+ type McpServerSettingsCardProps = {
42
+ allowLocalhostWithoutToken: boolean;
43
+ enabled: boolean;
44
+ /** Loopback URL for a locally-running dev server. */
45
+ localMcpUrl: string;
46
+ /** The publicly reachable endpoint, derived from NEXT_PUBLIC_URL. */
47
+ mcpUrl: string;
48
+ /**
49
+ * Locks every control that writes (the toggles, minting, revoking) while leaving
50
+ * the endpoint, the client snippets, and the copy buttons fully usable.
51
+ *
52
+ * Used by the shared sandbox: a visitor must still be able to see that MCP access
53
+ * exists and what connecting looks like, but the settings row and the token table
54
+ * belong to every other visitor too. Hiding the card instead — which is what the
55
+ * sandbox did before this — taught evaluators that NextBlock had no MCP support.
56
+ */
57
+ readOnly?: boolean;
58
+ /** Explains the lock. Required in spirit whenever `readOnly` is set. */
59
+ readOnlyNotice?: string;
60
+ tokens: McpAccessTokenSummary[];
61
+ };
62
+
63
+ const TOKEN_PLACEHOLDER = 'YOUR_TOKEN';
64
+
65
+ function CopyButton({ label = 'Copy', value }: { label?: string; value: string }) {
66
+ const [copied, setCopied] = useState(false);
67
+
68
+ return (
69
+ <Button
70
+ type="button"
71
+ variant="ghost"
72
+ size="sm"
73
+ className="h-7 shrink-0"
74
+ onClick={() => {
75
+ void navigator.clipboard.writeText(value).then(() => {
76
+ setCopied(true);
77
+ setTimeout(() => setCopied(false), 1800);
78
+ });
79
+ }}
80
+ >
81
+ {copied ? <Check className="mr-1.5 h-3.5 w-3.5" /> : <Copy className="mr-1.5 h-3.5 w-3.5" />}
82
+ {copied ? 'Copied' : label}
83
+ </Button>
84
+ );
85
+ }
86
+
87
+ function Snippet({ code, title }: { code: string; title: string }) {
88
+ return (
89
+ <div className="space-y-1.5">
90
+ <div className="flex items-center justify-between gap-2">
91
+ <p className="text-xs font-medium">{title}</p>
92
+ <CopyButton value={code} />
93
+ </div>
94
+ <pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 text-[11px] leading-relaxed">
95
+ <code>{code}</code>
96
+ </pre>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ function formatDate(value: string | null) {
102
+ if (!value) return null;
103
+ return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).format(
104
+ new Date(value)
105
+ );
106
+ }
107
+
108
+ export function McpServerSettingsCard({
109
+ allowLocalhostWithoutToken,
110
+ enabled,
111
+ localMcpUrl,
112
+ mcpUrl,
113
+ readOnly = false,
114
+ readOnlyNotice,
115
+ tokens,
116
+ }: McpServerSettingsCardProps) {
117
+ const router = useRouter();
118
+ const [isPending, startTransition] = useTransition();
119
+
120
+ const [isEnabled, setIsEnabled] = useState(enabled);
121
+ const [allowLocalhost, setAllowLocalhost] = useState(allowLocalhostWithoutToken);
122
+ const [tokenName, setTokenName] = useState('');
123
+ const [allowWrites, setAllowWrites] = useState(true);
124
+ const [expiresInDays, setExpiresInDays] = useState('');
125
+ const [mintedToken, setMintedToken] = useState<string | null>(null);
126
+ const [error, setError] = useState<string | null>(null);
127
+ const [activeClient, setActiveClient] = useState<'claude-code' | 'claude-desktop' | 'cursor' | 'vscode'>(
128
+ 'claude-code'
129
+ );
130
+ const [useLocalUrl, setUseLocalUrl] = useState(false);
131
+
132
+ const url = useLocalUrl ? localMcpUrl : mcpUrl;
133
+
134
+ // Once a token has been minted in this session, bake it into the snippets so the
135
+ // admin can copy a config that actually works instead of hand-substituting.
136
+ const tokenForSnippet = mintedToken ?? TOKEN_PLACEHOLDER;
137
+
138
+ /**
139
+ * Whether the generated snippets should carry an Authorization header at all.
140
+ *
141
+ * A loopback connection covered by localhost trust needs none — and must not send
142
+ * one: the route verifies any bearer it receives and rejects an invalid value
143
+ * outright rather than falling back to localhost trust, so pasting the literal
144
+ * placeholder would 401 instead of silently working. Once a real token exists we
145
+ * include it either way, since it works on both origins.
146
+ */
147
+ const usesLocalhostTrust = useLocalUrl && allowLocalhost && !mintedToken;
148
+
149
+ const snippets = useMemo(() => {
150
+ const authHeader = usesLocalhostTrust
151
+ ? undefined
152
+ : { Authorization: `Bearer ${tokenForSnippet}` };
153
+
154
+ const claudeCode = JSON.stringify(
155
+ {
156
+ mcpServers: {
157
+ nextblock: {
158
+ ...(authHeader ? { headers: authHeader } : {}),
159
+ type: 'http',
160
+ url,
161
+ },
162
+ },
163
+ },
164
+ null,
165
+ 2
166
+ );
167
+
168
+ const cursor = JSON.stringify(
169
+ {
170
+ mcpServers: {
171
+ nextblock: {
172
+ ...(authHeader ? { headers: authHeader } : {}),
173
+ url,
174
+ },
175
+ },
176
+ },
177
+ null,
178
+ 2
179
+ );
180
+
181
+ const vscode = JSON.stringify(
182
+ usesLocalhostTrust
183
+ ? { servers: { nextblock: { type: 'http', url } } }
184
+ : {
185
+ inputs: [
186
+ {
187
+ description: 'NextBlock MCP access token',
188
+ id: 'nextblockToken',
189
+ password: true,
190
+ type: 'promptString',
191
+ },
192
+ ],
193
+ servers: {
194
+ nextblock: {
195
+ headers: { Authorization: 'Bearer ${input:nextblockToken}' },
196
+ type: 'http',
197
+ url,
198
+ },
199
+ },
200
+ },
201
+ null,
202
+ 2
203
+ );
204
+
205
+ const claudeDesktop = JSON.stringify(
206
+ {
207
+ mcpServers: {
208
+ nextblock: {
209
+ args: [
210
+ '-y',
211
+ 'mcp-remote',
212
+ url,
213
+ ...(usesLocalhostTrust
214
+ ? []
215
+ : ['--header', `Authorization: Bearer ${tokenForSnippet}`]),
216
+ ],
217
+ command: 'npx',
218
+ },
219
+ },
220
+ },
221
+ null,
222
+ 2
223
+ );
224
+
225
+ return { claudeCode, claudeDesktop, cursor, vscode };
226
+ }, [tokenForSnippet, url, usesLocalhostTrust]);
227
+
228
+ const claudeCodeCli = usesLocalhostTrust
229
+ ? `claude mcp add --transport http nextblock ${url}`
230
+ : `claude mcp add --transport http nextblock ${url} --header "Authorization: Bearer ${tokenForSnippet}"`;
231
+
232
+ // Belt-and-braces: the controls below are disabled in read-only mode, and the
233
+ // server actions refuse sandbox writes on their own. These early returns just
234
+ // mean a stray call from here can never even reach the network.
235
+ function persistSettings(next: { allowLocalhostWithoutToken: boolean; enabled: boolean }) {
236
+ if (readOnly) return;
237
+
238
+ setError(null);
239
+ startTransition(async () => {
240
+ const result = await saveMcpSettingsAction(next);
241
+
242
+ if (!result.success) {
243
+ setError(result.error ?? 'Failed to save MCP settings.');
244
+ // Snap the optimistic toggle back so the UI never claims a state the DB rejected.
245
+ setIsEnabled(enabled);
246
+ setAllowLocalhost(allowLocalhostWithoutToken);
247
+ return;
248
+ }
249
+
250
+ router.refresh();
251
+ });
252
+ }
253
+
254
+ function handleCreateToken() {
255
+ if (readOnly) return;
256
+
257
+ setError(null);
258
+ setMintedToken(null);
259
+
260
+ startTransition(async () => {
261
+ const parsedDays = Number.parseInt(expiresInDays, 10);
262
+ const result = await createMcpAccessTokenAction({
263
+ expiresInDays: Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : null,
264
+ name: tokenName,
265
+ scopes: allowWrites ? (['read', 'write'] as McpScope[]) : (['read'] as McpScope[]),
266
+ });
267
+
268
+ if (!result.success || !result.token) {
269
+ setError(result.error ?? 'Failed to create the token.');
270
+ return;
271
+ }
272
+
273
+ setMintedToken(result.token);
274
+ setTokenName('');
275
+ setExpiresInDays('');
276
+ router.refresh();
277
+ });
278
+ }
279
+
280
+ function handleRevoke(id: string) {
281
+ if (readOnly) return;
282
+
283
+ setError(null);
284
+ startTransition(async () => {
285
+ const result = await revokeMcpAccessTokenAction({ id });
286
+
287
+ if (!result.success) {
288
+ setError(result.error ?? 'Failed to revoke the token.');
289
+ return;
290
+ }
291
+
292
+ router.refresh();
293
+ });
294
+ }
295
+
296
+ return (
297
+ <Card>
298
+ <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
299
+ <div>
300
+ <CardTitle className="flex flex-wrap items-center gap-2 text-base">
301
+ <Plug className="h-4 w-4" />
302
+ MCP server access
303
+ <Badge variant={isEnabled ? 'default' : 'outline'} className="ml-0.5 font-normal">
304
+ {isEnabled ? 'Enabled' : 'Disabled'}
305
+ </Badge>
306
+ {isEnabled && tokens.length > 0 && (
307
+ <Badge variant="secondary" className="font-normal">
308
+ {tokens.length} active {tokens.length === 1 ? 'token' : 'tokens'}
309
+ </Badge>
310
+ )}
311
+ {readOnly && (
312
+ <Badge variant="outline" className="gap-1 font-normal">
313
+ <Lock className="h-3 w-3" />
314
+ Read-only
315
+ </Badge>
316
+ )}
317
+ </CardTitle>
318
+ <CardDescription className="text-xs">
319
+ Expose this CMS to external AI clients over the Model Context Protocol. Claude Code,
320
+ Claude Desktop, Cursor, and VS Code get the same typed Cortex tools the dashboard agent
321
+ uses — building pages, editing blocks, querying analytics — from inside your editor.
322
+ </CardDescription>
323
+ </div>
324
+ </CardHeader>
325
+
326
+ <CardContent className="space-y-4 pt-0">
327
+ {readOnly && readOnlyNotice && (
328
+ <Alert>
329
+ <Info className="h-4 w-4" />
330
+ <AlertTitle>Configured per install</AlertTitle>
331
+ <AlertDescription className="text-xs">{readOnlyNotice}</AlertDescription>
332
+ </Alert>
333
+ )}
334
+
335
+ {error && (
336
+ <Alert variant="destructive">
337
+ <AlertTriangle className="h-4 w-4" />
338
+ <AlertTitle>Could not save</AlertTitle>
339
+ <AlertDescription className="text-xs">{error}</AlertDescription>
340
+ </Alert>
341
+ )}
342
+
343
+ {/* Toggles */}
344
+ <div className="space-y-3 rounded-md border bg-muted/20 p-3">
345
+ <div className="flex items-start gap-2.5">
346
+ <Checkbox
347
+ id="mcp_enabled"
348
+ checked={isEnabled}
349
+ disabled={isPending || readOnly}
350
+ onCheckedChange={(checked) => {
351
+ const next = checked === true;
352
+ setIsEnabled(next);
353
+ persistSettings({ allowLocalhostWithoutToken: allowLocalhost, enabled: next });
354
+ }}
355
+ />
356
+ <div className="space-y-0.5">
357
+ <Label htmlFor="mcp_enabled" className="text-sm font-medium">
358
+ Enable the MCP server
359
+ </Label>
360
+ <p className="text-xs text-muted-foreground">
361
+ While off, <span className="font-mono">/api/mcp</span> rejects every request. Off by
362
+ default because this is a remote write surface onto your live content.
363
+ </p>
364
+ </div>
365
+ </div>
366
+
367
+ <div className="flex items-start gap-2.5">
368
+ <Checkbox
369
+ id="mcp_allow_localhost"
370
+ checked={allowLocalhost}
371
+ disabled={isPending || !isEnabled || readOnly}
372
+ onCheckedChange={(checked) => {
373
+ const next = checked === true;
374
+ setAllowLocalhost(next);
375
+ persistSettings({ allowLocalhostWithoutToken: next, enabled: isEnabled });
376
+ }}
377
+ />
378
+ <div className="space-y-0.5">
379
+ <Label htmlFor="mcp_allow_localhost" className="text-sm font-medium">
380
+ Trust localhost without a token
381
+ </Label>
382
+ <p className="text-xs text-muted-foreground">
383
+ Convenience for local development only — it is ignored whenever{' '}
384
+ <span className="font-mono">NODE_ENV=production</span>, so your deployed site always
385
+ requires a token.
386
+ </p>
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ {/* Endpoint */}
392
+ <div className="space-y-1.5">
393
+ <Label className="text-xs">Endpoint URL</Label>
394
+ <div className="flex items-center gap-2">
395
+ <Input readOnly value={url} className="font-mono text-xs" />
396
+ <CopyButton value={url} />
397
+ </div>
398
+ <div className="flex gap-1.5 pt-0.5">
399
+ <Button
400
+ type="button"
401
+ size="sm"
402
+ variant={useLocalUrl ? 'ghost' : 'secondary'}
403
+ className="h-6 text-[11px]"
404
+ onClick={() => setUseLocalUrl(false)}
405
+ >
406
+ Live site
407
+ </Button>
408
+ <Button
409
+ type="button"
410
+ size="sm"
411
+ variant={useLocalUrl ? 'secondary' : 'ghost'}
412
+ className="h-6 text-[11px]"
413
+ onClick={() => setUseLocalUrl(true)}
414
+ >
415
+ Localhost
416
+ </Button>
417
+ </div>
418
+ </div>
419
+
420
+ {/* Token minting */}
421
+ <div className="space-y-3 rounded-md border p-3">
422
+ <div className="space-y-0.5">
423
+ <p className="text-sm font-medium">Access tokens</p>
424
+ <p className="text-xs text-muted-foreground">
425
+ Only the token&rsquo;s hash is stored, so it is shown once and cannot be recovered
426
+ afterwards. A read-only token cannot even list the mutating tools.
427
+ </p>
428
+ </div>
429
+
430
+ <div className="flex flex-wrap items-end gap-2">
431
+ <div className="min-w-[180px] flex-1 space-y-1.5">
432
+ <Label htmlFor="mcp_token_name" className="text-xs">
433
+ Name
434
+ </Label>
435
+ <Input
436
+ id="mcp_token_name"
437
+ value={tokenName}
438
+ maxLength={80}
439
+ disabled={readOnly}
440
+ placeholder="My laptop — Claude Code"
441
+ onChange={(event) => setTokenName(event.target.value)}
442
+ />
443
+ </div>
444
+ <div className="w-28 space-y-1.5">
445
+ <Label htmlFor="mcp_token_expiry" className="text-xs">
446
+ Expires (days)
447
+ </Label>
448
+ <Input
449
+ id="mcp_token_expiry"
450
+ type="number"
451
+ min={1}
452
+ max={3650}
453
+ value={expiresInDays}
454
+ disabled={readOnly}
455
+ placeholder="Never"
456
+ onChange={(event) => setExpiresInDays(event.target.value)}
457
+ />
458
+ </div>
459
+ <div className="flex h-9 items-center gap-2">
460
+ <Checkbox
461
+ id="mcp_token_write"
462
+ checked={allowWrites}
463
+ disabled={readOnly}
464
+ onCheckedChange={(checked) => setAllowWrites(checked === true)}
465
+ />
466
+ <Label htmlFor="mcp_token_write" className="text-xs">
467
+ Allow writes
468
+ </Label>
469
+ </div>
470
+ <Button
471
+ type="button"
472
+ size="sm"
473
+ disabled={isPending || readOnly || !tokenName.trim()}
474
+ onClick={handleCreateToken}
475
+ >
476
+ <Plus className="mr-1.5 h-3.5 w-3.5" />
477
+ Create
478
+ </Button>
479
+ </div>
480
+
481
+ {mintedToken && (
482
+ <Alert>
483
+ <KeyRound className="h-4 w-4" />
484
+ <AlertTitle>Copy this token now</AlertTitle>
485
+ <AlertDescription className="space-y-2">
486
+ <p className="text-xs">
487
+ This is the only time it will be shown. The snippets below already include it.
488
+ </p>
489
+ <div className="flex items-center gap-2">
490
+ <Input readOnly value={mintedToken} className="font-mono text-xs" />
491
+ <CopyButton value={mintedToken} />
492
+ </div>
493
+ </AlertDescription>
494
+ </Alert>
495
+ )}
496
+
497
+ {tokens.length > 0 && (
498
+ <div className="divide-y rounded-md border">
499
+ {tokens.map((token) => {
500
+ const lastUsed = formatDate(token.lastUsedAt);
501
+ const expires = formatDate(token.expiresAt);
502
+
503
+ return (
504
+ <div key={token.id} className="flex items-center justify-between gap-2 p-2.5">
505
+ <div className="min-w-0 space-y-0.5">
506
+ <div className="flex flex-wrap items-center gap-1.5">
507
+ <span className="truncate text-sm font-medium">{token.name}</span>
508
+ <Badge
509
+ variant={token.scopes.includes('write') ? 'secondary' : 'outline'}
510
+ className="font-normal"
511
+ >
512
+ {token.scopes.includes('write') ? 'read + write' : 'read only'}
513
+ </Badge>
514
+ </div>
515
+ <p className="text-[11px] text-muted-foreground">
516
+ <span className="font-mono">{token.tokenPrefix}…</span>
517
+ {' · '}
518
+ {lastUsed ? `last used ${lastUsed}` : 'never used'}
519
+ {expires ? ` · expires ${expires}` : ''}
520
+ </p>
521
+ </div>
522
+ <Button
523
+ type="button"
524
+ variant="ghost"
525
+ size="sm"
526
+ disabled={isPending || readOnly}
527
+ className="h-7 shrink-0 text-destructive hover:text-destructive"
528
+ onClick={() => handleRevoke(token.id)}
529
+ >
530
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
531
+ Revoke
532
+ </Button>
533
+ </div>
534
+ );
535
+ })}
536
+ </div>
537
+ )}
538
+ </div>
539
+
540
+ {/* Client configuration */}
541
+ <div className="space-y-3 rounded-md border p-3">
542
+ <div className="space-y-0.5">
543
+ <p className="text-sm font-medium">Connect a client</p>
544
+ <p className="text-xs text-muted-foreground">
545
+ {usesLocalhostTrust
546
+ ? 'No token needed: localhost trust covers this connection while the dev server runs. These snippets deliberately send no Authorization header — an invalid one would be rejected rather than falling back to localhost trust.'
547
+ : mintedToken
548
+ ? 'These snippets include the token you just created.'
549
+ : readOnly
550
+ ? `This is the exact configuration you would paste on your own install, with ${TOKEN_PLACEHOLDER} standing in for the token you mint there.`
551
+ : `Create a token above and these snippets will fill it in; otherwise replace ${TOKEN_PLACEHOLDER}.`}
552
+ </p>
553
+ </div>
554
+
555
+ <div className="flex flex-wrap gap-1.5">
556
+ {(
557
+ [
558
+ ['claude-code', 'Claude Code'],
559
+ ['claude-desktop', 'Claude Desktop'],
560
+ ['cursor', 'Cursor'],
561
+ ['vscode', 'VS Code'],
562
+ ] as const
563
+ ).map(([key, label]) => (
564
+ <Button
565
+ key={key}
566
+ type="button"
567
+ size="sm"
568
+ variant={activeClient === key ? 'secondary' : 'ghost'}
569
+ className="h-7 text-xs"
570
+ onClick={() => setActiveClient(key)}
571
+ >
572
+ {label}
573
+ </Button>
574
+ ))}
575
+ </div>
576
+
577
+ {activeClient === 'claude-code' && (
578
+ <div className="space-y-3">
579
+ <Snippet code={claudeCodeCli} title="One-line CLI setup" />
580
+ <Snippet
581
+ code={snippets.claudeCode}
582
+ title="…or add to .mcp.json in your project root"
583
+ />
584
+ <p className="text-[11px] text-muted-foreground">
585
+ The <span className="font-mono">type</span> field is required — Claude Code skips a
586
+ server entry that has a <span className="font-mono">url</span> but no{' '}
587
+ <span className="font-mono">type</span>.
588
+ </p>
589
+ </div>
590
+ )}
591
+
592
+ {activeClient === 'claude-desktop' && (
593
+ <div className="space-y-3">
594
+ <Alert>
595
+ <AlertTriangle className="h-4 w-4" />
596
+ <AlertTitle>Two options, and the easy one has a catch</AlertTitle>
597
+ <AlertDescription className="text-xs">
598
+ Settings &rarr; Connectors &rarr; Add custom connector accepts{' '}
599
+ <span className="font-mono">{url}</span> directly (type{' '}
600
+ <span className="font-mono">Bearer YOUR_TOKEN</span>, including the space, in the
601
+ auth field) — but custom connectors dial out from Anthropic&rsquo;s cloud, so a
602
+ localhost or firewalled site will not connect that way. Use the config below
603
+ instead in that case; it bridges over stdio from your own machine.
604
+ </AlertDescription>
605
+ </Alert>
606
+ <Snippet code={snippets.claudeDesktop} title="claude_desktop_config.json" />
607
+ </div>
608
+ )}
609
+
610
+ {activeClient === 'cursor' && (
611
+ <Snippet code={snippets.cursor} title=".cursor/mcp.json" />
612
+ )}
613
+
614
+ {activeClient === 'vscode' && (
615
+ <div className="space-y-3">
616
+ <Snippet code={snippets.vscode} title=".vscode/mcp.json" />
617
+ <p className="text-[11px] text-muted-foreground">
618
+ VS Code uses <span className="font-mono">servers</span> at the top level, not{' '}
619
+ <span className="font-mono">mcpServers</span>, and prompts for the token rather than
620
+ storing it in the file.
621
+ </p>
622
+ </div>
623
+ )}
624
+ </div>
625
+ </CardContent>
626
+ </Card>
627
+ );
628
+ }