From fd93079af44b035344802eb4d5e1189fcde70ba3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Mar 2026 01:47:24 +0000 Subject: [PATCH] feat(ui): add CRUD categorization for MCP tools Implement automatic CRUD categorization to organize MCP tools by operation type (Create, Read, Update, Delete, Other), making it easier for admins to review and configure tools. Features: - Automatic categorization based on tool names and descriptions - Color-coded category headers (green/blue/yellow/red/gray) - Category counts showing enabled/disabled ratio - Search functionality across all tools - Handles camelCase and snake_case tool names - Tools always grouped by default for better UX Changes: - Add tool_crud_categorization.ts with categorization logic - Add tool_crud_categorization.test.ts with 13 test cases - Update mcp_tool_configuration.tsx to display grouped tools - Update .gitignore to exclude build artifacts - Auto-update tsconfig.json for Next.js compatibility Addresses feedback from #product-qa-feedback: Reviewing 31+ tools in a flat list is overwhelming - now organized into clear CRUD categories for easier review. Co-authored-by: Ishaan Jaff --- .gitignore | 4 +- .../mcp_tools/mcp_tool_configuration.tsx | 112 +++++++++---- .../tool_crud_categorization.test.ts | 148 +++++++++++++++++ .../mcp_tools/tool_crud_categorization.ts | 149 ++++++++++++++++++ ui/litellm-dashboard/tsconfig.json | 2 +- 5 files changed, 379 insertions(+), 36 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.test.ts create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.ts diff --git a/.gitignore b/.gitignore index 76cf6fdba2a..c8a62182077 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,6 @@ STABILIZATION_TODO.md **/test-results **/playwright-report **/*.storageState.json -**/coverage \ No newline at end of file +**/coverage +ui/litellm-dashboard/out +litellm/proxy/_experimental/out diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx index 25c20dd0746..7d38641b03e 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx @@ -3,6 +3,12 @@ import { Card, Title, Text } from "@tremor/react"; import { ToolOutlined, CheckCircleOutlined, SearchOutlined } from "@ant-design/icons"; import { Badge, Spin, Checkbox, Input } from "antd"; import { useTestMCPConnection } from "../../hooks/useTestMCPConnection"; +import { + categorizeTools, + groupToolsByCategory, + CRUD_CATEGORY_ORDER, + CRUD_CATEGORY_COLORS, +} from "./tool_crud_categorization"; interface MCPToolConfigurationProps { accessToken: string | null; @@ -32,8 +38,9 @@ const MCPToolConfiguration: React.FC = ({ enabled: true, }); - // Filter tools based on search term - const filteredTools = tools.filter((tool) => { + // Categorize and filter tools based on search term + const categorizedTools = categorizeTools(tools); + const filteredTools = categorizedTools.filter((tool) => { const searchLower = toolSearchTerm.toLowerCase(); return ( tool.name.toLowerCase().includes(searchLower) || @@ -41,6 +48,9 @@ const MCPToolConfiguration: React.FC = ({ ); }); + // Group filtered tools by category (always grouped) + const groupedTools = groupToolsByCategory(filteredTools); + // Auto-select tools when tools are first loaded or when tools list changes useEffect(() => { // Check if the tools list has actually changed by comparing tool names @@ -205,7 +215,7 @@ const MCPToolConfiguration: React.FC = ({ size="large" /> - {/* Tool list with checkboxes */} + {/* Tool list grouped by CRUD category */}
{filteredTools.length === 0 ? (
@@ -213,39 +223,73 @@ const MCPToolConfiguration: React.FC = ({ No tools found matching "{toolSearchTerm}"
) : ( - filteredTools.map((tool, index) => ( -
handleToolToggle(tool.name)} - > -
- handleToolToggle(tool.name)} /> -
-
- {tool.name} - - {allowedTools.includes(tool.name) ? "Enabled" : "Disabled"} - + CRUD_CATEGORY_ORDER.map((category) => { + const categoryTools = groupedTools[category]; + if (categoryTools.length === 0) return null; + + const categoryColors = CRUD_CATEGORY_COLORS[category]; + const enabledCount = categoryTools.filter((tool) => allowedTools.includes(tool.name)).length; + + return ( +
+
+ {category} + + + {enabledCount}/{categoryTools.length} enabled + +
+
+ {categoryTools.map((tool, index) => ( +
handleToolToggle(tool.name)} + > +
+ handleToolToggle(tool.name)} + /> +
+
+ {tool.name} + + {allowedTools.includes(tool.name) ? "Enabled" : "Disabled"} + + + {category} + +
+ {tool.description && ( + {tool.description} + )} + + {allowedTools.includes(tool.name) + ? "✓ Users can call this tool" + : "✗ Users cannot call this tool"} + +
+
+
+ ))}
- {tool.description && {tool.description}} - - {allowedTools.includes(tool.name) - ? "✓ Users can call this tool" - : "✗ Users cannot call this tool"} -
-
-
- )) + ); + }) )}
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.test.ts b/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.test.ts new file mode 100644 index 00000000000..d8afac79772 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import { + categorizeTool, + categorizeTools, + groupToolsByCategory, + type CategorizedTool, +} from "./tool_crud_categorization"; + +describe("tool_crud_categorization", () => { + describe("categorizeTool", () => { + it("should categorize Create operations", () => { + expect(categorizeTool("createUser")).toBe("Create"); + expect(categorizeTool("addComment")).toBe("Create"); + expect(categorizeTool("insertRecord")).toBe("Create"); + expect(categorizeTool("postMessage")).toBe("Create"); + expect(categorizeTool("registerAccount")).toBe("Create"); + }); + + it("should categorize Read operations", () => { + expect(categorizeTool("getUser")).toBe("Read"); + expect(categorizeTool("fetchData")).toBe("Read"); + expect(categorizeTool("listItems")).toBe("Read"); + expect(categorizeTool("searchRecords")).toBe("Read"); + expect(categorizeTool("findUser")).toBe("Read"); + expect(categorizeTool("queryDatabase")).toBe("Read"); + expect(categorizeTool("atlassianUserInfo")).toBe("Read"); + expect(categorizeTool("getConfluencePage")).toBe("Read"); + }); + + it("should categorize Update operations", () => { + expect(categorizeTool("updateUser")).toBe("Update"); + expect(categorizeTool("editProfile")).toBe("Update"); + expect(categorizeTool("modifySettings")).toBe("Update"); + expect(categorizeTool("patchResource")).toBe("Update"); + expect(categorizeTool("changePassword")).toBe("Update"); + }); + + it("should categorize Delete operations", () => { + expect(categorizeTool("deleteUser")).toBe("Delete"); + expect(categorizeTool("removeItem")).toBe("Delete"); + expect(categorizeTool("dropTable")).toBe("Delete"); + expect(categorizeTool("clearCache")).toBe("Delete"); + expect(categorizeTool("destroySession")).toBe("Delete"); + }); + + it("should categorize as Other when no match", () => { + expect(categorizeTool("calculateSum")).toBe("Other"); + expect(categorizeTool("processPayment")).toBe("Other"); + expect(categorizeTool("validateInput")).toBe("Other"); + }); + + it("should use description for categorization", () => { + expect(categorizeTool("userTool", "Gets user information")).toBe("Read"); + expect(categorizeTool("dataTool", "Creates new records")).toBe("Create"); + expect(categorizeTool("toolName", "Updates existing data")).toBe("Update"); + expect(categorizeTool("actionTool", "Deletes old files")).toBe("Delete"); + }); + + it("should be case insensitive", () => { + expect(categorizeTool("GETUSER")).toBe("Read"); + expect(categorizeTool("CreateRecord")).toBe("Create"); + expect(categorizeTool("UpdateItem")).toBe("Update"); + expect(categorizeTool("DeleteFile")).toBe("Delete"); + }); + }); + + describe("categorizeTools", () => { + it("should categorize multiple tools", () => { + const tools = [ + { name: "getUser", description: "Get user info" }, + { name: "createPost", description: "Create a new post" }, + { name: "updateProfile" }, + { name: "deleteComment" }, + ]; + + const categorized = categorizeTools(tools); + + expect(categorized).toHaveLength(4); + expect(categorized[0].category).toBe("Read"); + expect(categorized[1].category).toBe("Create"); + expect(categorized[2].category).toBe("Update"); + expect(categorized[3].category).toBe("Delete"); + }); + + it("should handle empty array", () => { + const categorized = categorizeTools([]); + expect(categorized).toHaveLength(0); + }); + }); + + describe("groupToolsByCategory", () => { + it("should group tools by CRUD category", () => { + const tools: CategorizedTool[] = [ + { name: "getUser", category: "Read" }, + { name: "createPost", category: "Create" }, + { name: "updateProfile", category: "Update" }, + { name: "deleteComment", category: "Delete" }, + { name: "fetchData", category: "Read" }, + { name: "addItem", category: "Create" }, + { name: "processTask", category: "Other" }, + ]; + + const grouped = groupToolsByCategory(tools); + + expect(grouped.Read).toHaveLength(2); + expect(grouped.Create).toHaveLength(2); + expect(grouped.Update).toHaveLength(1); + expect(grouped.Delete).toHaveLength(1); + expect(grouped.Other).toHaveLength(1); + }); + + it("should handle empty categories", () => { + const tools: CategorizedTool[] = [ + { name: "getUser", category: "Read" }, + ]; + + const grouped = groupToolsByCategory(tools); + + expect(grouped.Read).toHaveLength(1); + expect(grouped.Create).toHaveLength(0); + expect(grouped.Update).toHaveLength(0); + expect(grouped.Delete).toHaveLength(0); + expect(grouped.Other).toHaveLength(0); + }); + + it("should handle empty array", () => { + const grouped = groupToolsByCategory([]); + + expect(grouped.Read).toHaveLength(0); + expect(grouped.Create).toHaveLength(0); + expect(grouped.Update).toHaveLength(0); + expect(grouped.Delete).toHaveLength(0); + expect(grouped.Other).toHaveLength(0); + }); + }); + + describe("real-world MCP tool examples", () => { + it("should categorize Confluence/Atlassian tools correctly", () => { + expect(categorizeTool("atlassianUserInfo")).toBe("Read"); + expect(categorizeTool("getAccessibleAtlassianResources")).toBe("Read"); + expect(categorizeTool("getConfluencePage")).toBe("Read"); + expect(categorizeTool("searchConfluenceUsingCql")).toBe("Read"); + expect(categorizeTool("getConfluenceSpaces")).toBe("Read"); + expect(categorizeTool("getPagesInConfluenceSpace")).toBe("Read"); + expect(categorizeTool("getConfluencePageFooterComments")).toBe("Read"); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.ts b/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.ts new file mode 100644 index 00000000000..f8fb67a0cd3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/tool_crud_categorization.ts @@ -0,0 +1,149 @@ +export type CRUDCategory = "Create" | "Read" | "Update" | "Delete" | "Other"; + +interface ToolCategoryKeywords { + Create: string[]; + Read: string[]; + Update: string[]; + Delete: string[]; +} + +const CRUD_KEYWORDS: ToolCategoryKeywords = { + Create: [ + "create", + "add", + "insert", + "new", + "post", + "register", + "init", + "initialize", + "setup", + "make", + "generate", + "build", + "construct", + ], + Read: [ + "get", + "fetch", + "list", + "search", + "find", + "read", + "query", + "view", + "retrieve", + "show", + "describe", + "check", + "load", + "export", + "download", + "info", + ], + Update: [ + "update", + "edit", + "modify", + "patch", + "change", + "alter", + "replace", + "rename", + ], + Delete: [ + "delete", + "remove", + "drop", + "clear", + "destroy", + "purge", + "revoke", + "cancel", + "unregister", + ], +}; + +function splitCamelCase(text: string): string { + return text.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2"); +} + +export function categorizeTool(toolName: string, description?: string): CRUDCategory { + const normalizedName = splitCamelCase(toolName).toLowerCase(); + const searchText = `${normalizedName} ${description || ""}`.toLowerCase(); + + for (const [category, keywords] of Object.entries(CRUD_KEYWORDS)) { + for (const keyword of keywords) { + const pattern = new RegExp(`\\b${keyword}`, "i"); + if (pattern.test(searchText)) { + return category as CRUDCategory; + } + } + } + + return "Other"; +} + +export interface CategorizedTool { + name: string; + description?: string; + category: CRUDCategory; +} + +export function categorizeTools(tools: Array<{ name: string; description?: string }>): CategorizedTool[] { + return tools.map((tool) => ({ + ...tool, + category: categorizeTool(tool.name, tool.description), + })); +} + +export function groupToolsByCategory(tools: CategorizedTool[]): Record { + const grouped: Record = { + Create: [], + Read: [], + Update: [], + Delete: [], + Other: [], + }; + + tools.forEach((tool) => { + grouped[tool.category].push(tool); + }); + + return grouped; +} + +export const CRUD_CATEGORY_ORDER: CRUDCategory[] = ["Create", "Read", "Update", "Delete", "Other"]; + +export const CRUD_CATEGORY_COLORS: Record = { + Create: { + bg: "bg-green-50", + border: "border-green-300", + text: "text-green-700", + badge: "bg-green-100 text-green-800", + }, + Read: { + bg: "bg-blue-50", + border: "border-blue-300", + text: "text-blue-700", + badge: "bg-blue-100 text-blue-800", + }, + Update: { + bg: "bg-yellow-50", + border: "border-yellow-300", + text: "text-yellow-700", + badge: "bg-yellow-100 text-yellow-800", + }, + Delete: { + bg: "bg-red-50", + border: "border-red-300", + text: "text-red-700", + badge: "bg-red-100 text-red-800", + }, + Other: { + bg: "bg-gray-50", + border: "border-gray-300", + text: "text-gray-700", + badge: "bg-gray-100 text-gray-800", + }, +}; diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index 5b0352feb98..d24bdd340f7 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ {