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.

-