diff --git a/lang/es.json b/lang/es.json index 2578aead..0ac4106a 100644 --- a/lang/es.json +++ b/lang/es.json @@ -2263,18 +2263,25 @@ "Rotate this token?": "¿Rotar este token?", "Rotating replaces the secret. Any AI client using the current token will stop working until you reconnect it with the new one.": "Rotar reemplaza el secreto. Cualquier cliente de IA que use el token actual dejará de funcionar hasta que lo reconectes con el nuevo.", "AI Connector": "Conector de IA", - "You can create a token now, but it only works on a paid plan.": "Puedes crear un token ahora, pero solo funciona con un plan de pago.", + "You can set it up now, but it only works on a paid plan.": "Puedes configurarlo ahora, pero solo funciona con un plan de pago.", "Anything you ask about is sent to the AI app you connect, and we cannot control what it does with your data. Connect one only if you are comfortable with that. You can revoke a token any time to cut off access.": "Todo lo que consultes se envía a la app de IA que conectes, y no podemos controlar qué hace con tus datos. Conéctala solo si te parece bien. Puedes revocar un token cuando quieras para cortar el acceso.", "This is the only time you will see it, so copy it somewhere safe now.": "Es la única vez que lo verás, así que cópialo ahora en un lugar seguro.", "Tokens can read and analyse your data, but never change it.": "Los tokens pueden leer y analizar tus datos, pero nunca modificarlos.", "Rotate a token if it leaks, or revoke it to cut off access.": "Rota un token si se filtra, o revócalo para cortar el acceso.", "Rotating gives you a new secret and cancels the old one. Anything using the current token stops working until you reconnect it with the new secret.": "Al rotar obtienes un secreto nuevo y se cancela el anterior. Lo que use el token actual dejará de funcionar hasta que lo reconectes con el nuevo secreto.", "Anything using this token loses access right away, and you cannot undo it.": "Cualquier cosa que use este token pierde el acceso al instante, y no se puede deshacer.", - "Connect your AI assistant using the details for it below.": "Conecta tu asistente de IA usando los datos correspondientes de abajo.", "Run this, using one of your tokens in place of .": "Ejecuta esto, usando uno de tus tokens en lugar de .", - "Claude Desktop & ChatGPT": "Claude Desktop y ChatGPT", - "These apps sign in instead of using a token. Add a custom connector pointing at the URL below, then approve the connection on the Whisper Money screen that opens. No token needed.": "Estas apps inician sesión en lugar de usar un token. Añade un conector personalizado que apunte a la URL de abajo y luego aprueba la conexión en la pantalla de Whisper Money que se abre. No hace falta ningún token.", - "Connected apps can read, analyse and make changes to your data (bank-connected accounts stay read-only). You approve the connection on the Whisper Money screen.": "Las apps conectadas pueden leer, analizar y modificar tus datos (las cuentas conectadas a un banco siguen siendo de solo lectura). Tú apruebas la conexión en la pantalla de Whisper Money.", + "Connected apps can read, analyse and make changes to your data (bank-connected accounts stay read-only).": "Las apps conectadas pueden leer, analizar y modificar tus datos (las cuentas conectadas a un banco siguen siendo de solo lectura).", + "Pick your app, then follow the steps. You sign in and approve the connection — no token needed.": "Elige tu app y sigue los pasos. Inicias sesión y apruebas la conexión: no hace falta ningún token.", + "Claude Desktop": "Claude Desktop", + "Open Settings → Connectors in Claude Desktop.": "Abre Ajustes → Conectores en Claude Desktop.", + "Click \"Add custom connector\".": "Haz clic en «Añadir conector personalizado».", + "Give it a name and paste this URL:": "Ponle un nombre y pega esta URL:", + "Approve the connection on the Whisper Money screen that opens.": "Aprueba la conexión en la pantalla de Whisper Money que se abre.", + "Turn on developer mode: Settings → Connectors → Advanced → Developer mode. Custom connectors are only available with it on.": "Activa el modo desarrollador: Ajustes → Conectores → Avanzado → Modo desarrollador. Los conectores personalizados solo están disponibles con él activado.", + "In Connectors, click \"Create\".": "En Conectores, haz clic en «Crear».", + "Connect with Claude Code": "Conectar con Claude Code", + "For developers. Claude Code signs in with a token instead of the browser flow.": "Para desarrolladores. Claude Code inicia sesión con un token en lugar del flujo del navegador.", "Connect to :app": "Conectar con :app", "Connect :client to :app": "Conectar :client con :app", ":client is asking to read and make changes to your finance data.": ":client solicita leer y modificar tus datos financieros.", diff --git a/resources/js/pages/settings/mcp.tsx b/resources/js/pages/settings/mcp.tsx index 7b91dfa7..43e76840 100644 --- a/resources/js/pages/settings/mcp.tsx +++ b/resources/js/pages/settings/mcp.tsx @@ -27,6 +27,11 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -36,13 +41,23 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { useClipboard } from '@/hooks/use-clipboard'; import AppLayout from '@/layouts/app-layout'; import SettingsLayout from '@/layouts/settings/layout'; +import { cn } from '@/lib/utils'; import { type BreadcrumbItem, type SharedData } from '@/types'; import { __ } from '@/utils/i18n'; import { Head, Link, router, useForm, usePage } from '@inertiajs/react'; -import { Copy, KeyRound, RefreshCw, ShieldAlert, Trash2 } from 'lucide-react'; +import { + ChevronDown, + Copy, + KeyRound, + RefreshCw, + ShieldAlert, + Trash2, +} from 'lucide-react'; +import { useState, type ReactNode } from 'react'; import { toast } from 'sonner'; interface TokenRow { @@ -69,10 +84,18 @@ function formatDate(value: string | null): string { return value ? new Date(value).toLocaleString() : __('Never'); } +type ConnectorApp = 'claude' | 'chatgpt'; + export default function Mcp() { const { tokens, serverUrl, oauthUrl, subscribeUrl, newToken, auth } = usePage().props; const [, copy] = useClipboard(); + const [connector, setConnector] = useState('claude'); + // A freshly minted token means the user just used the developer flow, so + // keep that section open; otherwise it starts collapsed for everyone else. + const [showDeveloper, setShowDeveloper] = useState( + Boolean(newToken), + ); const form = useForm<{ name: string; scope: 'read' | 'read_write' }>({ name: '', @@ -95,6 +118,59 @@ export default function Mcp() { }); } + const oauthUrlBlock = ( +
+ + {oauthUrl} + + +
+ ); + + const connectors: Record< + ConnectorApp, + { label: string; steps: ReactNode[] } + > = { + claude: { + label: __('Claude Desktop'), + steps: [ + __('Open Settings → Connectors in Claude Desktop.'), + __('Click "Add custom connector".'), + <> + {__('Give it a name and paste this URL:')} + {oauthUrlBlock} + , + __( + 'Approve the connection on the Whisper Money screen that opens.', + ), + ], + }, + chatgpt: { + label: __('ChatGPT'), + steps: [ + __( + 'Turn on developer mode: Settings → Connectors → Advanced → Developer mode. Custom connectors are only available with it on.', + ), + __('In Connectors, click "Create".'), + <> + {__('Give it a name and paste this URL:')} + {oauthUrlBlock} + , + __( + 'Approve the connection on the Whisper Money screen that opens.', + ), + ], + }, + }; + return ( @@ -118,7 +194,7 @@ export default function Mcp() { {__( - 'You can create a token now, but it only works on a paid plan.', + 'You can set it up now, but it only works on a paid plan.', )}{' '} )} - {/* Create token */} - - - {__('Create a token')} - - {__( - 'Read-only tokens can analyse your data. Read & write tokens can also create, edit and delete transactions, categories, labels and automation rules.', - )} - - - -
-
- - - form.setData('name', e.target.value) - } - placeholder={__('e.g. Claude Desktop')} - required - /> -
-
- - {/* Radix Select renders a hidden native - form.setData( - 'scope', - value as - | 'read' - | 'read_write', - ) - } - > - - - - - - {__('Read only')} - - - {__('Read & write')} - - - -
- - -
- {form.errors.name && ( -

- {form.errors.name} -

- )} -
-
- - {/* Token list */} - - - {__('Your tokens')} - - {__( - 'Rotate a token if it leaks, or revoke it to cut off access.', - )} - - - - {tokens.length === 0 ? ( -

- {__('You have no tokens yet.')} -

- ) : ( -
    - {tokens.map((token) => ( -
  • -
    -
    - - {token.name} - - - {token.scope === - 'read_write' - ? __('Read & write') - : __('Read only')} - -
    -

    - {__('Created')}:{' '} - {formatDate( - token.created_at, - )}{' '} - · {__('Last used')}:{' '} - {formatDate( - token.last_used_at, - )} -

    -
    -
    - - - - - - - - {__( - 'Rotate this token?', - )} - - - {__( - 'Rotating gives you a new secret and cancels the old one. Anything using the current token stops working until you reconnect it with the new secret.', - )} - - - - - {__('Cancel')} - - - router.post( - rotate( - token.id, - ).url, - {}, - { - preserveScroll: true, - }, - ) - } - > - {__('Rotate')} - - - - - - - - - - - - {__( - 'Revoke this token?', - )} - - - {__( - 'Anything using this token loses access right away, and you cannot undo it.', - )} - - - - - {__('Cancel')} - - - router.delete( - destroy( - token.id, - ).url, - { - preserveScroll: true, - }, - ) - } - > - {__('Revoke')} - - - - -
    -
  • - ))} -
