feat(mcp): add option to mirror MCP config to other app
- Add syncOtherSide parameter to upsert_mcp_server_in_config command - Implement checkbox UI in McpFormModal for cross-app sync - Automatically sync enabled MCP servers to both Claude and Codex when option is checked - Add i18n support for sync option labels and hints
This commit is contained in:
@@ -841,13 +841,46 @@ pub async fn upsert_mcp_server_in_config(
|
|||||||
app: Option<String>,
|
app: Option<String>,
|
||||||
id: String,
|
id: String,
|
||||||
spec: serde_json::Value,
|
spec: serde_json::Value,
|
||||||
|
sync_other_side: Option<bool>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let mut cfg = state
|
let mut cfg = state
|
||||||
.config
|
.config
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
let app_ty = crate::app_config::AppType::from(app.as_deref().unwrap_or("claude"));
|
let app_ty = crate::app_config::AppType::from(app.as_deref().unwrap_or("claude"));
|
||||||
let changed = crate::mcp::upsert_in_config_for(&mut cfg, &app_ty, &id, spec)?;
|
let mut sync_targets: Vec<crate::app_config::AppType> = Vec::new();
|
||||||
|
|
||||||
|
let changed = crate::mcp::upsert_in_config_for(&mut cfg, &app_ty, &id, spec.clone())?;
|
||||||
|
|
||||||
|
let should_sync_current = cfg
|
||||||
|
.mcp_for(&app_ty)
|
||||||
|
.servers
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|entry| entry.get("enabled"))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if should_sync_current {
|
||||||
|
sync_targets.push(app_ty.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if sync_other_side.unwrap_or(false) {
|
||||||
|
let other_app = match app_ty.clone() {
|
||||||
|
crate::app_config::AppType::Claude => crate::app_config::AppType::Codex,
|
||||||
|
crate::app_config::AppType::Codex => crate::app_config::AppType::Claude,
|
||||||
|
};
|
||||||
|
crate::mcp::upsert_in_config_for(&mut cfg, &other_app, &id, spec)?;
|
||||||
|
|
||||||
|
let should_sync_other = cfg
|
||||||
|
.mcp_for(&other_app)
|
||||||
|
.servers
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|entry| entry.get("enabled"))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if should_sync_other {
|
||||||
|
sync_targets.push(other_app.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
state.save()?;
|
state.save()?;
|
||||||
|
|
||||||
@@ -855,18 +888,11 @@ pub async fn upsert_mcp_server_in_config(
|
|||||||
.config
|
.config
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
let should_sync = cfg2
|
for app_ty_to_sync in sync_targets {
|
||||||
.mcp_for(&app_ty)
|
match app_ty_to_sync {
|
||||||
.servers
|
|
||||||
.get(&id)
|
|
||||||
.and_then(|entry| entry.get("enabled"))
|
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false);
|
|
||||||
if should_sync {
|
|
||||||
match app_ty {
|
|
||||||
crate::app_config::AppType::Claude => crate::mcp::sync_enabled_to_claude(&cfg2)?,
|
crate::app_config::AppType::Claude => crate::mcp::sync_enabled_to_claude(&cfg2)?,
|
||||||
crate::app_config::AppType::Codex => crate::mcp::sync_enabled_to_codex(&cfg2)?,
|
crate::app_config::AppType::Codex => crate::mcp::sync_enabled_to_codex(&cfg2)?,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ interface McpFormModalProps {
|
|||||||
appType: AppType;
|
appType: AppType;
|
||||||
editingId?: string;
|
editingId?: string;
|
||||||
initialData?: McpServer;
|
initialData?: McpServer;
|
||||||
onSave: (id: string, server: McpServer) => Promise<void>;
|
onSave: (
|
||||||
|
id: string,
|
||||||
|
server: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
|
) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
existingIds?: string[];
|
existingIds?: string[];
|
||||||
onNotify?: (
|
onNotify?: (
|
||||||
@@ -113,9 +117,16 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const [idError, setIdError] = useState("");
|
const [idError, setIdError] = useState("");
|
||||||
|
const [syncOtherSide, setSyncOtherSide] = useState(false);
|
||||||
|
|
||||||
// 判断是否使用 TOML 格式
|
// 判断是否使用 TOML 格式
|
||||||
const useToml = appType === "codex";
|
const useToml = appType === "codex";
|
||||||
|
const syncTargetLabel =
|
||||||
|
appType === "claude" ? t("apps.codex") : t("apps.claude");
|
||||||
|
const syncCheckboxId = useMemo(
|
||||||
|
() => `sync-other-side-${appType}`,
|
||||||
|
[appType],
|
||||||
|
);
|
||||||
|
|
||||||
const wizardInitialSpec = useMemo(() => {
|
const wizardInitialSpec = useMemo(() => {
|
||||||
const fallback = initialData?.server;
|
const fallback = initialData?.server;
|
||||||
@@ -432,7 +443,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显式等待父组件保存流程
|
// 显式等待父组件保存流程
|
||||||
await onSave(trimmedId, entry);
|
await onSave(trimmedId, entry, { syncOtherSide });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const detail = extractErrorMessage(error);
|
const detail = extractErrorMessage(error);
|
||||||
const mapped = translateMcpBackendError(detail, t);
|
const mapped = translateMcpBackendError(detail, t);
|
||||||
@@ -655,6 +666,28 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 双端同步选项 */}
|
||||||
|
<div className="mt-4 flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40">
|
||||||
|
<input
|
||||||
|
id={syncCheckboxId}
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
checked={syncOtherSide}
|
||||||
|
onChange={(event) => setSyncOtherSide(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={syncCheckboxId}
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<span className="font-medium">
|
||||||
|
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
@@ -135,10 +135,16 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (id: string, server: McpServer) => {
|
const handleSave = async (
|
||||||
|
id: string,
|
||||||
|
server: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const payload: McpServer = { ...server, id };
|
const payload: McpServer = { ...server, id };
|
||||||
await window.api.upsertMcpServerInConfig(appType, id, payload);
|
await window.api.upsertMcpServerInConfig(appType, id, payload, {
|
||||||
|
syncOtherSide: options?.syncOtherSide,
|
||||||
|
});
|
||||||
await reload();
|
await reload();
|
||||||
setIsFormOpen(false);
|
setIsFormOpen(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
|
|||||||
@@ -298,7 +298,9 @@
|
|||||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
"tomlConfig": "TOML Configuration",
|
"tomlConfig": "TOML Configuration",
|
||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "Config Wizard"
|
"useWizard": "Config Wizard",
|
||||||
|
"syncOtherSide": "Mirror to {{target}}",
|
||||||
|
"syncOtherSideHint": "Apply the same settings to {{target}}; existing entries with the same id will be overwritten."
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP Configuration Wizard",
|
"title": "MCP Configuration Wizard",
|
||||||
|
|||||||
@@ -298,7 +298,9 @@
|
|||||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
"tomlConfig": "TOML 配置",
|
"tomlConfig": "TOML 配置",
|
||||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
"useWizard": "配置向导"
|
"useWizard": "配置向导",
|
||||||
|
"syncOtherSide": "同步到 {{target}}",
|
||||||
|
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖"
|
||||||
},
|
},
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "MCP 配置向导",
|
"title": "MCP 配置向导",
|
||||||
|
|||||||
@@ -354,13 +354,18 @@ export const tauriAPI = {
|
|||||||
app: AppType = "claude",
|
app: AppType = "claude",
|
||||||
id: string,
|
id: string,
|
||||||
spec: McpServer,
|
spec: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke<boolean>("upsert_mcp_server_in_config", {
|
const payload = {
|
||||||
app,
|
app,
|
||||||
id,
|
id,
|
||||||
spec,
|
spec,
|
||||||
});
|
...(options?.syncOtherSide !== undefined
|
||||||
|
? { syncOtherSide: options.syncOtherSide }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
return await invoke<boolean>("upsert_mcp_server_in_config", payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("写入 MCP(config.json)失败:", error);
|
console.error("写入 MCP(config.json)失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -84,6 +84,7 @@ declare global {
|
|||||||
app: AppType | undefined,
|
app: AppType | undefined,
|
||||||
id: string,
|
id: string,
|
||||||
spec: McpServer,
|
spec: McpServer,
|
||||||
|
options?: { syncOtherSide?: boolean },
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
deleteMcpServerInConfig: (
|
deleteMcpServerInConfig: (
|
||||||
app: AppType | undefined,
|
app: AppType | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user