prevent duplicated mcp name

This commit is contained in:
Randy Lu
2025-10-07 18:53:06 +08:00
parent 3f909da0b3
commit c113ec1d00
7 changed files with 168 additions and 14 deletions
+1 -1
View File
@@ -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
+2 -1
View File
@@ -7,6 +7,7 @@
"core:default",
"opener:default",
"core:window:allow-start-dragging",
"dialog:allow-ask"
"dialog:allow-ask",
"dialog:allow-message"
]
}
+53
View File
@@ -629,6 +629,12 @@ pub async fn get_global_mcp_servers() -> Result<std::collections::HashMap<String
Ok(result)
}
#[tauri::command]
pub async fn check_mcp_server_exists(server_name: String) -> Result<bool, String> {
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,
+2
View File
@@ -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
+2 -1
View File
@@ -25,7 +25,8 @@
}
],
"security": {
"csp": null
"csp": null,
"capabilities": ["default"]
}
},
"bundle": {
+24
View File
@@ -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<boolean>("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<void>("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 {
+82 -9
View File
@@ -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<Record<string, string>>({});
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}
/>
<div className="flex bg-zinc-50">
<div className="flex justify-between bg-zinc-50">
<Button
variant="outline"
onClick={() => handleSaveConfig(serverName)}
@@ -114,6 +128,15 @@ function MCPPageContent() {
<SaveIcon size={14} className="" />
{updateMcpServer.isPending ? "Saving..." : "Save"}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteServer(serverName)}
disabled={deleteMcpServer.isPending}
>
<TrashIcon size={14} className="" />
</Button>
</div>
</div>
</AccordionContent>
@@ -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 <YOUR_GITHUB_TOKEN>"
}
}`
}
];
@@ -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 (
<div className="grid grid-cols-3 gap-5">
{builtInMcpServers.map((mcpServer) => (
<a role="button" key={mcpServer.name} className="border p-3 rounded-md h-[120px] flex justify-between flex-col hover:bg-primary/10 hover:border-primary/20 hover:text-primary">
<div
key={mcpServer.name}
className="border p-3 rounded-md h-[120px] flex justify-between flex-col hover:bg-primary/10 hover:border-primary/20 hover:text-primary cursor-pointer"
onClick={() => handleAddMcpServer(mcpServer)}
>
<div className="flex justify-between items-center">
<h3 className="font-bold text-primary">{mcpServer.name}</h3>
<a onClick={e => {
<a
onClick={e => {
e.stopPropagation()
openUrl(mcpServer.source)
}} className="text-sm text-muted-foreground flex items-center gap-1 hover:underline">
}}
className="text-sm text-muted-foreground flex items-center gap-1 hover:underline"
>
<ExternalLinkIcon size={12} />
Source
</a>
@@ -234,7 +307,7 @@ function RecommendMCPPanel() {
添加
</Button> */}
</div>
</a>
</div>
))}
</div>
)