Skip to content

Commit 1996b75

Browse files
authored
🤖 feat: add MCP server configuration and runtime support (#956)
## Summary Adds MCP (Model Context Protocol) server support, allowing users to extend agent capabilities with external tool providers. ## Features - **Per-project configuration** — Servers configured in `.mux/mcp.jsonc` apply to all workspaces from that project - **Per-workspace runtime** — Each workspace runs isolated server instances with independent state - **Slash commands** — `/mcp add|remove|edit` for quick configuration from chat - **Settings UI** — Manage servers in Settings → Projects with test button and tool discovery - **Hot reload** — Config changes apply on next message without restart - **10-minute idle timeout** — Servers automatically stop after inactivity to conserve resources - **Namespaced tools** — Tools prefixed with server name (e.g., `memory_create_entities`) to prevent collisions ## Configuration ```jsonc // .mux/mcp.jsonc { "servers": { "memory": "npx -y @modelcontextprotocol/server-memory", "chrome": "npx -y chrome-devtools-mcp@latest --headless" } } ``` ## Security - Commands are **not** exposed in the system prompt (may contain secrets) - Only server names are shown to the model ## Files - `src/node/services/mcpServerManager.ts` — Runtime server lifecycle with idle cleanup - `src/node/services/mcpConfigService.ts` — Config CRUD operations - `src/node/services/mcpStdioTransport.ts` — NDJSON stdio transport - `src/browser/components/Settings/sections/ProjectSettingsSection.tsx` — Settings UI - `docs/mcp-servers.mdx` — User documentation _Generated with `mux`_
1 parent e223c1d commit 1996b75

35 files changed

+1903
-17
lines changed

‎bun.lock‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@ai-sdk/amazon-bedrock": "^3.0.61",
99
"@ai-sdk/anthropic": "^2.0.47",
1010
"@ai-sdk/google": "^2.0.43",
11+
"@ai-sdk/mcp": "^0.0.11",
1112
"@ai-sdk/openai": "^2.0.72",
1213
"@ai-sdk/xai": "^2.0.36",
1314
"@aws-sdk/credential-providers": "^3.940.0",
@@ -175,6 +176,8 @@
175176

176177
"@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
177178

179+
"@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-n0oZsxhPdMaXhAn6LrpMpxABufmeSatfhR3epbrCjJcLEHPOucKwQciwE8CTgIGgwBxHNy1FFLZmcFI77JmJfg=="],
180+
178181
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.76", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ryUkhTDVxe3D1GSAGc94vPZsJlSY8ZuBDLkpf4L81Dm7Ik5AgLfhQrZa8+0hD4kp0dxdVaIoxhpa3QOt1CmncA=="],
179182

180183
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.28", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yKubDxLYtXyGUzkr9lNStf/lE/I+Okc8tmotvyABhsQHHieLKk6oV5fJeRJxhr67Ejhg+FRnwUOxAmjRoFM4dA=="],
@@ -2989,6 +2992,8 @@
29892992

29902993
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
29912994

2995+
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
2996+
29922997
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
29932998

29942999
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],

