Skip to content

Commit 81265d8

Browse files
vibeguiclaude
andcommitted
feat(connect-studio): one-command Claude Code connect (no OAuth), Link button left
Addresses the "I don't want to run /mcp and auth" flow. The OAuth path requires an interactive `/mcp` browser login (and was failing with HTTP 400 on reconnect). The modal's primary Claude Code action now mints a scoped full-access API key and embeds it in the command: claude mcp add --transport http --scope user studio <url> \ --header "Authorization: Bearer <key>" Claude Code sends the token on the first request — no /mcp, no browser login, tools live immediately. Best-effort auto-copy on generate with a visible copy button as fallback, plus a warning that the command carries a full-access token (revocable in Settings → Connect). Also per feedback: rename the topbar button "LINK" → "Link" and move it to the left column (next to the sidebar trigger). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent de37714 commit 81265d8

3 files changed

Lines changed: 106 additions & 32 deletions

File tree

apps/mesh/src/web/components/connect/connect-dialog.tsx

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
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

1517
import { useState } from "react";
1618
import { Link } from "@tanstack/react-router";
1719
import { toast } from "sonner";
20+
import { Alert, AlertDescription } from "@deco/ui/components/alert.tsx";
1821
import { Button } from "@deco/ui/components/button.tsx";
1922
import {
2023
Dialog,
@@ -26,6 +29,7 @@ import {
2629
import { useCopy } from "@deco/ui/hooks/use-copy.ts";
2730
import { useProjectContext } from "@decocms/mesh-sdk";
2831
import {
32+
AlertTriangle,
2933
ArrowRight,
3034
Check,
3135
Copy01,
@@ -35,22 +39,57 @@ import {
3539
Zap,
3640
} from "@untitledui/icons";
3741
import { 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

4048
const 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+
4661
function 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">

apps/mesh/src/web/components/connect/mcp-url.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ export function mcpUrl(orgSlug: string): string {
1717
}
1818

1919
/**
20-
* One-liner that adds this org to Claude Code over OAuth. Pasting it into a
21-
* terminal is the closest thing to a one-click "connect to Claude" — the
22-
* browser opens on first use to sign in, then every tool in the org is live.
20+
* One-liner that adds this org to Claude Code with a pre-minted bearer token
21+
* baked in as an `Authorization` header. Unlike the OAuth variant this needs
22+
* NO `/mcp` step and NO browser login — Claude Code sends the token on the
23+
* first request and every tool is live immediately. The token is a real
24+
* credential, so this command should be treated like a password.
2325
*/
24-
export function claudeCodeCommand(orgSlug: string): string {
25-
return `claude mcp add --transport http --scope user ${CONNECT_SERVER_NAME} ${mcpUrl(orgSlug)}`;
26+
export function claudeCodeCommandWithKey(
27+
orgSlug: string,
28+
apiKey: string,
29+
): string {
30+
return `claude mcp add --transport http --scope user ${CONNECT_SERVER_NAME} ${mcpUrl(orgSlug)} --header "Authorization: Bearer ${apiKey}"`;
2631
}

apps/mesh/src/web/layouts/org-shell-layout/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,14 @@ export default function OrgShellLayout() {
7777
<Toolbar.LeftColumn>
7878
<Toolbar.LogoLink />
7979
<SidebarTriggerButton />
80+
<ConnectLinkButton />
8081
<Toolbar.TogglesSlot />
8182
</Toolbar.LeftColumn>
8283
<Toolbar.CenterSlot />
8384
<Toolbar.RightColumn>
8485
<div className="min-w-0 flex-1 overflow-x-auto [scrollbar-width:none] flex justify-end">
8586
<Toolbar.TabsSlot />
8687
</div>
87-
<ConnectLinkButton />
8888
<Toolbar.RightSlot />
8989
</Toolbar.RightColumn>
9090
</Toolbar.Header>

0 commit comments

Comments
 (0)