From 657c61939f41d0b17825982b88d3944fd9f0dc0f Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 10:04:25 -0700 Subject: [PATCH] Remove MCP tab and per-project MCP UI from frontend Claude Code manages MCP natively now (`claude mcp add/list/remove`, `.mcp.json`, `/mcp`), so Triple-C's own MCP server library is redundant. Deletes components/mcp/, hooks/useMcpServers.ts, the MCP sidebar tab and rail icon, the per-project enable checkboxes on ProjectCard, the mcpServers slice of the Zustand store, the four IPC wrappers, and the McpServer/McpTransportType types. Rust backend is untouched in this commit; the commands simply become unreachable. Backend removal and the legacy container/network cleanup follow separately. Co-Authored-By: Claude Opus 5 (1M context) --- app/src/App.tsx | 3 - app/src/components/layout/Sidebar.test.tsx | 3 - app/src/components/layout/Sidebar.tsx | 26 +- app/src/components/mcp/McpPanel.tsx | 79 ----- app/src/components/mcp/McpServerCard.tsx | 331 ------------------ .../components/projects/ProjectCard.test.tsx | 11 - app/src/components/projects/ProjectCard.tsx | 49 +-- app/src/hooks/useMcpServers.ts | 55 --- app/src/lib/tauri-commands.ts | 11 +- app/src/lib/types.ts | 18 - app/src/store/appState.ts | 26 +- 11 files changed, 8 insertions(+), 604 deletions(-) delete mode 100644 app/src/components/mcp/McpPanel.tsx delete mode 100644 app/src/components/mcp/McpServerCard.tsx delete mode 100644 app/src/hooks/useMcpServers.ts diff --git a/app/src/App.tsx b/app/src/App.tsx index 79d00ca..64935bf 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -8,7 +8,6 @@ import DockerInstallDialog from "./components/DockerInstallDialog"; import { useDocker } from "./hooks/useDocker"; import { useSettings } from "./hooks/useSettings"; import { useProjects } from "./hooks/useProjects"; -import { useMcpServers } from "./hooks/useMcpServers"; import { useUpdates } from "./hooks/useUpdates"; import { useTerminal } from "./hooks/useTerminal"; import { useSTT } from "./hooks/useSTT"; @@ -19,7 +18,6 @@ export default function App() { const { checkDocker, checkImage, startDockerPolling } = useDocker(); const { loadSettings } = useSettings(); const { refresh } = useProjects(); - const { refresh: refreshMcp } = useMcpServers(); const { loadVersion, checkForUpdates, checkImageUpdate, startPeriodicCheck } = useUpdates(); const { sessions, activeSessionId, setProjects, setSttToggle } = useAppState( useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects, setSttToggle: s.setSttToggle })) @@ -56,7 +54,6 @@ export default function App() { } }); refresh(); - refreshMcp(); // Update detection loadVersion(); diff --git a/app/src/components/layout/Sidebar.test.tsx b/app/src/components/layout/Sidebar.test.tsx index be5c22c..d1072dd 100644 --- a/app/src/components/layout/Sidebar.test.tsx +++ b/app/src/components/layout/Sidebar.test.tsx @@ -22,9 +22,6 @@ vi.mock("../projects/ProjectList", () => ({ vi.mock("../settings/SettingsPanel", () => ({ default: () =>
SettingsPanel
, })); -vi.mock("../mcp/McpPanel", () => ({ - default: () =>
McpPanel
, -})); describe("Sidebar", () => { beforeEach(() => { diff --git a/app/src/components/layout/Sidebar.tsx b/app/src/components/layout/Sidebar.tsx index 2c3bbfc..3e2b719 100644 --- a/app/src/components/layout/Sidebar.tsx +++ b/app/src/components/layout/Sidebar.tsx @@ -2,10 +2,9 @@ import type { ReactNode } from "react"; import { useShallow } from "zustand/react/shallow"; import { useAppState } from "../../store/appState"; import ProjectList from "../projects/ProjectList"; -import McpPanel from "../mcp/McpPanel"; import SettingsPanel from "../settings/SettingsPanel"; -type SidebarView = "projects" | "mcp" | "settings"; +type SidebarView = "projects" | "settings"; const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [ { @@ -17,18 +16,6 @@ const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [ ), }, - { - view: "mcp", - label: "MCP", - icon: ( - - - - - - - ), - }, { view: "settings", label: "Settings", @@ -108,9 +95,6 @@ export default function Sidebar() { - @@ -128,13 +112,7 @@ export default function Sidebar() { {/* Content */}
- {sidebarView === "projects" ? ( - - ) : sidebarView === "mcp" ? ( - - ) : ( - - )} + {sidebarView === "projects" ? : }
); diff --git a/app/src/components/mcp/McpPanel.tsx b/app/src/components/mcp/McpPanel.tsx deleted file mode 100644 index 4fafca8..0000000 --- a/app/src/components/mcp/McpPanel.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState, useEffect } from "react"; -import { useMcpServers } from "../../hooks/useMcpServers"; -import McpServerCard from "./McpServerCard"; - -export default function McpPanel() { - const { mcpServers, refresh, add, update, remove } = useMcpServers(); - const [newName, setNewName] = useState(""); - const [error, setError] = useState(null); - - useEffect(() => { - refresh(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const handleAdd = async () => { - const name = newName.trim(); - if (!name) return; - setError(null); - try { - await add(name); - setNewName(""); - } catch (e) { - setError(String(e)); - } - }; - - return ( -
-
-

- MCP Servers{" "} - Beta -

-

- Define MCP servers globally, then enable them per-project. -

-
- - {/* Add new server */} -
- setNewName(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); }} - placeholder="Server name..." - className="flex-1 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]" - /> - -
- - {error && ( -
{error}
- )} - - {/* Server list */} -
- {mcpServers.length === 0 ? ( -

- No MCP servers configured. -

- ) : ( - mcpServers.map((server) => ( - - )) - )} -
-
- ); -} diff --git a/app/src/components/mcp/McpServerCard.tsx b/app/src/components/mcp/McpServerCard.tsx deleted file mode 100644 index 833bcd2..0000000 --- a/app/src/components/mcp/McpServerCard.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useState, useEffect } from "react"; -import type { McpServer, McpTransportType } from "../../lib/types"; - -interface Props { - server: McpServer; - onUpdate: (server: McpServer) => Promise; - onRemove: (id: string) => Promise; -} - -export default function McpServerCard({ server, onUpdate, onRemove }: Props) { - const [expanded, setExpanded] = useState(false); - const [name, setName] = useState(server.name); - const [transportType, setTransportType] = useState(server.transport_type); - const [command, setCommand] = useState(server.command ?? ""); - const [args, setArgs] = useState(server.args.join(" ")); - const [envPairs, setEnvPairs] = useState<[string, string][]>(Object.entries(server.env)); - const [url, setUrl] = useState(server.url ?? ""); - const [headerPairs, setHeaderPairs] = useState<[string, string][]>(Object.entries(server.headers)); - const [dockerImage, setDockerImage] = useState(server.docker_image ?? ""); - const [containerPort, setContainerPort] = useState(server.container_port?.toString() ?? "3000"); - - useEffect(() => { - setName(server.name); - setTransportType(server.transport_type); - setCommand(server.command ?? ""); - setArgs(server.args.join(" ")); - setEnvPairs(Object.entries(server.env)); - setUrl(server.url ?? ""); - setHeaderPairs(Object.entries(server.headers)); - setDockerImage(server.docker_image ?? ""); - setContainerPort(server.container_port?.toString() ?? "3000"); - }, [server]); - - const saveServer = async (patch: Partial) => { - try { - await onUpdate({ ...server, ...patch }); - } catch (err) { - console.error("Failed to update MCP server:", err); - } - }; - - const handleNameBlur = () => { - if (name !== server.name) saveServer({ name }); - }; - - const handleTransportChange = (t: McpTransportType) => { - setTransportType(t); - saveServer({ transport_type: t }); - }; - - const handleCommandBlur = () => { - saveServer({ command: command || null }); - }; - - const handleArgsBlur = () => { - const parsed = args.trim() ? args.trim().split(/\s+/) : []; - saveServer({ args: parsed }); - }; - - const handleUrlBlur = () => { - saveServer({ url: url || null }); - }; - - const handleDockerImageBlur = () => { - saveServer({ docker_image: dockerImage || null }); - }; - - const handleContainerPortBlur = () => { - const port = parseInt(containerPort, 10); - saveServer({ container_port: isNaN(port) ? null : port }); - }; - - const saveEnv = (pairs: [string, string][]) => { - const env: Record = {}; - for (const [k, v] of pairs) { - if (k.trim()) env[k.trim()] = v; - } - saveServer({ env }); - }; - - const saveHeaders = (pairs: [string, string][]) => { - const headers: Record = {}; - for (const [k, v] of pairs) { - if (k.trim()) headers[k.trim()] = v; - } - saveServer({ headers }); - }; - - const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"; - - const isDocker = !!dockerImage; - - const transportBadge = { - stdio: "Stdio", - http: "HTTP", - }[transportType]; - - const modeBadge = isDocker ? "Docker" : "Manual"; - - return ( -
- {/* Header */} -
- - -
- - {/* Expanded config */} - {expanded && ( -
- {/* Name */} -
- - setName(e.target.value)} - onBlur={handleNameBlur} - className={inputCls} - /> -
- - {/* Docker Image (primary field — determines Docker vs Manual mode) */} -
- - setDockerImage(e.target.value)} - onBlur={handleDockerImageBlur} - placeholder="e.g. mcp/filesystem:latest (leave empty for manual mode)" - className={inputCls} - /> -

- Set a Docker image to run this MCP server in its own container. Leave empty to run commands inside the project container. Images are pulled automatically if not present. -

-
- - {/* Transport type */} -
- -
- {(["stdio", "http"] as McpTransportType[]).map((t) => ( - - ))} -
-
- - {/* Mode description */} -

- {transportType === "stdio" && isDocker && "Runs via docker exec in a separate MCP container."} - {transportType === "stdio" && !isDocker && "Runs inside the project container (e.g. npx commands)."} - {transportType === "http" && isDocker && "Runs in a separate container, reached by hostname on the project network."} - {transportType === "http" && !isDocker && "Connects to an MCP server at the URL you specify."} -

- - {/* Container Port (HTTP+Docker only) */} - {transportType === "http" && isDocker && ( -
- - setContainerPort(e.target.value)} - onBlur={handleContainerPortBlur} - placeholder="3000" - className={inputCls} - /> -

- Port the MCP server listens on inside its container. The URL is auto-generated as http://<container>:<port>/mcp on the project network. -

-
- )} - - {/* Stdio fields */} - {transportType === "stdio" && ( - <> -
- - setCommand(e.target.value)} - onBlur={handleCommandBlur} - placeholder={isDocker ? "Command inside container" : "npx"} - className={inputCls} - /> -
-
- - setArgs(e.target.value)} - onBlur={handleArgsBlur} - placeholder="-y @modelcontextprotocol/server-filesystem /path" - className={inputCls} - /> -
- { setEnvPairs(pairs); }} - onSave={saveEnv} - /> - - )} - - {/* HTTP fields (only for manual mode — Docker mode auto-generates URL) */} - {transportType === "http" && !isDocker && ( - <> -
- - setUrl(e.target.value)} - onBlur={handleUrlBlur} - placeholder="http://localhost:3000/mcp" - className={inputCls} - /> -
- { setHeaderPairs(pairs); }} - onSave={saveHeaders} - /> - - )} - - {/* Environment variables for HTTP+Docker */} - {transportType === "http" && isDocker && ( - { setEnvPairs(pairs); }} - onSave={saveEnv} - /> - )} -
- )} -
- ); -} - -function KeyValueEditor({ - label, - pairs, - onChange, - onSave, -}: { - label: string; - pairs: [string, string][]; - onChange: (pairs: [string, string][]) => void; - onSave: (pairs: [string, string][]) => void; -}) { - const inputCls = "flex-1 min-w-0 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"; - - return ( -
- - {pairs.map(([key, value], i) => ( -
- { - const updated = [...pairs] as [string, string][]; - updated[i] = [e.target.value, value]; - onChange(updated); - }} - onBlur={() => onSave(pairs)} - placeholder="KEY" - className={inputCls} - /> - = - { - const updated = [...pairs] as [string, string][]; - updated[i] = [key, e.target.value]; - onChange(updated); - }} - onBlur={() => onSave(pairs)} - placeholder="value" - className={inputCls} - /> - -
- ))} - -
- ); -} diff --git a/app/src/components/projects/ProjectCard.test.tsx b/app/src/components/projects/ProjectCard.test.tsx index 3d2e468..c8e52b0 100644 --- a/app/src/components/projects/ProjectCard.test.tsx +++ b/app/src/components/projects/ProjectCard.test.tsx @@ -31,16 +31,6 @@ vi.mock("../../hooks/useTerminal", () => ({ }), })); -vi.mock("../../hooks/useMcpServers", () => ({ - useMcpServers: () => ({ - mcpServers: [], - refresh: vi.fn(), - add: vi.fn(), - update: vi.fn(), - remove: vi.fn(), - }), -})); - let mockSelectedProjectId: string | null = null; vi.mock("../../store/appState", () => ({ useAppState: vi.fn((selector) => @@ -67,7 +57,6 @@ const mockProject: Project = { custom_env_vars: [], port_mappings: [], claude_instructions: null, - enabled_mcp_servers: [], created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", }; diff --git a/app/src/components/projects/ProjectCard.tsx b/app/src/components/projects/ProjectCard.tsx index af83630..ff3221e 100644 --- a/app/src/components/projects/ProjectCard.tsx +++ b/app/src/components/projects/ProjectCard.tsx @@ -4,7 +4,6 @@ import * as commands from "../../lib/tauri-commands"; import { listen } from "@tauri-apps/api/event"; import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, OpenAiCompatibleConfig } from "../../lib/types"; import { useProjects } from "../../hooks/useProjects"; -import { useMcpServers } from "../../hooks/useMcpServers"; import { useTerminal } from "../../hooks/useTerminal"; import { useAppState } from "../../store/appState"; import EnvVarsModal from "./EnvVarsModal"; @@ -24,7 +23,6 @@ export default function ProjectCard({ project }: Props) { const selectedProjectId = useAppState(s => s.selectedProjectId); const setSelectedProject = useAppState(s => s.setSelectedProject); const { start, stop, rebuild, remove, update } = useProjects(); - const { mcpServers } = useMcpServers(); const { open: openTerminal } = useTerminal(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -196,7 +194,7 @@ export default function ProjectCard({ project }: Props) { setError(null); const bytes = await commands.downloadContainerBackup(project.id, hostPath); const mb = (bytes / (1024 * 1024)).toFixed(1); - const msg = `Backup saved (${mb} MB). Note: includes MCP/config — may contain MCP API keys. Keep it private.`; + const msg = `Backup saved (${mb} MB). Note: includes Claude config — may contain API keys. Keep it private.`; setProgressMsg(msg); // Auto-clear so the transient confirmation doesn't linger in the card // status; guard against clobbering a newer message (e.g. a later op). @@ -539,7 +537,7 @@ export default function ProjectCard({ project }: Props) { onClick={handleBackup} disabled={loading || backingUp} label={backingUp ? "Backing up…" : "Backup"} - title="Downloads /workspace plus a sanitized home config (MCP servers, settings, skills). OAuth tokens are excluded, but MCP server configs may embed their own API keys/tokens — keep the archive private." + title="Downloads /workspace plus a sanitized home config (Claude settings, skills, MCP config). OAuth tokens are excluded, but other config may embed its own API keys/tokens — keep the archive private." /> ) : ( @@ -864,49 +862,6 @@ export default function ProjectCard({ project }: Props) { - {/* MCP Servers */} - {mcpServers.length > 0 && ( -
- -
- {mcpServers.map((server) => { - const enabled = project.enabled_mcp_servers.includes(server.id); - const isDocker = !!server.docker_image; - return ( - - ); - })} -
- {mcpServers.some((s) => s.docker_image && s.transport_type === "stdio" && project.enabled_mcp_servers.includes(s.id)) && ( -

- Docker access will be auto-enabled for stdio+Docker MCP servers. -

- )} -
- )} - {/* Bedrock config */} {project.backend === "bedrock" && (() => { const bc = project.bedrock_config ?? defaultBedrockConfig; diff --git a/app/src/hooks/useMcpServers.ts b/app/src/hooks/useMcpServers.ts deleted file mode 100644 index 8ce32f8..0000000 --- a/app/src/hooks/useMcpServers.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback } from "react"; -import { useShallow } from "zustand/react/shallow"; -import { useAppState } from "../store/appState"; -import * as commands from "../lib/tauri-commands"; -import type { McpServer } from "../lib/types"; - -export function useMcpServers() { - const { - mcpServers, - setMcpServers, - updateMcpServerInList, - removeMcpServerFromList, - } = useAppState( - useShallow(s => ({ - mcpServers: s.mcpServers, - setMcpServers: s.setMcpServers, - updateMcpServerInList: s.updateMcpServerInList, - removeMcpServerFromList: s.removeMcpServerFromList, - })) - ); - - const refresh = useCallback(async () => { - const list = await commands.listMcpServers(); - setMcpServers(list); - }, [setMcpServers]); - - const add = useCallback( - async (name: string) => { - const server = await commands.addMcpServer(name); - const list = await commands.listMcpServers(); - setMcpServers(list); - return server; - }, - [setMcpServers], - ); - - const update = useCallback( - async (server: McpServer) => { - const updated = await commands.updateMcpServer(server); - updateMcpServerInList(updated); - return updated; - }, - [updateMcpServerInList], - ); - - const remove = useCallback( - async (id: string) => { - await commands.removeMcpServer(id); - removeMcpServerFromList(id); - }, - [removeMcpServerFromList], - ); - - return { mcpServers, refresh, add, update, remove }; -} diff --git a/app/src/lib/tauri-commands.ts b/app/src/lib/tauri-commands.ts index c894db2..abb01ce 100644 --- a/app/src/lib/tauri-commands.ts +++ b/app/src/lib/tauri-commands.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer, FileEntry, WebTerminalInfo, SttStatus, InstallOptions } from "./types"; +import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, WebTerminalInfo, SttStatus, InstallOptions } from "./types"; // Docker export const checkDocker = () => invoke("check_docker"); @@ -64,15 +64,6 @@ export const sendAudioData = (sessionId: string, data: number[]) => export const stopAudioBridge = (sessionId: string) => invoke("stop_audio_bridge", { sessionId }); -// MCP Servers -export const listMcpServers = () => invoke("list_mcp_servers"); -export const addMcpServer = (name: string) => - invoke("add_mcp_server", { name }); -export const updateMcpServer = (server: McpServer) => - invoke("update_mcp_server", { server }); -export const removeMcpServer = (serverId: string) => - invoke("remove_mcp_server", { serverId }); - // Files export const listContainerFiles = (projectId: string, path: string) => invoke("list_container_files", { projectId, path }); diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index b6e4d79..c4773cb 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -35,7 +35,6 @@ export interface Project { custom_env_vars: EnvVar[]; port_mappings: PortMapping[]; claude_instructions: string | null; - enabled_mcp_servers: string[]; claude_code_settings: ClaudeCodeSettings | null; renamed_session_names: Record; created_at: string; @@ -202,23 +201,6 @@ export interface ImageUpdateInfo { remote_updated_at: string | null; } -export type McpTransportType = "stdio" | "http"; - -export interface McpServer { - id: string; - name: string; - transport_type: McpTransportType; - command: string | null; - args: string[]; - env: Record; - url: string | null; - headers: Record; - docker_image: string | null; - container_port: number | null; - created_at: string; - updated_at: string; -} - export interface FileEntry { name: string; path: string; diff --git a/app/src/store/appState.ts b/app/src/store/appState.ts index 37a409e..b4681e7 100644 --- a/app/src/store/appState.ts +++ b/app/src/store/appState.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import type { Project, TerminalSession, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer } from "../lib/types"; +import type { Project, TerminalSession, AppSettings, UpdateInfo, ImageUpdateInfo } from "../lib/types"; const SIDEBAR_COLLAPSED_KEY = "triple-c.sidebar.collapsed"; @@ -35,12 +35,6 @@ interface AppState { removeSession: (id: string) => void; setActiveSession: (id: string | null) => void; - // MCP servers - mcpServers: McpServer[]; - setMcpServers: (servers: McpServer[]) => void; - updateMcpServerInList: (server: McpServer) => void; - removeMcpServerFromList: (id: string) => void; - // UI state terminalHasSelection: boolean; setTerminalHasSelection: (has: boolean) => void; @@ -54,8 +48,8 @@ interface AppState { setTerminalAtBottom: (v: boolean) => void; scrollActiveToBottom: () => void; setScrollActiveToBottom: (fn: () => void) => void; - sidebarView: "projects" | "mcp" | "settings"; - setSidebarView: (view: "projects" | "mcp" | "settings") => void; + sidebarView: "projects" | "settings"; + setSidebarView: (view: "projects" | "settings") => void; sidebarCollapsed: boolean; setSidebarCollapsed: (collapsed: boolean) => void; toggleSidebarCollapsed: () => void; @@ -118,20 +112,6 @@ export const useAppState = create((set) => ({ }), setActiveSession: (id) => set({ activeSessionId: id }), - // MCP servers - mcpServers: [], - setMcpServers: (servers) => set({ mcpServers: servers }), - updateMcpServerInList: (server) => - set((state) => ({ - mcpServers: state.mcpServers.map((s) => - s.id === server.id ? server : s, - ), - })), - removeMcpServerFromList: (id) => - set((state) => ({ - mcpServers: state.mcpServers.filter((s) => s.id !== id), - })), - // UI state terminalHasSelection: false, setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),