Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5e781dd
πŸ€– feat: add MCP server configuration and runtime
ammar-agent Dec 7, 2025
5ec5ffd
add IPC test for MCP config lifecycle
ammar-agent Dec 7, 2025
8765025
fix: prevent worker pool hang when tokenizer worker fails to load
ammar-agent Dec 7, 2025
c050505
feat: add MCP server test button in Settings UI
ammar-agent Dec 7, 2025
03fa43a
chore: reduce MCP test timeout to 10s
ammar-agent Dec 7, 2025
0c4ace6
feat: improve Projects settings page UX
ammar-agent Dec 7, 2025
7334de8
feat: redesign MCP settings with test-before-add UX
ammar-agent Dec 7, 2025
0e35707
πŸ€– docs: add MCP servers documentation
ammar-agent Dec 7, 2025
ea18660
πŸ€– feat: add MCP server edit functionality
ammar-agent Dec 7, 2025
c4cae87
πŸ€– fix: stop Escape propagation in Settings edit inputs
ammar-agent Dec 7, 2025
3742d08
πŸ€– refactor: extract createEditKeyHandler for inline edit inputs
ammar-agent Dec 7, 2025
6614cef
πŸ€– style: use monospace font for MCP command inputs
ammar-agent Dec 7, 2025
c0e7166
πŸ€– feat: add MCP servers section to system prompt
ammar-agent Dec 7, 2025
30ab4e6
πŸ€– chore: remove test MCP config
ammar-agent Dec 7, 2025
7c1422e
πŸ€– fix: transform MCP image content to AI SDK media format
ammar-agent Dec 7, 2025
f3e63b2
fix: ensure mediaType is present in MCP image transformation
ammar-agent Dec 8, 2025
512aead
docs: update MCP examples to chrome/memory, add slash command docs
ammar-agent Dec 8, 2025
899b0a3
refactor: DRY and consolidate MCP types
ammar-agent Dec 8, 2025
37bbe8f
docs: fix incorrect logs path in MCP troubleshooting
ammar-agent Dec 8, 2025
37720ce
refactor: address review feedback
ammar-agent Dec 8, 2025
509ba4b
refactor: use shadcn Button in ProjectSettingsSection
ammar-agent Dec 8, 2025
b784353
docs: clarify MCP server scope (config vs runtime)
ammar-agent Dec 8, 2025
015894f
feat: add 10-minute idle timeout for MCP servers
ammar-agent Dec 8, 2025
ed1de83
feat: namespace MCP tools with server name prefix
ammar-agent Dec 8, 2025
b5f287d
fix: don't expose MCP command strings in system prompt
ammar-agent Dec 8, 2025
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 bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@ai-sdk/amazon-bedrock": "^3.0.61",
"@ai-sdk/anthropic": "^2.0.47",
"@ai-sdk/google": "^2.0.43",
"@ai-sdk/mcp": "^0.0.11",
"@ai-sdk/openai": "^2.0.72",
"@ai-sdk/xai": "^2.0.36",
"@aws-sdk/credential-providers": "^3.940.0",
Expand Down Expand Up @@ -175,6 +176,8 @@

"@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=="],

"@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=="],

"@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=="],

"@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=="],
Expand Down Expand Up @@ -2989,6 +2992,8 @@

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

"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],

"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],