- )} -
-
- - {/* Connection instructions */} + {/* Connect Claude Desktop / ChatGPT — the primary flow */} {__('How to connect')} {__( - 'Connect your AI assistant using the details for it below.', + 'Pick your app, then follow the steps. You sign in and approve the connection — no token needed.', )} - -
-

- {__('Claude Desktop & ChatGPT')} -

-

- {__( - 'These apps sign in instead of using a token. Add a custom connector pointing at the URL below, then approve the connection on the Whisper Money screen that opens. No token needed.', - )} -

-
- - {oauthUrl} - - -
-

- {__( - 'Connected apps can read, analyse and make changes to your data (bank-connected accounts stay read-only). You approve the connection on the Whisper Money screen.', - )} -

-
+ {connectors[key].label} + + ))} + -
-

- {__('Claude Code')} -

-

- {__( - 'Run this, using one of your tokens in place of .', - )} -

- - {`claude mcp add --transport http whisper-money ${serverUrl} --header "Authorization: Bearer "`} - -
+
    + {connectors[connector].steps.map( + (step, index) => ( +
  1. {step}
  2. + ), + )} +
+ +

+ {__( + 'Connected apps can read, analyse and make changes to your data (bank-connected accounts stay read-only).', + )} +

+ + {/* Developer flow: Claude Code + token management, hidden by default */} + + + + + + + +
+

