diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 5006a03..281f263 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -1601,6 +1601,12 @@ export function registerIpcHandlers(bundle: EngineBundle): void { return engine.addToConfig(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId); }); + safeHandle('mcp:removeFromAgentConfig', async (_event: unknown, agentId: string) => { + const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine'); + const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle)); + return engine.removeFromConfig(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId); + }); + safeHandle('mcp:isConfigured', async (_event: unknown, agentId: string) => { const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine'); const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle)); diff --git a/src/main/preload.ts b/src/main/preload.ts index 8a382d3..0fa8aa8 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -392,6 +392,7 @@ export const electronAPI: ElectronAPI = { mcp: { getAgents: () => ipcRenderer.invoke('mcp:getAgents'), addToAgentConfig: (agentId: string) => ipcRenderer.invoke('mcp:addToAgentConfig', agentId), + removeFromAgentConfig: (agentId: string) => ipcRenderer.invoke('mcp:removeFromAgentConfig', agentId), isConfigured: (agentId: string) => ipcRenderer.invoke('mcp:isConfigured', agentId), getPort: () => ipcRenderer.invoke('mcp:getPort'), }, diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index 6322404..e91edcd 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -848,6 +848,7 @@ export interface ElectronAPI { mcp: { getAgents: () => Promise>; addToAgentConfig: (agentId: string) => Promise<{ success: boolean; configPath: string; error?: string }>; + removeFromAgentConfig: (agentId: string) => Promise<{ success: boolean; configPath: string; error?: string }>; isConfigured: (agentId: string) => Promise; getPort: () => Promise; }; diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index ec776a2..07d97a2 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -138,7 +138,7 @@ const MCPStatusBadge: React.FC = () => { ); }; -/** Button to add bDS MCP server to an agent's config. Shows "Configured" if already present. */ +/** Button to add/remove bDS MCP server to/from an agent's config. */ const MCPAgentButton: React.FC<{ agentId: string; agentLabel: string }> = ({ agentId, agentLabel }) => { const { t } = useI18n(); const [configured, setConfigured] = React.useState(false); @@ -149,7 +149,30 @@ const MCPAgentButton: React.FC<{ agentId: string; agentLabel: string }> = ({ age }, [agentId]); if (configured) { - return {t('settings.mcp.alreadyConfigured')}; + return ( + + ); } return ( diff --git a/src/renderer/i18n/locales/de.json b/src/renderer/i18n/locales/de.json index 8c70719..746dafe 100644 --- a/src/renderer/i18n/locales/de.json +++ b/src/renderer/i18n/locales/de.json @@ -988,8 +988,11 @@ "settings.mcp.agentsTitle": "Agenten-Konfiguration", "settings.mcp.agentsDescription": "Fügen Sie den bDS MCP-Server zur Konfiguration Ihres Programmieragenten hinzu. Vorhandene Einstellungen bleiben erhalten.", "settings.mcp.addToAgent": "Zu {agent} hinzufügen", + "settings.mcp.removeFromAgent": "Aus {agent} entfernen", "settings.mcp.alreadyConfigured": "Konfiguriert", "settings.toast.mcpConfigSuccess": "bDS MCP-Server zur {agent}-Konfiguration hinzugefügt", + "settings.toast.mcpConfigRemoveSuccess": "bDS MCP-Server aus der {agent}-Konfiguration entfernt", "settings.toast.mcpConfigFailed": "Konfiguration von {agent} fehlgeschlagen: {error}", + "settings.toast.mcpConfigRemoveFailed": "Entfernen aus {agent} fehlgeschlagen: {error}", "settings.toast.mcpConfigPath": "Konfiguration geschrieben nach {path}" } diff --git a/src/renderer/i18n/locales/en.json b/src/renderer/i18n/locales/en.json index 9e6e02b..6481573 100644 --- a/src/renderer/i18n/locales/en.json +++ b/src/renderer/i18n/locales/en.json @@ -988,8 +988,11 @@ "settings.mcp.agentsTitle": "Agent Configuration", "settings.mcp.agentsDescription": "Add the bDS MCP server to your coding agent's configuration. Existing settings are preserved.", "settings.mcp.addToAgent": "Add to {agent}", + "settings.mcp.removeFromAgent": "Remove from {agent}", "settings.mcp.alreadyConfigured": "Configured", "settings.toast.mcpConfigSuccess": "bDS MCP server added to {agent} configuration", + "settings.toast.mcpConfigRemoveSuccess": "bDS MCP server removed from {agent} configuration", "settings.toast.mcpConfigFailed": "Failed to configure {agent}: {error}", + "settings.toast.mcpConfigRemoveFailed": "Failed to remove from {agent}: {error}", "settings.toast.mcpConfigPath": "Config written to {path}" } diff --git a/src/renderer/i18n/locales/es.json b/src/renderer/i18n/locales/es.json index 97ba1f7..1c509c5 100644 --- a/src/renderer/i18n/locales/es.json +++ b/src/renderer/i18n/locales/es.json @@ -988,8 +988,11 @@ "settings.mcp.agentsTitle": "Configuración de agentes", "settings.mcp.agentsDescription": "Añada el servidor MCP de bDS a la configuración de su agente de programación. Las configuraciones existentes se conservan.", "settings.mcp.addToAgent": "Añadir a {agent}", + "settings.mcp.removeFromAgent": "Eliminar de {agent}", "settings.mcp.alreadyConfigured": "Configurado", "settings.toast.mcpConfigSuccess": "Servidor MCP de bDS añadido a la configuración de {agent}", + "settings.toast.mcpConfigRemoveSuccess": "Servidor MCP de bDS eliminado de la configuración de {agent}", "settings.toast.mcpConfigFailed": "Error al configurar {agent}: {error}", + "settings.toast.mcpConfigRemoveFailed": "Error al eliminar de {agent}: {error}", "settings.toast.mcpConfigPath": "Configuración escrita en {path}" } diff --git a/src/renderer/i18n/locales/fr.json b/src/renderer/i18n/locales/fr.json index cb15a92..943d8bc 100644 --- a/src/renderer/i18n/locales/fr.json +++ b/src/renderer/i18n/locales/fr.json @@ -986,8 +986,11 @@ "settings.mcp.agentsTitle": "Configuration des agents", "settings.mcp.agentsDescription": "Ajoutez le serveur MCP bDS à la configuration de votre agent de programmation. Les paramètres existants sont préservés.", "settings.mcp.addToAgent": "Ajouter à {agent}", + "settings.mcp.removeFromAgent": "Retirer de {agent}", "settings.mcp.alreadyConfigured": "Configuré", "settings.toast.mcpConfigSuccess": "Serveur MCP bDS ajouté à la configuration de {agent}", + "settings.toast.mcpConfigRemoveSuccess": "Serveur MCP bDS retiré de la configuration de {agent}", "settings.toast.mcpConfigFailed": "Échec de la configuration de {agent}: {error}", + "settings.toast.mcpConfigRemoveFailed": "Échec du retrait de {agent}: {error}", "settings.toast.mcpConfigPath": "Configuration écrite dans {path}" } diff --git a/src/renderer/i18n/locales/it.json b/src/renderer/i18n/locales/it.json index eda6768..f7f4cd9 100644 --- a/src/renderer/i18n/locales/it.json +++ b/src/renderer/i18n/locales/it.json @@ -986,8 +986,11 @@ "settings.mcp.agentsTitle": "Configurazione agenti", "settings.mcp.agentsDescription": "Aggiungi il server MCP bDS alla configurazione del tuo agente di programmazione. Le impostazioni esistenti vengono preservate.", "settings.mcp.addToAgent": "Aggiungi a {agent}", + "settings.mcp.removeFromAgent": "Rimuovi da {agent}", "settings.mcp.alreadyConfigured": "Configurato", "settings.toast.mcpConfigSuccess": "Server MCP bDS aggiunto alla configurazione di {agent}", + "settings.toast.mcpConfigRemoveSuccess": "Server MCP bDS rimosso dalla configurazione di {agent}", "settings.toast.mcpConfigFailed": "Configurazione di {agent} non riuscita: {error}", + "settings.toast.mcpConfigRemoveFailed": "Rimozione da {agent} non riuscita: {error}", "settings.toast.mcpConfigPath": "Configurazione scritta in {path}" } diff --git a/tests/renderer/components/SettingsView.test.tsx b/tests/renderer/components/SettingsView.test.tsx index d88dad9..b5c585e 100644 --- a/tests/renderer/components/SettingsView.test.tsx +++ b/tests/renderer/components/SettingsView.test.tsx @@ -1,9 +1,76 @@ import React from 'react'; import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { SettingsView } from '../../../src/renderer/components/SettingsView/SettingsView'; import { useAppStore } from '../../../src/renderer/store'; +describe('MCPAgentButton uninstall', () => { + beforeEach(() => { + vi.clearAllMocks(); + useAppStore.setState({ + activeProject: { + id: 'p1', + name: 'Test', + slug: 'test', + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + gitDiffPreferences: { wordWrap: true, viewStyle: 'inline', hideUnchangedRegions: false }, + }); + + (window as any).electronAPI = { + ...(window as any).electronAPI, + app: { getDefaultProjectPath: vi.fn().mockResolvedValue('/repo') }, + meta: { + getCategories: vi.fn().mockResolvedValue(['article']), + getProjectMetadata: vi.fn().mockResolvedValue({ + maxPostsPerPage: 75, + publicUrl: 'https://example.com', + categorySettings: { article: { renderInLists: true, showTitle: true } }, + }), + updateProjectMetadata: vi.fn().mockResolvedValue({}), + }, + chat: { + getSystemPrompt: vi.fn().mockResolvedValue({ success: true, prompt: '' }), + getApiKey: vi.fn().mockResolvedValue({ hasKey: false, maskedKey: '' }), + getAvailableModels: vi.fn().mockResolvedValue({ success: true, models: [], selectedModel: '' }), + }, + templates: { getEnabledByKind: vi.fn().mockResolvedValue([]) }, + projects: { update: vi.fn().mockResolvedValue({}) }, + mcp: { + getAgents: vi.fn().mockResolvedValue([ + { id: 'claude-code', label: 'Claude Code' }, + ]), + addToAgentConfig: vi.fn().mockResolvedValue({ success: true, configPath: '/p' }), + removeFromAgentConfig: vi.fn().mockResolvedValue({ success: true, configPath: '/p' }), + isConfigured: vi.fn().mockResolvedValue(true), + getPort: vi.fn().mockResolvedValue(4124), + }, + }; + }); + + it('shows an uninstall button when agent is already configured', async () => { + render(); + const btn = await screen.findByRole('button', { name: /remove from claude code/i }); + expect(btn).toBeInTheDocument(); + }); + + it('calls removeFromAgentConfig and shows add button after removal', async () => { + render(); + const btn = await screen.findByRole('button', { name: /remove from claude code/i }); + + await act(async () => { + fireEvent.click(btn); + }); + + expect((window as any).electronAPI.mcp.removeFromAgentConfig).toHaveBeenCalledWith('claude-code'); + + const addBtn = await screen.findByRole('button', { name: /add to claude code/i }); + expect(addBtn).toBeInTheDocument(); + }); +}); + describe('SettingsView Diff Preferences', () => { let updateProjectMock: ReturnType;