7 Commits

Author SHA1 Message Date
Jason
59644b29e6 chore: bump version to 3.5.1 2025-10-14 10:39:11 +08:00
Jason
5427ae04e4 feat(mcp): add overwrite warning and improve sync option visibility
- Move sync checkbox to footer alongside save/cancel buttons for better visibility
- Add real-time conflict detection to check for duplicate MCP IDs on other side
- Display amber warning icon when sync would overwrite existing config
- Add i18n keys for overwrite warning (zh: "将覆盖 {{target}} 中的同名配置")
- Update UI layout to use justify-between for better spacing
2025-10-14 10:22:57 +08:00
Jason
a2aa5f8434 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
2025-10-14 00:22:15 +08:00
Jason
06010ff78e feat(mcp): pre-fill wizard with existing configuration
Allow MCP wizard to load and edit existing server configuration:
- Parse current config (JSON/TOML) and pass to wizard as initial data
- Auto-detect server type (stdio/http) and populate form fields
- Convert objects (env/headers) to multi-line text format for editing
- Improves UX by avoiding manual re-entry of existing settings
2025-10-13 23:37:33 +08:00
Jason
e77eab2116 feat(mcp): auto-sync enabled MCP servers to app config on upsert
When upserting an MCP server that is enabled, automatically sync it to the corresponding app's live configuration (Claude or Codex) without requiring manual action.
2025-10-13 23:22:21 +08:00
Jason
ed9dd7bbc3 docs: update main interface screenshot 2025-10-13 11:38:29 +08:00
Jason
3d20245a80 docs: update README for v3.5.0 release
- Add v3.5.0 feature highlights (MCP management, import/export, speed testing)
- Update feature list section to v3.5.0
- Fix macOS download filename to .tar.gz (standardized naming)
- Add refactoring notice in contribution section
2025-10-13 11:30:23 +08:00
14 changed files with 263 additions and 41 deletions

View File

@@ -6,6 +6,10 @@
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
> **📢 重要通知**CC Switch 即将进行大规模重构,请暂缓提交新的 PR感谢理解与配合
> v3.5.0 :新增 **MCP 管理**、**配置导入/导出**、**端点速度测试**功能,完善国际化覆盖,新增 Longcat、kat-coder 预设,标准化发布文件命名规范。
> v3.4.0 :新增 i18next 国际化还有部分未完成、对新模型qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
> v3.3.0 VS Code Codex 插件一键配置/移除默认自动同步、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。(该 VS Code 写入功能已在 v3.4.x 停用)
@@ -16,15 +20,25 @@
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
## 功能特性v3.4.0
## 功能特性v3.5.0
- **国际化与语言切换**:内置 i18next默认显示中文可在设置中快速切换到英文界面文文案自动实时刷新。
- **MCP (Model Context Protocol) 管理**:完整的 MCP 服务器配置管理系统
- 支持 stdio 和 http 服务器类型,并提供命令校验
- 内置常用 MCP 服务器模板(如 mcp-fetch 等)
- 实时启用/禁用 MCP 服务器,原子文件写入防止配置损坏
- **配置导入/导出**:备份和恢复你的供应商配置
- 一键导出所有配置到 JSON 文件
- 导入配置时自动验证并备份,自动轮换备份(保留最近 10 个)
- 带有详细状态反馈的进度模态框
- **端点速度测试**:测试 API 端点响应时间
- 测量不同供应商端点的延迟,可视化连接质量指示器
- 帮助用户选择最快的供应商
- **国际化与语言切换**:完整的 i18next 国际化覆盖,默认显示中文,可在设置中快速切换到英文,界面文案自动实时刷新。
- **Claude 插件同步**:内置按钮可一键应用或恢复 Claude 插件配置,切换供应商后立即生效。
- **VS Code Codex 设置停用**:由于新版 Codex 插件无需修改 `settings.json`,应用不再写入 VS Code 设置,避免潜在冲突
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
- **供应商预设扩展**:新增 Longcat、kat-coder 等预设,更新 GLM 供应商配置至最新模型
- **系统托盘与窗口行为**窗口关闭可最小化到托盘macOS 支持托盘模式下隐藏/显示 Dock托盘切换时同步 Claude/Codex/插件状态。
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
- **UI 与安装体验优化**设置面板改为可滚动布局并加入保存图标按钮宽度与状态一致性加强Windows MSI 安装默认写入 per-user LocalAppData 并改进组件跟踪Windows 便携版现在指向最新 release 页面,不再自动更为为安装版
- **标准化发布命名**所有平台发布文件使用一致的版本标签命名macOS: `.tar.gz` / `.zip`Windows: `.msi` / `-Portable.zip`Linux: `.AppImage` / `.deb`
## 界面预览
@@ -211,7 +225,7 @@ cargo test
## 贡献
欢迎提交 Issue 和 Pull Request
欢迎提交 Issue 反馈问题和建议
## Star History