+ {__( + 'Run this, using one of your tokens in place of .', + )} +

+ + {`claude mcp add --transport http whisper-money ${serverUrl} --header "Authorization: Bearer "`} + +
+ + {/* Create token */} +
+
+

+ {__('Create a token')} +

+

+ {__( + 'Read-only tokens can analyse your data. Read & write tokens can also create, edit and delete transactions, categories, labels and automation rules.', + )} +

+
+
+
+ + + form.setData( + 'name', + e.target.value, + ) + } + placeholder={__( + 'e.g. Claude Desktop', + )} + required + /> +
+
+ + {/* Radix Select renders a hidden native + form.setData( + 'scope', + value as + | 'read' + | 'read_write', + ) + } + > + + + + + + {__( + 'Read only', + )} + + + {__( + 'Read & write', + )} + + + +
+
+ + + {form.errors.name && ( +

+ {form.errors.name} +

+ )} + + + {/* Token list */} +
+
+

+ {__('Your tokens')} +

+

+ {__( + 'Rotate a token if it leaks, or revoke it to cut off access.', + )} +

+
+ {tokens.length === 0 ? ( +

+ {__('You have no tokens yet.')} +

+ ) : ( +
    + {tokens.map((token) => ( +
  • +
    +
    + + {token.name} + + + {token.scope === + 'read_write' + ? __( + 'Read & write', + ) + : __( + 'Read only', + )} + +
    +

    + {__('Created')}:{' '} + {formatDate( + token.created_at, + )}{' '} + ·{' '} + {__( + 'Last used', + )} + :{' '} + {formatDate( + token.last_used_at, + )} +

    +
    +
    + + + + + + + + {__( + 'Rotate this token?', + )} + + + {__( + 'Rotating gives you a new secret and cancels the old one. Anything using the current token stops working until you reconnect it with the new secret.', + )} + + + + + {__( + 'Cancel', + )} + + + router.post( + rotate( + token.id, + ) + .url, + {}, + { + preserveScroll: true, + preserveState: true, + }, + ) + } + > + {__( + 'Rotate', + )} + + + + + + + + + + + + {__( + 'Revoke this token?', + )} + + + {__( + 'Anything using this token loses access right away, and you cannot undo it.', + )} + + + + + {__( + 'Cancel', + )} + + + router.delete( + destroy( + token.id, + ) + .url, + { + preserveScroll: true, + preserveState: true, + }, + ) + } + > + {__( + 'Revoke', + )} + + + + +
    +
  • + ))} +
+ )} +
+
+
+
+