'use client' import { useState, useTransition } from 'react' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useLanguage } from '@/lib/i18n' import { toast } from 'sonner' import { Info, Key, Server, Plus, Copy, Check, Trash2, Ban, Loader2, ExternalLink, ChevronDown, ChevronRight, } from 'lucide-react' import { generateMcpKey, revokeMcpKey, deleteMcpKey, type McpKeyInfo, type McpServerStatus, } from '@/app/actions/mcp-keys' interface McpSettingsPanelProps { initialKeys: McpKeyInfo[] serverStatus: McpServerStatus } export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanelProps) { const [keys, setKeys] = useState(initialKeys) const [createOpen, setCreateOpen] = useState(false) const [isPending, startTransition] = useTransition() const { t } = useLanguage() const handleGenerate = async (name: string) => { startTransition(async () => { try { const result = await generateMcpKey(name) setCreateOpen(false) // Show the raw key in a new dialog setShowRawKey(result.rawKey) setRawKeyName(result.info.name) // Refresh keys setKeys(prev => [ { shortId: result.info.shortId, name: result.info.name, userId: '', userName: '', active: true, createdAt: new Date().toISOString(), lastUsedAt: null, }, ...prev, ]) } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to generate key') } }) } const handleRevoke = (shortId: string) => { if (!confirm(t('mcpSettings.apiKeys.confirmRevoke'))) return startTransition(async () => { try { await revokeMcpKey(shortId) setKeys(prev => prev.map(k => (k.shortId === shortId ? { ...k, active: false } : k)) ) toast.success(t('toast.operationSuccess')) } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to revoke key') } }) } const handleDelete = (shortId: string) => { if (!confirm(t('mcpSettings.apiKeys.confirmDelete'))) return startTransition(async () => { try { await deleteMcpKey(shortId) setKeys(prev => prev.filter(k => k.shortId !== shortId)) toast.success(t('toast.operationSuccess')) } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to delete key') } }) } // Raw key display state const [showRawKey, setShowRawKey] = useState(null) const [rawKeyName, setRawKeyName] = useState('') const [copied, setCopied] = useState(false) const handleCopy = async (text: string) => { await navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } return (
{/* Section 1: What is MCP */}

{t('mcpSettings.whatIsMcp.title')}

{t('mcpSettings.whatIsMcp.description')}

{t('mcpSettings.whatIsMcp.learnMore')}
{/* Section 2: Server Status */}

{t('mcpSettings.serverStatus.title')}

{t('mcpSettings.serverStatus.mode')}: {serverStatus.mode.toUpperCase()}
{serverStatus.mode === 'sse' && serverStatus.url && (
{t('mcpSettings.serverStatus.url')}: {serverStatus.url}
)}
{/* Section 3: API Keys */}

{t('mcpSettings.apiKeys.title')}

{t('mcpSettings.apiKeys.description')}

{keys.length === 0 ? (

{t('mcpSettings.apiKeys.empty')}

) : (
{keys.map(k => ( ))}
)}
{/* Section 4: Configuration Instructions */} {/* Raw Key Display Dialog */} { if (!open) setShowRawKey(null) }}> {t('mcpSettings.createDialog.successTitle')} {t('mcpSettings.createDialog.successDescription')}
{showRawKey}
) } // ── Sub-components ────────────────────────────────────────────────────────────── function CreateKeyDialog({ onGenerate, isPending, }: { onGenerate: (name: string) => void isPending: boolean }) { const [name, setName] = useState('') const { t } = useLanguage() return ( {t('mcpSettings.createDialog.title')} {t('mcpSettings.createDialog.description')}
setName(e.target.value)} className="mt-1" />
) } function KeyCard({ keyInfo, onRevoke, onDelete, isPending, }: { keyInfo: McpKeyInfo onRevoke: (shortId: string) => void onDelete: (shortId: string) => void isPending: boolean }) { const { t } = useLanguage() const formatDate = (iso: string | null) => { if (!iso) return t('mcpSettings.apiKeys.never') return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }) } return (
{keyInfo.name} {keyInfo.active ? t('mcpSettings.apiKeys.active') : t('mcpSettings.apiKeys.revoked')}
{t('mcpSettings.apiKeys.createdAt')}: {formatDate(keyInfo.createdAt)} {t('mcpSettings.apiKeys.lastUsed')}: {formatDate(keyInfo.lastUsedAt)}
{keyInfo.active ? ( ) : ( )}
) } function ConfigInstructions({ serverStatus }: { serverStatus: McpServerStatus }) { const { t } = useLanguage() const [expanded, setExpanded] = useState(null) const baseUrl = serverStatus.url || 'http://localhost:3001' const configs = [ { id: 'claude-code', title: t('mcpSettings.configInstructions.claudeCode.title'), description: t('mcpSettings.configInstructions.claudeCode.description'), snippet: JSON.stringify( { mcpServers: { 'keep-notes': { command: 'node', args: ['path/to/mcp-server/index.js'], env: { DATABASE_URL: 'file:path/to/keep-notes/prisma/dev.db', APP_BASE_URL: 'http://localhost:3000', }, }, }, }, null, 2 ), }, { id: 'cursor', title: t('mcpSettings.configInstructions.cursor.title'), description: t('mcpSettings.configInstructions.cursor.description'), snippet: JSON.stringify( { mcpServers: { 'keep-notes': { url: baseUrl + '/mcp', headers: { 'x-api-key': 'YOUR_API_KEY', }, }, }, }, null, 2 ), }, { id: 'n8n', title: t('mcpSettings.configInstructions.n8n.title'), description: t('mcpSettings.configInstructions.n8n.description'), snippet: `MCP Server URL: ${baseUrl}/mcp Header: x-api-key: YOUR_API_KEY Transport: Streamable HTTP`, }, ] return (

{t('mcpSettings.configInstructions.title')}

{t('mcpSettings.configInstructions.description')}

{configs.map(cfg => (
{expanded === cfg.id && (

{cfg.description}

                  {cfg.snippet}
                
)}
))}
) }