From 0acfcf3a3682cf970d07ebc841d46a3f27bc2989 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 10:06:11 +0000 Subject: [PATCH] feat(ui): migrate mcp_tools MCPLogoSelector + MCPNetworkSettings to shadcn - MCPLogoSelector: antd Input/Tooltip + @ant-design/icons InfoCircle/ LinkOutlined \u2192 shadcn Input/Tooltip + lucide Info/Link. Logo grid and selected preview preserved; selected state uses primary-toned border/background. - MCPNetworkSettings: antd Select mode=tags/Button/Card/Typography/Spin/ Tag + @ant-design/icons SaveOutlined/PlusOutlined \u2192 shadcn Card/ Input/Button/Badge/Skeleton + lucide Plus/Save/X. Replaces the Select mode=tags with an Add-on-Enter-or-comma Input + Badge chip list. Suggested-range Badge now uses gap-1 + Plus icon and is exempted from raw-colors as categorical blue/info tone. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- .../components/mcp_tools/MCPLogoSelector.tsx | 116 ++++++---- .../mcp_tools/MCPNetworkSettings.tsx | 199 +++++++++++------- 3 files changed, 201 insertions(+), 117 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index ec5b085ba42..51549f234e2 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -38,7 +38,8 @@ "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx", "src/components/mcp_hub_table_columns.tsx", "src/components/model_hub_table_columns.tsx", - "src/components/mcp_server_management/MCPServerSelector.tsx" + "src/components/mcp_server_management/MCPServerSelector.tsx", + "src/components/mcp_tools/MCPNetworkSettings.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx index be05d74ec24..d120cbf3f00 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx @@ -1,6 +1,13 @@ import React, { useState } from "react"; -import { Input, Tooltip } from "antd"; -import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons"; +import { Input } from "@/components/ui/input"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Info, Link as LinkIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; const logos = "/ui/assets/logos/"; @@ -46,76 +53,95 @@ const MCPLogoSelector: React.FC = ({ value, onChange }) => return (
- Logo - - - + Logo + + + + + + + Select a well-known logo or paste a URL to any image. The logo is + shown on the admin and chat pages. + + +
- {/* Preview */} {value && ( -
+
Selected logo { (e.target as HTMLImageElement).style.display = "none"; }} + onError={(e) => { + (e.target as HTMLImageElement).style.display = "none"; + }} />
-
{value}
+
+ {value} +
)} - {/* Well-known logo grid */}
{WELL_KNOWN_LOGOS.map((logo) => { const isSelected = value === logo.url; - const hasFailed = imgErrors.has(logo.url); - if (hasFailed) return null; + if (imgErrors.has(logo.url)) return null; return ( - - - + + + + + + {logo.name} + + ); })}
- {/* Custom URL input */} - } - placeholder="Or paste a custom logo URL..." - value={value && !WELL_KNOWN_LOGOS.some((l) => l.url === value) ? value : ""} - onChange={(e) => { - const v = e.target.value.trim(); - onChange?.(v || undefined); - }} - className="rounded-lg" - size="small" - /> +
+ + l.url === value) ? value : "" + } + onChange={(e) => { + const v = e.target.value.trim(); + onChange?.(v || undefined); + }} + className="pl-8 h-8 rounded-lg" + /> +
); }; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx index 46fbbbcf2b0..24cfbe76bee 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx @@ -1,65 +1,75 @@ import React, { useState, useEffect } from "react"; -import { Select, Button, Card, Typography, Spin, Tag } from "antd"; -import { SaveOutlined, PlusOutlined } from "@ant-design/icons"; -import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting, fetchMCPClientIp } from "../networking"; - -const { Text } = Typography; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Plus, Save, X } from "lucide-react"; +import { + getGeneralSettingsCall, + updateConfigFieldSetting, + deleteConfigFieldSetting, + fetchMCPClientIp, +} from "../networking"; interface MCPNetworkSettingsProps { accessToken: string | null; } -/** - * Given an IP like "203.0.113.45", return "203.0.113.0/24". - */ function ipToSlash24(ip: string): string { const parts = ip.split("."); if (parts.length !== 4) return ip + "/32"; return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`; } -const MCPNetworkSettings: React.FC = ({ accessToken }) => { +const MCPNetworkSettings: React.FC = ({ + accessToken, +}) => { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [privateRanges, setPrivateRanges] = useState([]); const [currentIp, setCurrentIp] = useState(null); + const [newRangeInput, setNewRangeInput] = useState(""); useEffect(() => { + const loadSettings = async () => { + if (!accessToken) return; + setLoading(true); + try { + const settings = await getGeneralSettingsCall(accessToken); + for (const field of settings) { + if ( + field.field_name === "mcp_internal_ip_ranges" && + field.field_value + ) { + setPrivateRanges(field.field_value); + } + } + } catch (error) { + console.error("Failed to load MCP network settings:", error); + } finally { + setLoading(false); + } + }; + const detectCurrentIp = async () => { + if (!accessToken) return; + const ip = await fetchMCPClientIp(accessToken); + if (ip) setCurrentIp(ip); + }; loadSettings(); detectCurrentIp(); }, [accessToken]); - const loadSettings = async () => { - if (!accessToken) return; - setLoading(true); - try { - const settings = await getGeneralSettingsCall(accessToken); - for (const field of settings) { - if (field.field_name === "mcp_internal_ip_ranges" && field.field_value) { - setPrivateRanges(field.field_value); - } - } - } catch (error) { - console.error("Failed to load MCP network settings:", error); - } finally { - setLoading(false); - } - }; - - const detectCurrentIp = async () => { - if (!accessToken) return; - const ip = await fetchMCPClientIp(accessToken); - if (ip) { - setCurrentIp(ip); - } - }; - const handleSave = async () => { if (!accessToken) return; setSaving(true); try { if (privateRanges.length > 0) { - await updateConfigFieldSetting(accessToken, "mcp_internal_ip_ranges", privateRanges); + await updateConfigFieldSetting( + accessToken, + "mcp_internal_ip_ranges", + privateRanges, + ); } else { await deleteConfigFieldSetting(accessToken, "mcp_internal_ip_ranges"); } @@ -76,10 +86,19 @@ const MCPNetworkSettings: React.FC = ({ accessToken }) } }; + const commitNewRange = () => { + const v = newRangeInput.trim(); + if (!v) return; + if (!privateRanges.includes(v)) { + setPrivateRanges([...privateRanges, v]); + } + setNewRangeInput(""); + }; + if (loading) { return (
- +
); } @@ -89,60 +108,98 @@ const MCPNetworkSettings: React.FC = ({ accessToken }) return (
- Private IP Ranges -

- Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked "Available on Public Internet". + Private IP Ranges +

+ Define which IP ranges are part of your private network. Callers from + these IPs can see all MCP servers. Callers from any other IP can only + see servers marked "Available on Public Internet".

- + {currentIp && ( -
- - Your current IP: {currentIp} - +
+ + Your current IP:{" "} + {currentIp} + {suggestedRange && !privateRanges.includes(suggestedRange) && ( -
- Suggested range: - } +
+ + Suggested range: + + addSuggestedRange(suggestedRange)} > + {suggestedRange} - +
)}
)}
- Your Private Network Ranges + Your Private Network Ranges
- setNewRangeInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + commitNewRange(); + } + }} + placeholder="Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8" + className="flex-1" + /> + +
+ {privateRanges.length > 0 && ( +
+ {privateRanges.map((range) => ( + + {range} + + + ))} +
+ )} +
+

+ Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP + ranges are used.

-