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) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 10:04:25 -07:00
co-authored by Claude Opus 5
parent 401e28a658
commit 657c61939f
11 changed files with 8 additions and 604 deletions
-3
View File
@@ -8,7 +8,6 @@ import DockerInstallDialog from "./components/DockerInstallDialog";
import { useDocker } from "./hooks/useDocker"; import { useDocker } from "./hooks/useDocker";
import { useSettings } from "./hooks/useSettings"; import { useSettings } from "./hooks/useSettings";
import { useProjects } from "./hooks/useProjects"; import { useProjects } from "./hooks/useProjects";
import { useMcpServers } from "./hooks/useMcpServers";
import { useUpdates } from "./hooks/useUpdates"; import { useUpdates } from "./hooks/useUpdates";
import { useTerminal } from "./hooks/useTerminal"; import { useTerminal } from "./hooks/useTerminal";
import { useSTT } from "./hooks/useSTT"; import { useSTT } from "./hooks/useSTT";
@@ -19,7 +18,6 @@ export default function App() {
const { checkDocker, checkImage, startDockerPolling } = useDocker(); const { checkDocker, checkImage, startDockerPolling } = useDocker();
const { loadSettings } = useSettings(); const { loadSettings } = useSettings();
const { refresh } = useProjects(); const { refresh } = useProjects();
const { refresh: refreshMcp } = useMcpServers();
const { loadVersion, checkForUpdates, checkImageUpdate, startPeriodicCheck } = useUpdates(); const { loadVersion, checkForUpdates, checkImageUpdate, startPeriodicCheck } = useUpdates();
const { sessions, activeSessionId, setProjects, setSttToggle } = useAppState( const { sessions, activeSessionId, setProjects, setSttToggle } = useAppState(
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects, setSttToggle: s.setSttToggle })) useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects, setSttToggle: s.setSttToggle }))
@@ -56,7 +54,6 @@ export default function App() {
} }
}); });
refresh(); refresh();
refreshMcp();
// Update detection // Update detection
loadVersion(); loadVersion();
@@ -22,9 +22,6 @@ vi.mock("../projects/ProjectList", () => ({
vi.mock("../settings/SettingsPanel", () => ({ vi.mock("../settings/SettingsPanel", () => ({
default: () => <div data-testid="settings-panel">SettingsPanel</div>, default: () => <div data-testid="settings-panel">SettingsPanel</div>,
})); }));
vi.mock("../mcp/McpPanel", () => ({
default: () => <div data-testid="mcp-panel">McpPanel</div>,
}));
describe("Sidebar", () => { describe("Sidebar", () => {
beforeEach(() => { beforeEach(() => {
+2 -24
View File
@@ -2,10 +2,9 @@ import type { ReactNode } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useAppState } from "../../store/appState"; import { useAppState } from "../../store/appState";
import ProjectList from "../projects/ProjectList"; import ProjectList from "../projects/ProjectList";
import McpPanel from "../mcp/McpPanel";
import SettingsPanel from "../settings/SettingsPanel"; import SettingsPanel from "../settings/SettingsPanel";
type SidebarView = "projects" | "mcp" | "settings"; type SidebarView = "projects" | "settings";
const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [ const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [
{ {
@@ -17,18 +16,6 @@ const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [
</svg> </svg>
), ),
}, },
{
view: "mcp",
label: "MCP",
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 2v6" />
<path d="M15 2v6" />
<path d="M7 8h10v4a5 5 0 0 1-10 0V8z" />
<path d="M12 17v5" />
</svg>
),
},
{ {
view: "settings", view: "settings",
label: "Settings", label: "Settings",
@@ -108,9 +95,6 @@ export default function Sidebar() {
<button onClick={() => setSidebarView("projects")} className={tabCls("projects")}> <button onClick={() => setSidebarView("projects")} className={tabCls("projects")}>
Projects Projects
</button> </button>
<button onClick={() => setSidebarView("mcp")} className={tabCls("mcp")}>
MCP <span className="text-[0.6rem] px-1 py-0.5 rounded bg-yellow-500/20 text-yellow-400 ml-0.5">Beta</span>
</button>
<button onClick={() => setSidebarView("settings")} className={tabCls("settings")}> <button onClick={() => setSidebarView("settings")} className={tabCls("settings")}>
Settings Settings
</button> </button>
@@ -128,13 +112,7 @@ export default function Sidebar() {
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-1 min-w-0"> <div className="flex-1 overflow-y-auto overflow-x-hidden p-1 min-w-0">
{sidebarView === "projects" ? ( {sidebarView === "projects" ? <ProjectList /> : <SettingsPanel />}
<ProjectList />
) : sidebarView === "mcp" ? (
<McpPanel />
) : (
<SettingsPanel />
)}
</div> </div>
</div> </div>
); );
-79
View File
@@ -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<string | null>(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 (
<div className="space-y-3 p-2">
<div>
<h2 className="text-sm font-semibold text-[var(--text-primary)]">
MCP Servers{" "}
<span className="text-xs px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-400">Beta</span>
</h2>
<p className="text-xs text-[var(--text-secondary)] mt-0.5">
Define MCP servers globally, then enable them per-project.
</p>
</div>
{/* Add new server */}
<div className="flex gap-1">
<input
value={newName}
onChange={(e) => 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)]"
/>
<button
onClick={handleAdd}
disabled={!newName.trim()}
className="px-3 py-1 text-xs bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
>
Add
</button>
</div>
{error && (
<div className="text-xs text-[var(--error)]">{error}</div>
)}
{/* Server list */}
<div className="space-y-2">
{mcpServers.length === 0 ? (
<p className="text-xs text-[var(--text-secondary)] italic">
No MCP servers configured.
</p>
) : (
mcpServers.map((server) => (
<McpServerCard
key={server.id}
server={server}
onUpdate={update}
onRemove={remove}
/>
))
)}
</div>
</div>
);
}
-331
View File
@@ -1,331 +0,0 @@
import { useState, useEffect } from "react";
import type { McpServer, McpTransportType } from "../../lib/types";
interface Props {
server: McpServer;
onUpdate: (server: McpServer) => Promise<McpServer | void>;
onRemove: (id: string) => Promise<void>;
}
export default function McpServerCard({ server, onUpdate, onRemove }: Props) {
const [expanded, setExpanded] = useState(false);
const [name, setName] = useState(server.name);
const [transportType, setTransportType] = useState<McpTransportType>(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<McpServer>) => {
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<string, string> = {};
for (const [k, v] of pairs) {
if (k.trim()) env[k.trim()] = v;
}
saveServer({ env });
};
const saveHeaders = (pairs: [string, string][]) => {
const headers: Record<string, string> = {};
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 (
<div className="border border-[var(--border-color)] rounded bg-[var(--bg-primary)]">
{/* Header */}
<div className="flex items-center gap-2 px-3 py-2">
<button
onClick={() => setExpanded(!expanded)}
className="flex-1 flex items-center gap-2 text-left min-w-0"
>
<span className="text-xs text-[var(--text-secondary)]">{expanded ? "\u25BC" : "\u25B6"}</span>
<span className="text-sm font-medium truncate">{server.name}</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--bg-secondary)] text-[var(--text-secondary)]">
{transportBadge}
</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${isDocker ? "bg-blue-500/20 text-blue-400" : "bg-[var(--bg-secondary)] text-[var(--text-secondary)]"}`}>
{modeBadge}
</span>
</button>
<button
onClick={() => { if (confirm(`Remove MCP server "${server.name}"?`)) onRemove(server.id); }}
className="text-xs px-2 py-0.5 text-[var(--error)] hover:bg-[var(--bg-secondary)] rounded transition-colors"
>
Remove
</button>
</div>
{/* Expanded config */}
{expanded && (
<div className="px-3 pb-3 space-y-2 border-t border-[var(--border-color)] pt-2">
{/* Name */}
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Name</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={handleNameBlur}
className={inputCls}
/>
</div>
{/* Docker Image (primary field — determines Docker vs Manual mode) */}
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Docker Image</label>
<input
value={dockerImage}
onChange={(e) => setDockerImage(e.target.value)}
onBlur={handleDockerImageBlur}
placeholder="e.g. mcp/filesystem:latest (leave empty for manual mode)"
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-60">
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.
</p>
</div>
{/* Transport type */}
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Transport</label>
<div className="flex items-center gap-1">
{(["stdio", "http"] as McpTransportType[]).map((t) => (
<button
key={t}
onClick={() => handleTransportChange(t)}
className={`px-2 py-0.5 text-xs rounded transition-colors ${
transportType === t
? "bg-[var(--accent)] text-white"
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]"
}`}
>
{t === "stdio" ? "Stdio" : "HTTP"}
</button>
))}
</div>
</div>
{/* Mode description */}
<p className="text-xs text-[var(--text-secondary)] opacity-60">
{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."}
</p>
{/* Container Port (HTTP+Docker only) */}
{transportType === "http" && isDocker && (
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Container Port</label>
<input
value={containerPort}
onChange={(e) => setContainerPort(e.target.value)}
onBlur={handleContainerPortBlur}
placeholder="3000"
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-60">
Port the MCP server listens on inside its container. The URL is auto-generated as http://&lt;container&gt;:&lt;port&gt;/mcp on the project network.
</p>
</div>
)}
{/* Stdio fields */}
{transportType === "stdio" && (
<>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Command</label>
<input
value={command}
onChange={(e) => setCommand(e.target.value)}
onBlur={handleCommandBlur}
placeholder={isDocker ? "Command inside container" : "npx"}
className={inputCls}
/>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Arguments (space-separated)</label>
<input
value={args}
onChange={(e) => setArgs(e.target.value)}
onBlur={handleArgsBlur}
placeholder="-y @modelcontextprotocol/server-filesystem /path"
className={inputCls}
/>
</div>
<KeyValueEditor
label="Environment Variables"
pairs={envPairs}
onChange={(pairs) => { setEnvPairs(pairs); }}
onSave={saveEnv}
/>
</>
)}
{/* HTTP fields (only for manual mode — Docker mode auto-generates URL) */}
{transportType === "http" && !isDocker && (
<>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">URL</label>
<input
value={url}
onChange={(e) => setUrl(e.target.value)}
onBlur={handleUrlBlur}
placeholder="http://localhost:3000/mcp"
className={inputCls}
/>
</div>
<KeyValueEditor
label="Headers"
pairs={headerPairs}
onChange={(pairs) => { setHeaderPairs(pairs); }}
onSave={saveHeaders}
/>
</>
)}
{/* Environment variables for HTTP+Docker */}
{transportType === "http" && isDocker && (
<KeyValueEditor
label="Environment Variables"
pairs={envPairs}
onChange={(pairs) => { setEnvPairs(pairs); }}
onSave={saveEnv}
/>
)}
</div>
)}
</div>
);
}
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 (
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">{label}</label>
{pairs.map(([key, value], i) => (
<div key={i} className="flex gap-1 items-center mb-1">
<input
value={key}
onChange={(e) => {
const updated = [...pairs] as [string, string][];
updated[i] = [e.target.value, value];
onChange(updated);
}}
onBlur={() => onSave(pairs)}
placeholder="KEY"
className={inputCls}
/>
<span className="text-xs text-[var(--text-secondary)]">=</span>
<input
value={value}
onChange={(e) => {
const updated = [...pairs] as [string, string][];
updated[i] = [key, e.target.value];
onChange(updated);
}}
onBlur={() => onSave(pairs)}
placeholder="value"
className={inputCls}
/>
<button
onClick={() => {
const updated = pairs.filter((_, j) => j !== i);
onChange(updated);
onSave(updated);
}}
className="flex-shrink-0 px-1.5 py-1 text-xs text-[var(--error)] hover:bg-[var(--bg-secondary)] rounded transition-colors"
>
x
</button>
</div>
))}
<button
onClick={() => {
onChange([...pairs, ["", ""]]);
}}
className="text-xs text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors"
>
+ Add
</button>
</div>
);
}
@@ -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; let mockSelectedProjectId: string | null = null;
vi.mock("../../store/appState", () => ({ vi.mock("../../store/appState", () => ({
useAppState: vi.fn((selector) => useAppState: vi.fn((selector) =>
@@ -67,7 +57,6 @@ const mockProject: Project = {
custom_env_vars: [], custom_env_vars: [],
port_mappings: [], port_mappings: [],
claude_instructions: null, claude_instructions: null,
enabled_mcp_servers: [],
created_at: "2026-01-01T00:00:00Z", created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z",
}; };
+2 -47
View File
@@ -4,7 +4,6 @@ import * as commands from "../../lib/tauri-commands";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, OpenAiCompatibleConfig } from "../../lib/types"; import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, OpenAiCompatibleConfig } from "../../lib/types";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { useMcpServers } from "../../hooks/useMcpServers";
import { useTerminal } from "../../hooks/useTerminal"; import { useTerminal } from "../../hooks/useTerminal";
import { useAppState } from "../../store/appState"; import { useAppState } from "../../store/appState";
import EnvVarsModal from "./EnvVarsModal"; import EnvVarsModal from "./EnvVarsModal";
@@ -24,7 +23,6 @@ export default function ProjectCard({ project }: Props) {
const selectedProjectId = useAppState(s => s.selectedProjectId); const selectedProjectId = useAppState(s => s.selectedProjectId);
const setSelectedProject = useAppState(s => s.setSelectedProject); const setSelectedProject = useAppState(s => s.setSelectedProject);
const { start, stop, rebuild, remove, update } = useProjects(); const { start, stop, rebuild, remove, update } = useProjects();
const { mcpServers } = useMcpServers();
const { open: openTerminal } = useTerminal(); const { open: openTerminal } = useTerminal();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -196,7 +194,7 @@ export default function ProjectCard({ project }: Props) {
setError(null); setError(null);
const bytes = await commands.downloadContainerBackup(project.id, hostPath); const bytes = await commands.downloadContainerBackup(project.id, hostPath);
const mb = (bytes / (1024 * 1024)).toFixed(1); 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); setProgressMsg(msg);
// Auto-clear so the transient confirmation doesn't linger in the card // Auto-clear so the transient confirmation doesn't linger in the card
// status; guard against clobbering a newer message (e.g. a later op). // status; guard against clobbering a newer message (e.g. a later op).
@@ -539,7 +537,7 @@ export default function ProjectCard({ project }: Props) {
onClick={handleBackup} onClick={handleBackup}
disabled={loading || backingUp} disabled={loading || backingUp}
label={backingUp ? "Backing up…" : "Backup"} 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) {
</button> </button>
</div> </div>
{/* MCP Servers */}
{mcpServers.length > 0 && (
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-1">MCP Servers<Tooltip text="Model Context Protocol servers give Claude access to external tools and data sources." /></label>
<div className="space-y-1">
{mcpServers.map((server) => {
const enabled = project.enabled_mcp_servers.includes(server.id);
const isDocker = !!server.docker_image;
return (
<label key={server.id} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={enabled}
disabled={!isStopped}
onChange={async () => {
const updated = enabled
? project.enabled_mcp_servers.filter((id) => id !== server.id)
: [...project.enabled_mcp_servers, server.id];
try {
await update({ ...project, enabled_mcp_servers: updated });
} catch (err) {
console.error("Failed to update MCP servers:", err);
}
}}
className="rounded border-[var(--border-color)] disabled:opacity-50"
/>
<span className="text-xs text-[var(--text-primary)]">{server.name}</span>
<span className="text-xs text-[var(--text-secondary)]">({server.transport_type})</span>
<span className={`text-xs px-1 py-0.5 rounded ${isDocker ? "bg-blue-500/20 text-blue-400" : "bg-[var(--bg-secondary)] text-[var(--text-secondary)]"}`}>
{isDocker ? "Docker" : "Manual"}
</span>
</label>
);
})}
</div>
{mcpServers.some((s) => s.docker_image && s.transport_type === "stdio" && project.enabled_mcp_servers.includes(s.id)) && (
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-70">
Docker access will be auto-enabled for stdio+Docker MCP servers.
</p>
)}
</div>
)}
{/* Bedrock config */} {/* Bedrock config */}
{project.backend === "bedrock" && (() => { {project.backend === "bedrock" && (() => {
const bc = project.bedrock_config ?? defaultBedrockConfig; const bc = project.bedrock_config ?? defaultBedrockConfig;
-55
View File
@@ -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 };
}
+1 -10
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core"; 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 // Docker
export const checkDocker = () => invoke<boolean>("check_docker"); export const checkDocker = () => invoke<boolean>("check_docker");
@@ -64,15 +64,6 @@ export const sendAudioData = (sessionId: string, data: number[]) =>
export const stopAudioBridge = (sessionId: string) => export const stopAudioBridge = (sessionId: string) =>
invoke<void>("stop_audio_bridge", { sessionId }); invoke<void>("stop_audio_bridge", { sessionId });
// MCP Servers
export const listMcpServers = () => invoke<McpServer[]>("list_mcp_servers");
export const addMcpServer = (name: string) =>
invoke<McpServer>("add_mcp_server", { name });
export const updateMcpServer = (server: McpServer) =>
invoke<McpServer>("update_mcp_server", { server });
export const removeMcpServer = (serverId: string) =>
invoke<void>("remove_mcp_server", { serverId });
// Files // Files
export const listContainerFiles = (projectId: string, path: string) => export const listContainerFiles = (projectId: string, path: string) =>
invoke<FileEntry[]>("list_container_files", { projectId, path }); invoke<FileEntry[]>("list_container_files", { projectId, path });
-18
View File
@@ -35,7 +35,6 @@ export interface Project {
custom_env_vars: EnvVar[]; custom_env_vars: EnvVar[];
port_mappings: PortMapping[]; port_mappings: PortMapping[];
claude_instructions: string | null; claude_instructions: string | null;
enabled_mcp_servers: string[];
claude_code_settings: ClaudeCodeSettings | null; claude_code_settings: ClaudeCodeSettings | null;
renamed_session_names: Record<string, string>; renamed_session_names: Record<string, string>;
created_at: string; created_at: string;
@@ -202,23 +201,6 @@ export interface ImageUpdateInfo {
remote_updated_at: string | null; 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<string, string>;
url: string | null;
headers: Record<string, string>;
docker_image: string | null;
container_port: number | null;
created_at: string;
updated_at: string;
}
export interface FileEntry { export interface FileEntry {
name: string; name: string;
path: string; path: string;
+3 -23
View File
@@ -1,5 +1,5 @@
import { create } from "zustand"; 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"; const SIDEBAR_COLLAPSED_KEY = "triple-c.sidebar.collapsed";
@@ -35,12 +35,6 @@ interface AppState {
removeSession: (id: string) => void; removeSession: (id: string) => void;
setActiveSession: (id: string | null) => void; setActiveSession: (id: string | null) => void;
// MCP servers
mcpServers: McpServer[];
setMcpServers: (servers: McpServer[]) => void;
updateMcpServerInList: (server: McpServer) => void;
removeMcpServerFromList: (id: string) => void;
// UI state // UI state
terminalHasSelection: boolean; terminalHasSelection: boolean;
setTerminalHasSelection: (has: boolean) => void; setTerminalHasSelection: (has: boolean) => void;
@@ -54,8 +48,8 @@ interface AppState {
setTerminalAtBottom: (v: boolean) => void; setTerminalAtBottom: (v: boolean) => void;
scrollActiveToBottom: () => void; scrollActiveToBottom: () => void;
setScrollActiveToBottom: (fn: () => void) => void; setScrollActiveToBottom: (fn: () => void) => void;
sidebarView: "projects" | "mcp" | "settings"; sidebarView: "projects" | "settings";
setSidebarView: (view: "projects" | "mcp" | "settings") => void; setSidebarView: (view: "projects" | "settings") => void;
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void; setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebarCollapsed: () => void; toggleSidebarCollapsed: () => void;
@@ -118,20 +112,6 @@ export const useAppState = create<AppState>((set) => ({
}), }),
setActiveSession: (id) => set({ activeSessionId: id }), 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 // UI state
terminalHasSelection: false, terminalHasSelection: false,
setTerminalHasSelection: (has) => set({ terminalHasSelection: has }), setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),