Skip to content

Commit 8842702

Browse files
committed
feat: enhance MCP server management with new components and functionality
- Added CommandDisplay, McpServerTable, ServerActionButtons, ServerStatusSwitch, and SyncConfigDialog components for improved server management. - Implemented enable/disable functionality for MCP servers with corresponding Tauri commands. - Updated package.json and bun.lock to include new dependencies for Radix UI components. - Refactored manage page to utilize the new table component for displaying server configurations. - Improved error handling and user feedback with toast notifications during server actions.
1 parent c4ca574 commit 8842702

19 files changed

+1467
-230
lines changed

bun.lock

Lines changed: 62 additions & 17 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@
1818
},
1919
"dependencies": {
2020
"@hookform/resolvers": "^4.1.3",
21+
"@radix-ui/react-alert-dialog": "^1.1.14",
2122
"@radix-ui/react-checkbox": "^1.1.4",
22-
"@radix-ui/react-dialog": "^1.1.11",
23+
"@radix-ui/react-dialog": "^1.1.14",
2324
"@radix-ui/react-dropdown-menu": "^2.1.6",
2425
"@radix-ui/react-label": "^2.1.2",
2526
"@radix-ui/react-progress": "^1.1.7",
2627
"@radix-ui/react-select": "^2.1.6",
2728
"@radix-ui/react-slot": "^1.2.3",
28-
"@radix-ui/react-tabs": "^1.1.3",
29+
"@radix-ui/react-switch": "^1.2.5",
30+
"@radix-ui/react-tabs": "^1.1.12",
2931
"@radix-ui/react-tooltip": "^1.2.7",
3032
"@supabase/supabase-js": "^2.49.4",
3133
"@tanstack/react-query": "^5.69.0",
34+
"@tanstack/react-table": "^8.21.3",
3235
"@tauri-apps/api": "^2.5.0",
3336
"@tauri-apps/plugin-deep-link": "~2.3.0",
3437
"@tauri-apps/plugin-dialog": "^2.2.0",

src-tauri/src/cmd.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,81 @@ pub async fn update_mcp_server(
9393
JsonManager::update_mcp_server(file_path, &client_name, &server_name, server_config)
9494
}
9595

96+
#[tauri::command]
97+
pub async fn disable_mcp_server(
98+
client_name: String,
99+
path: Option<String>,
100+
server_name: String,
101+
) -> Result<Value, String> {
102+
let app_config = ClientConfig::new(&client_name, path.as_deref());
103+
let file_path = app_config.get_path();
104+
105+
JsonManager::disable_mcp_server(file_path, &client_name, &server_name)
106+
}
107+
108+
#[tauri::command]
109+
pub async fn enable_mcp_server(
110+
client_name: String,
111+
path: Option<String>,
112+
server_name: String,
113+
) -> Result<Value, String> {
114+
let app_config = ClientConfig::new(&client_name, path.as_deref());
115+
let file_path = app_config.get_path();
116+
117+
JsonManager::enable_mcp_server(file_path, &client_name, &server_name)
118+
}
119+
120+
#[tauri::command]
121+
pub async fn list_disabled_servers(
122+
client_name: String,
123+
path: Option<String>,
124+
) -> Result<Value, String> {
125+
let app_config = ClientConfig::new(&client_name, path.as_deref());
126+
let file_path = app_config.get_path();
127+
128+
JsonManager::list_disabled_servers(file_path)
129+
}
130+
96131
#[tauri::command]
97132
pub fn check_mcplinker_config_exists() -> bool {
98133
let home_dir = dirs::home_dir().unwrap_or_default();
99134
let config_path: PathBuf = home_dir.join(".config/mcplinker/mcp.json");
100135
config_path.exists()
101136
}
137+
138+
#[tauri::command]
139+
pub async fn sync_mcp_config(
140+
from_client: String,
141+
to_client: String,
142+
from_path: Option<String>,
143+
to_path: Option<String>,
144+
override_all: bool, // true 表示直接覆盖,false 表示 merge
145+
) -> Result<(), String> {
146+
let from_config = ClientConfig::new(&from_client, from_path.as_deref());
147+
let to_config = ClientConfig::new(&to_client, to_path.as_deref());
148+
149+
let from_path = from_config.get_path();
150+
let to_path = to_config.get_path();
151+
152+
let from_json = JsonManager::read_json_file(from_path)?;
153+
let mut to_json = JsonManager::read_json_file(to_path).unwrap_or_else(|_| json!({}));
154+
155+
let from_servers = from_json.get("mcpServers").cloned().unwrap_or(json!({}));
156+
let from_disabled = from_json.get("__disabled").cloned().unwrap_or(json!({}));
157+
158+
if override_all {
159+
to_json["mcpServers"] = from_servers;
160+
to_json["__disabled"] = from_disabled;
161+
} else {
162+
to_json["mcpServers"]
163+
.as_object_mut()
164+
.unwrap_or(&mut serde_json::Map::new())
165+
.extend(from_servers.as_object().unwrap_or(&serde_json::Map::new()).clone());
166+
to_json["__disabled"]
167+
.as_object_mut()
168+
.unwrap_or(&mut serde_json::Map::new())
169+
.extend(from_disabled.as_object().unwrap_or(&serde_json::Map::new()).clone());
170+
}
171+
172+
JsonManager::write_json_file(to_path, &to_json)
173+
}

src-tauri/src/json_manager.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,96 @@ impl JsonManager {
141141
// Normalize response key to mcpServers for client
142142
Self::normalize_response_key(json, client)
143143
}
144+
145+
pub fn disable_mcp_server(path: &Path, client: &str, name: &str) -> Result<Value, String> {
146+
let mut json = Self::read_json_file(path)?;
147+
let key = Self::get_key_by_client(client);
148+
149+
if !json.is_object() {
150+
return Err("Invalid JSON structure".to_string());
151+
}
152+
153+
// Check if server exists in active servers
154+
if !json.as_object().unwrap().contains_key(key)
155+
|| !json[key].is_object()
156+
|| !json[key].as_object().unwrap().contains_key(name)
157+
{
158+
return Err(format!("Server '{}' not found in active servers", name));
159+
}
160+
161+
// Get server config
162+
let server_config = json[key][name].clone();
163+
164+
// Remove from active servers
165+
json[key].as_object_mut().unwrap().remove(name);
166+
167+
// Ensure __disabled section exists
168+
if !json.as_object().unwrap().contains_key("__disabled") {
169+
json["__disabled"] = json!({});
170+
}
171+
172+
// Add to disabled section
173+
json["__disabled"][name] = server_config;
174+
175+
Self::write_json_file(path, &json)?;
176+
177+
// Normalize response key to mcpServers for client
178+
Self::normalize_response_key(json, client)
179+
}
180+
181+
pub fn enable_mcp_server(path: &Path, client: &str, name: &str) -> Result<Value, String> {
182+
let mut json = Self::read_json_file(path)?;
183+
let key = Self::get_key_by_client(client);
184+
185+
if !json.is_object() {
186+
return Err("Invalid JSON structure".to_string());
187+
}
188+
189+
// Check if server exists in disabled section
190+
if !json.as_object().unwrap().contains_key("__disabled")
191+
|| !json["__disabled"].is_object()
192+
|| !json["__disabled"].as_object().unwrap().contains_key(name)
193+
{
194+
return Err(format!("Server '{}' not found in disabled servers", name));
195+
}
196+
197+
// Get server config from disabled section
198+
let server_config = json["__disabled"][name].clone();
199+
200+
// Remove from disabled section
201+
json["__disabled"].as_object_mut().unwrap().remove(name);
202+
203+
// Remove empty __disabled section
204+
if json["__disabled"].as_object().unwrap().is_empty() {
205+
json.as_object_mut().unwrap().remove("__disabled");
206+
}
207+
208+
// Ensure active servers section exists
209+
if !json.as_object().unwrap().contains_key(key) {
210+
json[key] = json!({});
211+
}
212+
213+
// Check if server already exists in active servers
214+
if json[key].as_object().unwrap().contains_key(name) {
215+
return Err(format!("Server '{}' already exists in active servers", name));
216+
}
217+
218+
// Add to active servers
219+
json[key][name] = server_config;
220+
221+
Self::write_json_file(path, &json)?;
222+
223+
// Normalize response key to mcpServers for client
224+
Self::normalize_response_key(json, client)
225+
}
226+
227+
pub fn list_disabled_servers(path: &Path) -> Result<Value, String> {
228+
let json = Self::read_json_file(path)?;
229+
230+
if json.is_object() && json.as_object().unwrap().contains_key("__disabled") {
231+
Ok(json["__disabled"].clone())
232+
} else {
233+
Ok(json!({}))
234+
}
235+
}
144236
}

