zudoku 0.82.2 → 0.82.4

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 (39) hide show
  1. package/dist/cli/cli.js +283 -225
  2. package/dist/cli/worker.js +23 -0
  3. package/dist/declarations/app/wrapProtectedRoutes.d.ts +5 -0
  4. package/dist/declarations/config/config.d.ts +3 -0
  5. package/dist/declarations/config/plugin-versions.d.ts +5 -0
  6. package/dist/declarations/config/validators/ZudokuConfig.d.ts +38 -0
  7. package/dist/declarations/lib/authentication/providers/entra.d.ts +13 -0
  8. package/dist/declarations/lib/authentication/providers/openid.d.ts +4 -1
  9. package/dist/declarations/lib/authentication/providers/util.d.ts +4 -0
  10. package/dist/declarations/lib/plugins/openapi/McpClientLogos.d.ts +4 -0
  11. package/dist/declarations/lib/plugins/openapi/mcp-configs.d.ts +3 -0
  12. package/dist/declarations/vite/package-root.d.ts +1 -0
  13. package/dist/flat-config.d.ts +17 -0
  14. package/docs/components/landing-page.mdx +1 -1
  15. package/docs/configuration/authentication-azure-ad.md +17 -15
  16. package/docs/configuration/authentication.md +22 -2
  17. package/docs/guides/mcp-servers.md +26 -0
  18. package/package.json +21 -17
  19. package/src/app/entry.server.tsx +9 -5
  20. package/src/app/wrapProtectedRoutes.ts +25 -0
  21. package/src/config/config.ts +4 -0
  22. package/src/config/loader.ts +15 -0
  23. package/src/config/plugin-versions.ts +38 -0
  24. package/src/config/validators/ZudokuConfig.ts +19 -0
  25. package/src/lib/auth/issuer.ts +7 -1
  26. package/src/lib/authentication/providers/entra.tsx +97 -0
  27. package/src/lib/authentication/providers/openid.tsx +29 -13
  28. package/src/lib/authentication/providers/util.ts +7 -0
  29. package/src/lib/components/navigation/NavigationCategory.tsx +3 -2
  30. package/src/lib/components/navigation/StackRows.tsx +1 -1
  31. package/src/lib/plugins/openapi/MCPEndpoint.tsx +347 -228
  32. package/src/lib/plugins/openapi/McpClientLogos.tsx +72 -0
  33. package/src/lib/plugins/openapi/mcp-configs.ts +55 -0
  34. package/src/vite/api/SchemaManager.ts +11 -25
  35. package/src/vite/plugin-mdx.ts +3 -0
  36. package/src/vite/prerender/prerender.ts +28 -3
  37. package/src/vite/prerender/utils.ts +33 -0
  38. package/src/vite/prerender/worker.ts +8 -1
  39. package/src/zuplo/enrich-with-zuplo-mcp.ts +14 -2
@@ -1,20 +1,20 @@
1
- import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
2
- import { useState } from "react";
1
+ import {
2
+ ArrowUpRightIcon,
3
+ CheckIcon,
4
+ CopyIcon,
5
+ ExternalLinkIcon,
6
+ } from "lucide-react";
7
+ import { type ReactNode, useState } from "react";
3
8
  import { InlineCode } from "../../components/InlineCode.js";
4
9
  import { Typography } from "../../components/Typography.js";
5
10
  import { Button } from "../../ui/Button.js";
6
11
  import { Callout } from "../../ui/Callout.js";
7
12
  import { Card } from "../../ui/Card.js";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "../../ui/Select.js";
15
13
  import { SyntaxHighlight } from "../../ui/SyntaxHighlight.js";
