diff --git a/bun.lock b/bun.lock index 1117a5d002..c319e107f8 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -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=="], @@ -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=="], diff --git a/docs/docs.json b/docs/docs.json index 7b69809c5a..96be48abd8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -54,6 +54,7 @@ }, "context-management", "instruction-files", + "mcp-servers", { "group": "Project Secrets", "pages": ["project-secrets", "agentic-git-identity"] diff --git a/docs/mcp-servers.mdx b/docs/mcp-servers.mdx new file mode 100644 index 0000000000..265f4bce4a --- /dev/null +++ b/docs/mcp-servers.mdx @@ -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 ` | Add a new MCP server | +| `/mcp remove ` | Remove an MCP server | +| `/mcp edit ` | 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 diff --git a/docs/system-prompt.mdx b/docs/system-prompt.mdx index 9a794dbe39..320432df00 100644 --- a/docs/system-prompt.mdx +++ b/docs/system-prompt.mdx @@ -59,6 +59,28 @@ You are in a git worktree at ${workspacePath} `; } + +/** + * 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 (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. + +`; +} ``` {/* END SYSTEM_PROMPT_DOCS */} diff --git a/package.json b/package.json index 143256e1d7..1a1f6fcb0e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index 66adf09fd5..a5c1939dd9 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -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"; @@ -167,6 +169,8 @@ export const ChatInput: React.FC = (props) => { [setInput] ); const preEditDraftRef = useRef({ text: "", images: [] }); + const { open } = useSettings(); + const { selectedWorkspace } = useWorkspaceContext(); const [mode, setMode] = useMode(); const { recentModels, addModel, defaultModel, setDefaultModel } = useModelLRU(); const commandListId = useId(); @@ -730,6 +734,82 @@ export const ChatInput: React.FC = (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); diff --git a/src/browser/components/Settings/SettingsModal.tsx b/src/browser/components/Settings/SettingsModal.tsx index 41202f6370..913e377739 100644 --- a/src/browser/components/Settings/SettingsModal.tsx +++ b/src/browser/components/Settings/SettingsModal.tsx @@ -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[] = [ @@ -21,6 +22,12 @@ const SECTIONS: SettingsSection[] = [ icon: , component: ProvidersSection, }, + { + id: "projects", + label: "Projects", + icon: , + component: ProjectSettingsSection, + }, { id: "models", label: "Models", @@ -62,7 +69,7 @@ export function SettingsModal() { -