Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,10 @@ function TabRuntime({
(raw: string) => sendRpc({ cmd: "mcp_specs_retry", raw }),
[sendRpc],
);
const toggleMcpSpec = useCallback(
(raw: string) => sendRpc({ cmd: "mcp_specs_toggle", raw }),
[sendRpc],
);
const newChat = useCallback(() => {
clearAbortDraft();
resetPromptHistoryNav();
Expand Down Expand Up @@ -2891,6 +2895,7 @@ function TabRuntime({
onRemoveMcpSpec={removeMcpSpec}
onUpdateMcpSpec={updateMcpSpec}
onRetryMcpSpec={retryMcpSpec}
onToggleMcpSpec={toggleMcpSpec}
onReadMemory={(path) => sendRpc({ cmd: "memory_read", path })}
/>
) : null}
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,8 @@ export const en = {
hideTools: "Hide tools",
availableTools: "{count} available tools",
noTools: "Connected, but this server did not expose callable tools",
toggleOn: "ON",
toggleOff: "OFF",
},
jobs: {
title: "Background jobs",
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,8 @@ export const zhCN: typeof en = {
hideTools: "收起工具",
availableTools: "{count} 个可用工具",
noTools: "已连接,但该服务器未暴露可调用工具",
toggleOn: "开启",
toggleOff: "关闭",
},
contextPanel: {
reservedKey: "预留",
Expand Down
1 change: 1 addition & 0 deletions desktop/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ export type OutgoingCommand = { tabId?: string } & (
| { cmd: "mcp_import_servers"; servers: ImportedMcpServer[] }
| { cmd: "mcp_specs_update"; raw: string; server: ImportedMcpServer }
| { cmd: "mcp_specs_retry"; raw: string }
| { cmd: "mcp_specs_toggle"; raw: string }
| { cmd: "skills_get" }
| { cmd: "skill_run"; name: string; args?: string }
| { cmd: "jobs_list" }
Expand Down
57 changes: 57 additions & 0 deletions desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5718,6 +5718,63 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.mcp-server-card .rot {
transform: rotate(180deg);
}
.mcp-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.mcp-toggle.on {
background: color-mix(in srgb, var(--success) 15%, transparent);
border-color: color-mix(in srgb, var(--success) 40%, var(--border));
}
.mcp-toggle.off {
background: var(--bg-2);
border-color: var(--border);
}
.mcp-toggle:hover {
border-color: color-mix(in srgb, var(--fg) 30%, var(--border));
}
.mcp-toggle-track {
position: relative;
width: 28px;
height: 16px;
border-radius: 8px;
background: var(--bg-3);
transition: background 0.2s ease;
}
.mcp-toggle.on .mcp-toggle-track {
background: var(--success);
}
.mcp-toggle.off .mcp-toggle-track {
background: var(--muted);
}
.mcp-toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: white;
transition: transform 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.mcp-toggle.on .mcp-toggle-thumb {
transform: translateX(12px);
}
.mcp-toggle.off .mcp-toggle-thumb {
transform: translateX(0);
}
.mcp-toggle-label {
font-size: 11px;
font-weight: 500;
color: var(--fg);
}
.mcp-tools-detail {
margin-top: 10px;
padding: 10px;
Expand Down
18 changes: 18 additions & 0 deletions desktop/src/ui/mcp-server-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export function McpServerCard({
onEdit,
onRetry,
onRemove,
onToggle,
}: {
spec: McpSpecInfo;
mode?: McpServerCardMode;
onEdit?: (spec: McpSpecInfo) => void;
onRetry?: (raw: string) => void;
onRemove?: (raw: string) => void;
onToggle?: (raw: string) => void;
}) {
const [toolsOpen, setToolsOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
Expand All @@ -66,6 +68,7 @@ export function McpServerCard({
spec.status === "failed" && spec.statusReason
? `${statusText}\n${spec.statusReason}`
: statusText;
const isDisabled = spec.status === "disabled";

return (
<div className="scard mcp-server-card" data-mode={mode} data-status={spec.status}>
Expand All @@ -85,6 +88,21 @@ export function McpServerCard({
</Tooltip>
</div>
<div className="mcp-card-actions">
{mode === "settings" && onToggle && !spec.parseError ? (
<button
type="button"
className={`btn ghost mcp-toggle ${isDisabled ? "off" : "on"}`}
onClick={() => onToggle(spec.raw)}
title={isDisabled ? t("mcpCard.toggleOn") : t("mcpCard.toggleOff")}
>
<span className="mcp-toggle-track">
<span className="mcp-toggle-thumb" />
</span>
<span className="mcp-toggle-label">
{isDisabled ? t("mcpCard.toggleOff") : t("mcpCard.toggleOn")}
</span>
</button>
) : null}
{canExpandTools ? (
<button type="button" className="btn ghost" onClick={() => setToolsOpen((v) => !v)}>
<I.chev size={13} className={toolsOpen ? "rot" : ""} />
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/ui/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function SettingsModal({
onRemoveMcpSpec,
onUpdateMcpSpec,
onRetryMcpSpec,
onToggleMcpSpec,
onReadMemory,
}: {
settings: SettingsType;
Expand Down Expand Up @@ -129,6 +130,7 @@ export function SettingsModal({
onRemoveMcpSpec: (spec: string) => void;
onUpdateMcpSpec: (raw: string, server: ImportedMcpServer) => void;
onRetryMcpSpec: (raw: string) => void;
onToggleMcpSpec: (raw: string) => void;
onReadMemory: (path: string) => void;
}) {
const [page, setPage] = useState<PageId>(initialPage ?? "general");
Expand Down Expand Up @@ -213,6 +215,7 @@ export function SettingsModal({
onRemove={onRemoveMcpSpec}
onUpdate={onUpdateMcpSpec}
onRetry={onRetryMcpSpec}
onToggle={onToggleMcpSpec}
/>
)}
{page === "skills" && (
Expand Down Expand Up @@ -1146,6 +1149,7 @@ function PageMCP({
onRemove,
onUpdate,
onRetry,
onToggle,
}: {
specs: McpSpecInfo[];
bridged: boolean;
Expand All @@ -1156,6 +1160,7 @@ function PageMCP({
onRemove: (spec: string) => void;
onUpdate: (raw: string, server: ImportedMcpServer) => void;
onRetry: (raw: string) => void;
onToggle: (raw: string) => void;
}) {
const [draft, setDraft] = useState("");
const [importing, setImporting] = useState(false);
Expand Down Expand Up @@ -1268,6 +1273,7 @@ function PageMCP({
onEdit={setEditing}
onRetry={onRetry}
onRemove={onRemove}
onToggle={onToggle}
/>
))
)}
Expand Down
43 changes: 43 additions & 0 deletions src/cli/commands/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ type InMessage = { tabId?: string } & (
| { cmd: "mcp_import_servers"; servers: unknown[] }
| { cmd: "mcp_specs_update"; raw: string; server: unknown }
| { cmd: "mcp_specs_retry"; raw: string }
| { cmd: "mcp_specs_toggle"; raw: string }
| { cmd: "skills_get" }
| { cmd: "skill_run"; name: string; args?: string }
| { cmd: "jobs_list" }
Expand Down Expand Up @@ -3051,6 +3052,48 @@ export async function desktopCommand(opts: DesktopOptions): Promise<void> {
}
return;
}
if (msg.cmd === "mcp_specs_toggle") {
try {
const cfg = readConfig();
const parsed = parseMcpSpec(msg.raw);
const isCurrentlyDisabled =
(parsed.name && cfg.mcpServers?.[parsed.name]?.disabled === true) ||
(parsed.name && (cfg.mcpDisabled ?? []).includes(parsed.name));
const serverCfg = parsed.name ? cfg.mcpServers?.[parsed.name] : undefined;
if (parsed.name && serverCfg) {
serverCfg.disabled = !isCurrentlyDisabled;
if (!serverCfg.disabled) serverCfg.disabled = undefined;
if (Object.keys(serverCfg).length === 0 && cfg.mcpServers) {
delete cfg.mcpServers[parsed.name];
}
if (cfg.mcpServers && Object.keys(cfg.mcpServers).length === 0) {
cfg.mcpServers = undefined;
}
} else {
const disabled = new Set(cfg.mcpDisabled ?? []);
if (parsed.name && isCurrentlyDisabled) {
disabled.delete(parsed.name);
} else if (parsed.name) {
disabled.add(parsed.name);
}
cfg.mcpDisabled = disabled.size > 0 ? [...disabled].sort() : undefined;
}
writeConfig(cfg);
if (isCurrentlyDisabled) {
tab.mcpStatuses.delete(msg.raw);
} else {
tab.mcpStatuses.set(msg.raw, { kind: "disabled" });
}
emitMcpSpecs(tab);
void tab.mcpRuntime?.removeSpec(msg.raw, tab.runtime?.loop).catch(() => {});
if (!isCurrentlyDisabled) {
void tab.mcpRuntime?.addSpec(msg.raw, tab.runtime?.loop).catch(() => {});
}
} catch (err) {
emit({ type: "$error", message: `mcp_specs_toggle: ${(err as Error).message}` }, tab.id);
}
return;
}
if (msg.cmd === "skills_get") {
emitSkills(tab);
return;
Expand Down