View File

@@ -1,6 +1,6 @@
{
"name": "cc-switch",
"version": "3.5.0",
"version": "3.5.1",
"description": "Claude Code & Codex 供应商切换工具",
"scripts": {
"dev": "pnpm tauri dev",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 200 KiB

2
src-tauri/Cargo.lock generated
View File

@@ -563,7 +563,7 @@ dependencies = [
[[package]]
name = "cc-switch"
version = "3.5.0"
version = "3.5.1"
dependencies = [
"chrono",
"dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.5.0"
version = "3.5.1"
description = "Claude Code & Codex 供应商配置管理工具"
authors = ["Jason Young"]
license = "MIT"

View File

@@ -841,15 +841,59 @@ pub async fn upsert_mcp_server_in_config(
app: Option<String>,
id: String,
spec: serde_json::Value,
sync_other_side: Option<bool>,
) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
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);
state.save()?;
let cfg2 = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
for app_ty_to_sync in sync_targets {
match app_ty_to_sync {
crate::app_config::AppType::Claude => crate::mcp::sync_enabled_to_claude(&cfg2)?,
crate::app_config::AppType::Codex => crate::mcp::sync_enabled_to_codex(&cfg2)?,
};
}
Ok(changed)
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch",
"version": "3.5.0",
"version": "3.5.1",
"identifier": "com.ccswitch.desktop",
"build": {
"frontendDist": "../dist",

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import React, { useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { X, Save, AlertCircle, ChevronDown, ChevronUp } from "lucide-react";
import { X, Save, AlertCircle, ChevronDown, ChevronUp, AlertTriangle } from "lucide-react";
import { McpServer, McpServerSpec } from "../../types";
import {
mcpPresets,
@@ -24,7 +24,11 @@ interface McpFormModalProps {
appType: AppType;
editingId?: string;
initialData?: McpServer;
onSave: (id: string, server: McpServer) => Promise<void>;
onSave: (
id: string,
server: McpServer,
options?: { syncOtherSide?: boolean },
) => Promise<void>;
onClose: () => void;
existingIds?: string[];
onNotify?: (
@@ -113,9 +117,65 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
const [saving, setSaving] = useState(false);
const [isWizardOpen, setIsWizardOpen] = useState(false);
const [idError, setIdError] = useState("");
const [syncOtherSide, setSyncOtherSide] = useState(false);
const [otherSideHasConflict, setOtherSideHasConflict] = useState(false);
// 判断是否使用 TOML 格式
const useToml = appType === "codex";
const syncTargetLabel =
appType === "claude" ? t("apps.codex") : t("apps.claude");
const otherAppType: AppType = appType === "claude" ? "codex" : "claude";
const syncCheckboxId = useMemo(
() => `sync-other-side-${appType}`,
[appType],
);
// 检测另一侧是否有同名 MCP
useEffect(() => {
const checkOtherSide = async () => {
const currentId = formId.trim();
if (!currentId) {
setOtherSideHasConflict(false);
return;
}
try {
const otherConfig = await window.api.getMcpConfig(otherAppType);
const hasConflict = Object.keys(otherConfig.servers || {}).includes(currentId);
setOtherSideHasConflict(hasConflict);
} catch (error) {
console.error("检查另一侧 MCP 配置失败:", error);
setOtherSideHasConflict(false);
}
};
checkOtherSide();
}, [formId, otherAppType]);
const wizardInitialSpec = useMemo(() => {
const fallback = initialData?.server;
if (!formConfig.trim()) {
return fallback;
}
if (useToml) {
try {
return tomlToMcpServer(formConfig);
} catch {
return fallback;
}
}
try {
const parsed = JSON.parse(formConfig);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as McpServerSpec;
}
return fallback;
} catch {
return fallback;
}
}, [formConfig, initialData, useToml]);
// 预设选择状态(仅新增模式显示;-1 表示自定义)
const [selectedPreset, setSelectedPreset] = useState<number | null>(
@@ -407,7 +467,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
}
// 显式等待父组件保存流程
await onSave(trimmedId, entry);
await onSave(trimmedId, entry, { syncOtherSide });
} catch (error: any) {
const detail = extractErrorMessage(error);
const mapped = translateMcpBackendError(detail, t);
@@ -633,7 +693,37 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</div>
{/* Footer */}
<div className="flex-shrink-0 flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
<div className="flex-shrink-0 flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
{/* 双端同步选项 */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<input
id={syncCheckboxId}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-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 cursor-pointer select-none"
title={t("mcp.form.syncOtherSideHint", { target: syncTargetLabel })}
>
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
</label>
</div>
{syncOtherSide && otherSideHasConflict && (
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
<AlertTriangle size={14} />
<span className="text-xs font-medium">
{t("mcp.form.willOverwriteWarning", { target: syncTargetLabel })}
</span>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex items-center gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium"
@@ -654,6 +744,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
</button>
</div>
</div>
</div>
{/* Wizard Modal */}
<McpWizardModal
@@ -661,6 +752,8 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
onClose={() => setIsWizardOpen(false)}
onApply={handleWizardApply}
onNotify={onNotify}
initialTitle={formId}
initialServer={wizardInitialSpec}
/>
</div>
);

View File

@@ -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 {
const payload: McpServer = { ...server, id };
await window.api.upsertMcpServerInConfig(appType, id, payload);
await window.api.upsertMcpServerInConfig(appType, id, payload, {
syncOtherSide: options?.syncOtherSide,
});
await reload();
setIsFormOpen(false);
setEditingId(null);

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { X, Save } from "lucide-react";
import { McpServerSpec } from "../../types";
@@ -13,6 +13,8 @@ interface McpWizardModalProps {
type: "success" | "error",
duration?: number,
) => void;
initialTitle?: string;
initialServer?: McpServerSpec;
}
/**
@@ -72,6 +74,8 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
onClose,
onApply,
onNotify,
initialTitle,
initialServer,
}) => {
const { t } = useTranslation();
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
@@ -162,6 +166,55 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
}
};
useEffect(() => {
if (!isOpen) return;
const title = initialTitle ?? "";
setWizardTitle(title);
const resolvedType =
initialServer?.type ??
(initialServer?.url ? "http" : "stdio");
setWizardType(resolvedType);
if (resolvedType === "http") {
setWizardUrl(initialServer?.url ?? "");
const headersCandidate = initialServer?.headers;
const headers =
headersCandidate && typeof headersCandidate === "object"
? headersCandidate
: undefined;
setWizardHeaders(
headers
? Object.entries(headers)
.map(([k, v]) => `${k}: ${v ?? ""}`)
.join("\n")
: "",
);
setWizardCommand("");
setWizardArgs("");
setWizardEnv("");
return;
}
setWizardCommand(initialServer?.command ?? "");
const argsValue = initialServer?.args;
setWizardArgs(Array.isArray(argsValue) ? argsValue.join("\n") : "");
const envCandidate = initialServer?.env;
const env =
envCandidate && typeof envCandidate === "object" ? envCandidate : undefined;
setWizardEnv(
env
? Object.entries(env)
.map(([k, v]) => `${k}=${v ?? ""}`)
.join("\n")
: "",
);
setWizardUrl("");
setWizardHeaders("");
}, [isOpen]);
if (!isOpen) return null;
const preview = generatePreview();

View File

@@ -298,7 +298,10 @@
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
"tomlConfig": "TOML Configuration",
"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.",
"willOverwriteWarning": "Will overwrite existing config in {{target}}"
},
"wizard": {
"title": "MCP Configuration Wizard",

View File

@@ -298,7 +298,10 @@
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
"tomlConfig": "TOML 配置",
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
"useWizard": "配置向导"
"useWizard": "配置向导",
"syncOtherSide": "同步到 {{target}}",
"syncOtherSideHint": "勾选后会把当前配置同时写入 {{target}},若存在同名配置将被覆盖",
"willOverwriteWarning": "将覆盖 {{target}} 中的同名配置"
},
"wizard": {
"title": "MCP 配置向导",

View File

@@ -354,13 +354,18 @@ export const tauriAPI = {
app: AppType = "claude",
id: string,
spec: McpServer,
options?: { syncOtherSide?: boolean },
): Promise<boolean> => {
try {
return await invoke<boolean>("upsert_mcp_server_in_config", {
const payload = {
app,
id,
spec,
});
...(options?.syncOtherSide !== undefined
? { syncOtherSide: options.syncOtherSide }
: {}),
};
return await invoke<boolean>("upsert_mcp_server_in_config", payload);
} catch (error) {
console.error("写入 MCPconfig.json失败:", error);
throw error;

1
src/vite-env.d.ts vendored
View File

@@ -84,6 +84,7 @@ declare global {
app: AppType | undefined,
id: string,
spec: McpServer,
options?: { syncOtherSide?: boolean },
) => Promise<boolean>;
deleteMcpServerInConfig: (
app: AppType | undefined,