"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"context-management",
"instruction-files",
"mcp-servers",
{
"group": "Project Secrets",
"pages": ["project-secrets", "agentic-git-identity"]
Expand Down
79 changes: 79 additions & 0 deletions docs/mcp-servers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: MCP Servers
description: Extend agent capabilities with Model Context Protocol servers
---

MCP (Model Context Protocol) servers provide additional tools to agents. Configure them per-project in `.mux/mcp.jsonc`.

## Configuration

Create `.mux/mcp.jsonc` in your project root:

```jsonc
{
"servers": {
// Knowledge graph for persistent memory
"memory": "npx -y @modelcontextprotocol/server-memory",
// Browser automation and screenshots
"chrome": "npx -y chrome-devtools-mcp@latest --headless",
},
}
```

Each entry maps a server name to its shell command. The command must start a process that speaks MCP over stdio (NDJSON format).

## Slash Commands

Manage MCP servers directly from chat:

| Command | Description |
| ---------------------------- | ----------------------------------- |
| `/mcp add <name> <command>` | Add a new MCP server |
| `/mcp remove <name>` | Remove an MCP server |
| `/mcp edit <name> <command>` | Update an existing server's command |

Examples:

```
/mcp add memory npx -y @modelcontextprotocol/server-memory
/mcp add chrome npx -y chrome-devtools-mcp@latest --headless
/mcp remove github
/mcp edit chrome npx -y chrome-devtools-mcp@latest --headless --isolated
```

## Settings UI

Configure servers in **Settings β†’ Projects**:

1. Select a project from the dropdown
2. Add servers with name and command
3. Use **Test** (play button) to verify before adding
4. Use **Edit** (pencil) or **Remove** (trash) to manage existing servers

## Scope

MCP servers have two scopes:

- **Configuration** is per-project β€” The `.mux/mcp.jsonc` file lives in your project root and applies to all workspaces created from that project
- **Runtime instances** are per-workspace β€” Each workspace runs its own server processes, so state in one workspace doesn't affect another

This means you configure servers once per project, but each workspace (branch) gets isolated server instances with independent state.

## Behavior

- **Hot reload** β€” Config changes apply on your next message (no restart needed)
- **Isolated** β€” Server processes run in the workspace directory with its environment
- **Lazy start** β€” Servers start when you send your first message in a workspace
- **Idle timeout** β€” Servers stop after 10 minutes of inactivity to conserve resources, then restart automatically when needed

## Finding MCP Servers

Browse available servers at [mcp.so](https://mcp.so/) or the [MCP servers repository](https://github.com/modelcontextprotocol/servers).

## Troubleshooting

If a server fails to start:

1. **Test the command manually** β€” Run the command in your terminal to verify it works
2. **Check dependencies** β€” Ensure required packages are installed (`npx -y` downloads on first run)
3. **Use the Test button** β€” Settings β†’ Projects shows connection errors inline
22 changes: 22 additions & 0 deletions docs/system-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ You are in a git worktree at ${workspacePath}
</environment>
`;
}

/**
* Build MCP servers context XML block.
* Only included when at least one MCP server is configured.
* Note: We only expose server names, not commands, to avoid leaking secrets.
*/
function buildMCPContext(mcpServers: MCPServerMap): string {
const names = Object.keys(mcpServers);
if (names.length === 0) return "";

const serverList = names.map((name) => `- ${name}`).join("\n");

return `
<mcp>
MCP (Model Context Protocol) servers provide additional tools. Configured in user's local project's .mux/mcp.jsonc:

${serverList}

Use /mcp add|edit|remove or Settings β†’ Projects to manage servers.
</mcp>
`;
}
```

{/* END SYSTEM_PROMPT_DOCS */}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@ai-sdk/amazon-bedrock": "^3.0.61",
"@ai-sdk/anthropic": "^2.0.47",
"@ai-sdk/google": "^2.0.43",
"@ai-sdk/mcp": "^0.0.11",
"@ai-sdk/openai": "^2.0.72",
"@ai-sdk/xai": "^2.0.36",
"@aws-sdk/credential-providers": "^3.940.0",
Expand Down
80 changes: 80 additions & 0 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { ChatInputToast } from "../ChatInputToast";
import { createCommandToast, createErrorToast } from "../ChatInputToasts";
import { parseCommand } from "@/browser/utils/slashCommands/parser";
import { usePersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
import { useSettings } from "@/browser/contexts/SettingsContext";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useMode } from "@/browser/contexts/ModeContext";
import { ThinkingSliderComponent } from "../ThinkingSlider";
import { ModelSettings } from "../ModelSettings";
Expand Down Expand Up @@ -167,6 +169,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
[setInput]
);
const preEditDraftRef = useRef<DraftState>({ text: "", images: [] });
const { open } = useSettings();
const { selectedWorkspace } = useWorkspaceContext();
const [mode, setMode] = useMode();
const { recentModels, addModel, defaultModel, setDefaultModel } = useModelLRU();
const commandListId = useId();
Expand Down Expand Up @@ -730,6 +734,82 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
}

