feat(mcp): app-aware MCP panel and Codex MCP sync to config.toml
- Make MCP panel app-aware; pass appType from App and call APIs with current app - Show active app in title: “MCP Management · Claude Code/Codex” - Add sync_enabled_to_codex: project enabled servers from SSOT to ~/.codex/config.toml as [mcp.servers.*] - Sync on enable/disable, delete, and provider switch (post live write) - Add Tauri command sync_enabled_mcp_to_codex and expose window.api.syncEnabledMcpToCodex() - Fix Rust borrow scopes in switch_provider to avoid E0502 - Add TS declarations for new Codex sync API
This commit is contained in:
@@ -314,16 +314,20 @@ pub async fn switch_provider(
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
let manager = config
|
// 为避免长期可变借用,尽快获取必要数据并缩小借用范围
|
||||||
.get_manager_mut(&app_type)
|
let provider = {
|
||||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
let manager = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
|
||||||
// 检查供应商是否存在
|
// 检查供应商是否存在
|
||||||
let provider = manager
|
let provider = manager
|
||||||
.providers
|
.providers
|
||||||
.get(&id)
|
.get(&id)
|
||||||
.ok_or_else(|| format!("供应商不存在: {}", id))?
|
.ok_or_else(|| format!("供应商不存在: {}", id))?
|
||||||
.clone();
|
.clone();
|
||||||
|
provider
|
||||||
|
};
|
||||||
|
|
||||||
// SSOT 切换:先回填 live 配置到当前供应商,然后从内存写入目标主配置
|
// SSOT 切换:先回填 live 配置到当前供应商,然后从内存写入目标主配置
|
||||||
match app_type {
|
match app_type {
|
||||||
@@ -331,7 +335,12 @@ pub async fn switch_provider(
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
// 回填:读取 live(auth.json + config.toml)写回当前供应商 settings_config
|
// 回填:读取 live(auth.json + config.toml)写回当前供应商 settings_config
|
||||||
if !manager.current.is_empty() {
|
if !{
|
||||||
|
let cur = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
cur.current.is_empty()
|
||||||
|
} {
|
||||||
let auth_path = codex_config::get_codex_auth_path();
|
let auth_path = codex_config::get_codex_auth_path();
|
||||||
let config_path = codex_config::get_codex_config_path();
|
let config_path = codex_config::get_codex_config_path();
|
||||||
if auth_path.exists() {
|
if auth_path.exists() {
|
||||||
@@ -353,7 +362,16 @@ pub async fn switch_provider(
|
|||||||
"config": config_str,
|
"config": config_str,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(cur) = manager.providers.get_mut(&manager.current) {
|
let cur_id2 = {
|
||||||
|
let m = config
|
||||||
|
.get_manager(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
m.current.clone()
|
||||||
|
};
|
||||||
|
let m = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
if let Some(cur) = m.providers.get_mut(&cur_id2) {
|
||||||
cur.settings_config = live;
|
cur.settings_config = live;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,10 +394,21 @@ pub async fn switch_provider(
|
|||||||
let settings_path = get_claude_settings_path();
|
let settings_path = get_claude_settings_path();
|
||||||
|
|
||||||
// 回填:读取 live settings.json 写回当前供应商 settings_config
|
// 回填:读取 live settings.json 写回当前供应商 settings_config
|
||||||
if settings_path.exists() && !manager.current.is_empty() {
|
if settings_path.exists() {
|
||||||
if let Ok(live) = read_json_file::<serde_json::Value>(&settings_path) {
|
let cur_id = {
|
||||||
if let Some(cur) = manager.providers.get_mut(&manager.current) {
|
let m = config
|
||||||
cur.settings_config = live;
|
.get_manager(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
m.current.clone()
|
||||||
|
};
|
||||||
|
if !cur_id.is_empty() {
|
||||||
|
if let Ok(live) = read_json_file::<serde_json::Value>(&settings_path) {
|
||||||
|
let m = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
if let Some(cur) = m.providers.get_mut(&cur_id) {
|
||||||
|
cur.settings_config = live;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,8 +423,18 @@ pub async fn switch_provider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新当前供应商
|
// 更新当前供应商(短借用范围)
|
||||||
manager.current = id;
|
{
|
||||||
|
let manager = config
|
||||||
|
.get_manager_mut(&app_type)
|
||||||
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
|
manager.current = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对 Codex:切换完成且释放可变借用后,再依据 SSOT 同步 MCP 到 config.toml
|
||||||
|
if let AppType::Codex = app_type {
|
||||||
|
crate::mcp::sync_enabled_to_codex(&config)?;
|
||||||
|
}
|
||||||
|
|
||||||
log::info!("成功切换到供应商: {}", provider.name);
|
log::info!("成功切换到供应商: {}", provider.name);
|
||||||
|
|
||||||
@@ -752,13 +791,14 @@ pub async fn delete_mcp_server_in_config(
|
|||||||
let existed = crate::mcp::delete_in_config_for(&mut cfg, &app_ty, &id)?;
|
let existed = crate::mcp::delete_in_config_for(&mut cfg, &app_ty, &id)?;
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
state.save()?;
|
state.save()?;
|
||||||
// 若删除的是 Claude 客户端的条目,则同步一次,确保启用项从 ~/.claude.json 中移除
|
// 若删除的是 Claude/Codex 客户端的条目,则同步一次,确保启用项从对应 live 配置中移除
|
||||||
if matches!(app_ty, crate::app_config::AppType::Claude) {
|
let cfg2 = state
|
||||||
let cfg2 = state
|
.config
|
||||||
.config
|
.lock()
|
||||||
.lock()
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
match app_ty {
|
||||||
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)?,
|
||||||
}
|
}
|
||||||
Ok(existed)
|
Ok(existed)
|
||||||
}
|
}
|
||||||
@@ -793,6 +833,17 @@ pub async fn sync_enabled_mcp_to_claude(state: State<'_, AppState>) -> Result<bo
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 手动同步:将启用的 MCP 投影到 ~/.codex/config.toml(不更改 config.json)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sync_enabled_mcp_to_codex(state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
let cfg = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
crate::mcp::sync_enabled_to_codex(&cfg)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// 从 ~/.claude.json 导入 MCP 定义到 config.json,返回变更数量
|
/// 从 ~/.claude.json 导入 MCP 定义到 config.json,返回变更数量
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn import_mcp_from_claude(state: State<'_, AppState>) -> Result<usize, String> {
|
pub async fn import_mcp_from_claude(state: State<'_, AppState>) -> Result<usize, String> {
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ pub fn run() {
|
|||||||
commands::delete_mcp_server_in_config,
|
commands::delete_mcp_server_in_config,
|
||||||
commands::set_mcp_enabled,
|
commands::set_mcp_enabled,
|
||||||
commands::sync_enabled_mcp_to_claude,
|
commands::sync_enabled_mcp_to_claude,
|
||||||
|
commands::sync_enabled_mcp_to_codex,
|
||||||
commands::import_mcp_from_claude,
|
commands::import_mcp_from_claude,
|
||||||
// ours: endpoint speed test + custom endpoint management
|
// ours: endpoint speed test + custom endpoint management
|
||||||
commands::test_api_endpoints,
|
commands::test_api_endpoints,
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ pub fn set_enabled_and_sync_for(
|
|||||||
sync_enabled_to_claude(config)?;
|
sync_enabled_to_claude(config)?;
|
||||||
}
|
}
|
||||||
AppType::Codex => {
|
AppType::Codex => {
|
||||||
// Codex 的 MCP 写入尚未实现(TOML 结构未定),此处先跳过
|
// 将启用项投影到 ~/.codex/config.toml
|
||||||
|
sync_enabled_to_codex(config)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@@ -160,3 +161,122 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result<usize, String>
|
|||||||
}
|
}
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将 config.json 中 Codex 的 enabled==true 项以 TOML 形式写入 ~/.codex/config.toml 的 [mcp.servers]
|
||||||
|
/// 策略:
|
||||||
|
/// - 读取现有 config.toml;若语法无效则报错,不尝试覆盖
|
||||||
|
/// - 重写根下的 `mcp` 节点(整体替换),其他节点保持不变
|
||||||
|
/// - 仅写入启用项;无启用项时移除 `mcp` 节点
|
||||||
|
pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), String> {
|
||||||
|
use toml::{value::Value as TomlValue, Table as TomlTable};
|
||||||
|
|
||||||
|
// 1) 收集启用项(Codex 维度)
|
||||||
|
let enabled = collect_enabled_servers(&config.mcp.codex);
|
||||||
|
|
||||||
|
// 2) 读取现有 config.toml 并解析为 Table(允许空文件)
|
||||||
|
let base_text = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||||
|
let mut root: TomlTable = if base_text.trim().is_empty() {
|
||||||
|
TomlTable::new()
|
||||||
|
} else {
|
||||||
|
toml::from_str::<TomlTable>(&base_text)
|
||||||
|
.map_err(|e| format!("解析 config.toml 失败: {}", e))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3) 构建 mcp.servers 表
|
||||||
|
if enabled.is_empty() {
|
||||||
|
// 无启用项:清理 mcp 节点
|
||||||
|
root.remove("mcp");
|
||||||
|
} else {
|
||||||
|
let mut servers_tbl = TomlTable::new();
|
||||||
|
|
||||||
|
for (id, spec) in enabled.iter() {
|
||||||
|
let mut s = TomlTable::new();
|
||||||
|
|
||||||
|
// 类型(缺省视为 stdio)
|
||||||
|
let typ = spec
|
||||||
|
.get("type")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("stdio");
|
||||||
|
s.insert("type".into(), TomlValue::String(typ.to_string()));
|
||||||
|
|
||||||
|
match typ {
|
||||||
|
"stdio" => {
|
||||||
|
let cmd = spec
|
||||||
|
.get("command")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
s.insert("command".into(), TomlValue::String(cmd));
|
||||||
|
|
||||||
|
if let Some(args) = spec.get("args").and_then(|v| v.as_array()) {
|
||||||
|
let arr = args
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(|x| TomlValue::String(x.to_string()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !arr.is_empty() {
|
||||||
|
s.insert("args".into(), TomlValue::Array(arr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cwd) = spec.get("cwd").and_then(|v| v.as_str()) {
|
||||||
|
if !cwd.trim().is_empty() {
|
||||||
|
s.insert("cwd".into(), TomlValue::String(cwd.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(env) = spec.get("env").and_then(|v| v.as_object()) {
|
||||||
|
let mut env_tbl = TomlTable::new();
|
||||||
|
for (k, v) in env.iter() {
|
||||||
|
if let Some(sv) = v.as_str() {
|
||||||
|
env_tbl.insert(k.clone(), TomlValue::String(sv.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !env_tbl.is_empty() {
|
||||||
|
s.insert("env".into(), TomlValue::Table(env_tbl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"http" => {
|
||||||
|
let url = spec
|
||||||
|
.get("url")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
s.insert("url".into(), TomlValue::String(url));
|
||||||
|
|
||||||
|
if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) {
|
||||||
|
let mut h_tbl = TomlTable::new();
|
||||||
|
for (k, v) in headers.iter() {
|
||||||
|
if let Some(sv) = v.as_str() {
|
||||||
|
h_tbl.insert(k.clone(), TomlValue::String(sv.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !h_tbl.is_empty() {
|
||||||
|
s.insert("headers".into(), TomlValue::Table(h_tbl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// 已在 validate_mcp_spec 保障,这里忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servers_tbl.insert(id.clone(), TomlValue::Table(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mcp_tbl = TomlTable::new();
|
||||||
|
mcp_tbl.insert("servers".into(), TomlValue::Table(servers_tbl));
|
||||||
|
|
||||||
|
// 覆盖写入 mcp 节点
|
||||||
|
root.insert("mcp".into(), TomlValue::Table(mcp_tbl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 序列化并写回 config.toml(仅改 TOML,不触碰 auth.json)
|
||||||
|
let new_text = toml::to_string(&TomlValue::Table(root))
|
||||||
|
.map_err(|e| format!("序列化 config.toml 失败: {}", e))?;
|
||||||
|
let path = crate::codex_config::get_codex_config_path();
|
||||||
|
crate::config::write_text_file(&path, &new_text)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ function App() {
|
|||||||
|
|
||||||
{isMcpOpen && (
|
{isMcpOpen && (
|
||||||
<McpPanel
|
<McpPanel
|
||||||
|
appType={activeApp}
|
||||||
onClose={() => setIsMcpOpen(false)}
|
onClose={() => setIsMcpOpen(false)}
|
||||||
onNotify={showNotification}
|
onNotify={showNotification}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { extractErrorMessage } from "../../utils/errorUtils";
|
|||||||
import { mcpPresets } from "../../config/mcpPresets";
|
import { mcpPresets } from "../../config/mcpPresets";
|
||||||
import McpToggle from "./McpToggle";
|
import McpToggle from "./McpToggle";
|
||||||
import { buttonStyles, cardStyles, cn } from "../../lib/styles";
|
import { buttonStyles, cardStyles, cn } from "../../lib/styles";
|
||||||
|
import { AppType } from "../../lib/tauri-api";
|
||||||
|
|
||||||
interface McpPanelProps {
|
interface McpPanelProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -17,13 +18,14 @@ interface McpPanelProps {
|
|||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration?: number,
|
duration?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
appType: AppType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP 管理面板
|
* MCP 管理面板
|
||||||
* 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行
|
* 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行
|
||||||
*/
|
*/
|
||||||
const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [servers, setServers] = useState<Record<string, McpServer>>({});
|
const [servers, setServers] = useState<Record<string, McpServer>>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -39,7 +41,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const cfg = await window.api.getMcpConfig("claude");
|
const cfg = await window.api.getMcpConfig(appType);
|
||||||
setServers(cfg.servers || {});
|
setServers(cfg.servers || {});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -49,11 +51,13 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
try {
|
try {
|
||||||
// 先从 ~/.claude.json 导入已存在的 MCP(设为 enabled=true)
|
// Claude:从 ~/.claude.json 导入已存在的 MCP(设为 enabled=true)
|
||||||
await window.api.importMcpFromClaude();
|
if (appType === "claude") {
|
||||||
|
await window.api.importMcpFromClaude();
|
||||||
|
}
|
||||||
|
|
||||||
// 读取现有 config.json 内容
|
// 读取现有 config.json 内容
|
||||||
const cfg = await window.api.getMcpConfig("claude");
|
const cfg = await window.api.getMcpConfig(appType);
|
||||||
const existing = cfg.servers || {};
|
const existing = cfg.servers || {};
|
||||||
|
|
||||||
// 将预设落库为禁用(若缺失)
|
// 将预设落库为禁用(若缺失)
|
||||||
@@ -64,7 +68,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
source: "preset",
|
source: "preset",
|
||||||
} as unknown as McpServer;
|
} as unknown as McpServer;
|
||||||
await window.api.upsertMcpServerInConfig("claude", p.id, seed);
|
await window.api.upsertMcpServerInConfig(appType, p.id, seed);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("MCP 初始化导入/落库失败(忽略继续)", e);
|
console.warn("MCP 初始化导入/落库失败(忽略继续)", e);
|
||||||
@@ -73,7 +77,8 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
setup();
|
setup();
|
||||||
}, []);
|
// appType 改变时重新初始化
|
||||||
|
}, [appType]);
|
||||||
|
|
||||||
const handleToggle = async (id: string, enabled: boolean) => {
|
const handleToggle = async (id: string, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
@@ -81,9 +86,9 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
if (!server) {
|
if (!server) {
|
||||||
const preset = mcpPresets.find((p) => p.id === id);
|
const preset = mcpPresets.find((p) => p.id === id);
|
||||||
if (!preset) return;
|
if (!preset) return;
|
||||||
await window.api.upsertMcpServerInConfig("claude", id, preset.server as McpServer);
|
await window.api.upsertMcpServerInConfig(appType, id, preset.server as McpServer);
|
||||||
}
|
}
|
||||||
await window.api.setMcpEnabled("claude", id, enabled);
|
await window.api.setMcpEnabled(appType, id, enabled);
|
||||||
await reload();
|
await reload();
|
||||||
onNotify?.(
|
onNotify?.(
|
||||||
enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"),
|
enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"),
|
||||||
@@ -117,7 +122,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
message: t("mcp.confirm.deleteMessage", { id }),
|
message: t("mcp.confirm.deleteMessage", { id }),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await window.api.deleteMcpServerInConfig("claude", id);
|
await window.api.deleteMcpServerInConfig(appType, id);
|
||||||
await reload();
|
await reload();
|
||||||
setConfirmDialog(null);
|
setConfirmDialog(null);
|
||||||
onNotify?.(t("mcp.msg.deleted"), "success", 1500);
|
onNotify?.(t("mcp.msg.deleted"), "success", 1500);
|
||||||
@@ -135,7 +140,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
|
|
||||||
const handleSave = async (id: string, server: McpServer) => {
|
const handleSave = async (id: string, server: McpServer) => {
|
||||||
try {
|
try {
|
||||||
await window.api.upsertMcpServerInConfig("claude", id, server);
|
await window.api.upsertMcpServerInConfig(appType, id, server);
|
||||||
await reload();
|
await reload();
|
||||||
setIsFormOpen(false);
|
setIsFormOpen(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
@@ -172,7 +177,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify }) => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{t("mcp.title")}
|
{t("mcp.title")} · {t(appType === "claude" ? "apps.claude" : "apps.codex")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -396,6 +396,16 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 手动同步:将启用的 MCP 投影到 ~/.codex/config.toml
|
||||||
|
syncEnabledMcpToCodex: async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("sync_enabled_mcp_to_codex");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("同步启用 MCP 到 config.toml 失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
importMcpFromClaude: async (): Promise<number> => {
|
importMcpFromClaude: async (): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
return await invoke<number>("import_mcp_from_claude");
|
return await invoke<number>("import_mcp_from_claude");
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -84,6 +84,7 @@ declare global {
|
|||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
syncEnabledMcpToClaude: () => Promise<boolean>;
|
syncEnabledMcpToClaude: () => Promise<boolean>;
|
||||||
|
syncEnabledMcpToCodex: () => Promise<boolean>;
|
||||||
importMcpFromClaude: () => Promise<number>;
|
importMcpFromClaude: () => Promise<number>;
|
||||||
testApiEndpoints: (
|
testApiEndpoints: (
|
||||||
urls: string[],
|
urls: string[],
|
||||||
|
|||||||
Reference in New Issue
Block a user