From e14b662d427735b140b0230ec0d91677276577bb Mon Sep 17 00:00:00 2001 From: Kevin Houston Date: Thu, 21 May 2026 15:25:30 -0500 Subject: [PATCH 1/2] Group MCP tools by capability --- .../components/parts/ServerDetailView.tsx | 70 +++++++++++++++-- .../mcp-servers/hooks/mcpToolGroups.test.ts | 76 +++++++++++++++++++ .../mcp-servers/hooks/mcpToolGroups.ts | 73 ++++++++++++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts create mode 100644 apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.ts 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..a03bef805 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,11 @@ export function ServerDetailView({ return visibleTools.filter((t) => t.tool_name.toLowerCase().includes(term)); }, [visibleTools, toolSearch]); + const groupedTools = useMemo( + () => groupMcpToolsByCapability(filteredTools), + [filteredTools], + ); + const removedCount = tools.filter((t) => !!t.removed_at).length; return ( @@ -385,18 +431,28 @@ export function ServerDetailView({ ) : ( - filteredTools.map((tool) => ( - + <> + setToolApproval({ - toolName: tool.tool_name, + toolName, approval_state, }) } /> - )) + + setToolApproval({ + toolName, + 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..10fa73d87 --- /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("classifies clear read verbs from tool names", () => { + expect(getMcpToolGroup(tool("get_ticket"))).toBe("read"); + expect(getMcpToolGroup(tool("list_projects"))).toBe("read"); + expect(getMcpToolGroup(tool("search_docs"))).toBe("read"); + expect(getMcpToolGroup(tool("lookup_customer"))).toBe("read"); + }); + + it("classifies clear write and delete verbs from tool names", () => { + expect(getMcpToolGroup(tool("create_ticket"))).toBe("write_delete"); + expect(getMcpToolGroup(tool("delete_file"))).toBe("write_delete"); + expect(getMcpToolGroup(tool("send_message"))).toBe("write_delete"); + expect(getMcpToolGroup(tool("run_query"))).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[]; + }, + ); +} From 2deae3b87e3667ef6b2364d601f1794e9c778480 Mon Sep 17 00:00:00 2001 From: Kevin Houston Date: Thu, 21 May 2026 15:36:47 -0500 Subject: [PATCH 2/2] Address MCP tool grouping review cleanup --- .../components/parts/ServerDetailView.tsx | 24 +++++++++---------- .../mcp-servers/hooks/mcpToolGroups.test.ts | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) 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 a03bef805..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 @@ -165,6 +165,16 @@ export function ServerDetailView({ [filteredTools], ); + const handleToolApprovalChange = ( + toolName: string, + approval_state: McpApprovalState, + ) => { + setToolApproval({ + toolName, + approval_state, + }); + }; + const removedCount = tools.filter((t) => !!t.removed_at).length; return ( @@ -435,22 +445,12 @@ export function ServerDetailView({ - setToolApproval({ - toolName, - approval_state, - }) - } + onToolApprovalChange={handleToolApprovalChange} /> - setToolApproval({ - toolName, - approval_state, - }) - } + onToolApprovalChange={handleToolApprovalChange} /> )} 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 index 10fa73d87..5a2a1c146 100644 --- a/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts +++ b/apps/code/src/renderer/features/mcp-servers/hooks/mcpToolGroups.test.ts @@ -22,19 +22,19 @@ function tool( } describe("getMcpToolGroup", () => { - it("classifies clear read verbs from tool names", () => { - expect(getMcpToolGroup(tool("get_ticket"))).toBe("read"); - expect(getMcpToolGroup(tool("list_projects"))).toBe("read"); - expect(getMcpToolGroup(tool("search_docs"))).toBe("read"); - expect(getMcpToolGroup(tool("lookup_customer"))).toBe("read"); - }); + it.each(["get_ticket", "list_projects", "search_docs", "lookup_customer"])( + "classifies %s as read", + (toolName) => { + expect(getMcpToolGroup(tool(toolName))).toBe("read"); + }, + ); - it("classifies clear write and delete verbs from tool names", () => { - expect(getMcpToolGroup(tool("create_ticket"))).toBe("write_delete"); - expect(getMcpToolGroup(tool("delete_file"))).toBe("write_delete"); - expect(getMcpToolGroup(tool("send_message"))).toBe("write_delete"); - expect(getMcpToolGroup(tool("run_query"))).toBe("write_delete"); - }); + 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(