diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx b/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx index 6d18f209103..7a78856b3cf 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx @@ -30,14 +30,16 @@ const OpenAPIFormSection: React.FC = ({ const handlePresetSelect = (entry: OpenAPIRegistryEntry) => { setSelectedPreset(entry.name); onKeyToolsChange?.(entry.key_tools ?? []); - const updates = { + const updates: Record = { spec_path: entry.spec_url, - auth_type: AUTH_TYPE.OAUTH2, - credentials: { + }; + if (entry.oauth) { + updates.auth_type = AUTH_TYPE.OAUTH2; + updates.credentials = { authorization_url: entry.oauth.authorization_url, token_url: entry.oauth.token_url, - }, - }; + }; + } form.setFieldsValue(updates); onValuesChange(updates); }; 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 551f6243021..6e21339c8a6 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 @@ -24,6 +24,118 @@ interface MCPToolConfigurationProps { keyTools?: KeyTool[]; } +interface ToolEntry { + name: string; + description?: string; +} + +interface ToolRowProps { + tool: ToolEntry; + isEnabled: boolean; + isEditExpanded: boolean; + toolNameToDisplayName: Record; + toolNameToDescription: Record; + onToggle: (name: string) => void; + onToggleExpand: (name: string, e: React.MouseEvent) => void; + onDisplayNameChange: (name: string, value: string) => void; + onDescriptionChange: (name: string, value: string) => void; +} + +const ToolRow: React.FC = ({ + tool, + isEnabled, + isEditExpanded, + toolNameToDisplayName, + toolNameToDescription, + onToggle, + onToggleExpand, + onDisplayNameChange, + onDescriptionChange, +}) => ( +
+
onToggle(tool.name)}> +
+ onToggle(tool.name)} /> +
+
+ + {toolNameToDisplayName[tool.name] || tool.name} + + + {isEnabled ? "Enabled" : "Disabled"} + + {toolNameToDisplayName[tool.name] && ( + + Custom name + + )} +
+ {(toolNameToDescription[tool.name] || tool.description) && ( + + {toolNameToDescription[tool.name] || tool.description} + + )} + + {isEnabled ? "✓ Users can call this tool" : "✗ Users cannot call this tool"} + +
+ +
+
+ {isEditExpanded && ( +
e.stopPropagation()} + > +
+ Display Name + onDisplayNameChange(tool.name, e.target.value)} + /> + + Override how this tool's name appears to users. Leave blank to use original. + +
+
+ Description + onDescriptionChange(tool.name, e.target.value)} + rows={2} + /> + + Override the tool description shown to users. Leave blank to use original. + +
+
+ )} +
+); + const MCPToolConfiguration: React.FC = ({ accessToken, oauthAccessToken, @@ -37,7 +149,7 @@ const MCPToolConfiguration: React.FC = ({ onToolNameToDescriptionChange, keyTools, }) => { - const previousToolsRef = useRef([]); + const previousToolsRef = useRef([]); const [toolSearchTerm, setToolSearchTerm] = useState(""); const hasInitializedRef = useRef(false); const [expandedTools, setExpandedTools] = useState>(new Set()); @@ -83,52 +195,62 @@ const MCPToolConfiguration: React.FC = ({ ); // Filter tools based on search term - const filteredTools = tools.filter((tool) => { - const searchLower = toolSearchTerm.toLowerCase(); - return ( - tool.name.toLowerCase().includes(searchLower) || - (tool.description && tool.description.toLowerCase().includes(searchLower)) - ); - }); + const filteredTools = useMemo( + () => + tools.filter((tool) => { + const searchLower = toolSearchTerm.toLowerCase(); + return ( + tool.name.toLowerCase().includes(searchLower) || + (tool.description && tool.description.toLowerCase().includes(searchLower)) + ); + }), + [tools, toolSearchTerm] + ); + + const pinnedFiltered = useMemo( + () => filteredTools.filter((t) => suggestedToolNames.has(t.name)), + [filteredTools, suggestedToolNames] + ); + + const restFiltered = useMemo( + () => filteredTools.filter((t) => !suggestedToolNames.has(t.name)), + [filteredTools, suggestedToolNames] + ); // 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 const currentToolNames = tools.map((tool) => tool.name).sort().join(","); const previousToolNames = previousToolsRef.current.map((tool) => tool.name).sort().join(","); const toolsListChanged = currentToolNames !== previousToolNames; if (tools.length > 0 && toolsListChanged) { const availableToolNames = tools.map((tool) => tool.name); - - // On initial load (first time tools are fetched) + if (!hasInitializedRef.current) { hasInitializedRef.current = true; - + if (existingAllowedTools && existingAllowedTools.length > 0) { // Edit mode: pre-select tools that match existing allowed tools - const validExistingTools = existingAllowedTools.filter((toolName) => availableToolNames.includes(toolName)); + const validExistingTools = existingAllowedTools.filter((toolName) => + availableToolNames.includes(toolName) + ); onAllowedToolsChange(validExistingTools); } else if (suggestedTools.length > 0) { // OpenAPI preset: only enable suggested tools by default - onAllowedToolsChange(suggestedTools.map((t) => t.name).filter((name) => availableToolNames.includes(name))); + onAllowedToolsChange( + suggestedTools.map((t) => t.name).filter((name) => availableToolNames.includes(name)) + ); } else { // Create mode: auto-select all tools onAllowedToolsChange(availableToolNames); } } else { // Tools list changed after initial load (e.g., URL was edited) - // Keep any tools from the current selection that exist in the new tools list const matchingTools = allowedTools.filter((toolName) => availableToolNames.includes(toolName)); onAllowedToolsChange(matchingTools); } - } else if (tools.length === 0 && previousToolsRef.current.length > 0) { - // Tools were cleared (e.g., URL became invalid or is being edited) - // Don't clear allowedTools here - let the user keep their selection - // until new tools are loaded } - - // Update ref to track current tools + previousToolsRef.current = tools; }, [tools, allowedTools, existingAllowedTools, onAllowedToolsChange, suggestedTools]); @@ -173,6 +295,27 @@ const MCPToolConfiguration: React.FC = ({ onToolNameToDescriptionChange(next); }; + const handleEnableSuggested = () => { + const suggestedNames = pinnedFiltered.map((t) => t.name); + const others = allowedTools.filter((n) => !suggestedToolNames.has(n)); + onAllowedToolsChange([...others, ...suggestedNames]); + }; + + const handleDisableSuggested = () => { + onAllowedToolsChange(allowedTools.filter((n) => !suggestedToolNames.has(n))); + }; + + const handleEnableRest = () => { + const restNames = restFiltered.map((t) => t.name); + const current = new Set(allowedTools); + onAllowedToolsChange([...allowedTools, ...restNames.filter((n) => !current.has(n))]); + }; + + const handleDisableRest = () => { + const restNameSet = new Set(restFiltered.map((t) => t.name)); + onAllowedToolsChange(allowedTools.filter((n) => !restNameSet.has(n))); + }; + // Don't show anything if required fields aren't filled if (!canFetchTools && !formValues.url && !formValues.spec_path) { return null; @@ -265,158 +408,91 @@ const MCPToolConfiguration: React.FC = ({ /> {/* Tool list with checkboxes */} - {(() => { - const pinnedFiltered = filteredTools.filter((t) => suggestedToolNames.has(t.name)); - const restFiltered = filteredTools.filter((t) => !suggestedToolNames.has(t.name)); - - if (filteredTools.length === 0) { - return ( -
- - No tools found matching "{toolSearchTerm}" -
- ); - } - - const renderRow = (tool: typeof filteredTools[0], index: number) => { - const isEnabled = allowedTools.includes(tool.name); - const isEditExpanded = expandedTools.has(tool.name); - return ( -
-
handleToolToggle(tool.name)}> -
- handleToolToggle(tool.name)} /> -
-
- - {toolNameToDisplayName[tool.name] || tool.name} - - - {isEnabled ? "Enabled" : "Disabled"} - - {toolNameToDisplayName[tool.name] && ( - - Custom name - - )} -
- {(toolNameToDescription[tool.name] || tool.description) && ( - - {toolNameToDescription[tool.name] || tool.description} - - )} - - {isEnabled ? "✓ Users can call this tool" : "✗ Users cannot call this tool"} - -
+ {filteredTools.length === 0 ? ( +
+ + No tools found matching "{toolSearchTerm}" +
+ ) : ( +
+ {pinnedFiltered.length > 0 && ( + <> +
+

+ Suggested tools +

+
+
- {isEditExpanded && ( -
e.stopPropagation()} + {pinnedFiltered.map((tool) => ( + + ))} + + )} + {restFiltered.length > 0 && ( +
+

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

+
+
- )} -
- ); - }; - - const handleEnableSuggested = () => { - const suggestedNames = pinnedFiltered.map((t) => t.name); - const others = allowedTools.filter((n) => !suggestedToolNames.has(n)); - onAllowedToolsChange([...others, ...suggestedNames]); - }; - const handleDisableSuggested = () => { - onAllowedToolsChange(allowedTools.filter((n) => !suggestedToolNames.has(n))); - }; - const handleEnableRest = () => { - const restNames = restFiltered.map((t) => t.name); - const current = new Set(allowedTools); - onAllowedToolsChange([...allowedTools, ...restNames.filter((n) => !current.has(n))]); - }; - const handleDisableRest = () => { - const restNames = new Set(restFiltered.map((t) => t.name)); - onAllowedToolsChange(allowedTools.filter((n) => !restNames.has(n))); - }; - - return ( -
- {pinnedFiltered.length > 0 && ( - <> -
-

- Suggested tools -

-
- - -
-
- {pinnedFiltered.map((tool, i) => renderRow(tool, i))} - - )} - {restFiltered.length > 0 && ( -
-

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

-
- - -
+ Enable all + +
- )} - {restFiltered.map((tool, i) => renderRow(tool, i))} -
- ); - })()} +
+ )} + {restFiltered.map((tool) => ( + + ))} +
+ )}
)}
@@ -424,5 +500,4 @@ const MCPToolConfiguration: React.FC = ({ ); }; - export default MCPToolConfiguration;