mirror of
https://github.com/mfakbar127/Claude-Samurai
synced 2026-06-21 13:57:42 +00:00
prevent duplicated mcp name
This commit is contained in:
@@ -70,7 +70,7 @@ The app handles these configuration file types:
|
|||||||
- Place React Query/mutation logic in `src/lib/query.ts` by default
|
- 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
|
- Write Tauri commands in `src-tauri/src/commands.rs` with well-designed names
|
||||||
- Do not separate components into smaller files unless explicitly requested
|
- 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
|
## Important Notes
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"dialog:allow-ask"
|
"dialog:allow-ask",
|
||||||
|
"dialog:allow-message"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,6 +629,12 @@ pub async fn get_global_mcp_servers() -> Result<std::collections::HashMap<String
|
|||||||
Ok(result)
|
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]
|
#[tauri::command]
|
||||||
pub async fn update_global_mcp_server(
|
pub async fn update_global_mcp_server(
|
||||||
server_name: String,
|
server_name: String,
|
||||||
@@ -669,6 +675,53 @@ pub async fn update_global_mcp_server(
|
|||||||
Ok(())
|
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)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct UpdateInfo {
|
pub struct UpdateInfo {
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ pub fn run() {
|
|||||||
open_config_path,
|
open_config_path,
|
||||||
get_global_mcp_servers,
|
get_global_mcp_servers,
|
||||||
update_global_mcp_server,
|
update_global_mcp_server,
|
||||||
|
delete_global_mcp_server,
|
||||||
|
check_mcp_server_exists,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
install_and_restart,
|
install_and_restart,
|
||||||
rebuild_tray_menu_command
|
rebuild_tray_menu_command
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null,
|
||||||
|
"capabilities": ["default"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -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
|
// Helper function to rebuild tray menu
|
||||||
const rebuildTrayMenu = async () => {
|
const rebuildTrayMenu = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+84
-11
@@ -2,15 +2,17 @@ import { useState, Suspense } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { ExternalLinkIcon, HammerIcon, PlusIcon, SaveIcon } from "lucide-react";
|
import { ExternalLinkIcon, HammerIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react";
|
||||||
import { useGlobalMcpServers, useUpdateGlobalMcpServer, type McpServer } from "@/lib/query";
|
import { useGlobalMcpServers, useUpdateGlobalMcpServer, useAddGlobalMcpServer, useDeleteGlobalMcpServer, type McpServer } from "@/lib/query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
|
|
||||||
function MCPPageContent() {
|
function MCPPageContent() {
|
||||||
const { data: mcpServers } = useGlobalMcpServers();
|
const { data: mcpServers } = useGlobalMcpServers();
|
||||||
const updateMcpServer = useUpdateGlobalMcpServer();
|
const updateMcpServer = useUpdateGlobalMcpServer();
|
||||||
|
const deleteMcpServer = useDeleteGlobalMcpServer();
|
||||||
const [serverConfigs, setServerConfigs] = useState<Record<string, string>>({});
|
const [serverConfigs, setServerConfigs] = useState<Record<string, string>>({});
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
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 => {
|
const formatConfigForDisplay = (server: McpServer): string => {
|
||||||
return JSON.stringify(server, null, 2);
|
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"
|
className="min-h-[100px] w-full p-2 text-md outline-none resize-none bg-white rounded-lg"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<div className="flex bg-zinc-50">
|
<div className="flex justify-between bg-zinc-50">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleSaveConfig(serverName)}
|
onClick={() => handleSaveConfig(serverName)}
|
||||||
@@ -114,6 +128,15 @@ function MCPPageContent() {
|
|||||||
<SaveIcon size={14} className="" />
|
<SaveIcon size={14} className="" />
|
||||||
{updateMcpServer.isPending ? "Saving..." : "Save"}
|
{updateMcpServer.isPending ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteServer(serverName)}
|
||||||
|
disabled={deleteMcpServer.isPending}
|
||||||
|
>
|
||||||
|
<TrashIcon size={14} className="" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
@@ -163,11 +186,14 @@ const builtInMcpServers = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "github",
|
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",
|
description: "GitHub's official MCP Server",
|
||||||
prefill: `"github": {
|
prefill: `"github": {
|
||||||
"type": "http",
|
"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() {
|
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 (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-5">
|
<div className="grid grid-cols-3 gap-5">
|
||||||
{builtInMcpServers.map((mcpServer) => (
|
{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">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="font-bold text-primary">{mcpServer.name}</h3>
|
<h3 className="font-bold text-primary">{mcpServer.name}</h3>
|
||||||
<a onClick={e => {
|
<a
|
||||||
e.stopPropagation()
|
onClick={e => {
|
||||||
openUrl(mcpServer.source)
|
e.stopPropagation()
|
||||||
}} className="text-sm text-muted-foreground flex items-center gap-1 hover:underline">
|
openUrl(mcpServer.source)
|
||||||
|
}}
|
||||||
|
className="text-sm text-muted-foreground flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
<ExternalLinkIcon size={12} />
|
<ExternalLinkIcon size={12} />
|
||||||
Source
|
Source
|
||||||
</a>
|
</a>
|
||||||
@@ -234,7 +307,7 @@ function RecommendMCPPanel() {
|
|||||||
添加
|
添加
|
||||||
</Button> */}
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user