diff --git a/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx b/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx
index 905e9da2b..263783f9c 100644
--- a/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx
+++ b/apps/code/src/renderer/features/mcp-servers/components/parts/ServerDetailView.tsx
@@ -1,3 +1,4 @@
+import { groupMcpToolsByCapability } from "@features/mcp-servers/hooks/mcpToolGroups";
import { useMcpInstallationTools } from "@features/mcp-servers/hooks/useMcpInstallationTools";
import {
ArrowClockwise,
@@ -25,6 +26,7 @@ import {
} from "@radix-ui/themes";
import type {
McpApprovalState,
+ McpInstallationTool,
McpRecommendedServer,
McpServerInstallation,
} from "@renderer/api/posthogClient";
@@ -50,6 +52,45 @@ interface ServerDetailViewProps {
onUninstall: () => void;
}
+interface ToolGroupSectionProps {
+ title: string;
+ tools: McpInstallationTool[];
+ onToolApprovalChange: (
+ toolName: string,
+ approval_state: McpApprovalState,
+ ) => void;
+}
+
+function ToolGroupSection({
+ title,
+ tools,
+ onToolApprovalChange,
+}: ToolGroupSectionProps) {
+ if (tools.length === 0) return null;
+
+ return (
+
+
+
+ {title}
+
+
+ {tools.length}
+
+
+ {tools.map((tool) => (
+
+ onToolApprovalChange(tool.tool_name, approval_state)
+ }
+ />
+ ))}
+
+ );
+}
+
export function ServerDetailView({
installation,
template,
@@ -119,6 +160,21 @@ export function ServerDetailView({
return visibleTools.filter((t) => t.tool_name.toLowerCase().includes(term));
}, [visibleTools, toolSearch]);
+ const groupedTools = useMemo(
+ () => groupMcpToolsByCapability(filteredTools),
+ [filteredTools],
+ );
+
+ const handleToolApprovalChange = (
+ toolName: string,
+ approval_state: McpApprovalState,
+ ) => {
+ setToolApproval({
+ toolName,
+ approval_state,
+ });
+ };
+
const removedCount = tools.filter((t) => !!t.removed_at).length;
return (
@@ -385,18 +441,18 @@ export function ServerDetailView({
) : (
- filteredTools.map((tool) => (
-
- setToolApproval({
- toolName: tool.tool_name,
- approval_state,
- })
- }
+ <>
+
- ))
+
+ >
)}
)}
diff --git a/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts
new file mode 100644
index 000000000..5a2a1c146
--- /dev/null
+++ b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts
@@ -0,0 +1,76 @@
+import type { McpInstallationTool } from "@renderer/api/posthogClient";
+import { describe, expect, it } from "vitest";
+import { getMcpToolGroup, groupMcpToolsByCapability } from "./mcpToolGroups";
+
+function tool(
+ name: string,
+ overrides: Partial = {},
+): McpInstallationTool {
+ return {
+ id: `tool-${name}`,
+ tool_name: name,
+ display_name: name,
+ description: "",
+ input_schema: {},
+ approval_state: "needs_approval",
+ last_seen_at: "2026-01-01T00:00:00Z",
+ removed_at: null,
+ created_at: "2026-01-01T00:00:00Z",
+ updated_at: "2026-01-01T00:00:00Z",
+ ...overrides,
+ };
+}
+
+describe("getMcpToolGroup", () => {
+ it.each(["get_ticket", "list_projects", "search_docs", "lookup_customer"])(
+ "classifies %s as read",
+ (toolName) => {
+ expect(getMcpToolGroup(tool(toolName))).toBe("read");
+ },
+ );
+
+ it.each(["create_ticket", "delete_file", "send_message", "run_query"])(
+ "classifies %s as write/delete",
+ (toolName) => {
+ expect(getMcpToolGroup(tool(toolName))).toBe("write_delete");
+ },
+ );
+
+ it("falls back to display name and description when the tool name is ambiguous", () => {
+ expect(
+ getMcpToolGroup(
+ tool("ticket", {
+ display_name: "Find ticket",
+ }),
+ ),
+ ).toBe("read");
+ expect(
+ getMcpToolGroup(
+ tool("message", {
+ display_name: "Message",
+ description: "Send a message to the current channel",
+ }),
+ ),
+ ).toBe("write_delete");
+ });
+
+ it("defaults ambiguous tools to write/delete for safety", () => {
+ expect(getMcpToolGroup(tool("ticket"))).toBe("write_delete");
+ });
+});
+
+describe("groupMcpToolsByCapability", () => {
+ it("groups tools while preserving their input order within each group", () => {
+ const tools = [
+ tool("create_ticket"),
+ tool("get_ticket"),
+ tool("search_tickets"),
+ tool("update_ticket"),
+ ];
+
+ expect(groupMcpToolsByCapability(tools)).toEqual({
+ read: [tools[1], tools[2]],
+ write_delete: [tools[0], tools[3]],
+ });
+ });
+});
diff --git a/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.ts b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.ts
new file mode 100644
index 000000000..7fe7fafb2
--- /dev/null
+++ b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.ts
@@ -0,0 +1,73 @@
+import type { McpInstallationTool } from "@renderer/api/posthogClient";
+
+export type McpToolGroup = "read" | "write_delete";
+
+const READ_VERBS = new Set([
+ "fetch",
+ "find",
+ "get",
+ "list",
+ "lookup",
+ "query",
+ "read",
+ "search",
+ "view",
+]);
+
+const WRITE_DELETE_VERBS = new Set([
+ "add",
+ "archive",
+ "assign",
+ "close",
+ "create",
+ "delete",
+ "edit",
+ "execute",
+ "remove",
+ "run",
+ "send",
+ "set",
+ "update",
+ "upload",
+]);
+
+function firstVerb(value: string | null | undefined): string | null {
+ const [verb] =
+ value
+ ?.trim()
+ .toLowerCase()
+ .match(/[a-z]+/) ?? [];
+ return verb ?? null;
+}
+
+function classifyVerb(verb: string | null): McpToolGroup | null {
+ if (!verb) return null;
+ if (READ_VERBS.has(verb)) return "read";
+ if (WRITE_DELETE_VERBS.has(verb)) return "write_delete";
+ return null;
+}
+
+export function getMcpToolGroup(tool: McpInstallationTool): McpToolGroup {
+ return (
+ classifyVerb(firstVerb(tool.tool_name)) ??
+ classifyVerb(firstVerb(tool.display_name)) ??
+ classifyVerb(firstVerb(tool.description)) ??
+ "write_delete"
+ );
+}
+
+export function groupMcpToolsByCapability(tools: McpInstallationTool[]): {
+ read: McpInstallationTool[];
+ write_delete: McpInstallationTool[];
+} {
+ return tools.reduce(
+ (groups, tool) => {
+ groups[getMcpToolGroup(tool)].push(tool);
+ return groups;
+ },
+ { read: [], write_delete: [] } as {
+ read: McpInstallationTool[];
+ write_delete: McpInstallationTool[];
+ },
+ );
+}