create-nextblock 0.16.1 → 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 (258) 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/sandboxResetSql.ts +3092 -3092
  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 -558
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  42. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  43. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  44. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  45. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  46. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  47. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  48. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  49. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  50. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  51. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  52. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  53. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  54. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  55. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  57. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  58. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  59. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  60. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  61. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  62. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  63. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  64. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  65. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  66. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  67. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  68. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  69. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  70. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  71. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
  72. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  73. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  74. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  75. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  76. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  77. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  78. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  79. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  80. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  81. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  82. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  83. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  84. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  85. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  86. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  87. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  88. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  92. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  93. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  94. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  95. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  96. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  97. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  98. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  99. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  100. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  101. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  102. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  103. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  104. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  105. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  106. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  107. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  108. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  109. package/templates/nextblock-template/app/layout.tsx +671 -671
  110. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  111. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  112. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  113. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  114. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  115. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  116. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  117. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  118. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  119. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  120. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  121. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  122. package/templates/nextblock-template/app/providers.tsx +96 -96
  123. package/templates/nextblock-template/app/robots.ts +123 -123
  124. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  125. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  126. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  127. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  128. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  129. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  130. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  131. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  132. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  133. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  134. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  135. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  136. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  137. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  138. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  139. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  140. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  141. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  142. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  143. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  144. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  145. package/templates/nextblock-template/components/Header.tsx +38 -38
  146. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  147. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  148. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  149. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  150. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  151. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  152. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  153. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  154. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  155. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  156. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  157. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  158. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  159. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  160. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  161. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  162. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  163. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  164. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  165. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  166. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  167. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  168. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  169. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  170. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  171. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  172. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  173. package/templates/nextblock-template/components/form-message.tsx +32 -32
  174. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  175. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
  176. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  177. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  178. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  179. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  180. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  181. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  182. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  183. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  184. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  185. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  186. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  187. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  188. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  189. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  190. package/templates/nextblock-template/docs/README.md +42 -42
  191. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  192. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  193. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  194. package/templates/nextblock-template/index.d.ts +7 -7
  195. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  196. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  197. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  198. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  199. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  200. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  201. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  202. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  203. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  204. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  205. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  206. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  207. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  208. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  209. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  210. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  211. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  212. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  213. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  214. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  217. package/templates/nextblock-template/lib/search/types.ts +27 -27
  218. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  219. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  220. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  221. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  222. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  223. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
  224. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
  225. package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
  226. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  227. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  228. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  229. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  230. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  231. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  232. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
  233. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  234. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  235. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  236. package/templates/nextblock-template/lib/site-url.ts +102 -102
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  240. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  241. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  242. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  243. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  244. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  245. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  246. package/templates/nextblock-template/next-env.d.ts +1 -2
  247. package/templates/nextblock-template/package.json +1 -1
  248. package/templates/nextblock-template/postcss.config.js +6 -6
  249. package/templates/nextblock-template/scripts/backup.js +115 -115
  250. package/templates/nextblock-template/scripts/restore.js +385 -385
  251. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  252. package/templates/nextblock-template/tailwind.config.js +25 -25
  253. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  254. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  255. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  256. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  257. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  258. 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
+ }