From c113ec1d00c06aef92622df5c3932b20c9b4330a Mon Sep 17 00:00:00 2001 From: Randy Lu Date: Tue, 7 Oct 2025 18:53:06 +0800 Subject: [PATCH] prevent duplicated mcp name --- CLAUDE.md | 2 +- src-tauri/capabilities/default.json | 3 +- src-tauri/src/commands.rs | 53 ++++++++++++++++ src-tauri/src/lib.rs | 2 + src-tauri/tauri.conf.json | 3 +- src/lib/query.ts | 24 ++++++++ src/pages/MCPPage.tsx | 95 +++++++++++++++++++++++++---- 7 files changed, 168 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7a9f0de..4260117 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ The app handles these configuration file types: - Place React Query/mutation logic in `src/lib/query.ts` by default - Write Tauri commands in `src-tauri/src/commands.rs` with well-designed names - Do not separate components into smaller files unless explicitly requested -- Use `pnpm tsc --noEmit` to check TypeScript lint errors after modifying frontend code +- Use `pnpm tsc --noEmit` instead of `pnpm tauri dev` to check TypeScript lint errors after modifying frontend code ## Important Notes diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 89fd8e8..f72c456 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ "core:default", "opener:default", "core:window:allow-start-dragging", - "dialog:allow-ask" + "dialog:allow-ask", + "dialog:allow-message" ] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9a2093e..a6de551 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -629,6 +629,12 @@ pub async fn get_global_mcp_servers() -> Result Result { + let mcp_servers = get_global_mcp_servers().await?; + Ok(mcp_servers.contains_key(&server_name)) +} + #[tauri::command] pub async fn update_global_mcp_server( server_name: String, @@ -669,6 +675,53 @@ pub async fn update_global_mcp_server( Ok(()) } +#[tauri::command] +pub async fn delete_global_mcp_server(server_name: String) -> Result<(), String> { + let home_dir = dirs::home_dir().ok_or("Could not find home directory")?; + let claude_json_path = home_dir.join(".claude.json"); + + if !claude_json_path.exists() { + return Err("Claude configuration file does not exist".to_string()); + } + + // Read existing .claude.json + let content = std::fs::read_to_string(&claude_json_path) + .map_err(|e| format!("Failed to read .claude.json: {}", e))?; + + let mut json_value: Value = serde_json::from_str(&content) + .map_err(|e| format!("Failed to parse .claude.json: {}", e))?; + + // Check if mcpServers exists + let mcp_servers = json_value + .as_object_mut() + .unwrap() + .get_mut("mcpServers") + .and_then(|servers| servers.as_object_mut()) + .ok_or("No mcpServers found in .claude.json")?; + + // Check if the server exists + if !mcp_servers.contains_key(&server_name) { + return Err(format!("MCP server '{}' not found", server_name)); + } + + // Remove the server + mcp_servers.remove(&server_name); + + // If mcpServers is now empty, we can optionally remove the entire mcpServers object + if mcp_servers.is_empty() { + json_value.as_object_mut().unwrap().remove("mcpServers"); + } + + // Write back to file + let json_content = serde_json::to_string_pretty(&json_value) + .map_err(|e| format!("Failed to serialize JSON: {}", e))?; + + std::fs::write(&claude_json_path, json_content) + .map_err(|e| format!("Failed to write .claude.json: {}", e))?; + + Ok(()) +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct UpdateInfo { pub available: bool, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5890e62..4f31b2f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -159,6 +159,8 @@ pub fn run() { open_config_path, get_global_mcp_servers, update_global_mcp_server, + delete_global_mcp_server, + check_mcp_server_exists, check_for_updates, install_and_restart, rebuild_tray_menu_command diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3c28811..52b2fb8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,7 +25,8 @@ } ], "security": { - "csp": null + "csp": null, + "capabilities": ["default"] } }, "bundle": { diff --git a/src/lib/query.ts b/src/lib/query.ts index 693d43e..a191f30 100644 --- a/src/lib/query.ts +++ b/src/lib/query.ts @@ -290,6 +290,30 @@ export const useAddGlobalMcpServer = () => { }); }; +export const useCheckMcpServerExists = (serverName: string, options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: ["check-mcp-server-exists", serverName], + queryFn: () => invoke("check_mcp_server_exists", { serverName }), + enabled: options?.enabled !== false && !!serverName, // Only run when serverName is provided + }); +}; + +export const useDeleteGlobalMcpServer = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (serverName: string) => invoke("delete_global_mcp_server", { serverName }), + onSuccess: () => { + toast.success("MCP server deleted successfully"); + queryClient.invalidateQueries({ queryKey: ["global-mcp-servers"] }); + }, + onError: (error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + toast.error(`Failed to delete MCP server: ${errorMessage}`); + }, + }); +}; + // Helper function to rebuild tray menu const rebuildTrayMenu = async () => { try { diff --git a/src/pages/MCPPage.tsx b/src/pages/MCPPage.tsx index 3a78da8..9df7e34 100644 --- a/src/pages/MCPPage.tsx +++ b/src/pages/MCPPage.tsx @@ -2,15 +2,17 @@ import { useState, Suspense } from "react"; import { Button } from "@/components/ui/button"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { ExternalLinkIcon, HammerIcon, PlusIcon, SaveIcon } from "lucide-react"; -import { useGlobalMcpServers, useUpdateGlobalMcpServer, type McpServer } from "@/lib/query"; +import { ExternalLinkIcon, HammerIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react"; +import { useGlobalMcpServers, useUpdateGlobalMcpServer, useAddGlobalMcpServer, useDeleteGlobalMcpServer, type McpServer } from "@/lib/query"; import { toast } from "sonner"; import { openUrl } from "@tauri-apps/plugin-opener"; +import { ask, message } from "@tauri-apps/plugin-dialog"; import { match } from "ts-pattern"; function MCPPageContent() { const { data: mcpServers } = useGlobalMcpServers(); const updateMcpServer = useUpdateGlobalMcpServer(); + const deleteMcpServer = useDeleteGlobalMcpServer(); const [serverConfigs, setServerConfigs] = useState>({}); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -36,6 +38,18 @@ function MCPPageContent() { } }; + const handleDeleteServer = async (serverName: string) => { + // Show confirmation dialog + const confirmed = await ask( + `Are you sure you want to delete the MCP server "${serverName}"? This action cannot be undone.`, + { title: "Delete MCP Server", kind: "warning" } + ); + + if (confirmed) { + deleteMcpServer.mutate(serverName); + } + }; + const formatConfigForDisplay = (server: McpServer): string => { return JSON.stringify(server, null, 2); @@ -104,7 +118,7 @@ function MCPPageContent() { className="min-h-[100px] w-full p-2 text-md outline-none resize-none bg-white rounded-lg" spellCheck={false} /> -
+
+ +
@@ -163,11 +186,14 @@ const builtInMcpServers = [ }, { name: "github", - source: "https://github.com/github/github-mcp-server", + source: "https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-claude.md", description: "GitHub's official MCP Server", prefill: `"github": { "type": "http", - "url": "https://api.githubcopilot.com/mcp/" + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer " + } }` } ]; @@ -210,16 +236,63 @@ function MCPCreatePanel() { } function RecommendMCPPanel() { + const addMcpServer = useAddGlobalMcpServer(); + const { data: mcpServers } = useGlobalMcpServers(); + + const handleAddMcpServer = async (mcpServer: typeof builtInMcpServers[0]) => { + try { + // Check if MCP server already exists using cached data + const exists = mcpServers && Object.keys(mcpServers).includes(mcpServer.name); + + if (exists) { + await message(`MCP server "${mcpServer.name}" already exists`, { + title: "MCP Server Exists", + kind: "info" + }); + return; + } + + // Show confirmation dialog + const confirmed = await ask( + `Do you want to add the ${mcpServer.name} MCP server?`, + { title: "Add MCP Server", kind: "info" } + ); + + if (confirmed) { + // Parse the prefill JSON to get the config object + const configObject = JSON.parse(`{${mcpServer.prefill}}`); + + addMcpServer.mutate({ + serverName: mcpServer.name, + serverConfig: configObject[mcpServer.name] + }); + } + } catch (error) { + console.error("Failed to add MCP server:", error); + // Only show toast for unexpected errors, not duplicate server errors + if (error instanceof Error && !error.message.includes("already exists")) { + toast.error("Failed to add MCP server"); + } + } + }; + return ( )