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": [ {