// Handle /vim command
if (parsed.type === "mcp-open") {
setInput("");
open("project");
return;
}

if (
parsed.type === "mcp-add" ||
parsed.type === "mcp-edit" ||
parsed.type === "mcp-remove"
) {
if (!api) {
setToast({
id: Date.now().toString(),
type: "error",
message: "Not connected to server",
});
return;
}
if (!selectedWorkspace?.projectPath) {
setToast({
id: Date.now().toString(),
type: "error",
message: "Select a workspace to manage MCP servers",
});
return;
}

setIsSending(true);
setInput("");
try {
const projectPath = selectedWorkspace.projectPath;
const result =
parsed.type === "mcp-add" || parsed.type === "mcp-edit"
? await api.projects.mcp.add({
projectPath,
name: parsed.name,
command: parsed.command,
})
: await api.projects.mcp.remove({ projectPath, name: parsed.name });

if (!result.success) {
setToast({
id: Date.now().toString(),
type: "error",
message: result.error ?? "Failed to update MCP servers",
});
setInput(messageText);
} else {
const successMessage =
parsed.type === "mcp-add"
? `Added MCP server ${parsed.name}`
: parsed.type === "mcp-edit"
? `Updated MCP server ${parsed.name}`
: `Removed MCP server ${parsed.name}`;
setToast({
id: Date.now().toString(),
type: "success",
message: successMessage,
});
}
} catch (error) {
console.error("Failed to update MCP servers", error);
setToast({
id: Date.now().toString(),
type: "error",
message: error instanceof Error ? error.message : "Failed to update MCP servers",
});
setInput(messageText);
} finally {
setIsSending(false);
}

return;
}

if (parsed.type === "vim-toggle") {
setInput(""); // Clear input immediately
setVimEnabled((prev) => !prev);
Expand Down
11 changes: 9 additions & 2 deletions src/browser/components/Settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from "react";
import { Settings, Key, Cpu, X } from "lucide-react";
import { Settings, Key, Cpu, X, Briefcase } from "lucide-react";
import { useSettings } from "@/browser/contexts/SettingsContext";
import { Dialog, DialogContent, DialogTitle, VisuallyHidden } from "@/browser/components/ui/dialog";
import { GeneralSection } from "./sections/GeneralSection";
import { ProvidersSection } from "./sections/ProvidersSection";
import { ModelsSection } from "./sections/ModelsSection";
import { Button } from "@/browser/components/ui/button";
import { ProjectSettingsSection } from "./sections/ProjectSettingsSection";
import type { SettingsSection } from "./types";

const SECTIONS: SettingsSection[] = [
Expand All @@ -21,6 +22,12 @@ const SECTIONS: SettingsSection[] = [
icon: <Key className="h-4 w-4" />,
component: ProvidersSection,
},
{
id: "projects",
label: "Projects",
icon: <Briefcase className="h-4 w-4" />,
component: ProjectSettingsSection,
},
{
id: "models",
label: "Models",
Expand Down Expand Up @@ -62,7 +69,7 @@ export function SettingsModal() {
<X className="h-4 w-4" />
</Button>
</div>
<nav className="flex overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
<nav className="flex gap-1 overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
{SECTIONS.map((section) => (
<Button
key={section.id}
Expand Down
9 changes: 5 additions & 4 deletions src/browser/components/Settings/sections/ModelRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { Check, Pencil, Star, Trash2, X } from "lucide-react";
import { createEditKeyHandler } from "@/browser/utils/ui/keybinds";
import { GatewayIcon } from "@/browser/components/icons/GatewayIcon";
import { cn } from "@/common/lib/utils";
import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip";
Expand Down Expand Up @@ -45,10 +46,10 @@ export function ModelRow(props: ModelRowProps) {
type="text"
value={props.editValue ?? props.modelId}
onChange={(e) => props.onEditChange?.(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") props.onSaveEdit?.();
if (e.key === "Escape") props.onCancelEdit?.();
}}
onKeyDown={createEditKeyHandler({
onSave: () => props.onSaveEdit?.(),
onCancel: () => props.onCancelEdit?.(),
})}
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"
autoFocus
/>
Expand Down
Loading