16
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/Tabs.js";
14
+ import { ToggleGroup, ToggleGroupItem } from "../../ui/ToggleGroup.js";
15
+ import { cn } from "../../util/cn.js";
17
16
  import {
17
+ CLAUDE_CONNECTORS_URL,
18
18
  type McpApp,
19
19
  type McpServerData,
20
20
  getAuthHeader,
@@ -23,30 +23,62 @@ import {
23
23
  getCodexCliCommand,
24
24
  getCodexConfig,
25
25
  getCursorConfig,
26
+ getCursorDeepLink,
26
27
  getGenericConfig,
27
28
  getMcpServerName,
28
29
  getMcpUrl,
29
30
  getVisibleApps,
30
31
  getVscodeConfig,
32
+ getVscodeDeepLink,
31
33
  } from "./mcp-configs.js";
34
+ import { McpClientLogo } from "./McpClientLogos.js";
32
35
 
33
- const SubAppSection = ({
34
- label,
35
- showLabel,
36
+ const DocsLink = ({
37
+ href,
36
38
  children,
37
39
  }: {
38
- label: string;
39
- showLabel: boolean;
40
- children: React.ReactNode;
41
- }) =>
42
- showLabel ? (
43
- <div className="space-y-3">
44
- <h4 className="text-sm font-semibold">{label}</h4>
40
+ href: string;
41
+ children: ReactNode;
42
+ }) => (
43
+ <a
44
+ href={href}
45
+ target="_blank"
46
+ rel="noopener noreferrer"
47
+ className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
48
+ >
49
+ {children}
50
+ <ExternalLinkIcon className="h-3 w-3" aria-hidden="true" />
51
+ </a>
52
+ );
53
+
54
+ // Wraps a list of <li> steps in the shared `.stepper` visual (numbered bullets
55
+ // with a connecting line, see main.css).
56
+ const Steps = ({ children }: { children: ReactNode }) => (
57
+ <div className="stepper">
58
+ <ol>{children}</ol>
59
+ </div>
60
+ );
61
+
62
+ // One-click deep link rendered as a primary button. Protocol-handler links
63
+ // (cursor://, vscode:) open in place; external https links open a new tab.
64
+ const InstallButton = ({
65
+ href,
66
+ external,
67
+ children,
68
+ }: {
69
+ href: string;
70
+ external?: boolean;
71
+ children: ReactNode;
72
+ }) => (
73
+ <Button asChild size="sm" className="not-prose gap-1.5">
74
+ <a
75
+ href={href}
76
+ {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
77
+ >
45
78
  {children}
46
- </div>
47
- ) : (
48
- <div className="space-y-3">{children}</div>
49
- );
79
+ </a>
80
+ </Button>
81
+ );
50
82
 
51
83
  export const MCPEndpoint = ({
52
84
  serverUrl,
@@ -72,9 +104,29 @@ export const MCPEndpoint = ({
72
104
  const codexConfig = getCodexConfig(name, mcpUrl, auth);
73
105
  const genericConfig = getGenericConfig(name, mcpUrl, auth);
74
106
  const vscodeConfig = getVscodeConfig(name, mcpUrl, auth);
107
+ const cursorDeepLink = getCursorDeepLink(name, mcpUrl, auth);
108
+ const vscodeDeepLink = getVscodeDeepLink(name, mcpUrl, auth);
109
+
110
+ const [selectedAppId, setSelectedAppId] = useState(
111
+ visibleApps[0]?.id ?? "generic",
112
+ );
113
+ const selectedApp =
114
+ visibleApps.find((app) => app.id === selectedAppId) ?? visibleApps[0];
115
+
116
+ const [selectedSubAppId, setSelectedSubAppId] = useState<string | undefined>(
117
+ selectedApp?.subApps[0]?.id,
118
+ );
119
+
120
+ const selectApp = (id: string) => {
121
+ setSelectedAppId(id);
122
+ const next = visibleApps.find((app) => app.id === id);
123
+ setSelectedSubAppId(next?.subApps[0]?.id);
124
+ };
75
125
 
76
- const defaultTab = visibleApps[0]?.id ?? "generic";
77
- const [activeTab, setActiveTab] = useState(defaultTab);
126
+ // Keep the active sub-app consistent with the selected app.
127
+ const activeSubApp =
128
+ selectedApp?.subApps.find((sub) => sub.id === selectedSubAppId) ??
129
+ selectedApp?.subApps[0];
78
130
 
79
131
  const handleCopy = () => {
80
132
  void navigator.clipboard.writeText(mcpUrl).then(() => {
@@ -83,55 +135,75 @@ export const MCPEndpoint = ({
83
135
  });
84
136
  };
85
137
 
86
- const hasSubApp = (app: McpApp, subAppId: string) =>
87
- app.subApps.some((s) => s.id === subAppId);
88
-
89
- const renderAppContent = (app: McpApp) => {
90
- const multiSub = app.subApps.length > 1;
138
+ const renderSetup = (app: McpApp) => {
139
+ const subAppId = activeSubApp?.id ?? app.subApps[0]?.id;
91
140
 
92
141
  switch (app.id) {
93
142
  case "claude":
94
- return (
95
- <div className="space-y-4">
96
- {hasSubApp(app, "claude-desktop") && (
97
- <SubAppSection label="Claude Desktop" showLabel={multiSub}>
98
- <ol>
99
- <li>
100
- Open Claude Desktop and navigate to{" "}
101
- <strong>Settings</strong>
102
- </li>
143
+ if (subAppId === "claude-code") {
144
+ return (
145
+ <>
146
+ <Steps>
147
+ <li>
148
+ <p>Add {name} to Claude Code by running:</p>
149
+ <SyntaxHighlight
150
+ showLanguageIndicator
151
+ title="Terminal"
152
+ language="bash"
153
+ code={claudeCodeCommand}
154
+ />
155
+ </li>
156
+ {auth && (
103
157
  <li>
104
- Go to <strong>Connectors</strong> →{" "}
105
- <strong>Add custom connector</strong> and paste the MCP URL
158
+ <p>
159
+ Replace <InlineCode>{auth.placeholder}</InlineCode> with
160
+ your API key.
161
+ </p>
106
162
  </li>
107
- <li>Save and the server will appear in your conversations</li>
108
- </ol>
109
- <a
110
- href="https://modelcontextprotocol.io/quickstart/user"
111
- target="_blank"
112
- rel="noopener noreferrer"
113
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
114
- >
115
- View official docs
116
- <ExternalLinkIcon className="h-3 w-3" />
117
- </a>
118
- </SubAppSection>
119
- )}
120
- {hasSubApp(app, "claude-code") && (
121
- <SubAppSection label="Claude Code CLI" showLabel={multiSub}>
122
- <p className="text-xs text-muted-foreground">
123
- Add it to Claude Code CLI by running:
163
+ )}
164
+ <li>
165
+ <p>
166
+ Restart Claude Code — the {name} tools are now available.
167
+ </p>
168
+ </li>
169
+ </Steps>
170
+ <DocsLink href="https://docs.anthropic.com/en/docs/claude-code/mcp">
171
+ View official docs
172
+ </DocsLink>
173
+ </>
174
+ );
175
+ }
176
+ return (
177
+ <>
178
+ <Steps>
179
+ <li>
180
+ <p>
181
+ <strong>Copy the {name} URL.</strong> Use the copy button
182
+ above — you'll need it in the next step.
124
183
  </p>
125
- <SyntaxHighlight
126
- showLanguageIndicator
127
- title="Terminal"
128
- language="bash"
129
- code={claudeCodeCommand}
130
- className="mt-2"
131
- />
132
- </SubAppSection>
133
- )}
134
- </div>
184
+ </li>
185
+ <li>
186
+ <p>
187
+ <strong>Open Settings → Connectors.</strong> Add a custom
188
+ connector, name it {name}, and paste the URL.
189
+ </p>
190
+ <InstallButton href={CLAUDE_CONNECTORS_URL} external>
191
+ Open connector settings
192
+ <ArrowUpRightIcon className="size-3.5" aria-hidden="true" />
193
+ </InstallButton>
194
+ </li>
195
+ <li>
196
+ <p>
197
+ <strong>Connect and sign in.</strong> Click{" "}
198
+ <strong>Add → Connect</strong> and sign in — you're all set,
199
+ and the {name} tools appear in your conversations.
200
+ </p>
201
+ </li>
202
+ </Steps>
203
+ <DocsLink href="https://modelcontextprotocol.io/quickstart/user">
204
+ View official docs
205
+ </DocsLink>
206
+ </>
135
207
  );
136
208
 
137
209
  case "chatgpt":
@@ -140,118 +212,124 @@ export const MCPEndpoint = ({
140
212
  <Callout type="note" title="Requirements">
141
213
  ChatGPT Plus, Team, Enterprise, or Edu subscription.
142
214
  </Callout>
143
- <ol>
215
+ <Steps>
144
216
  <li>
145
- Go to <strong>Settings</strong> → <strong>Apps</strong> →{" "}
146
- <strong>Advanced Settings</strong>
217
+ <p>
218
+ <strong>Turn on Developer Mode.</strong> Go to{" "}
219
+ <strong>Settings → Apps → Advanced settings</strong>, enable{" "}
220
+ <strong>Developer Mode</strong>, then click{" "}
221
+ <strong>Create app</strong>.
222
+ </p>
147
223
  </li>
148
224
  <li>
149
- Click <strong>Create app</strong> and fill out the form
225
+ <p>
226
+ <strong>Create the app.</strong> Name it {name} and paste the
227
+ MCP server URL above as the connection.
228
+ </p>
150
229
  </li>
151
230
  <li>
152
- Enter the MCP server URL:
153
- <InlineCode className="ml-2">{mcpUrl}</InlineCode>
231
+ <p>
232
+ <strong>Connect and sign in.</strong> Click{" "}
233
+ <strong>Create</strong> and sign in — you're all set, and the
234
+ app is available in your conversations.
235
+ </p>
154
236
  </li>
155
- <li>Save and the app will be available in your conversations</li>
156
- </ol>
157
- <a
158
- href="https://developers.openai.com/apps-sdk/deploy/connect-chatgpt#create-a-connector"
159
- target="_blank"
160
- rel="noopener noreferrer"
161
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
162
- >
237
+ </Steps>
238
+ <DocsLink href="https://developers.openai.com/apps-sdk/deploy/connect-chatgpt#create-a-connector">
163
239
  View official docs
164
- <ExternalLinkIcon className="h-3 w-3" />
165
- </a>
240
+ </DocsLink>
166
241
  </>
167
242
  );
168
243
 
169
244
  case "codex":
245
+ if (subAppId === "codex-cli") {
246
+ return (
247
+ <>
248
+ <Steps>
249
+ <li>
250
+ <p>Add {name} to Codex CLI by running:</p>
251
+ <SyntaxHighlight
252
+ showLanguageIndicator
253
+ title="Terminal"
254
+ language="bash"
255
+ code={codexCliCommand}
256
+ />
257
+ </li>
258
+ <li>
259
+ <p>
260
+ Or add it to <InlineCode>~/.codex/config.json</InlineCode>:
261
+ </p>
262
+ <SyntaxHighlight
263
+ showLanguageIndicator
264
+ title="config.json"
265
+ language="json"
266
+ code={codexConfig}
267
+ />
268
+ </li>
269
+ </Steps>
270
+ <DocsLink href="https://openai.com/index/introducing-codex/">
271
+ View official docs
272
+ </DocsLink>
273
+ </>
274
+ );
275
+ }
170
276
  return (
171
- <div className="space-y-4">
172
- {hasSubApp(app, "codex-gui") && (
173
- <SubAppSection label="Codex" showLabel={multiSub}>
174
- <ol>
175
- <li>
176
- Open Codex and go to <strong>Settings</strong> →{" "}
177
- <strong>MCP Servers</strong>
178
- </li>
179
- <li>Add a new server and paste the MCP URL</li>
180
- <li>
181
- Save and the server will be available in your sessions
182
- </li>
183
- </ol>
184
- <a
185
- href="https://openai.com/index/introducing-codex/"
186
- target="_blank"
187
- rel="noopener noreferrer"
188
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
189
- >
190
- View official docs
191
- <ExternalLinkIcon className="h-3 w-3" />
192
- </a>
193
- </SubAppSection>
194
- )}
195
- {hasSubApp(app, "codex-cli") && (
196
- <SubAppSection label="Codex CLI" showLabel={multiSub}>
197
- <p className="text-xs text-muted-foreground">
198
- Add it to Codex CLI by running:
199
- </p>
200
- <SyntaxHighlight
201
- showLanguageIndicator
202
- title="Terminal"
203
- language="bash"
204
- code={codexCliCommand}
205
- className="mt-2"
206
- />
207
- <p className="text-xs text-muted-foreground">
208
- Or add to <InlineCode>~/.codex/config.json</InlineCode>:
277
+ <>
278
+ <Steps>
279
+ <li>
280
+ <p>
281
+ Open Codex and go to <strong>Settings → MCP Servers</strong>.
209
282
  </p>
210
- <SyntaxHighlight
211
- showLanguageIndicator
212
- title="config.json"
213
- language="json"
214
- code={codexConfig}
215
- className="mt-2"
216
- />
217
- </SubAppSection>
218
- )}
219
- </div>
283
+ </li>
284
+ <li>
285
+ <p>Add a new server and paste the MCP server URL above.</p>
286
+ </li>
287
+ <li>
288
+ <p>Save — the server is available in your sessions.</p>
289
+ </li>
290
+ </Steps>
291
+ <DocsLink href="https://openai.com/index/introducing-codex/">
292
+ View official docs
293
+ </DocsLink>
294
+ </>
220
295
  );
221
296
 
222
297
  case "cursor":
223
298
  return (
224
299
  <>
225
- <ol>
300
+ {!auth && (
301
+ <div className="not-prose space-y-2">
302
+ <InstallButton href={cursorDeepLink}>
303
+ <McpClientLogo appId="cursor" className="size-3.5" />
304
+ Add to Cursor
305
+ </InstallButton>
306
+ <p className="text-xs text-muted-foreground">
307
+ Opens Cursor and prompts to install. Or configure it manually:
308
+ </p>
309
+ </div>
310
+ )}
311
+ <Steps>
226
312
  <li>
227
- <span>
228
- Go to <strong>Settings</strong> →{" "}
229
- <strong>Tools & MCPs</strong> →{" "}
230
- <strong>New MCP Server</strong>, or edit:{" "}
231
- </span>
232
- <InlineCode>~/.cursor/mcp.json</InlineCode>
233
- <span> (global) or </span>
234
- <InlineCode>.cursor/mcp.json</InlineCode>
235
- <span> (project)</span>
313
+ <p>
314
+ Go to{" "}
315
+ <strong>Settings → Tools & MCPs → New MCP Server</strong>, or
316
+ edit <InlineCode>~/.cursor/mcp.json</InlineCode> (global) /{" "}
317
+ <InlineCode>.cursor/mcp.json</InlineCode> (project):
318
+ </p>
236
319
  <SyntaxHighlight
237
320
  showLanguageIndicator
238
321
  title="mcp.json"
239
322
  language="json"
240
323
  code={cursorConfig}
241
- className="mt-2"
242
324
  />
243
325
  </li>
244
- <li>Restart Cursor to apply the configuration</li>
245
- </ol>
246
- <a
247
- href="https://cursor.com/docs/context/mcp"
248
- target="_blank"
249
- rel="noopener noreferrer"
250
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
251
- >
326
+ <li>
327
+ <p>Restart Cursor to apply the configuration.</p>
328
+ </li>
329
+ </Steps>
330
+ <DocsLink href="https://cursor.com/docs/context/mcp">
252
331
  View official docs
253
- <ExternalLinkIcon className="h-3 w-3" />
254
- </a>
332
+ </DocsLink>
255
333
  </>
256
334
  );
257
335
 
@@ -259,35 +337,46 @@ export const MCPEndpoint = ({
259
337
  return (
260
338
  <>
261
339
  <Callout type="note" title="Requirements">
262
- VS Code with GitHub Copilot extension
340
+ VS Code with the GitHub Copilot extension.
263
341
  </Callout>
264
- <ol>
342
+ {!auth && (
343
+ <div className="not-prose space-y-2">
344
+ <InstallButton href={vscodeDeepLink}>
345
+ <McpClientLogo appId="vscode" className="size-3.5" />
346
+ Install in VS Code
347
+ </InstallButton>
348
+ <p className="text-xs text-muted-foreground">
349
+ Opens VS Code and prompts to install. Or configure it
350
+ manually:
351
+ </p>
352
+ </div>
353
+ )}
354
+ <Steps>
265
355
  <li>
266
- <span>Create </span>
267
- <InlineCode>.vscode/mcp.json</InlineCode>
268
- <span> in your workspace (or user-level mcp.json):</span>
356
+ <p>
357
+ Create <InlineCode>.vscode/mcp.json</InlineCode> in your
358
+ workspace (or a user-level mcp.json):
359
+ </p>
269
360
  <SyntaxHighlight
270
361
  showLanguageIndicator
271
362
  title="mcp.json"
272
363
  language="json"
273
364
  code={vscodeConfig}
274
- className="mt-2"
275
365
  />
276
366
  </li>
277
- <li>Restart VS Code to apply the configuration</li>
278
367
  <li>
279
- Use MCP tools in GitHub Copilot Chat by selecting Agent mode
368
+ <p>Restart VS Code to apply the configuration.</p>
280
369
  </li>
281
- </ol>
282
- <a
283
- href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers"
284
- target="_blank"
285
- rel="noopener noreferrer"
286
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
287
- >
370
+ <li>
371
+ <p>
372
+ Use the MCP tools in GitHub Copilot Chat by selecting Agent
373
+ mode.
374
+ </p>
375
+ </li>
376
+ </Steps>
377
+ <DocsLink href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">
288
378
  View official docs
289
- <ExternalLinkIcon className="h-3 w-3" />
290
- </a>
379
+ </DocsLink>
291
380
  </>
292
381
  );
293
382
 
@@ -295,30 +384,23 @@ export const MCPEndpoint = ({
295
384
  return (
296
385
  <>
297
386
  <p>
298
- Generic <InlineCode>.mcp.json</InlineCode> configuration format
299
- that works with most MCP-compatible apps.
387
+ A generic <InlineCode>.mcp.json</InlineCode> configuration that
388
+ works with most MCP-compatible apps.
300
389
  </p>
301
390
  <SyntaxHighlight
302
391
  showLanguageIndicator
303
392
  title=".mcp.json"
304
393
  language="json"
305
394
  code={genericConfig}
306
- className="mt-2"
307
395
  />
308
396
  <p className="text-sm text-muted-foreground">
309
397
  Place this file in your project root or the appropriate
310
398
  configuration directory for your app. The exact location depends
311
399
  on your specific tool.
312
400
  </p>
313
- <a
314
- href="https://modelcontextprotocol.io/"
315
- target="_blank"
316
- rel="noopener noreferrer"
317
- className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
318
- >
401
+ <DocsLink href="https://modelcontextprotocol.io/">
319
402
  Learn more about MCP
320
- <ExternalLinkIcon className="h-3 w-3" />
321
- </a>
403
+ </DocsLink>
322
404
  </>
323
405
  );
324
406
 
@@ -329,69 +411,106 @@ export const MCPEndpoint = ({
329
411
 
330
412
  return (
331
413
  <Card className="p-6 mb-6 max-w-screen-md">
332
- <div className="space-y-4">
414
+ <div className="space-y-5">
333
415
  <div>
334
- <div className="flex items-center justify-between mb-2">
335
- <h3 className="text-lg font-semibold">App Configuration</h3>
416
+ <h3 className="text-lg font-semibold">Connect {name}</h3>
417
+ <p className="text-sm text-muted-foreground">
418
+ Add this MCP server to your favorite AI tools in a few steps.
419
+ </p>
420
+ </div>
421
+
422
+ {/* Step 1 — the one thing every client needs: the server URL. */}
423
+ <div className="rounded-lg border bg-muted/30 p-3">
424
+ <div className="flex items-center justify-between gap-3">
425
+ <div className="min-w-0">
426
+ <div className="text-xs font-medium text-muted-foreground mb-1">
427
+ MCP Server URL
428
+ </div>
429
+ <code className="block truncate font-mono text-sm">{mcpUrl}</code>
430
+ </div>
336
431
  <Button
337
432
  onClick={handleCopy}
338
433
  variant="outline"
339
434
  size="sm"
340
- className="gap-1.5"
435
+ className="gap-1.5 shrink-0"
341
436
  >
342
437
  {isCopied ? (
343
438
  <CheckIcon className="h-3.5 w-3.5 text-green-600" />
344
439
  ) : (
345
440
  <CopyIcon className="h-3.5 w-3.5" />
346
441
  )}
347
- {isCopied ? "Copied!" : "Copy URL"}
442
+ {isCopied ? "Copied!" : "Copy"}
348
443
  </Button>
349
444
  </div>
350
- <p className="text-sm text-muted-foreground mb-3">
351
- Choose your app and copy the configuration to get started.
352
- </p>
353
-
354
- <hr className="my-4" />
445
+ {auth && (
446
+ <p className="mt-2 text-xs text-muted-foreground">
447
+ Requires an <InlineCode>{auth.headerName}</InlineCode> header —
448
+ replace <InlineCode>{auth.placeholder}</InlineCode> with your key.
449
+ </p>
450
+ )}
451
+ </div>
355
452
 
356
- <Tabs
357
- value={activeTab}
358
- onValueChange={setActiveTab}
359
- className="w-full"
360
- >
361
- <Select value={activeTab} onValueChange={setActiveTab}>
362
- <SelectTrigger className="w-full sm:hidden">
363
- <SelectValue />
364
- </SelectTrigger>
365
- <SelectContent>
366
- {visibleApps.map((app) => (
367
- <SelectItem key={app.id} value={app.id}>
368
- {app.label}
369
- </SelectItem>
370
- ))}
371
- </SelectContent>
372
- </Select>
373
- <TabsList
374
- className="hidden sm:grid w-full"
375
- style={{
376
- gridTemplateColumns: `repeat(${visibleApps.length}, minmax(0, 1fr))`,
377
- }}
378
- >
379
- {visibleApps.map((app) => (
380
- <TabsTrigger key={app.id} value={app.id}>
381
- {app.label}
382
- </TabsTrigger>
383
- ))}
384
- </TabsList>
453
+ {/* Step 2 — pick your client. */}
454
+ <div className="space-y-2">
455
+ <div className="text-sm font-medium">Choose your client</div>
456
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
457
+ {visibleApps.map((app) => {
458
+ const isActive = app.id === selectedApp?.id;
459
+ return (
460
+ <button
461
+ key={app.id}
462
+ type="button"
463
+ onClick={() => selectApp(app.id)}
464
+ aria-pressed={isActive}
465
+ data-active={isActive}
466
+ className={cn(
467
+ "flex flex-col items-center justify-center gap-2 rounded-lg border p-3 transition",
468
+ "hover:bg-muted/60",
469
+ isActive
470
+ ? "border-primary bg-primary/5 ring-1 ring-primary text-foreground"
471
+ : "border-border text-muted-foreground",
472
+ )}
473
+ >
474
+ <McpClientLogo appId={app.id} className="size-6" />
475
+ <span className="text-xs font-medium">{app.label}</span>
476
+ </button>
477
+ );
478
+ })}
479
+ </div>
480
+ </div>
385
481
 
482
+ {/* Step 3 — client-specific setup walkthrough. */}
483
+ {selectedApp && (
484
+ <div className="space-y-3">
485
+ <div className="flex flex-wrap items-center justify-between gap-2">
486
+ <div className="text-sm font-medium">
487
+ Set up {selectedApp.label}
488
+ </div>
489
+ {selectedApp.subApps.length > 1 && (
490
+ <ToggleGroup
491
+ size="sm"
492
+ variant="outline"
493
+ spacing={2}
494
+ aria-label={`Set up method for ${selectedApp.label}`}
495
+ value={activeSubApp ? [activeSubApp.id] : []}
496
+ onValueChange={(value: string[]) => {
497
+ const next = value.at(0);
498
+ if (next) setSelectedSubAppId(next);
499
+ }}
500
+ >
501
+ {selectedApp.subApps.map((sub) => (
502
+ <ToggleGroupItem key={sub.id} value={sub.id}>
503
+ {sub.label}
504
+ </ToggleGroupItem>
505
+ ))}
506
+ </ToggleGroup>
507
+ )}
508
+ </div>
386
509
  <Typography className="text-sm max-w-full">
387
- {visibleApps.map((app) => (
388
- <TabsContent key={app.id} value={app.id} className="space-y-3">
389
- {renderAppContent(app)}
390
- </TabsContent>
391
- ))}
510
+ {renderSetup(selectedApp)}
392
511
  </Typography>
393
- </Tabs>
394
- </div>
512
+ </div>
513
+ )}
395
514
  </div>
396
515
  </Card>
397
516
  );