import Section from './Section' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import { Trash2, Copy, Terminal, Plus, Check, KeyRound, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react' import { authApi, oauthApi } from '../../api/client' import { useAddonStore } from '../../store/addonStore' import PhotoProvidersSection from './PhotoProvidersSection' import AirTrailConnectionSection from './AirTrailConnectionSection' import { ALL_SCOPES } from '../../api/oauthScopes' import ScopeGroupPicker from '../OAuth/ScopeGroupPicker' interface OAuthPreset { id: string label: string name: string uris: string scopes: string[] } const OAUTH_PRESETS: OAuthPreset[] = [ { id: 'claude-web', label: 'Claude.ai', name: 'Claude.ai', uris: 'https://claude.ai/api/mcp/auth_callback', scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), }, { id: 'claude-desktop', label: 'Claude Desktop', name: 'Claude Desktop', uris: 'http://localhost', scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), }, { id: 'cursor', label: 'Cursor', name: 'Cursor', uris: 'http://localhost', scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), }, { id: 'vscode', label: 'VS Code', name: 'VS Code / Copilot', uris: 'http://localhost', scopes: ALL_SCOPES.filter(s => s.endsWith(':read')), }, { id: 'windsurf', label: 'Windsurf', name: 'Windsurf', uris: 'http://localhost', scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), }, { id: 'zed', label: 'Zed', name: 'Zed', uris: 'http://localhost', scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), }, ] interface OAuthClient { id: string name: string client_id: string redirect_uris: string[] allowed_scopes: string[] allows_client_credentials: boolean created_at: string client_secret?: string // only present on create } interface OAuthSession { id: number client_id: string client_name: string scopes: string[] access_token_expires_at: string refresh_token_expires_at: string created_at: string } interface McpToken { id: number name: string token_prefix: string created_at: string last_used_at: string | null } export default function IntegrationsTab(): React.ReactElement { const S = useIntegrations() return ( <> {S.airtrailEnabled && } {S.mcpEnabled && } ) } function useIntegrations() { const { t, locale } = useTranslation() const toast = useToast() const { isEnabled: addonEnabled, loadAddons } = useAddonStore() const mcpEnabled = addonEnabled('mcp') const airtrailEnabled = addonEnabled('airtrail') useEffect(() => { loadAddons() }, [loadAddons]) // OAuth clients state const [oauthClients, setOauthClients] = useState([]) const [oauthSessions, setOauthSessions] = useState([]) const [oauthCreateOpen, setOauthCreateOpen] = useState(false) const [oauthNewName, setOauthNewName] = useState('') const [oauthNewUris, setOauthNewUris] = useState('') const [oauthNewScopes, setOauthNewScopes] = useState([]) const [oauthCreating, setOauthCreating] = useState(false) const [oauthCreatedClient, setOauthCreatedClient] = useState(null) const [oauthDeleteId, setOauthDeleteId] = useState(null) const [oauthRevokeId, setOauthRevokeId] = useState(null) const [oauthRotateId, setOauthRotateId] = useState(null) const [oauthRotatedSecret, setOauthRotatedSecret] = useState(null) const [oauthRotating, setOauthRotating] = useState(false) // oauthScopesOpen is managed internally by ScopeGroupPicker const [oauthScopesExpanded, setOauthScopesExpanded] = useState>({}) const [oauthIsMachine, setOauthIsMachine] = useState(false) // MCP sub-tab state const [activeMcpTab, setActiveMcpTab] = useState<'oauth' | 'apitokens'>('oauth') const [configOpenOAuth, setConfigOpenOAuth] = useState(false) const [configOpenToken, setConfigOpenToken] = useState(false) // MCP state const [mcpTokens, setMcpTokens] = useState([]) const [mcpModalOpen, setMcpModalOpen] = useState(false) const [mcpNewName, setMcpNewName] = useState('') const [mcpCreatedToken, setMcpCreatedToken] = useState(null) const [mcpCreating, setMcpCreating] = useState(false) const [mcpDeleteId, setMcpDeleteId] = useState(null) const [copiedKey, setCopiedKey] = useState(null) const copyTimerRef = useRef | null>(null) useEffect(() => { return () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) } }, []) const mcpEndpoint = `${window.location.origin}/mcp` const mcpJsonConfigOAuth = `{ "mcpServers": { "trek": { "command": "npx", "args": [ "mcp-remote", "${mcpEndpoint}", "--static-oauth-client-info", "{\\"client_id\\": \\"\\", \\"client_secret\\": \\"\\"}" ] } } }` const mcpJsonConfig = `{ "mcpServers": { "trek": { "command": "npx", "args": [ "mcp-remote", "${mcpEndpoint}", "--header", "Authorization: Bearer " ] } } }` useEffect(() => { if (mcpEnabled) { authApi.mcpTokens.list().then(d => setMcpTokens(d.tokens || [])).catch(() => {}) } }, [mcpEnabled]) const handleCreateMcpToken = async () => { if (!mcpNewName.trim()) return setMcpCreating(true) try { const d = await authApi.mcpTokens.create(mcpNewName.trim()) setMcpCreatedToken(d.token.raw_token) setMcpNewName('') setMcpTokens(prev => [{ id: d.token.id, name: d.token.name, token_prefix: d.token.token_prefix, created_at: d.token.created_at, last_used_at: null }, ...prev]) } catch { toast.error(t('settings.mcp.toast.createError')) } finally { setMcpCreating(false) } } const handleDeleteMcpToken = async (id: number) => { try { await authApi.mcpTokens.delete(id) setMcpTokens(prev => prev.filter(tk => tk.id !== id)) setMcpDeleteId(null) toast.success(t('settings.mcp.toast.deleted')) } catch { toast.error(t('settings.mcp.toast.deleteError')) } } const handleCopy = (text: string, key: string) => { navigator.clipboard.writeText(text).then(() => { setCopiedKey(key) if (copyTimerRef.current) clearTimeout(copyTimerRef.current) copyTimerRef.current = setTimeout(() => setCopiedKey(null), 2000) }) } // Load OAuth clients and sessions useEffect(() => { if (mcpEnabled) { oauthApi.clients.list().then(d => setOauthClients(d.clients || [])).catch(() => {}) oauthApi.sessions.list().then(d => setOauthSessions(d.sessions || [])).catch(() => {}) } }, [mcpEnabled]) const handleCreateOAuthClient = async () => { if (!oauthNewName.trim()) return if (!oauthIsMachine && !oauthNewUris.trim()) return setOauthCreating(true) try { const uris = oauthIsMachine ? [] : oauthNewUris.split('\n').map(u => u.trim()).filter(Boolean) const d = await oauthApi.clients.create({ name: oauthNewName.trim(), redirect_uris: uris, allowed_scopes: oauthNewScopes, ...(oauthIsMachine ? { allows_client_credentials: true } : {}), }) setOauthCreatedClient(d.client) setOauthClients(prev => [...prev, { ...d.client, client_secret: undefined }]) setOauthNewName('') setOauthNewUris('') setOauthNewScopes([]) setOauthIsMachine(false) } catch { toast.error(t('settings.oauth.toast.createError')) } finally { setOauthCreating(false) } } const handleDeleteOAuthClient = async (id: string) => { try { await oauthApi.clients.delete(id) setOauthClients(prev => prev.filter(c => c.id !== id)) setOauthDeleteId(null) toast.success(t('settings.oauth.toast.deleted')) } catch { toast.error(t('settings.oauth.toast.deleteError')) } } const handleRotateSecret = async (id: string) => { setOauthRotating(true) try { const d = await oauthApi.clients.rotate(id) setOauthRotatedSecret(d.client_secret) setOauthRotateId(null) } catch { toast.error(t('settings.oauth.toast.rotateError')) } finally { setOauthRotating(false) } } const handleRevokeSession = async (id: number) => { try { await oauthApi.sessions.revoke(id) setOauthSessions(prev => prev.filter(s => s.id !== id)) setOauthRevokeId(null) toast.success(t('settings.oauth.toast.revoked')) } catch { toast.error(t('settings.oauth.toast.revokeError')) } } return { t, locale, toast, mcpEnabled, airtrailEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession, } } function IntegrationsMcpSection(props: any) { const { t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession, } = props return (
{/* Endpoint URL */}
{mcpEndpoint}
{/* Sub-tab bar */}
{/* OAuth 2.1 Clients tab */} {activeMcpTab === 'oauth' && ( <> {/* JSON config — OAuth (collapsible) */}
{configOpenOAuth && (
                      {mcpJsonConfigOAuth}
                    

{t('settings.mcp.clientConfigHintOAuth')}

)}

{t('settings.oauth.clientsHint')}

{oauthClients.length === 0 ? (

{t('settings.oauth.noClients')}

) : (
{oauthClients.map((client, i) => (

{client.name}

{client.allows_client_credentials && ( {t('settings.oauth.badge.machine')} )}

{t('settings.oauth.clientId')}: {client.client_id} {t('settings.mcp.tokenCreatedAt')} {new Date(client.created_at).toLocaleDateString(locale)}

{(oauthScopesExpanded[client.id] ? client.allowed_scopes : client.allowed_scopes.slice(0, 5)).map(s => ( {s} ))} {client.allowed_scopes.length > 5 && ( )}
))}
)}
{/* Active OAuth Sessions */} {oauthSessions.length > 0 && (
{oauthSessions.map((session, i) => (

{session.client_name}

{t('settings.oauth.sessionScopes')}: {session.scopes.join(', ')} {t('settings.oauth.sessionExpires')} {new Date(session.access_token_expires_at).toLocaleDateString(locale)}

))}
)} )} {/* API Tokens tab (deprecated) */} {activeMcpTab === 'apitokens' && ( <>

{t('settings.mcp.apiTokensDeprecated')}

{/* JSON config — API Token (collapsible) */}
{configOpenToken && (
                      {mcpJsonConfig}
                    

{t('settings.mcp.clientConfigHint')}

)}
{mcpTokens.length === 0 ? (

{t('settings.mcp.noTokens')}

) : (
{mcpTokens.map((token, i) => (

{token.name}

{token.token_prefix}... {t('settings.mcp.tokenCreatedAt')} {new Date(token.created_at).toLocaleDateString(locale)} {token.last_used_at && ( · {t('settings.mcp.tokenUsedAt')} {new Date(token.last_used_at).toLocaleDateString(locale)} )}

))}
)} )}
) } function McpTokenModals(props: any) { const { t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession, } = props return ( <> {/* Create MCP Token modal */} {mcpModalOpen && (
{ if (e.target === e.currentTarget && !mcpCreatedToken) setMcpModalOpen(false) }}>
{!mcpCreatedToken ? ( <>

{t('settings.mcp.modal.createTitle')}

setMcpNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleCreateMcpToken()} placeholder={t('settings.mcp.modal.tokenNamePlaceholder')} className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content" autoFocus />
) : ( <>

{t('settings.mcp.modal.createdTitle')}

{t('settings.mcp.modal.createdWarning')}

                    {mcpCreatedToken}
                  
)}
)} {/* Delete MCP Token confirm */} {mcpDeleteId !== null && (
{ if (e.target === e.currentTarget) setMcpDeleteId(null) }}>

{t('settings.mcp.deleteTokenTitle')}

{t('settings.mcp.deleteTokenMessage')}

)} ) } function OAuthClientModals(props: any) { const { t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession, } = props return ( <> {/* Create OAuth Client modal */} {oauthCreateOpen && (
{ if (e.target === e.currentTarget && !oauthCreatedClient) setOauthCreateOpen(false) }}>
{!oauthCreatedClient ? ( <>

{t('settings.oauth.modal.createTitle')}

{OAUTH_PRESETS.map(preset => ( ))}
setOauthNewName(e.target.value)} placeholder={t('settings.oauth.modal.clientNamePlaceholder')} className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content" autoFocus />
{!oauthIsMachine && (