src-tauri/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ pub fn run() {
8282
cmd::add_mcp_server,
8383
cmd::remove_mcp_server,
8484
cmd::update_mcp_server,
85+
cmd::disable_mcp_server,
86+
cmd::enable_mcp_server,
87+
cmd::list_disabled_servers,
88+
cmd::sync_mcp_config,
8589
cmd::check_mcplinker_config_exists,
8690
installer::check_command_exists,
8791
installer::install_command,

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const queryClient = new QueryClient({
2828
function App() {
2929
const { triggerPendingDeepLink } = useUnifiedDeepLink();
3030
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
31-
const isTauri = window.__TAURI__ !== "undefined";
31+
const isTauri = typeof window.__TAURI__ !== "undefined";
3232

3333
useEffect(() => {
3434
if (import.meta.env.VITE_IS_CHECK_UPDATE === "true") {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Terminal } from "lucide-react";
2+
3+
interface CommandDisplayProps {
4+
config: {
5+
command?: string;
6+
args?: string[];
7+
url?: string;
8+
};
9+
}
10+
11+
export function CommandDisplay({ config }: CommandDisplayProps) {
12+
// Helper function to format command display
13+
const formatCommand = (config: any) => {
14+
if ("command" in config) {
15+
const fullCommand =
16+
config.args && config.args.length > 0
17+
? `${config.command} ${config.args.join(" ")}`
18+
: config.command;
19+
return fullCommand.length > 50
20+
? `${fullCommand.substring(0, 50)}...`
21+
: fullCommand;
22+
}
23+
if ("url" in config) {
24+
return config.url;
25+
}
26+
return "N/A";
27+
};
28+
29+
const isStdio = "command" in config;
30+
const command = formatCommand(config);
31+
32+
return (
33+
<div className="flex items-center gap-2 max-w-xs">
34+
{isStdio ? (
35+
<Terminal className="h-4 w-4 text-blue-500 flex-shrink-0" />
36+
) : (
37+
<div className="h-4 w-4 rounded-full bg-orange-500 flex-shrink-0" />
38+
)}
39+
<code className="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded truncate">
40+
{command}
41+
</code>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)