Skip to content

Commit d6ee74a

Browse files
🤖 feat: add ability to disable MCP servers (#1091)
Adds a toggle to disable MCP servers without removing them from config. Disabled servers retain their configuration but don't inject tools into conversations. **Why:** Tool definitions consume context tokens. When you don't need a server's tools for a particular task, disabling it avoids unnecessarily blowing up the context. This mirrors similar functionality in Claude Code. **Changes:** - Switch toggle in project settings (left side of each server row) - Config format: string when enabled, `{ command, disabled: true }` when disabled - Backwards compatible with existing configs - Optimistic UI updates --- _Generated with `mux`_
1 parent b2d2806 commit d6ee74a

File tree

16 files changed

+274
-25
lines changed

16 files changed

+274
-25
lines changed

.github/actions/setup-mux/action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ runs:
4444
restore-keys: |
4545
${{ runner.os }}-bun-cache-
4646
47+
# Always run install if we didn't get an exact cache hit.
48+
# This handles restore-key matches which may have stale/incomplete node_modules.
4749
- name: Install dependencies
48-
if: steps.check-node-modules.outputs.exists != 'true'
50+
if: steps.cache-node-modules.outputs.cache-hit != 'true'
4951
shell: bash
5052
run: bun install --frozen-lockfile
5153

bun.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@radix-ui/react-select": "^2.2.6",
3030
"@radix-ui/react-separator": "^1.1.7",
3131
"@radix-ui/react-slot": "^1.2.4",
32+
"@radix-ui/react-switch": "^1.2.6",
3233
"@radix-ui/react-tabs": "^1.1.13",
3334
"@radix-ui/react-toggle-group": "^1.1.11",
3435
"@radix-ui/react-tooltip": "^1.2.8",
@@ -898,6 +899,8 @@
898899

899900
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
900901

902+
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
903+
901904
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
902905

903906
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
@@ -3784,6 +3787,8 @@
37843787

37853788
"@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
37863789

3790+
"@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
3791+
37873792
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
37883793

37893794
"@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -4244,6 +4249,8 @@
42444249

42454250
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
42464251

4252+
"@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
4253+
42474254
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
42484255

42494256
"@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],

mobile/bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"lockfileVersion": 1,
3+
"configVersion": 0,
34
"workspaces": {
45
"": {
56
"name": "@coder/mux-mobile",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@radix-ui/react-select": "^2.2.6",
7070
"@radix-ui/react-separator": "^1.1.7",
7171
"@radix-ui/react-slot": "^1.2.4",
72+
"@radix-ui/react-switch": "^1.2.6",
7273
"@radix-ui/react-tabs": "^1.1.13",
7374
"@radix-ui/react-toggle-group": "^1.1.11",
7475
"@radix-ui/react-tooltip": "^1.2.8",

src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import {
2222
SelectValue,
2323
} from "@/browser/components/ui/select";
2424
import { createEditKeyHandler } from "@/browser/utils/ui/keybinds";
25+
import { Switch } from "@/browser/components/ui/switch";
26+
import { cn } from "@/common/lib/utils";
2527
import { formatRelativeTime } from "@/browser/utils/ui/dateTime";
26-
import type { CachedMCPTestResult } from "@/common/types/mcp";
28+
import type { CachedMCPTestResult, MCPServerInfo } from "@/common/types/mcp";
2729
import { getMCPTestResultsKey } from "@/common/constants/storage";
2830
import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
2931

@@ -83,7 +85,7 @@ export const ProjectSettingsSection: React.FC = () => {
8385

8486
// Core state
8587
const [selectedProject, setSelectedProject] = useState<string>("");
86-
const [servers, setServers] = useState<Record<string, string>>({});
88+
const [servers, setServers] = useState<Record<string, MCPServerInfo>>({});
8789
const [loading, setLoading] = useState(false);
8890
const [error, setError] = useState<string | null>(null);
8991

@@ -156,6 +158,40 @@ export const ProjectSettingsSection: React.FC = () => {
156158
[api, selectedProject, refresh, clearTestResult]
157159
);
158160

161+
const handleToggleEnabled = useCallback(
162+
async (name: string, enabled: boolean) => {
163+
if (!api || !selectedProject) return;
164+
// Optimistic update
165+
setServers((prev) => ({
166+
...prev,
167+
[name]: { ...prev[name], disabled: !enabled },
168+
}));
169+
try {
170+
const result = await api.projects.mcp.setEnabled({
171+
projectPath: selectedProject,
172+
name,
173+
enabled,
174+
});
175+
if (!result.success) {
176+
// Revert on error
177+
setServers((prev) => ({
178+
...prev,
179+
[name]: { ...prev[name], disabled: enabled },
180+
}));
181+
setError(result.error ?? "Failed to update server");
182+
}
183+
} catch (err) {
184+
// Revert on error
185+
setServers((prev) => ({
186+
...prev,
187+
[name]: { ...prev[name], disabled: enabled },
188+
}));
189+
setError(err instanceof Error ? err.message : "Failed to update server");
190+
}
191+
},
192+
[api, selectedProject]
193+
);
194+
159195
const handleTest = useCallback(
160196
async (name: string) => {
161197
if (!api || !selectedProject) return;
@@ -320,24 +356,34 @@ export const ProjectSettingsSection: React.FC = () => {
320356
<p className="text-muted-foreground py-4 text-sm">No MCP servers configured yet.</p>
321357
) : (
322358
<ul className="space-y-2">
323-
{Object.entries(servers).map(([name, command]) => {
359+
{Object.entries(servers).map(([name, entry]) => {
324360
const isTesting = testingServer === name;
325361
const cached = testCache[name];
326362
const isEditing = editing?.name === name;
363+
const isEnabled = !entry.disabled;
327364
return (
328365
<li key={name} className="border-border-medium bg-secondary/20 rounded-lg border p-3">
329-
<div className="flex items-start justify-between gap-3">
330-
<div className="min-w-0 flex-1">
366+
<div className="flex items-start gap-3">
367+
<Switch
368+
checked={isEnabled}
369+
onCheckedChange={(checked) => void handleToggleEnabled(name, checked)}
370+
title={isEnabled ? "Disable server" : "Enable server"}
371+
className="mt-0.5 shrink-0"
372+
/>
373+
<div className={cn("min-w-0 flex-1", !isEnabled && "opacity-50")}>
331374
<div className="flex items-center gap-2">
332375
<span className="font-medium">{name}</span>
333-
{cached?.result.success && !isEditing && (
376+
{cached?.result.success && !isEditing && isEnabled && (
334377
<span
335378
className="rounded bg-green-500/10 px-1.5 py-0.5 text-xs text-green-500"
336379
title={`Tested ${formatRelativeTime(cached.testedAt)}`}
337380
>
338381
{cached.result.tools.length} tools
339382
</span>
340383
)}
384+
{!isEnabled && (
385+
<span className="text-muted-foreground text-xs">disabled</span>
386+
)}
341387
</div>
342388
{isEditing ? (
343389
<input
@@ -354,11 +400,11 @@ export const ProjectSettingsSection: React.FC = () => {
354400
/>
355401
) : (
356402
<p className="text-muted-foreground mt-0.5 font-mono text-xs break-all">
357-
{command}
403+
{entry.command}
358404
</p>
359405
)}
360406
</div>
361-
<div className="flex shrink-0 gap-1">
407+
<div className="flex shrink-0 items-center gap-1">
362408
{isEditing ? (
363409
<>
364410
<Button
@@ -405,7 +451,7 @@ export const ProjectSettingsSection: React.FC = () => {
405451
<Button
406452
variant="ghost"
407453
size="icon"
408-
onClick={() => handleStartEdit(name, command)}
454+
onClick={() => handleStartEdit(name, entry.command)}
409455
className="text-muted hover:text-accent h-7 w-7"
410456
title="Edit command"
411457
>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as React from "react";
2+
import * as SwitchPrimitives from "@radix-ui/react-switch";
3+
4+
import { cn } from "@/common/lib/utils";
5+
6+
const Switch = React.forwardRef<
7+
React.ElementRef<typeof SwitchPrimitives.Root>,
8+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
9+
>(({ className, ...props }, ref) => (
10+
<SwitchPrimitives.Root
11+
className={cn(
12+
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=unchecked]:bg-zinc-600",
13+
className
14+
)}
15+
{...props}
16+
ref={ref}
17+
>
18+
<SwitchPrimitives.Thumb
19+
className={cn(
20+
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
21+
)}
22+
/>
23+
</SwitchPrimitives.Root>
24+
));
25+
Switch.displayName = SwitchPrimitives.Root.displayName;
26+
27+
export { Switch };

src/common/orpc/schemas.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ export { MuxProviderOptionsSchema } from "./schemas/providerOptions";
4040

4141
// MCP schemas
4242
export {
43-
MCPServerMapSchema,
4443
MCPAddParamsSchema,
4544
MCPRemoveParamsSchema,
45+
MCPServerMapSchema,
46+
MCPSetEnabledParamsSchema,
4647
MCPTestParamsSchema,
4748
MCPTestResultSchema,
4849
} from "./schemas/mcp";

src/common/orpc/schemas/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
MCPAddParamsSchema,
2020
MCPRemoveParamsSchema,
2121
MCPServerMapSchema,
22+
MCPSetEnabledParamsSchema,
2223
MCPTestParamsSchema,
2324
MCPTestResultSchema,
2425
} from "./mcp";
@@ -153,6 +154,10 @@ export const projects = {
153154
input: MCPTestParamsSchema,
154155
output: MCPTestResultSchema,
155156
},
157+
setEnabled: {
158+
input: MCPSetEnabledParamsSchema,
159+
output: ResultSchema(z.void(), z.string()),
160+
},
156161
},
157162
secrets: {
158163
get: {

src/common/orpc/schemas/mcp.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { z } from "zod";
22

3-
export const MCPServerMapSchema = z.record(z.string(), z.string());
4-
53
export const MCPAddParamsSchema = z.object({
64
projectPath: z.string(),
75
name: z.string(),
@@ -13,6 +11,17 @@ export const MCPRemoveParamsSchema = z.object({
1311
name: z.string(),
1412
});
1513

14+
export const MCPSetEnabledParamsSchema = z.object({
15+
projectPath: z.string(),
16+
name: z.string(),
17+
enabled: z.boolean(),
18+
});
19+
20+
export const MCPServerMapSchema = z.record(
21+
z.string(),
22+
z.object({ command: z.string(), disabled: z.boolean() })
23+
);
24+
1625
/**
1726
* Unified test params - provide either name (to test configured server) or command (to test arbitrary command).
1827
* At least one of name or command must be provided.

src/common/types/mcp.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
/** Normalized server info (always has disabled field) */
2+
export interface MCPServerInfo {
3+
command: string;
4+
disabled: boolean;
5+
}
6+
17
export interface MCPConfig {
2-
servers: Record<string, string>;
8+
servers: Record<string, MCPServerInfo>;
39
}
410

11+
/** Internal map of server name → command string (used after filtering disabled) */
512
export type MCPServerMap = Record<string, string>;
613

714
/** Result of testing an MCP server connection */

0 commit comments

Comments
 (0)