‎docs/docs.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
},
5555
"context-management",
5656
"instruction-files",
57+
"mcp-servers",
5758
{
5859
"group": "Project Secrets",
5960
"pages": ["project-secrets", "agentic-git-identity"]

‎docs/mcp-servers.mdx‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
title: MCP Servers
3+
description: Extend agent capabilities with Model Context Protocol servers
4+
---
5+
6+
MCP (Model Context Protocol) servers provide additional tools to agents. Configure them per-project in `.mux/mcp.jsonc`.
7+
8+
## Configuration
9+
10+
Create `.mux/mcp.jsonc` in your project root:
11+
12+
```jsonc
13+
{
14+
"servers": {
15+
// Knowledge graph for persistent memory
16+
"memory": "npx -y @modelcontextprotocol/server-memory",
17+
// Browser automation and screenshots
18+
"chrome": "npx -y chrome-devtools-mcp@latest --headless",
19+
},
20+
}
21+
```
22+
23+
Each entry maps a server name to its shell command. The command must start a process that speaks MCP over stdio (NDJSON format).
24+
25+
## Slash Commands
26+
27+
Manage MCP servers directly from chat:
28+
29+
| Command | Description |
30+
| ---------------------------- | ----------------------------------- |
31+
| `/mcp add <name> <command>` | Add a new MCP server |
32+
| `/mcp remove <name>` | Remove an MCP server |
33+
| `/mcp edit <name> <command>` | Update an existing server's command |
34+
35+
Examples:
36+
37+
```
38+
/mcp add memory npx -y @modelcontextprotocol/server-memory
39+
/mcp add chrome npx -y chrome-devtools-mcp@latest --headless
40+
/mcp remove github
41+
/mcp edit chrome npx -y chrome-devtools-mcp@latest --headless --isolated
42+
```
43+
44+
## Settings UI
45+
46+
Configure servers in **Settings → Projects**:
47+
48+
1. Select a project from the dropdown
49+
2. Add servers with name and command
50+
3. Use **Test** (play button) to verify before adding
51+
4. Use **Edit** (pencil) or **Remove** (trash) to manage existing servers
52+
53+
## Scope
54+
55+
MCP servers have two scopes:
56+
57+
- **Configuration** is per-project — The `.mux/mcp.jsonc` file lives in your project root and applies to all workspaces created from that project
58+
- **Runtime instances** are per-workspace — Each workspace runs its own server processes, so state in one workspace doesn't affect another
59+
60+
This means you configure servers once per project, but each workspace (branch) gets isolated server instances with independent state.
61+
62+
## Behavior
63+
64+
- **Hot reload** — Config changes apply on your next message (no restart needed)
65+
- **Isolated** — Server processes run in the workspace directory with its environment
66+
- **Lazy start** — Servers start when you send your first message in a workspace
67+
- **Idle timeout** — Servers stop after 10 minutes of inactivity to conserve resources, then restart automatically when needed
68+
69+
## Finding MCP Servers
70+
71+
Browse available servers at [mcp.so](https://mcp.so/) or the [MCP servers repository](https://github.com/modelcontextprotocol/servers).
72+
73+
## Troubleshooting
74+
75+
If a server fails to start:
76+
77+
1. **Test the command manually** — Run the command in your terminal to verify it works
78+
2. **Check dependencies** — Ensure required packages are installed (`npx -y` downloads on first run)
79+
3. **Use the Test button** — Settings → Projects shows connection errors inline

‎docs/system-prompt.mdx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ You are in a git worktree at ${workspacePath}
5959
</environment>
6060
`;
6161
}
62+
63+
/**
64+
* Build MCP servers context XML block.
65+
* Only included when at least one MCP server is configured.
66+
* Note: We only expose server names, not commands, to avoid leaking secrets.
67+
*/
68+
function buildMCPContext(mcpServers: MCPServerMap): string {
69+
const names = Object.keys(mcpServers);
70+
if (names.length === 0) return "";
71+
72+
const serverList = names.map((name) => `- ${name}`).join("\n");
73+
74+
return `
75+
<mcp>
76+
MCP (Model Context Protocol) servers provide additional tools. Configured in user's local project's .mux/mcp.jsonc:
77+
78+
${serverList}
79+
80+
Use /mcp add|edit|remove or Settings → Projects to manage servers.
81+
</mcp>
82+
`;
83+
}
6284
```
6385

6486
{/* END SYSTEM_PROMPT_DOCS */}

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@ai-sdk/amazon-bedrock": "^3.0.61",
4949
"@ai-sdk/anthropic": "^2.0.47",
5050
"@ai-sdk/google": "^2.0.43",
51+
"@ai-sdk/mcp": "^0.0.11",
5152
"@ai-sdk/openai": "^2.0.72",
5253
"@ai-sdk/xai": "^2.0.36",
5354
"@aws-sdk/credential-providers": "^3.940.0",

‎src/browser/components/ChatInput/index.tsx‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { ChatInputToast } from "../ChatInputToast";
1414
import { createCommandToast, createErrorToast } from "../ChatInputToasts";
1515
import { parseCommand } from "@/browser/utils/slashCommands/parser";
1616
import { usePersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
17+
import { useSettings } from "@/browser/contexts/SettingsContext";
18+
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
1719
import { useMode } from "@/browser/contexts/ModeContext";
1820
import { ThinkingSliderComponent } from "../ThinkingSlider";
1921
import { ModelSettings } from "../ModelSettings";
@@ -167,6 +169,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
167169
[setInput]
168170
);
169171
const preEditDraftRef = useRef<DraftState>({ text: "", images: [] });
172+
const { open } = useSettings();
173+
const { selectedWorkspace } = useWorkspaceContext();
170174
const [mode, setMode] = useMode();
171175
const { recentModels, addModel, defaultModel, setDefaultModel } = useModelLRU();
172176
const commandListId = useId();
@@ -730,6 +734,82 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
730734
}
731735

732736
// Handle /vim command
737+
if (parsed.type === "mcp-open") {
738+
setInput("");
739+
open("project");
740+
return;
741+
}
742+
743+
if (
744+
parsed.type === "mcp-add" ||
745+
parsed.type === "mcp-edit" ||
746+
parsed.type === "mcp-remove"
747+
) {
748+
if (!api) {
749+
setToast({
750+
id: Date.now().toString(),
751+
type: "error",
752+
message: "Not connected to server",
753+
});
754+
return;
755+
}
756+
if (!selectedWorkspace?.projectPath) {
757+
setToast({
758+
id: Date.now().toString(),
759+
type: "error",
760+
message: "Select a workspace to manage MCP servers",
761+
});
762+
return;
763+
}
764+
765+
setIsSending(true);
766+
setInput("");
767+
try {
768+
const projectPath = selectedWorkspace.projectPath;
769+
const result =
770+
parsed.type === "mcp-add" || parsed.type === "mcp-edit"
771+
? await api.projects.mcp.add({
772+
projectPath,
773+
name: parsed.name,
774+
command: parsed.command,
775+
})
776+
: await api.projects.mcp.remove({ projectPath, name: parsed.name });
777+
778+
if (!result.success) {
779+
setToast({
780+
id: Date.now().toString(),
781+
type: "error",
782+
message: result.error ?? "Failed to update MCP servers",
783+
});
784+
setInput(messageText);
785+
} else {
786+
const successMessage =
787+
parsed.type === "mcp-add"
788+
? `Added MCP server ${parsed.name}`
789+
: parsed.type === "mcp-edit"
790+
? `Updated MCP server ${parsed.name}`
791+
: `Removed MCP server ${parsed.name}`;
792+
setToast({
793+
id: Date.now().toString(),
794+
type: "success",
795+
message: successMessage,
796+
});
797+
}
798+
} catch (error) {
799+
console.error("Failed to update MCP servers", error);
800+
setToast({
801+
id: Date.now().toString(),
802+
type: "error",
803+
message: error instanceof Error ? error.message : "Failed to update MCP servers",
804+
});
805+
setInput(messageText);
806+
} finally {
807+
setIsSending(false);
808+
}
809+
810+
return;
811+
}
812+
733813
if (parsed.type === "vim-toggle") {
734814
setInput(""); // Clear input immediately
735815
setVimEnabled((prev) => !prev);

‎src/browser/components/Settings/SettingsModal.tsx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React from "react";
2-
import { Settings, Key, Cpu, X } from "lucide-react";
2+
import { Settings, Key, Cpu, X, Briefcase } from "lucide-react";
33
import { useSettings } from "@/browser/contexts/SettingsContext";
44
import { Dialog, DialogContent, DialogTitle, VisuallyHidden } from "@/browser/components/ui/dialog";
55
import { GeneralSection } from "./sections/GeneralSection";
66
import { ProvidersSection } from "./sections/ProvidersSection";
77
import { ModelsSection } from "./sections/ModelsSection";
88
import { Button } from "@/browser/components/ui/button";
9+
import { ProjectSettingsSection } from "./sections/ProjectSettingsSection";
910
import type { SettingsSection } from "./types";
1011

1112
const SECTIONS: SettingsSection[] = [
@@ -21,6 +22,12 @@ const SECTIONS: SettingsSection[] = [
2122
icon: <Key className="h-4 w-4" />,
2223
component: ProvidersSection,
2324
},
25+
{
26+
id: "projects",
27+
label: "Projects",
28+
icon: <Briefcase className="h-4 w-4" />,
29+
component: ProjectSettingsSection,
30+
},
2431
{
2532
id: "models",
2633
label: "Models",
@@ -62,7 +69,7 @@ export function SettingsModal() {
6269
<X className="h-4 w-4" />
6370
</Button>
6471
</div>
65-
<nav className="flex overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
72+
<nav className="flex gap-1 overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
6673
{SECTIONS.map((section) => (
6774
<Button
6875
key={section.id}

‎src/browser/components/Settings/sections/ModelRow.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { Check, Pencil, Star, Trash2, X } from "lucide-react";
3+
import { createEditKeyHandler } from "@/browser/utils/ui/keybinds";
34
import { GatewayIcon } from "@/browser/components/icons/GatewayIcon";
45
import { cn } from "@/common/lib/utils";
56
import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip";
@@ -45,10 +46,10 @@ export function ModelRow(props: ModelRowProps) {
4546
type="text"
4647
value={props.editValue ?? props.modelId}
4748
onChange={(e) => props.onEditChange?.(e.target.value)}
48-
onKeyDown={(e) => {
49-
if (e.key === "Enter") props.onSaveEdit?.();
50-
if (e.key === "Escape") props.onCancelEdit?.();
51-
}}
49+
onKeyDown={createEditKeyHandler({
50+
onSave: () => props.onSaveEdit?.(),
51+
onCancel: () => props.onCancelEdit?.(),
52+
})}
5253
className="bg-modal-bg border-border-medium focus:border-accent min-w-0 flex-1 rounded border px-2 py-0.5 font-mono text-xs focus:outline-none"
5354
autoFocus
5455
/>

0 commit comments

Comments
 (0)