From a2114ec2f27c2b800dfe8d1eeaf2546544fdd5ff Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Mon, 4 May 2026 18:28:07 +0200 Subject: [PATCH 1/2] feat(code): add MCP servers entry to main sidebar under Skills Adds a top-level "MCP servers" sidebar item directly underneath the Skills item. Clicking it opens the existing MCP servers management UI as a main view (header set via useSetHeaderContent), reusing the McpServersSettings component already shown in Settings. Generated-By: PostHog Code Task-Id: 3516a98e-d297-4c74-a78e-ed7a96855927 --- .../src/renderer/components/MainLayout.tsx | 3 ++ .../mcp-servers/components/McpServersView.tsx | 30 +++++++++++++++++++ .../sidebar/components/SidebarMenu.tsx | 13 ++++++++ .../components/items/McpServersItem.tsx | 19 ++++++++++++ .../features/sidebar/hooks/useSidebarData.ts | 6 +++- .../src/renderer/stores/navigationStore.ts | 11 ++++++- 6 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx create mode 100644 apps/code/src/renderer/features/sidebar/components/items/McpServersItem.tsx diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index af6b83c5a..cb2696f69 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -10,6 +10,7 @@ import { CommandMenu } from "@features/command/components/CommandMenu"; import { CommandCenterView } from "@features/command-center/components/CommandCenterView"; import { InboxView } from "@features/inbox/components/InboxView"; import { useInboxDeepLink } from "@features/inbox/hooks/useInboxDeepLink"; +import { McpServersView } from "@features/mcp-servers/components/McpServersView"; import { FolderSettingsView } from "@features/settings/components/FolderSettingsView"; import { SettingsDialog } from "@features/settings/components/SettingsDialog"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; @@ -129,6 +130,8 @@ export function MainLayout() { {view.type === "command-center" && } {view.type === "skills" && } + + {view.type === "mcp-servers" && } diff --git a/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx b/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx new file mode 100644 index 000000000..732431d10 --- /dev/null +++ b/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx @@ -0,0 +1,30 @@ +import { McpServersSettings } from "@features/settings/components/sections/McpServersSettings"; +import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; +import { Plugs } from "@phosphor-icons/react"; +import { Box, Flex, Text } from "@radix-ui/themes"; +import { useMemo } from "react"; + +export function McpServersView() { + const headerContent = useMemo( + () => ( + + + + MCP servers + + + ), + [], + ); + + useSetHeaderContent(headerContent); + + return ( + + + + ); +} diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx index 47222ff98..14b768738 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx @@ -28,6 +28,7 @@ import { useSidebarData } from "../hooks/useSidebarData"; import { useTaskViewed } from "../hooks/useTaskViewed"; import { CommandCenterItem } from "./items/CommandCenterItem"; import { InboxItem, NewTaskItem } from "./items/HomeItem"; +import { McpServersItem } from "./items/McpServersItem"; import { SkillsItem } from "./items/SkillsItem"; import { SidebarItem } from "./SidebarItem"; import { TaskListView } from "./TaskListView"; @@ -40,6 +41,7 @@ function SidebarMenuComponent() { navigateToInbox, navigateToCommandCenter, navigateToSkills, + navigateToMcpServers, } = useNavigationStore(); const { data: allTasks = [] } = useTasks(); @@ -114,6 +116,10 @@ function SidebarMenuComponent() { navigateToSkills(); }; + const handleMcpServersClick = () => { + navigateToMcpServers(); + }; + const handleTaskClick = (taskId: string) => { const task = taskMap.get(taskId); if (task) { @@ -292,6 +298,13 @@ function SidebarMenuComponent() { /> + + + + void; +} + +export function McpServersItem({ isActive, onClick }: McpServersItemProps) { + return ( + } + label="MCP servers" + isActive={isActive} + onClick={onClick} + /> + ); +} diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts index 141049b25..4dc20b092 100644 --- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts +++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts @@ -40,6 +40,7 @@ export interface SidebarData { isInboxActive: boolean; isCommandCenterActive: boolean; isSkillsActive: boolean; + isMcpServersActive: boolean; isLoading: boolean; activeTaskId: string | null; pinnedTasks: TaskData[]; @@ -58,7 +59,8 @@ interface ViewState { | "inbox" | "archived" | "command-center" - | "skills"; + | "skills" + | "mcp-servers"; data?: Task; } @@ -123,6 +125,7 @@ export function useSidebarData({ const isInboxActive = activeView.type === "inbox"; const isCommandCenterActive = activeView.type === "command-center"; const isSkillsActive = activeView.type === "skills"; + const isMcpServersActive = activeView.type === "mcp-servers"; const activeTaskId = activeView.type === "task-detail" && activeView.data @@ -232,6 +235,7 @@ export function useSidebarData({ isInboxActive, isCommandCenterActive, isSkillsActive, + isMcpServersActive, isLoading, activeTaskId, pinnedTasks, diff --git a/apps/code/src/renderer/stores/navigationStore.ts b/apps/code/src/renderer/stores/navigationStore.ts index 32232fd4f..3edf3bcba 100644 --- a/apps/code/src/renderer/stores/navigationStore.ts +++ b/apps/code/src/renderer/stores/navigationStore.ts @@ -19,7 +19,8 @@ type ViewType = | "inbox" | "archived" | "command-center" - | "skills"; + | "skills" + | "mcp-servers"; export interface TaskInputReportAssociation { reportId: string; @@ -60,6 +61,7 @@ interface NavigationStore { navigateToArchived: () => void; navigateToCommandCenter: () => void; navigateToSkills: () => void; + navigateToMcpServers: () => void; goBack: () => void; goForward: () => void; canGoBack: () => boolean; @@ -93,6 +95,9 @@ const isSameView = (view1: ViewState, view2: ViewState): boolean => { if (view1.type === "skills" && view2.type === "skills") { return true; } + if (view1.type === "mcp-servers" && view2.type === "mcp-servers") { + return true; + } return false; }; @@ -271,6 +276,10 @@ export const useNavigationStore = create()( navigate({ type: "skills" }); }, + navigateToMcpServers: () => { + navigate({ type: "mcp-servers" }); + }, + goBack: () => { const { history, historyIndex } = get(); if (historyIndex > 0) { From 71fad955a83fe5b7a7df82f1e8d62b1c8d560d74 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Mon, 4 May 2026 18:37:42 +0200 Subject: [PATCH 2/2] refactor(code): move MCP servers to dedicated feature, fix rail height - Move McpServersSettings + supporting components/hooks out of features/settings into a dedicated features/mcp-servers feature. - Rename the entrypoint to McpServersView; render it as a real top-level view (Flex height="100%") so the installed-rail extends the full content height instead of collapsing to its inner content. - Remove the MCP servers entry from the Settings sidebar (id, title, component map, SettingsCategory union) and drop the now-unused fullwidth branch in SettingsDialog. Generated-By: PostHog Code Task-Id: 3516a98e-d297-4c74-a78e-ed7a96855927 --- .../mcp-servers/components/McpServersView.tsx | 335 ++++++++++++++++- .../components/parts}/AddCustomServerForm.tsx | 0 .../components/parts}/MarketplaceView.tsx | 2 +- .../components/parts}/McpInstalledRail.tsx | 2 +- .../components/parts}/ServerCard.tsx | 0 .../components/parts}/ServerDetailView.tsx | 2 +- .../components/parts}/ToolPolicyToggle.tsx | 0 .../components/parts}/ToolRow.tsx | 0 .../components/parts}/icons.tsx | 0 .../components/parts}/statusBadge.test.ts | 0 .../components/parts}/statusBadge.ts | 0 .../hooks/mcpFilters.test.ts | 0 .../hooks/mcpFilters.ts | 0 .../hooks/mcpToolBulk.test.ts | 0 .../hooks/mcpToolBulk.ts | 0 .../hooks/useMcpInstallationTools.ts | 0 .../hooks/useMcpServers.ts | 0 .../settings/components/SettingsDialog.tsx | 39 +- .../sections/McpServersSettings.tsx | 338 ------------------ .../settings/stores/settingsDialogStore.ts | 1 - 20 files changed, 341 insertions(+), 378 deletions(-) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/AddCustomServerForm.tsx (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/MarketplaceView.tsx (99%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/McpInstalledRail.tsx (98%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/ServerCard.tsx (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/ServerDetailView.tsx (99%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/ToolPolicyToggle.tsx (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/ToolRow.tsx (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/icons.tsx (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/statusBadge.test.ts (100%) rename apps/code/src/renderer/features/{settings/components/sections/mcp => mcp-servers/components/parts}/statusBadge.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/mcpFilters.test.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/mcpFilters.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/mcpToolBulk.test.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/mcpToolBulk.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/useMcpInstallationTools.ts (100%) rename apps/code/src/renderer/features/{settings => mcp-servers}/hooks/useMcpServers.ts (100%) delete mode 100644 apps/code/src/renderer/features/settings/components/sections/McpServersSettings.tsx diff --git a/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx b/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx index 732431d10..0c13063da 100644 --- a/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx +++ b/apps/code/src/renderer/features/mcp-servers/components/McpServersView.tsx @@ -1,10 +1,50 @@ -import { McpServersSettings } from "@features/settings/components/sections/McpServersSettings"; +import { useMcpServers } from "@features/mcp-servers/hooks/useMcpServers"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; import { Plugs } from "@phosphor-icons/react"; -import { Box, Flex, Text } from "@radix-ui/themes"; -import { useMemo } from "react"; +import { + AlertDialog, + Box, + Button, + Flex, + ScrollArea, + Spinner, + Text, +} from "@radix-ui/themes"; +import type { + McpRecommendedServer, + McpServerInstallation, +} from "@renderer/api/posthogClient"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { AddCustomServerForm } from "./parts/AddCustomServerForm"; +import { MarketplaceView } from "./parts/MarketplaceView"; +import { McpInstalledRail } from "./parts/McpInstalledRail"; +import { ServerDetailView } from "./parts/ServerDetailView"; + +type SceneView = + | { kind: "marketplace" } + | { kind: "detail-installation"; installationId: string } + | { kind: "detail-template"; templateId: string } + | { kind: "add-custom" }; export function McpServersView() { + const queryClient = useQueryClient(); + const [view, setView] = useState({ kind: "marketplace" }); + const [query, setQuery] = useState(""); + const [category, setCategory] = + useState[0]["category"]>("all"); + const [uninstallTarget, setUninstallTarget] = + useState(null); + // Snapshot of installation IDs taken when the user submits the Add Custom + // form. The new installation is whichever id appears that wasn't in the + // snapshot — robust against backend URL normalisation that would break a + // string-equality match on `installation.url`. + const [pendingCustomKnownIds, setPendingCustomKnownIds] = + useState | null>(null); + const [pendingTemplateId, setPendingTemplateId] = useState( + null, + ); + const headerContent = useMemo( () => ( @@ -19,12 +59,293 @@ export function McpServersView() { ), [], ); - useSetHeaderContent(headerContent); + const { + installations, + installationsLoading, + servers, + serversLoading, + installingId, + uninstallMutation, + toggleEnabled, + installTemplate, + installCustom, + installCustomPending, + reauthorize, + reauthorizePending, + } = useMcpServers(); + + useEffect(() => { + const refreshMcpState = () => { + queryClient.invalidateQueries({ queryKey: ["mcp"] }); + }; + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") refreshMcpState(); + }; + window.addEventListener("focus", refreshMcpState); + document.addEventListener("visibilitychange", handleVisibilityChange); + return () => { + window.removeEventListener("focus", refreshMcpState); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [queryClient]); + + const serverList = servers ?? []; + const installationList = installations ?? []; + + const selectedInstallation = useMemo(() => { + if (view.kind !== "detail-installation") return null; + return installationList.find((i) => i.id === view.installationId) ?? null; + }, [view, installationList]); + + const selectedTemplate = useMemo(() => { + if (view.kind === "detail-template") { + return serverList.find((s) => s.id === view.templateId) ?? null; + } + if (view.kind === "detail-installation" && selectedInstallation) { + return ( + serverList.find((s) => s.id === selectedInstallation.template_id) ?? + null + ); + } + return null; + }, [view, serverList, selectedInstallation]); + + const handleConnect = useCallback( + (template: McpRecommendedServer) => { + setPendingTemplateId(template.id); + installTemplate(template); + }, + [installTemplate], + ); + + const handleUninstallConfirm = useCallback(() => { + if (!uninstallTarget) return; + uninstallMutation.mutate(uninstallTarget.id, { + onSuccess: () => { + setUninstallTarget(null); + setView({ kind: "marketplace" }); + }, + }); + }, [uninstallTarget, uninstallMutation]); + + // When installations list updates, if the opened installation disappears, go back. + useEffect(() => { + if ( + view.kind === "detail-installation" && + !installationList.some((i) => i.id === view.installationId) + ) { + setView({ kind: "marketplace" }); + } + }, [view, installationList]); + + // When viewing a template and it gets installed, switch to the installation + // detail so the freshly-fetched tools and status render. + useEffect(() => { + if (view.kind !== "detail-template") return; + const installation = installationList.find( + (i) => i.template_id === view.templateId, + ); + if (installation) { + setView({ kind: "detail-installation", installationId: installation.id }); + } + }, [view, installationList]); + + // After a custom server install resolves, jump to its detail panel once the + // new installation appears in the list. Identifies the new one as any id + // not present in the pre-submit snapshot — does not rely on URL equality. + useEffect(() => { + if (!pendingCustomKnownIds) return; + const newOne = installationList.find( + (i) => !pendingCustomKnownIds.has(i.id), + ); + if (newOne) { + setPendingCustomKnownIds(null); + setView({ kind: "detail-installation", installationId: newOne.id }); + } + }, [pendingCustomKnownIds, installationList]); + + // After a template install resolves, jump to the new installation's detail + // panel. Stays put if the install fails (no matching installation appears). + useEffect(() => { + if (!pendingTemplateId) return; + const installation = installationList.find( + (i) => i.template_id === pendingTemplateId, + ); + if (installation) { + setPendingTemplateId(null); + setView({ kind: "detail-installation", installationId: installation.id }); + } + }, [pendingTemplateId, installationList]); + + const selectedInstallationId = + view.kind === "detail-installation" ? view.installationId : null; + + const mainContent = (() => { + if (view.kind === "add-custom") { + return ( + setView({ kind: "marketplace" })} + onSubmit={(values) => { + setPendingCustomKnownIds( + new Set(installationList.map((i) => i.id)), + ); + installCustom(values, { + onError: () => setPendingCustomKnownIds(null), + }); + }} + /> + ); + } + + if ( + view.kind === "detail-installation" || + view.kind === "detail-template" + ) { + const install = + view.kind === "detail-installation" ? selectedInstallation : null; + const template = selectedTemplate; + + if (!install && !template) { + return ( + + {installationsLoading || serversLoading ? ( + + ) : ( + + Server not found. + + )} + + ); + } + + return ( + setView({ kind: "marketplace" })} + onConnect={() => { + if (template) { + setPendingTemplateId(template.id); + installTemplate(template); + } + }} + onReauthorize={() => { + if (install) reauthorize(install.id); + }} + onToggleEnabled={(enabled) => { + if (install) toggleEnabled(install.id, enabled); + }} + onUninstall={() => { + if (install) setUninstallTarget(install); + }} + /> + ); + } + + return ( + + setView({ kind: "detail-template", templateId }) + } + onOpenInstallation={(installationId) => + setView({ kind: "detail-installation", installationId }) + } + onConnect={handleConnect} + onAddCustom={() => setView({ kind: "add-custom" })} + /> + ); + })(); + + return ( + + setView({ kind: "add-custom" })} + onSelectInstallation={(installationId) => + setView({ kind: "detail-installation", installationId }) + } + /> + + + + {mainContent} + + + + setUninstallTarget(null)} + onConfirm={handleUninstallConfirm} + /> + + ); +} + +function UninstallConfirmDialog({ + target, + isPending, + onCancel, + onConfirm, +}: { + target: McpServerInstallation | null; + isPending: boolean; + onCancel: () => void; + onConfirm: () => void; +}) { + const open = !!target; + const name = + target?.display_name || target?.name || target?.url || "this server"; return ( - - - + { + if (!next) onCancel(); + }} + > + + Remove MCP server + + Are you sure you want to remove{" "} + {name}? This will revoke its tools + from your agent. + + + + + + + + + + + ); } diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/AddCustomServerForm.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/AddCustomServerForm.tsx similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/AddCustomServerForm.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/AddCustomServerForm.tsx diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/MarketplaceView.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/MarketplaceView.tsx similarity index 99% rename from apps/code/src/renderer/features/settings/components/sections/mcp/MarketplaceView.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/MarketplaceView.tsx index bb72a8061..6abfbcaff 100644 --- a/apps/code/src/renderer/features/settings/components/sections/mcp/MarketplaceView.tsx +++ b/apps/code/src/renderer/features/mcp-servers/components/parts/MarketplaceView.tsx @@ -1,7 +1,7 @@ import { filterServersByCategory, filterServersByQuery, -} from "@features/settings/hooks/mcpFilters"; +} from "@features/mcp-servers/hooks/mcpFilters"; import { MagnifyingGlass, Plus, X } from "@phosphor-icons/react"; import { Button, diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/McpInstalledRail.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/McpInstalledRail.tsx similarity index 98% rename from apps/code/src/renderer/features/settings/components/sections/mcp/McpInstalledRail.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/McpInstalledRail.tsx index ce97ce103..13665b15a 100644 --- a/apps/code/src/renderer/features/settings/components/sections/mcp/McpInstalledRail.tsx +++ b/apps/code/src/renderer/features/mcp-servers/components/parts/McpInstalledRail.tsx @@ -1,4 +1,4 @@ -import { filterInstallationsByQuery } from "@features/settings/hooks/mcpFilters"; +import { filterInstallationsByQuery } from "@features/mcp-servers/hooks/mcpFilters"; import { MagnifyingGlass, Plus, X } from "@phosphor-icons/react"; import { Flex, diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/ServerCard.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/ServerCard.tsx similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/ServerCard.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/ServerCard.tsx diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/ServerDetailView.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx similarity index 99% rename from apps/code/src/renderer/features/settings/components/sections/mcp/ServerDetailView.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx index a1f5e609a..3b6ec7784 100644 --- a/apps/code/src/renderer/features/settings/components/sections/mcp/ServerDetailView.tsx +++ b/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx @@ -1,4 +1,4 @@ -import { useMcpInstallationTools } from "@features/settings/hooks/useMcpInstallationTools"; +import { useMcpInstallationTools } from "@features/mcp-servers/hooks/useMcpInstallationTools"; import { ArrowClockwise, ArrowLeft, diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/ToolPolicyToggle.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/ToolPolicyToggle.tsx similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/ToolPolicyToggle.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/ToolPolicyToggle.tsx diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/ToolRow.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/ToolRow.tsx similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/ToolRow.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/ToolRow.tsx diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/icons.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/icons.tsx similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/icons.tsx rename to apps/code/src/renderer/features/mcp-servers/components/parts/icons.tsx diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/statusBadge.test.ts b/apps/code/src/renderer/features/mcp-servers/components/parts/statusBadge.test.ts similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/statusBadge.test.ts rename to apps/code/src/renderer/features/mcp-servers/components/parts/statusBadge.test.ts diff --git a/apps/code/src/renderer/features/settings/components/sections/mcp/statusBadge.ts b/apps/code/src/renderer/features/mcp-servers/components/parts/statusBadge.ts similarity index 100% rename from apps/code/src/renderer/features/settings/components/sections/mcp/statusBadge.ts rename to apps/code/src/renderer/features/mcp-servers/components/parts/statusBadge.ts diff --git a/apps/code/src/renderer/features/settings/hooks/mcpFilters.test.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpFilters.test.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/mcpFilters.test.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/mcpFilters.test.ts diff --git a/apps/code/src/renderer/features/settings/hooks/mcpFilters.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpFilters.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/mcpFilters.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/mcpFilters.ts diff --git a/apps/code/src/renderer/features/settings/hooks/mcpToolBulk.test.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolBulk.test.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/mcpToolBulk.test.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/mcpToolBulk.test.ts diff --git a/apps/code/src/renderer/features/settings/hooks/mcpToolBulk.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolBulk.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/mcpToolBulk.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/mcpToolBulk.ts diff --git a/apps/code/src/renderer/features/settings/hooks/useMcpInstallationTools.ts b/apps/code/src/renderer/features/mcp-servers/hooks/useMcpInstallationTools.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/useMcpInstallationTools.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/useMcpInstallationTools.ts diff --git a/apps/code/src/renderer/features/settings/hooks/useMcpServers.ts b/apps/code/src/renderer/features/mcp-servers/hooks/useMcpServers.ts similarity index 100% rename from apps/code/src/renderer/features/settings/hooks/useMcpServers.ts rename to apps/code/src/renderer/features/mcp-servers/hooks/useMcpServers.ts diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 43ccb5914..01c73dc4d 100644 --- a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx +++ b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx @@ -22,7 +22,6 @@ import { HardDrives, Keyboard, Palette, - Plugs, SignOut, TrafficSignal, TreeStructure, @@ -37,7 +36,6 @@ import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings"; import { CloudEnvironmentsSettings } from "./sections/CloudEnvironmentsSettings"; import { EnvironmentsSettings } from "./sections/environments/EnvironmentsSettings"; import { GeneralSettings } from "./sections/GeneralSettings"; -import { McpServersSettings } from "./sections/McpServersSettings"; import { PersonalizationSettings } from "./sections/PersonalizationSettings"; import { PlanUsageSettings } from "./sections/PlanUsageSettings"; import { ShortcutsSettings } from "./sections/ShortcutsSettings"; @@ -51,7 +49,6 @@ interface SidebarItem { label: string; icon: ReactNode; hasChevron?: boolean; - fullwidth?: boolean; } const SIDEBAR_ITEMS: SidebarItem[] = [ @@ -75,12 +72,6 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ icon: , }, { id: "claude-code", label: "Claude Code", icon: }, - { - id: "mcp-servers", - label: "MCP servers", - icon: , - fullwidth: true, - }, { id: "shortcuts", label: "Shortcuts", icon: }, { @@ -101,7 +92,6 @@ const CATEGORY_TITLES: Record = { "cloud-environments": "Cloud environments", personalization: "Personalization", "claude-code": "Claude Code", - "mcp-servers": "MCP Servers", shortcuts: "Shortcuts", signals: "Signals", @@ -118,7 +108,6 @@ const CATEGORY_COMPONENTS: Record = { "cloud-environments": CloudEnvironmentsSettings, personalization: PersonalizationSettings, "claude-code": ClaudeCodeSettings, - "mcp-servers": McpServersSettings, shortcuts: ShortcutsSettings, signals: SignalSourcesSettings, @@ -169,8 +158,6 @@ export function SettingsDialog() { } const ActiveComponent = CATEGORY_COMPONENTS[activeCategory]; - const activeItem = sidebarItems.find((i) => i.id === activeCategory); - const isFullwidth = !!activeItem?.fullwidth; const initials = user ? user.first_name && user.last_name @@ -278,22 +265,16 @@ export function SettingsDialog() { fill="url(#settings-dot-pattern)" /> - {isFullwidth ? ( -
- -
- ) : ( - - - - - {CATEGORY_TITLES[activeCategory]} - - - - - - )} + + + + + {CATEGORY_TITLES[activeCategory]} + + + + + diff --git a/apps/code/src/renderer/features/settings/components/sections/McpServersSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/McpServersSettings.tsx deleted file mode 100644 index 1623735e7..000000000 --- a/apps/code/src/renderer/features/settings/components/sections/McpServersSettings.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { useMcpServers } from "@features/settings/hooks/useMcpServers"; -import { - AlertDialog, - Box, - Button, - Flex, - ScrollArea, - Spinner, - Text, -} from "@radix-ui/themes"; -import type { - McpRecommendedServer, - McpServerInstallation, -} from "@renderer/api/posthogClient"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { AddCustomServerForm } from "./mcp/AddCustomServerForm"; -import { MarketplaceView } from "./mcp/MarketplaceView"; -import { McpInstalledRail } from "./mcp/McpInstalledRail"; -import { ServerDetailView } from "./mcp/ServerDetailView"; - -type SceneView = - | { kind: "marketplace" } - | { kind: "detail-installation"; installationId: string } - | { kind: "detail-template"; templateId: string } - | { kind: "add-custom" }; - -export function McpServersSettings() { - const queryClient = useQueryClient(); - const [view, setView] = useState({ kind: "marketplace" }); - const [query, setQuery] = useState(""); - const [category, setCategory] = - useState[0]["category"]>("all"); - const [uninstallTarget, setUninstallTarget] = - useState(null); - // Snapshot of installation IDs taken when the user submits the Add Custom - // form. The new installation is whichever id appears that wasn't in the - // snapshot — robust against backend URL normalisation that would break a - // string-equality match on `installation.url`. - const [pendingCustomKnownIds, setPendingCustomKnownIds] = - useState | null>(null); - const [pendingTemplateId, setPendingTemplateId] = useState( - null, - ); - - const { - installations, - installationsLoading, - servers, - serversLoading, - installingId, - uninstallMutation, - toggleEnabled, - installTemplate, - installCustom, - installCustomPending, - reauthorize, - reauthorizePending, - } = useMcpServers(); - - useEffect(() => { - const refreshMcpState = () => { - queryClient.invalidateQueries({ queryKey: ["mcp"] }); - }; - const handleVisibilityChange = () => { - if (document.visibilityState === "visible") refreshMcpState(); - }; - window.addEventListener("focus", refreshMcpState); - document.addEventListener("visibilitychange", handleVisibilityChange); - return () => { - window.removeEventListener("focus", refreshMcpState); - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, [queryClient]); - - const serverList = servers ?? []; - const installationList = installations ?? []; - - const selectedInstallation = useMemo(() => { - if (view.kind !== "detail-installation") return null; - return installationList.find((i) => i.id === view.installationId) ?? null; - }, [view, installationList]); - - const selectedTemplate = useMemo(() => { - if (view.kind === "detail-template") { - return serverList.find((s) => s.id === view.templateId) ?? null; - } - if (view.kind === "detail-installation" && selectedInstallation) { - return ( - serverList.find((s) => s.id === selectedInstallation.template_id) ?? - null - ); - } - return null; - }, [view, serverList, selectedInstallation]); - - const handleConnect = useCallback( - (template: McpRecommendedServer) => { - setPendingTemplateId(template.id); - installTemplate(template); - }, - [installTemplate], - ); - - const handleUninstallConfirm = useCallback(() => { - if (!uninstallTarget) return; - uninstallMutation.mutate(uninstallTarget.id, { - onSuccess: () => { - setUninstallTarget(null); - setView({ kind: "marketplace" }); - }, - }); - }, [uninstallTarget, uninstallMutation]); - - // When installations list updates, if the opened installation disappears, go back. - useEffect(() => { - if ( - view.kind === "detail-installation" && - !installationList.some((i) => i.id === view.installationId) - ) { - setView({ kind: "marketplace" }); - } - }, [view, installationList]); - - // When viewing a template and it gets installed, switch to the installation - // detail so the freshly-fetched tools and status render. - useEffect(() => { - if (view.kind !== "detail-template") return; - const installation = installationList.find( - (i) => i.template_id === view.templateId, - ); - if (installation) { - setView({ kind: "detail-installation", installationId: installation.id }); - } - }, [view, installationList]); - - // After a custom server install resolves, jump to its detail panel once the - // new installation appears in the list. Identifies the new one as any id - // not present in the pre-submit snapshot — does not rely on URL equality. - useEffect(() => { - if (!pendingCustomKnownIds) return; - const newOne = installationList.find( - (i) => !pendingCustomKnownIds.has(i.id), - ); - if (newOne) { - setPendingCustomKnownIds(null); - setView({ kind: "detail-installation", installationId: newOne.id }); - } - }, [pendingCustomKnownIds, installationList]); - - // After a template install resolves, jump to the new installation's detail - // panel. Stays put if the install fails (no matching installation appears). - useEffect(() => { - if (!pendingTemplateId) return; - const installation = installationList.find( - (i) => i.template_id === pendingTemplateId, - ); - if (installation) { - setPendingTemplateId(null); - setView({ kind: "detail-installation", installationId: installation.id }); - } - }, [pendingTemplateId, installationList]); - - const selectedInstallationId = - view.kind === "detail-installation" ? view.installationId : null; - - const mainContent = (() => { - if (view.kind === "add-custom") { - return ( - setView({ kind: "marketplace" })} - onSubmit={(values) => { - setPendingCustomKnownIds( - new Set(installationList.map((i) => i.id)), - ); - installCustom(values, { - onError: () => setPendingCustomKnownIds(null), - }); - }} - /> - ); - } - - if ( - view.kind === "detail-installation" || - view.kind === "detail-template" - ) { - const install = - view.kind === "detail-installation" ? selectedInstallation : null; - const template = selectedTemplate; - - if (!install && !template) { - return ( - - {installationsLoading || serversLoading ? ( - - ) : ( - - Server not found. - - )} - - ); - } - - return ( - setView({ kind: "marketplace" })} - onConnect={() => { - if (template) { - setPendingTemplateId(template.id); - installTemplate(template); - } - }} - onReauthorize={() => { - if (install) reauthorize(install.id); - }} - onToggleEnabled={(enabled) => { - if (install) toggleEnabled(install.id, enabled); - }} - onUninstall={() => { - if (install) setUninstallTarget(install); - }} - /> - ); - } - - return ( - - setView({ kind: "detail-template", templateId }) - } - onOpenInstallation={(installationId) => - setView({ kind: "detail-installation", installationId }) - } - onConnect={handleConnect} - onAddCustom={() => setView({ kind: "add-custom" })} - /> - ); - })(); - - return ( - - setView({ kind: "add-custom" })} - onSelectInstallation={(installationId) => - setView({ kind: "detail-installation", installationId }) - } - /> - - - - {mainContent} - - - - setUninstallTarget(null)} - onConfirm={handleUninstallConfirm} - /> - - ); -} - -function UninstallConfirmDialog({ - target, - isPending, - onCancel, - onConfirm, -}: { - target: McpServerInstallation | null; - isPending: boolean; - onCancel: () => void; - onConfirm: () => void; -}) { - const open = !!target; - const name = - target?.display_name || target?.name || target?.url || "this server"; - return ( - { - if (!next) onCancel(); - }} - > - - Remove MCP server - - Are you sure you want to remove{" "} - {name}? This will revoke its tools - from your agent. - - - - - - - - - - - - ); -} diff --git a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts index 69660dded..6e15a18c6 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts @@ -10,7 +10,6 @@ export type SettingsCategory = | "personalization" | "claude-code" | "shortcuts" - | "mcp-servers" | "signals" | "updates" | "advanced";