import { ChevronDown, CircleAlert, Database, FlaskConical, MonitorCog, Network, Pencil, Plus, RefreshCw, Trash2, Wrench, X } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { builtinMcpServers } from '../../shared/builtin-mcp-servers' import { builtinModelToolGroups, builtinModelTools } from '../../shared/builtin-model-tools' import type { BuiltinMcpServerId, CapabilityDiagnosticReport, CapabilityAssignments, CapabilitySnapshot, ComputerCapabilityId, McpServerInput, McpServerSummary, McpServerTestResult, McpTransport, ObsidianConnectionTestResult, RuntimeTarget, WebSearchTestResult } from '../../shared/capability-contracts' import { activateModalFocus, trapTabFocus } from './dialog-focus' import { PageTabs } from './WorkspacePrimitives' const configurableMcpTargets: RuntimeTarget[] = [ 'model', 'opencode', 'continue', 'deepseek-harness' ] const imageTools = builtinModelTools.filter((tool) => tool.group === 'image') const imageExpansionId = 'builtin:image-generation' type McpSettingsTab = | 'builtin' | 'custom' | 'model-tools' | 'computer' type McpEditor = { id?: string name: string description: string enabled: boolean allowDynamicTools: boolean assignments: CapabilityAssignments transport: McpTransport command: string args: string url: string token: string clearToken: boolean } const emptyEditor: McpEditor = { name: '', description: '', enabled: true, allowDynamicTools: false, assignments: ['model'], transport: 'stdio', command: '', args: '', url: '', token: '', clearToken: false } function editorFromServer(server: McpServerSummary): McpEditor { return { id: server.id, name: server.name, description: server.description, enabled: server.enabled, allowDynamicTools: server.allowDynamicTools, assignments: server.assignments, transport: server.transport, command: server.transport === 'stdio' ? server.command : '', args: server.transport === 'stdio' ? server.args.join('\n') : '', url: server.transport === 'stdio' ? '' : server.url, token: '', clearToken: false } } export function McpSettingsSection({ onOpenImageModelSettings, magicNotesEnabled = false }: { onOpenImageModelSettings: () => void magicNotesEnabled?: boolean }): React.JSX.Element { const { t } = useTranslation('integrations') const tRef = useRef(t) useEffect(() => { tRef.current = t }, [t]) const runtimeLabels: Record = { model: t('mcp.runtimeLabels.model'), opencode: t('mcp.runtimeLabels.opencode'), continue: t('mcp.runtimeLabels.continue'), 'deepseek-harness': 'DeepSeek Harness' } const diagnosticStatusLabels: Record< CapabilityDiagnosticReport['status'], string > = { available: t('mcp.diagnosticStatuses.available'), degraded: t('mcp.diagnosticStatuses.degraded'), unavailable: t('mcp.diagnosticStatuses.unavailable'), disabled: t('mcp.diagnosticStatuses.disabled') } const [snapshot, setSnapshot] = useState() const [obsidianPath, setObsidianPath] = useState() const [obsidianResult, setObsidianResult] = useState() const [obsidianError, setObsidianError] = useState() const [obsidianSpecified, setObsidianSpecified] = useState() const vaultPath = obsidianPath ?? snapshot?.obsidian?.vaultPath ?? '' const specifiedVault = obsidianSpecified ?? Boolean(snapshot?.obsidian?.vaultPath) const [editor, setEditor] = useState() const [busy, setBusy] = useState() const [error, setError] = useState() const [testResults, setTestResults] = useState< Record >({}) const [webSearchTestResult, setWebSearchTestResult] = useState() const [expandedItemIds, setExpandedItemIds] = useState>( () => new Set() ) const [diagnostics, setDiagnostics] = useState< Partial> >({}) const [activeTab, setActiveTab] = useState('builtin') const editorDialogRef = useRef(null) const editorNameRef = useRef(null) const editorTriggerRef = useRef( undefined ) const editorOpen = Boolean(editor) const toggleItem = (itemId: string): void => { setExpandedItemIds((current) => { const next = new Set(current) if (next.has(itemId)) { next.delete(itemId) } else { next.add(itemId) } return next }) } useEffect(() => { void window.goodbuddy.capabilities .getSnapshot() .then(setSnapshot) .catch((reason: unknown) => { setError( reason instanceof Error ? reason.message : tRef.current('mcp.errors.load') ) }) }, []) useEffect(() => { if (!editorOpen) { return } return activateModalFocus( () => editorNameRef.current, () => editorTriggerRef.current ?? null ) }, [editorOpen]) const run = async ( key: string, operation: () => Promise ): Promise => { setBusy(key) setError(undefined) try { setSnapshot(await operation()) return true } catch (reason) { setError( reason instanceof Error ? reason.message : t('mcp.errors.operation') ) return false } finally { setBusy(undefined) } } const diagnose = async ( capabilityId: ComputerCapabilityId ): Promise => { setBusy(`diagnose:${capabilityId}`) setError(undefined) try { const diagnoseCapability = window.goodbuddy.capabilities.diagnoseComputerCapability if (!diagnoseCapability) { throw new Error(t('mcp.errors.unsupportedDiagnostics')) } const report = await diagnoseCapability(capabilityId) setDiagnostics((current) => ({ ...current, [capabilityId]: report })) } catch (reason) { setError( reason instanceof Error ? reason.message : t('mcp.errors.diagnostics') ) } finally { setBusy(undefined) } } const save = async (): Promise => { if (!editor) { return } const secret: McpServerInput['secret'] = editor.clearToken ? { action: 'clear' } : editor.token.trim() ? { action: 'replace', value: editor.token } : { action: 'keep' } const common = { name: editor.name, description: editor.description, enabled: editor.enabled, allowDynamicTools: editor.allowDynamicTools, assignments: editor.assignments, secret } const input: McpServerInput = editor.transport === 'stdio' ? { ...common, transport: 'stdio', command: editor.command, args: editor.args .split(/\r?\n/u) .map((value) => value.trim()) .filter(Boolean) } : { ...common, transport: editor.transport, url: editor.url } const saved = await run('save', () => window.goodbuddy.capabilities.saveMcpServer(editor.id, input) ) if (saved) { closeEditor() } } const test = async (server: McpServerSummary): Promise => { setBusy(`test:${server.id}`) setError(undefined) try { const result = await window.goodbuddy.capabilities.testMcpServer(server.id) setTestResults((current) => ({ ...current, [server.id]: result })) setExpandedItemIds((current) => { const next = new Set(current) next.add(`custom:${server.id}`) return next }) } catch (reason) { setError( reason instanceof Error ? reason.message : t('mcp.errors.test') ) } finally { setBusy(undefined) } } const testDirectModelWebSearch = async (): Promise => { setBusy('test:web-search') setError(undefined) try { const testCapability = window.goodbuddy.capabilities.testWebSearch if (!testCapability) { throw new Error(t('mcp.webSearch.unsupported')) } setWebSearchTestResult(await testCapability()) } catch (reason) { setError( reason instanceof Error ? reason.message : t('mcp.webSearch.testFailed') ) } finally { setBusy(undefined) } } const configureObsidian = async (action: 'save' | 'test' | 'select'): Promise => { setBusy(`obsidian:${action}`) setObsidianError(undefined) try { const api = window.goodbuddy.capabilities if (action === 'select') { const path = await api.selectObsidianVault() if (path !== null) { setObsidianPath(path) setObsidianSpecified(true) setObsidianResult(undefined) } } else { const input = { vaultPath: specifiedVault ? vaultPath.trim() : '' } if (specifiedVault && !input.vaultPath) { throw new Error(t('mcp.obsidian.folderRequired')) } if (action === 'save') { const next = await api.updateObsidianSettings(input) setSnapshot(next) setObsidianPath(undefined) setObsidianSpecified(undefined) } else { setObsidianResult(undefined) setObsidianResult(await api.testObsidianConnection(input)) } } } catch (reason) { setObsidianError(reason instanceof Error ? reason.message : t('mcp.errors.operation')) } finally { setBusy(undefined) } } const updateAssignment = ( target: RuntimeTarget, checked: boolean ): void => { if (!editor) { return } setEditor({ ...editor, assignments: checked ? [...editor.assignments, target] : editor.assignments.filter((item) => item !== target) }) } const updateBuiltinAssignment = ( serverId: BuiltinMcpServerId, assignments: CapabilityAssignments, target: RuntimeTarget, checked: boolean ): void => { const next = checked ? [...assignments, target] : assignments.filter((item) => item !== target) void run(`builtin:assign:${serverId}`, () => window.goodbuddy.capabilities.setBuiltinMcpServerAssignments( serverId, next ) ) } const openEditor = ( nextEditor: McpEditor, trigger: HTMLButtonElement ): void => { editorTriggerRef.current = trigger trigger.focus() setError(undefined) setEditor(nextEditor) } const closeEditor = (): void => { if (busy === 'save') { return } setError(undefined) setEditor(undefined) } const handleEditorKeyDown = ( event: React.KeyboardEvent ): void => { event.stopPropagation() if (event.key === 'Escape') { event.preventDefault() closeEditor() return } trapTabFocus(event, editorDialogRef.current) } const computerCapabilities = useMemo(() => snapshot?.computerCapabilities ?? [], [snapshot?.computerCapabilities]) const desktopCapabilities = useMemo(() => computerCapabilities.filter( (capability) => capability.id !== 'host-browser-control' ), [computerCapabilities]) const builtinMcpStates = useMemo(() => new Map( snapshot?.builtinMcpServers?.map((server) => [server.id, server]) ), [snapshot?.builtinMcpServers]) const webSearch = snapshot?.webSearch ?? { provider: 'exa' as const, enabled: true, availableIn: ['ask', 'execute'] as const, tools: ['web_search', 'web_fetch'] as const } return ( <>
{ setError(undefined) setActiveTab(tab) }} tabs={[ { id: 'builtin', icon: , label: t('mcp.tabs.builtin') }, { id: 'model-tools', icon: , label: t('mcp.tabs.modelTools') }, { id: 'custom', icon: , label: t('mcp.tabs.custom') }, { id: 'computer', icon: , label: t('mcp.tabs.computer') } ]} value={activeTab} variant="segmented" />
{activeTab === 'custom' && ( <>
{error && !editor && (

{error}

)}

{t('mcp.customNotice')}

)} {activeTab === 'computer' && (
{t('mcp.computer.title')}
{desktopCapabilities.map((capability) => { const report = diagnostics[capability.id] return (
{capability.name} {capability.supported ? t('mcp.computer.supported') : t('mcp.computer.unsupported')}

{capability.description}

{report && (
{t('mcp.computer.result', { status: diagnosticStatusLabels[report.status] })} {report.checks.map((check) => (

{check.summary} {check.remedy ? t('mcp.computer.remedy', { remedy: check.remedy }) : ''}

))}
)}
) })}
)} {activeTab === 'builtin' && (
{t('mcp.builtin.title')}
{t('mcp.profiles.count', { count: builtinMcpServers.length + 1 })}
{t('conversationImages.title')} {t('conversationImages.managed')}

{t('conversationImages.description')}

{t('conversationImages.runtimeScope')}

{t('mcp.builtin.assignedTo')} {configurableMcpTargets.map((target) => ( ))}
{expandedItemIds.has(imageExpansionId) && (
{t('mcp.builtin.tools')} {t('mcp.profiles.count', { count: imageTools.length })}
    {imageTools.map((tool) => (
  • {tool.name} {tool.access === 'write' ? t('mcp.builtin.write') : t('mcp.builtin.readOnly')}

    {t(`conversationImages.tools.${tool.name}`)}

  • ))}
)}
{builtinMcpServers.map((server) => { const expansionId = `builtin:${server.id}` const expanded = expandedItemIds.has(expansionId) const panelId = `mcp-server-tools-${server.id}` const state = builtinMcpStates.get(server.id) ?? { id: server.id, enabled: server.id !== 'obsidian', assignments: [...server.supportedAssignments] as CapabilityAssignments } const computerCapability = 'computerCapabilityId' in server ? computerCapabilities.find( (capability) => capability.id === server.computerCapabilityId ) : undefined const featureAvailable = (!('requiresFeature' in server) || magicNotesEnabled === true) && (!computerCapability || computerCapability.supported) return (
{server.name} {!state.enabled ? t('mcp.builtin.disabled') : computerCapability && !computerCapability.supported ? t('mcp.browser.unsupported') : !featureAvailable ? t('mcp.builtin.serverSummaryDisabled') : server.id === 'obsidian' ? t('mcp.obsidian.access') : server.id === 'builtin-browser' ? t('mcp.builtin.serverSummaryExecuteOnly') : server.access === 'mixed' ? t('mcp.builtin.serverSummaryMixed') : t('mcp.builtin.serverSummaryReadOnly')}

{server.id === 'obsidian' ? t('mcp.obsidian.description') : server.description}

{server.id === 'obsidian' && (
{specifiedVault && ( )}

{t('mcp.obsidian.scopeHelp')}

{t('mcp.obsidian.savedScope', { path: snapshot?.obsidian?.vaultPath || t('mcp.obsidian.all') })}

{obsidianError && } {obsidianResult && (
{t('mcp.obsidian.result', { vaults: obsidianResult.vaults.length, tools: obsidianResult.toolCount })} {obsidianResult.vaults.length === 0 &&

{t('mcp.obsidian.empty')}

} {obsidianResult.vaults.map((vault) =>

{vault.name}: {vault.path}

)}
)}
)} {!featureAvailable && (

)} {computerCapability && ( <>

{diagnostics[computerCapability.id] && (
{t('mcp.computer.result', { status: diagnosticStatusLabels[ diagnostics[computerCapability.id]! .status ] })} {diagnostics[ computerCapability.id ]!.checks.map((check) => (

{check.summary} {check.remedy ? t('mcp.computer.remedy', { remedy: check.remedy }) : ''}

))}
)}
)}
{t('mcp.builtin.assignedTo')} {configurableMcpTargets.map((target) => { const unsupported = !server.supportedAssignments.some( (supportedTarget) => supportedTarget === target ) return ( ) })}
{expanded && (
{t('mcp.builtin.tools')} {t('mcp.profiles.count', { count: server.tools.length })}
    {server.tools.map((tool) => (
  • {tool.name} {tool.access === 'write' ? t('mcp.builtin.write') : t('mcp.builtin.readOnly')}

    {tool.description}

  • ))}
)}
) })}
)} {activeTab === 'model-tools' && (
{t('mcp.modelTools.title')}
{t('mcp.modelTools.groupCount', { count: builtinModelToolGroups.length })}
{builtinModelToolGroups .filter((group) => group.id === 'web') .map((group) => { const expansionId = `model-tools:${group.id}` const expanded = expandedItemIds.has(expansionId) const panelId = `model-tool-group-${group.id}` return (
{expanded && (

{t('mcp.webSearch.description')}

{webSearchTestResult && (
{t('mcp.webSearch.result', { duration: webSearchTestResult.durationMs })}

{webSearchTestResult.preview}

)}
{t('mcp.builtin.tools')} {t('mcp.profiles.count', { count: group.tools.length })}
    {group.tools.map((tool) => (
  • {tool.displayName} {tool.name} {t('mcp.builtin.readOnly')}

    {tool.description}

  • ))}
)}
) })} {builtinModelToolGroups .filter((group) => group.id !== 'web') .map((group) => { const expansionId = `model-tools:${group.id}` const expanded = expandedItemIds.has(expansionId) const panelId = `model-tool-group-${group.id}` const groupName = group.name const groupDescription = group.description return (
{expanded && (

{groupDescription}

{t('mcp.builtin.tools')} {t('mcp.profiles.count', { count: group.tools.length })}
    {group.tools.map((tool) => (
  • {tool.displayName} {tool.name} {tool.access === 'write' ? t('mcp.builtin.write') : t('mcp.builtin.readOnly')}

    {tool.description}

  • ))}
)}
) })}
)} {editor && createPortal(
{ if (event.target === event.currentTarget) { event.preventDefault() closeEditor() } }} >
{editor.id ? t('mcp.editor.editTitle') : t('mcp.editor.addTitle')}
{error && (

{error}

)} {editor.transport === 'stdio' ? ( <>