11/**
2- * Topbar "LINK " button + one-click "Connect to Claude" modal.
2+ * Topbar "Link " button + one-command "Connect to Claude" modal.
33 *
44 * The org's unified MCP endpoint (`/api/<slug>/mcp`) already exposes every
55 * connection enabled in the org — the library filesystem, your agents, and any
6- * MCP tool — behind OAuth 2.1. So "connecting Claude" is just handing Claude
7- * that one URL. This dialog does exactly that with a single primary action:
8- * • Claude Code → copy the `claude mcp add …` one-liner (paste in a terminal)
9- * • Claude Desktop / claude.ai → copy the URL to add as a custom connector
6+ * MCP tool. So "connecting Claude" is just handing Claude that one URL plus a
7+ * credential. The primary path here is designed to "just work" with ZERO
8+ * interactive auth: we mint a scoped API key and embed it in the
9+ * `claude mcp add … --header "Authorization: Bearer <key>"` command, so Claude
10+ * Code connects on the first request — no `/mcp`, no browser login.
1011 *
11- * The full Connect settings page (Cursor, Codex, API keys, key management)
12- * stays reachable via the footer link for power users.
12+ * Claude Desktop / claude.ai can't take a custom header, so those still use the
13+ * URL + OAuth connector flow. The full Connect settings page (Cursor, Codex,
14+ * OAuth, key management) stays reachable via the footer link.
1315 */
1416
1517import { useState } from "react" ;
1618import { Link } from "@tanstack/react-router" ;
1719import { toast } from "sonner" ;
20+ import { Alert , AlertDescription } from "@deco/ui/components/alert.tsx" ;
1821import { Button } from "@deco/ui/components/button.tsx" ;
1922import {
2023 Dialog ,
@@ -26,6 +29,7 @@ import {
2629import { useCopy } from "@deco/ui/hooks/use-copy.ts" ;
2730import { useProjectContext } from "@decocms/mesh-sdk" ;
2831import {
32+ AlertTriangle ,
2933 ArrowRight ,
3034 Check ,
3135 Copy01 ,
@@ -35,22 +39,57 @@ import {
3539 Zap ,
3640} from "@untitledui/icons" ;
3741import { track } from "@/web/lib/posthog-client" ;
38- import { claudeCodeCommand , mcpUrl } from "@/web/components/connect/mcp-url" ;
42+ import {
43+ claudeCodeCommandWithKey ,
44+ mcpUrl ,
45+ } from "@/web/components/connect/mcp-url" ;
46+ import { useCreateApiKey } from "@/web/hooks/use-api-keys" ;
3947
4048const CAPABILITIES = [
4149 "Browse and edit your Library files" ,
4250 "Run your agents" ,
4351 "Enable and call any MCP tool in this org" ,
4452] ;
4553
54+ const KEY_NAME_PREFIX = "Connect: " ;
55+
56+ function hostnameLabel ( ) : string {
57+ if ( typeof window === "undefined" ) return "unknown host" ;
58+ return window . location . hostname ;
59+ }
60+
4661function ConnectDialogBody ( { onClose } : { onClose : ( ) => void } ) {
4762 const { org } = useProjectContext ( ) ;
4863 const url = mcpUrl ( org . slug ) ;
49- const command = claudeCodeCommand ( org . slug ) ;
5064
65+ const createKey = useCreateApiKey ( ) ;
66+ const [ command , setCommand ] = useState < string | null > ( null ) ;
5167 const commandCopy = useCopy ( ) ;
5268 const urlCopy = useCopy ( ) ;
5369
70+ const handleGenerate = ( ) => {
71+ createKey . mutate (
72+ {
73+ name : `${ KEY_NAME_PREFIX } Claude Code on ${ hostnameLabel ( ) } ` ,
74+ permissions : { "*" : [ "*" ] } ,
75+ } ,
76+ {
77+ onSuccess : ( key ) => {
78+ const cmd = claudeCodeCommandWithKey ( org . slug , key . key ) ;
79+ setCommand ( cmd ) ;
80+ track ( "connect_studio_generate" , { target : "claude-code" } ) ;
81+ // Best-effort auto-copy so it's truly one click; the visible copy
82+ // button is the reliable fallback if the browser blocks it.
83+ navigator . clipboard ?. writeText ( cmd ) . then (
84+ ( ) => toast . success ( "Command copied — paste it in your terminal" ) ,
85+ ( ) => toast . success ( "Command ready — copy it below" ) ,
86+ ) ;
87+ } ,
88+ onError : ( err ) => toast . error ( err . message ) ,
89+ } ,
90+ ) ;
91+ } ;
92+
5493 return (
5594 < >
5695 < DialogHeader >
@@ -74,27 +113,57 @@ function ConnectDialogBody({ onClose }: { onClose: () => void }) {
74113 ) ) }
75114 </ ul >
76115
77- { /* Claude Code — the true one-click: copy, paste, done. */ }
116+ { /* Claude Code — one command, no login. We mint a scoped token and embed
117+ it so `claude mcp add` connects on the first request. */ }
78118 < div className = "rounded-lg border border-border p-3 flex flex-col gap-2.5" >
79119 < div className = "flex items-center gap-2 text-sm font-medium" >
80120 < Terminal size = { 15 } className = "text-muted-foreground" />
81121 Claude Code
82122 </ div >
83- < div className = "flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2" >
84- < code className = "text-xs flex-1 truncate font-mono" > { command } </ code >
85- </ div >
86- < Button
87- size = "sm"
88- className = "gap-1.5 self-start"
89- onClick = { ( ) => {
90- commandCopy . handleCopy ( command ) ;
91- track ( "connect_studio_copy" , { target : "claude-code" } ) ;
92- toast . success ( "Command copied — paste it in your terminal" ) ;
93- } }
94- >
95- { commandCopy . copied ? < Check size = { 14 } /> : < Copy01 size = { 14 } /> }
96- { commandCopy . copied ? "Copied" : "Copy connect command" }
97- </ Button >
123+
124+ { command ? (
125+ < >
126+ < div className = "rounded-md border border-border bg-muted/40 px-3 py-2" >
127+ < code className = "text-xs break-all font-mono" > { command } </ code >
128+ </ div >
129+ < Button
130+ size = "sm"
131+ className = "gap-1.5 self-start"
132+ onClick = { ( ) => {
133+ commandCopy . handleCopy ( command ) ;
134+ track ( "connect_studio_copy" , { target : "claude-code" } ) ;
135+ toast . success ( "Command copied — paste it in your terminal" ) ;
136+ } }
137+ >
138+ { commandCopy . copied ? < Check size = { 14 } /> : < Copy01 size = { 14 } /> }
139+ { commandCopy . copied ? "Copied" : "Copy command" }
140+ </ Button >
141+ < Alert variant = "warning" className = "text-xs" >
142+ < AlertTriangle />
143+ < AlertDescription >
144+ Runs with no login step — the command embeds a full-access token
145+ for this org. Treat it like a password; revoke it any time in
146+ Settings → Connect.
147+ </ AlertDescription >
148+ </ Alert >
149+ </ >
150+ ) : (
151+ < >
152+ < p className = "text-xs text-muted-foreground" >
153+ One command, no browser login. We'll mint a scoped access token
154+ and embed it so Claude Code connects instantly.
155+ </ p >
156+ < Button
157+ size = "sm"
158+ className = "gap-1.5 self-start"
159+ disabled = { createKey . isPending }
160+ onClick = { handleGenerate }
161+ >
162+ < Terminal size = { 14 } />
163+ { createKey . isPending ? "Generating…" : "Generate connect command" }
164+ </ Button >
165+ </ >
166+ ) }
98167 </ div >
99168
100169 { /* Claude Desktop / claude.ai — paste the URL as a custom connector. */ }
@@ -158,7 +227,7 @@ export function ConnectLinkButton() {
158227 } }
159228 >
160229 < Link01 size = { 14 } />
161- LINK
230+ Link
162231 </ Button >
163232 < Dialog open = { open } onOpenChange = { setOpen } >
164233 < DialogContent className = "max-w-md gap-4" >
0 commit comments