|
23 | 23 | let { data, form } = $props(); |
24 | 24 |
|
25 | 25 | const tokens = $derived(data.tokens || []); |
| 26 | + // Real, ready-to-paste API host (e.g. https://api.bottlecrm.io). The MCP |
| 27 | + // client appends /api/... itself, so this is exactly BCRM_BASE_URL. |
| 28 | + const baseUrl = $derived(data.baseUrl || 'https://api.bottlecrm.io'); |
26 | 29 |
|
27 | 30 | let formName = $state(''); |
28 | 31 | let formExpiresAt = $state(''); |
|
33 | 36 | /** @type {string} */ |
34 | 37 | let confirmingId = $state(''); |
35 | 38 | let helpOpen = $state(false); |
| 39 | + let configCopied = $state(false); |
| 40 | + let selectedClient = $state('claude'); |
36 | 41 |
|
37 | | - const claudeConfig = `{ |
| 42 | + // The just-created raw token if we have it, else a paste-your-token hint. We |
| 43 | + // can only ever show the real token in the same response that created it. |
| 44 | + const tokenValue = $derived(form?.created?.token || 'bcrm_pat_…paste-your-token'); |
| 45 | +
|
| 46 | + /** |
| 47 | + * MCP clients we give a ready-to-paste config for. Claude Desktop, Cursor and |
| 48 | + * Gemini CLI share the identical `mcpServers` JSON schema (only the config |
| 49 | + * file differs); Codex CLI uses TOML. |
| 50 | + */ |
| 51 | + const CLIENTS = [ |
| 52 | + { id: 'claude', label: 'Claude Desktop', lang: 'json', file: 'claude_desktop_config.json — Settings → Developer → Edit Config' }, |
| 53 | + { id: 'cursor', label: 'Cursor', lang: 'json', file: '~/.cursor/mcp.json (global) or .cursor/mcp.json (per project)' }, |
| 54 | + { id: 'codex', label: 'Codex CLI', lang: 'toml', file: '~/.codex/config.toml' }, |
| 55 | + { id: 'gemini', label: 'Gemini CLI', lang: 'json', file: '~/.gemini/settings.json' } |
| 56 | + ]; |
| 57 | +
|
| 58 | + /** @param {string} base @param {string} token */ |
| 59 | + function jsonConfig(base, token) { |
| 60 | + return `{ |
38 | 61 | "mcpServers": { |
39 | 62 | "bottlecrm": { |
40 | 63 | "command": "uvx", |
41 | 64 | "args": ["bcrm-mcp"], |
42 | 65 | "env": { |
43 | | - "BCRM_BASE_URL": "<your CRM URL>", |
44 | | - "BCRM_TOKEN": "bcrm_pat_… (paste the token you just created)" |
| 66 | + "BCRM_BASE_URL": "${base}", |
| 67 | + "BCRM_TOKEN": "${token}" |
45 | 68 | } |
46 | 69 | } |
47 | 70 | } |
48 | 71 | }`; |
| 72 | + } |
| 73 | +
|
| 74 | + /** @param {string} base @param {string} token */ |
| 75 | + function tomlConfig(base, token) { |
| 76 | + return `[mcp_servers.bottlecrm] |
| 77 | +command = "uvx" |
| 78 | +args = ["bcrm-mcp"] |
| 79 | +
|
| 80 | +[mcp_servers.bottlecrm.env] |
| 81 | +BCRM_BASE_URL = "${base}" |
| 82 | +BCRM_TOKEN = "${token}"`; |
| 83 | + } |
| 84 | +
|
| 85 | + const selectedClientMeta = $derived( |
| 86 | + CLIENTS.find((c) => c.id === selectedClient) || CLIENTS[0] |
| 87 | + ); |
| 88 | + const configSnippet = $derived( |
| 89 | + selectedClientMeta.lang === 'toml' |
| 90 | + ? tomlConfig(baseUrl, tokenValue) |
| 91 | + : jsonConfig(baseUrl, tokenValue) |
| 92 | + ); |
49 | 93 |
|
50 | 94 | $effect(() => { |
51 | 95 | if (form?.created?.token) { |
52 | 96 | // Reset the create form after a successful creation. |
53 | 97 | formName = ''; |
54 | 98 | formExpiresAt = ''; |
55 | 99 | copied = false; |
| 100 | + // Surface the connect instructions immediately — the config now carries |
| 101 | + // the real token, which the user can only copy from this one response. |
| 102 | + helpOpen = true; |
56 | 103 | } else if (form?.revoked) { |
57 | 104 | toast.success('Token revoked'); |
58 | 105 | confirmingId = ''; |
|
74 | 121 | } |
75 | 122 | } |
76 | 123 |
|
| 124 | + /** @param {string} value */ |
| 125 | + async function copyConfig(value) { |
| 126 | + try { |
| 127 | + await navigator.clipboard.writeText(value); |
| 128 | + configCopied = true; |
| 129 | + toast.success('Config copied to clipboard'); |
| 130 | + setTimeout(() => (configCopied = false), 2000); |
| 131 | + } catch { |
| 132 | + toast.error('Could not copy — select and copy manually'); |
| 133 | + } |
| 134 | + } |
| 135 | +
|
77 | 136 | /** @param {string | null | undefined} value */ |
78 | 137 | function formatDate(value) { |
79 | 138 | if (!value) return 'Never'; |
|
305 | 364 | <ChevronRight class="h-4 w-4 text-[var(--text-secondary)]" /> |
306 | 365 | {/if} |
307 | 366 | <KeyRound class="h-4 w-4 text-[var(--text-secondary)]" /> |
308 | | - <span class="text-base font-medium text-[var(--text-primary)]"> |
309 | | - Connect your AI (Claude Desktop) |
310 | | - </span> |
| 367 | + <span class="text-base font-medium text-[var(--text-primary)]"> Connect your AI </span> |
311 | 368 | </button> |
312 | 369 | {#if helpOpen} |
313 | 370 | <div class="space-y-3 border-t border-[var(--border-default)] p-4"> |
| 371 | + <!-- Client picker --> |
| 372 | + <div class="flex flex-wrap gap-2"> |
| 373 | + {#each CLIENTS as client (client.id)} |
| 374 | + <Button |
| 375 | + type="button" |
| 376 | + size="sm" |
| 377 | + variant={selectedClient === client.id ? 'default' : 'outline'} |
| 378 | + onclick={() => (selectedClient = client.id)} |
| 379 | + > |
| 380 | + {client.label} |
| 381 | + </Button> |
| 382 | + {/each} |
| 383 | + </div> |
| 384 | +
|
314 | 385 | <p class="text-sm text-[var(--text-secondary)]"> |
315 | | - Add the following to your Claude Desktop MCP config, replacing |
316 | | - <code class="font-mono text-xs">BCRM_BASE_URL</code> with your CRM URL and |
317 | | - <code class="font-mono text-xs">BCRM_TOKEN</code> with the token you just created. |
| 386 | + Add this to |
| 387 | + <code class="font-mono text-xs text-[var(--text-primary)]">{selectedClientMeta.file}</code |
| 388 | + >, then restart {selectedClientMeta.label}. |
| 389 | + {#if form?.created?.token} |
| 390 | + The token below is yours — it's shown only this once. |
| 391 | + {:else} |
| 392 | + Replace <code class="font-mono text-xs">BCRM_TOKEN</code> with a token you created above. |
| 393 | + {/if} |
| 394 | + </p> |
| 395 | +
|
| 396 | + <div class="relative"> |
| 397 | + <Button |
| 398 | + type="button" |
| 399 | + size="sm" |
| 400 | + variant="outline" |
| 401 | + class="absolute right-2 top-2 gap-1" |
| 402 | + onclick={() => copyConfig(configSnippet)} |
| 403 | + > |
| 404 | + {#if configCopied} |
| 405 | + <Check class="h-3.5 w-3.5" /> |
| 406 | + Copied |
| 407 | + {:else} |
| 408 | + <Copy class="h-3.5 w-3.5" /> |
| 409 | + Copy |
| 410 | + {/if} |
| 411 | + </Button> |
| 412 | + <pre |
| 413 | + class="overflow-x-auto rounded-md border border-[var(--border-default)] bg-[var(--surface-muted)] p-3 pr-20 font-mono text-xs text-[var(--text-primary)]">{configSnippet}</pre> |
| 414 | + </div> |
| 415 | +
|
| 416 | + <p class="text-xs text-[var(--text-secondary)]"> |
| 417 | + Requires <a |
| 418 | + href="https://docs.astral.sh/uv/" |
| 419 | + target="_blank" |
| 420 | + rel="noopener noreferrer" |
| 421 | + class="underline">uv</a |
| 422 | + > |
| 423 | + (provides <code class="font-mono">uvx</code>) on your machine. The agent acts as you and |
| 424 | + inherits your role — it can't see or do anything you can't. |
318 | 425 | </p> |
319 | | - <pre |
320 | | - class="overflow-x-auto rounded-md border border-[var(--border-default)] bg-[var(--surface-muted)] p-3 font-mono text-xs text-[var(--text-primary)]">{claudeConfig}</pre> |
321 | 426 | </div> |
322 | 427 | {/if} |
323 | 428 | </section> |
|
0 commit comments