diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 16a9e52825e..e4e96b02a8a 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -7997,6 +7997,80 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "black_forest_labs/flux-kontext-pro": { + "litellm_provider": "black_forest_labs", + "mode": "image_edit", + "output_cost_per_image": 0.04, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/edits", + "/v1/images/generations" + ] + }, + "black_forest_labs/flux-kontext-max": { + "litellm_provider": "black_forest_labs", + "mode": "image_edit", + "output_cost_per_image": 0.08, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/edits", + "/v1/images/generations" + ] + }, + "black_forest_labs/flux-pro-1.0-fill": { + "litellm_provider": "black_forest_labs", + "mode": "image_edit", + "output_cost_per_image": 0.05, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/edits" + ] + }, + "black_forest_labs/flux-pro-1.0-expand": { + "litellm_provider": "black_forest_labs", + "mode": "image_edit", + "output_cost_per_image": 0.05, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/edits" + ] + }, + "black_forest_labs/flux-pro-1.1": { + "litellm_provider": "black_forest_labs", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "black_forest_labs/flux-pro-1.1-ultra": { + "litellm_provider": "black_forest_labs", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "black_forest_labs/flux-dev": { + "litellm_provider": "black_forest_labs", + "mode": "image_generation", + "output_cost_per_image": 0.025, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "black_forest_labs/flux-pro": { + "litellm_provider": "black_forest_labs", + "mode": "image_generation", + "output_cost_per_image": 0.05, + "source": "https://bfl.ai/pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, "cerebras/llama-3.3-70b": { "input_cost_per_token": 8.5e-07, "litellm_provider": "cerebras", diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx index 4f884d3303b..a33f97978da 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx @@ -2,9 +2,11 @@ import React, { useEffect, useState, useMemo } from "react"; import { listMCPTools } from "../networking"; import { MCPTool, MCPServer } from "../mcp_tools/types"; import { Text } from "@tremor/react"; -import { Spin, Checkbox } from "antd"; +import { Spin, Radio } from "antd"; import { XIcon } from "lucide-react"; import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers"; +import McpCrudPermissionPanel from "../mcp_tools/McpCrudPermissionPanel"; +import { classifyToolOp } from "../../utils/mcpToolCrudClassification"; interface MCPToolPermissionsProps { accessToken: string; @@ -25,6 +27,7 @@ const MCPToolPermissions: React.FC = ({ const [serverTools, setServerTools] = useState>({}); const [loadingTools, setLoadingTools] = useState>({}); const [toolErrors, setToolErrors] = useState>({}); + const [viewModes, setViewModes] = useState>({}); // Filter servers based on selectedServers const servers = useMemo(() => { @@ -32,7 +35,7 @@ const MCPToolPermissions: React.FC = ({ return allServers.filter((server: MCPServer) => selectedServers.includes(server.server_id)); }, [allServers, selectedServers]); - // Fetch tools for a specific server + // Fetch tools for a specific server; applies delete-blocked-by-default for new servers. const fetchToolsForServer = async (serverId: string) => { setLoadingTools((prev) => ({ ...prev, [serverId]: true })); setToolErrors((prev) => ({ ...prev, [serverId]: "" })); @@ -44,7 +47,16 @@ const MCPToolPermissions: React.FC = ({ setToolErrors((prev) => ({ ...prev, [serverId]: response.message || "Failed to fetch tools" })); setServerTools((prev) => ({ ...prev, [serverId]: [] })); } else { - setServerTools((prev) => ({ ...prev, [serverId]: response.tools || [] })); + const fetchedTools: MCPTool[] = response.tools || []; + setServerTools((prev) => ({ ...prev, [serverId]: fetchedTools })); + + // For servers that have no permissions stored yet, block delete tools by default. + if (!toolPermissions[serverId] && fetchedTools.length > 0) { + const nonDeleteTools = fetchedTools + .filter((t) => classifyToolOp(t.name, t.description || "") !== "delete") + .map((t) => t.name); + onChange({ ...toolPermissions, [serverId]: nonDeleteTools }); + } } } catch (err) { console.error(`Error fetching tools for server ${serverId}:`, err); @@ -64,35 +76,17 @@ const MCPToolPermissions: React.FC = ({ }); }, [servers]); - // Handle tool selection - const handleToolToggle = (serverId: string, toolName: string) => { - const currentTools = toolPermissions[serverId] || []; - const newTools = currentTools.includes(toolName) - ? currentTools.filter((name) => name !== toolName) - : [...currentTools, toolName]; - - const updatedPermissions = { - ...toolPermissions, - [serverId]: newTools, - }; - onChange(updatedPermissions); + const handleCrudPanelChange = (serverId: string, allowed: string[] | undefined) => { + onChange({ ...toolPermissions, [serverId]: allowed ?? [] }); }; const handleSelectAll = (serverId: string) => { const tools = serverTools[serverId] || []; - const newPermissions = { - ...toolPermissions, - [serverId]: tools.map((t) => t.name), - }; - onChange(newPermissions); + onChange({ ...toolPermissions, [serverId]: tools.map((t) => t.name) }); }; const handleDeselectAll = (serverId: string) => { - const newPermissions = { - ...toolPermissions, - [serverId]: [], - }; - onChange(newPermissions); + onChange({ ...toolPermissions, [serverId]: [] }); }; if (selectedServers.length === 0) { @@ -117,22 +111,41 @@ const MCPToolPermissions: React.FC = ({ {server.description && {server.description}}
- - + {!disabled && tools.length > 0 && ( + + setViewModes((prev) => ({ ...prev, [server.server_id]: e.target.value })) + } + size="small" + optionType="button" + buttonStyle="solid" + options={[ + { label: "Risk Groups", value: "crud" }, + { label: "Flat List", value: "flat" }, + ]} + /> + )} + {!disabled && ( + <> + + + + )} + + {!readOnly && ( +
+ + {fullyAllowed ? "All on" : partial ? "Partial" : "All off"} + + toggleGroup(op, checked)} + /> +
+ )} +
+ + {/* Description row */} + {!isCollapsed && ( +
+ {meta.description} +
+ )} + + {/* Tool list */} + {!isCollapsed && ( +
+ {group.map((tool) => { + const allowed = isToolAllowed(tool.name); + return ( +
toggleTool(tool.name)} + > + toggleTool(tool.name)} + disabled={readOnly} + onClick={(e) => e.stopPropagation()} + /> +
+ {tool.name} + {tool.description && ( + {tool.description} + )} +
+ + {allowed ? "on" : "off"} + +
+ ); + })} +
+ )} + + ); + })} + + ); +}; + +export default McpCrudPermissionPanel; + +export { classifyToolOp, groupToolsByCrud }; 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 360bb893b52..1b6d4d46f88 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 @@ -1,8 +1,9 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Card, Title, Text } from "@tremor/react"; import { ToolOutlined, CheckCircleOutlined, SearchOutlined, EditOutlined } from "@ant-design/icons"; -import { Badge, Spin, Checkbox, Input } from "antd"; +import { Badge, Spin, Checkbox, Input, Radio } from "antd"; import { useTestMCPConnection } from "../../hooks/useTestMCPConnection"; +import McpCrudPermissionPanel from "./McpCrudPermissionPanel"; interface KeyTool { name: string; @@ -163,6 +164,7 @@ const MCPToolConfiguration: React.FC = ({ const hasInitializedRef = useRef(false); const previousSuggestedToolNamesRef = useRef(""); const [expandedTools, setExpandedTools] = useState>(new Set()); + const [viewMode, setViewMode] = useState<"crud" | "flat">("crud"); // Use external tool state when provided (avoids duplicate fetch with MCPConnectionStatus). // Fall back to internal hook when used standalone (e.g., edit flow). @@ -367,6 +369,19 @@ const MCPToolConfiguration: React.FC = ({ /> )} + {tools.length > 0 && ( + setViewMode(e.target.value)} + size="small" + optionType="button" + buttonStyle="solid" + options={[ + { label: "Risk Groups", value: "crud" }, + { label: "Flat List", value: "flat" }, + ]} + /> + )} {/* Description */} @@ -436,104 +451,118 @@ const MCPToolConfiguration: React.FC = ({ - {/* Search bar */} - } - value={toolSearchTerm} - onChange={(e) => setToolSearchTerm(e.target.value)} - allowClear - className="rounded-lg" - size="large" - /> + {/* CRUD grouped view */} + {viewMode === "crud" && ( + onAllowedToolsChange(allowed ?? [])} + /> + )} - {/* Tool list with checkboxes */} - {filteredTools.length === 0 ? ( -
- - No tools found matching "{toolSearchTerm}" -
- ) : ( -
- {pinnedFiltered.length > 0 && ( - <> -
-

- Suggested tools -

-
- - -
-
- {pinnedFiltered.map((tool) => ( - - ))} - + {/* Flat list view */} + {viewMode === "flat" && ( + <> + {/* Search bar */} + } + value={toolSearchTerm} + onChange={(e) => setToolSearchTerm(e.target.value)} + allowClear + className="rounded-lg" + size="large" + /> + + {/* Tool list with checkboxes */} + {filteredTools.length === 0 ? ( +
+ + No tools found matching "{toolSearchTerm}" +
+ ) : ( +
+ {pinnedFiltered.length > 0 && ( + <> +
+

+ Suggested tools +

+
+ + +
+
+ {pinnedFiltered.map((tool) => ( + + ))} + + )} + {restFiltered.length > 0 && ( + <> +
+

+ {pinnedFiltered.length > 0 ? "All tools" : "Tools"} +

+
+ + +
+
+ {restFiltered.map((tool) => ( + + ))} + + )} +
)} - {restFiltered.length > 0 && ( - <> -
-

- {pinnedFiltered.length > 0 ? "All tools" : "Tools"} -

-
- - -
-
- {restFiltered.map((tool) => ( - - ))} - - )} -
+ )} )} diff --git a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts new file mode 100644 index 00000000000..afd32fa2458 --- /dev/null +++ b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts @@ -0,0 +1,71 @@ +export type CrudOp = "read" | "create" | "update" | "delete" | "unknown"; + +const DELETE_RE = /\b(delete|remove|destroy|purge|drop|clear|erase|unlink|disconnect)\b/i; +const CREATE_RE = /\b(create|add|insert|new|post|submit|register|make|generate|write|upload)\b/i; +const UPDATE_RE = /\b(update|edit|modify|change|patch|put|set|rename|move|transform)\b/i; +const READ_RE = /\b(get|read|list|fetch|search|find|query|retrieve|show|view|check|describe|info)\b/i; + +export interface MCPToolEntry { + name: string; + description?: string; +} + +export function classifyToolOp(name: string, description = ""): CrudOp { + const text = (name + " " + description).toLowerCase(); + if (DELETE_RE.test(text)) return "delete"; + if (UPDATE_RE.test(text)) return "update"; + if (CREATE_RE.test(text)) return "create"; + if (READ_RE.test(text)) return "read"; + return "unknown"; +} + +export function groupToolsByCrud(tools: MCPToolEntry[]): Record { + const groups: Record = { + read: [], + create: [], + update: [], + delete: [], + unknown: [], + }; + for (const tool of tools) { + const op = classifyToolOp(tool.name, tool.description); + groups[op].push(tool); + } + return groups; +} + +export const CRUD_GROUP_META: Record< + CrudOp, + { label: string; description: string; risk: "low" | "medium" | "high" | "unknown"; color: string } +> = { + read: { + label: "Read", + description: "Safe operations — fetch, list, search. No side effects.", + risk: "low", + color: "green", + }, + create: { + label: "Create", + description: "Add new resources — insert, upload, register.", + risk: "medium", + color: "blue", + }, + update: { + label: "Update", + description: "Modify existing resources — edit, patch, rename.", + risk: "medium", + color: "yellow", + }, + delete: { + label: "Delete", + description: "Destructive operations — remove, purge, destroy.", + risk: "high", + color: "red", + }, + unknown: { + label: "Other", + description: "Operations that could not be automatically classified.", + risk: "unknown", + color: "gray", + }, +};