From 428d23249a4cb56d2ea6aac7a0a292e7212a5d06 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 23 Jul 2026 23:30:14 -0700 Subject: [PATCH] refactor(ui): migrate mcp-servers, tag-management and tool-policies to shadcn Replaces antd and Tremor with shadcn primitives across the 18 files these three routes exclusively own. Markup only: no behaviour, data flow or copy changed, and no shared or form-bearing component is touched, so the blast radius stops at these pages. The 12 tests covering these components are unchanged from the previous commit and still pass, which is the evidence that the rewrite preserved behaviour. Also prunes the six antd no-restricted-imports suppressions these files no longer need. --- ui/litellm-dashboard/eslint-suppressions.json | 66 --- .../_components/MCPLogoSelector.tsx | 143 ++--- .../_components/MCPNetworkSettings.tsx | 93 ++- .../mcp-servers/_components/MCPServerCard.tsx | 447 +++++++------- .../_components/OpenAPIQuickPicker.tsx | 27 +- .../_components/TruePassthroughWarning.tsx | 20 +- .../_components/mcp_connection_status.tsx | 139 ++--- .../mcp-servers/_components/mcp_discovery.tsx | 347 +++++------ .../_components/mcp_server_cost_config.tsx | 274 +++++---- .../_components/mcp_server_cost_display.tsx | 35 +- .../_components/mcp_server_view.tsx | 549 +++++++++--------- .../mcp-servers/_components/mcp_servers.tsx | 492 +++++++++------- .../_components/mcp_tool_configuration.tsx | 249 ++++---- .../mcp-servers/_components/mcp_tools.tsx | 232 ++++---- .../tag-management/_components/index.tsx | 32 +- .../MCPSemanticFilterTestPanel.tsx | 268 ++++----- .../src/components/ToolDetail.tsx | 150 ++--- .../components/ToolPolicies/PolicySelect.tsx | 81 +-- .../ToolPolicies/ToolPoliciesTableColumns.tsx | 11 +- 19 files changed, 1792 insertions(+), 1863 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index ec1e3ac05ba..1add58c5221 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -838,15 +838,7 @@ "count": 1 } }, - "src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/immutability": { "count": 2 } @@ -861,11 +853,6 @@ "count": 1 } }, - "src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx": { "react-hooks/set-state-in-effect": { "count": 1 @@ -904,9 +891,6 @@ } }, "src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -947,11 +931,6 @@ "count": 1 } }, - "src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx": { "no-nested-ternary": { "count": 2 @@ -999,9 +978,6 @@ }, "no-nested-ternary": { "count": 3 - }, - "no-restricted-imports": { - "count": 2 } }, "src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx": { @@ -1011,9 +987,6 @@ "local/no-complex-jsx-arrow": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 2 } @@ -1021,17 +994,11 @@ "src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx": { @@ -1057,9 +1024,6 @@ "src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx": { @@ -1072,9 +1036,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 2 } @@ -1082,9 +1043,6 @@ "src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx": { @@ -1097,9 +1055,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 2 } @@ -1864,9 +1819,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -2435,11 +2387,6 @@ "count": 1 } }, - "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/AdminSettings/PluginSettings/PluginSettings.tsx": { "no-restricted-imports": { "count": 1 @@ -2605,20 +2552,7 @@ } }, "src/components/ToolDetail.tsx": { - "no-restricted-imports": { - "count": 1 - }, "unused-imports/no-unused-imports": { - "count": 2 - } - }, - "src/components/ToolPolicies/PolicySelect.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/ToolPolicies/ToolPoliciesTableColumns.tsx": { - "no-restricted-imports": { "count": 1 } }, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx index a67a0dc882d..1cb8ed6f5b5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx @@ -1,6 +1,8 @@ import React from "react"; -import { Input, Tooltip } from "antd"; -import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons"; +import { Info, Link as LinkIcon } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { cn } from "@/lib/cva.config"; import { Logo } from "@/components/molecules/logo/Logo"; import githubLogo from "../../../../../public/assets/logos/github.svg"; import slackLogo from "../../../../../public/assets/logos/slack.svg"; @@ -61,72 +63,83 @@ const MCPLogoSelector: React.FC = ({ value, onChange }) => }; return ( -
-
- Logo - - - -
- - {/* Preview */} - {value && ( -
- -
-
{value}
-
- + +
+
+ Logo + + } + /> + + Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages. + +
- )} - {/* Well-known logo grid */} -
- {WELL_KNOWN_LOGOS.map((logo) => { - const isSelected = value === logo.url; - return ( - - - - ); - })} + {/* Preview */} + {value && ( +
+ +
+
{value}
+
+ +
+ )} + + {/* Well-known logo grid */} +
+ {WELL_KNOWN_LOGOS.map((logo) => { + const isSelected = value === logo.url; + return ( + + handleSelect(logo.url)} + className={cn( + "flex size-10 cursor-pointer items-center justify-center rounded-lg border p-2 transition-all", + isSelected ? "border-primary bg-accent shadow-xs" : "border-border hover:bg-accent", + )} + > + {logo.name} + + } + /> + {logo.name} + + ); + })} +
+ + {/* Custom URL input */} + + + + + { + const v = e.target.value.trim(); + onChange?.(v || undefined); + }} + /> +
- - {/* Custom URL input */} - } - placeholder="Or paste a custom logo URL..." - value={value && !selectedWellKnown ? value : ""} - onChange={(e) => { - const v = e.target.value.trim(); - onChange?.(v || undefined); - }} - className="rounded-lg" - size="small" - /> -
+
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx index 7ab240389f3..9226bb36a11 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx @@ -1,6 +1,10 @@ import React, { useState, useEffect } from "react"; -import { Select, Button, Card, Typography, Spin, Tag } from "antd"; -import { SaveOutlined, PlusOutlined } from "@ant-design/icons"; +import { Save, Plus, X } from "lucide-react"; +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 { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { DeprecationBanner } from "@/components/DeprecationBanner"; import { getGeneralSettingsCall, @@ -9,8 +13,6 @@ import { fetchMCPClientIp, } from "@/components/networking"; -const { Text } = Typography; - interface MCPNetworkSettingsProps { accessToken: string | null; } @@ -29,6 +31,7 @@ const MCPNetworkSettings: React.FC = ({ accessToken }) const [saving, setSaving] = useState(false); const [privateRanges, setPrivateRanges] = useState([]); const [currentIp, setCurrentIp] = useState(null); + const [rangeDraft, setRangeDraft] = useState(""); useEffect(() => { loadSettings(); @@ -82,10 +85,22 @@ const MCPNetworkSettings: React.FC = ({ accessToken }) } }; + // Commas separate entries, matching the old tokenised input. + const commitDraft = () => { + const added = rangeDraft + .split(",") + .map((r) => r.trim()) + .filter((r) => r !== "" && !privateRanges.includes(r)); + if (added.length > 0) { + setPrivateRanges([...privateRanges, ...added]); + } + setRangeDraft(""); + }; + if (loading) { return (
- +
); } @@ -96,55 +111,75 @@ const MCPNetworkSettings: React.FC = ({ accessToken })
- Private IP Ranges -

+

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} - +

{suggestedRange && !privateRanges.includes(suggestedRange) && ( -
- Suggested range: - +

Suggested range:

+ } onClick={() => addSuggestedRange(suggestedRange)} > + {suggestedRange} -
+
)}
)} -
- Your Private Network Ranges +
+

Your Private Network Ranges

- setRangeDraft(e.target.value)} + onBlur={commitDraft} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + commitDraft(); + } + }} /> -

+

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

-
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx index c7dd6e47f76..de53d34c3ac 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx @@ -1,19 +1,20 @@ import { type FC, type KeyboardEvent, type MouseEvent } from "react"; -import { Dropdown, Tooltip, Typography, Tag } from "antd"; -import type { MenuProps } from "antd"; +import { Check, CircleAlert, Ellipsis, Trash2, Zap } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { - CheckOutlined, - DeleteOutlined, - ExclamationCircleFilled, - MoreOutlined, - ThunderboltOutlined, -} from "@ant-design/icons"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/cva.config"; import { AUTH_TYPE, type MCPServer } from "@/components/mcp_tools/types"; import { Logo } from "@/components/molecules/logo/Logo"; import { getMaskedAndFullUrl } from "./utils"; -const { Text } = Typography; - interface MCPServerCardProps { server: MCPServer; // Per-user env-var fields this user still needs to fill in for this server. @@ -73,8 +74,8 @@ const MCPServerCard: FC = ({ const needsAttention = missing.length > 0; const cardClass = needsAttention - ? "border-2 border-red-300 bg-red-50/40 hover:border-red-400 hover:shadow-md" - : "border border-gray-200 bg-white hover:border-gray-300 hover:shadow-md"; + ? "border-2 border-destructive/40 bg-destructive/5 hover:border-destructive/60 hover:shadow-md" + : "border border-border bg-card hover:shadow-md"; const url = server.url || ""; const { maskedUrl } = url ? getMaskedAndFullUrl(url) : { maskedUrl: "" }; @@ -105,174 +106,198 @@ const MCPServerCard: FC = ({ } }; - const menuItems: MenuProps["items"] = []; - if (onRecheckHealth) { - menuItems.push({ - key: "test-connection", - label: "Test Connection", - icon: , - disabled: isRechecking, - onClick: ({ domEvent }) => { - domEvent.stopPropagation(); - onRecheckHealth(); - }, - }); - } - if (onDelete) { - if (menuItems.length > 0) { - menuItems.push({ key: "divider", type: "divider" }); - } - menuItems.push({ - key: "delete", - label: "Delete", - icon: , - danger: true, - onClick: ({ domEvent }) => { - domEvent.stopPropagation(); - onDelete(); - }, - }); - } + const hasMenu = !!onRecheckHealth || !!onDelete; // Card uses role="button" + nested + } + /> + + {onRecheckHealth && ( + { + stop(e); + onRecheckHealth(); + }} + > + + Test Connection + + )} + {onRecheckHealth && onDelete && } + {onDelete && ( + { + stop(e); + onDelete(); + }} + > + + Delete + + )} + + + )}
- {menuItems.length > 0 && ( - - - - )} -
- {subtitle ? ( - - - {subtitle} - - - ) : ( - // Defensive placeholder: keep the row even when no identifier is - // available so the tag row stays vertically aligned across the grid. -
- )} - -
- - {displayTransport.toUpperCase()} - {authType} - {oauthFlowUnset && ( - - - - - OAuth flow not set - - + {subtitle ? ( + + {subtitle}

} /> + {subtitleTooltip}
+ ) : ( + // Defensive placeholder: keep the row even when no identifier is + // available so the badge row stays vertically aligned across the grid. +
)} - - - + +
+ + {displayTransport.toUpperCase()} + {authType} + {oauthFlowUnset && ( + + + + OAuth flow not set + + } + /> + + This OAuth server has no flow set (Machine-to-Machine vs Interactive). Open it and choose an OAuth Flow + Type so LiteLLM authenticates it as you intend. + + + )} + + {isPublic ? "Public" : "Internal"} - - - {accessGroups.slice(0, 2).map((g) => ( - - {g} - - ))} - {accessGroups.length > 2 && ( - - +{accessGroups.length - 2} - - )} -
+ + {accessGroups.slice(0, 2).map((g) => ( + + + {g} + + } + /> + {g} + + ))} + {accessGroups.length > 2 && ( + + +{accessGroups.length - 2}} /> + {accessGroups.slice(2).join(", ")} + + )} +
- {(server.is_byok || needsAttention) && ( -
- {server.is_byok && } - {needsAttention && ( -
- -
Missing user fields:
+ {(server.is_byok || needsAttention) && ( +
+ {server.is_byok && } + {needsAttention && ( +
+ + + + {missing.length} user field + {missing.length === 1 ? "" : "s"} missing + + } + /> + +
Missing user fields:
    {missing.map((m) => (
  • • {m}
  • ))}
-
- } - > - - - {missing.length} user field - {missing.length === 1 ? "" : "s"} missing - - - {onOpenFillFields && ( - - )} -
- )} -
- )} -
+ +
+ {onOpenFillFields && ( + + )} +
+ )} +
+ )} +
+ ); }; @@ -297,46 +322,45 @@ const HealthChip: FC = ({ }) => { if (isLoadingHealth || isRechecking) { return ( - - - - Checking - - + + + Checking + ); } - const tooltip = ( -
-
Health: {status}
- {lastCheck &&
Last check: {new Date(lastCheck).toLocaleString()}
} - {error && ( -
-
Error
-
{error}
-
- )} - {!lastCheck && !error &&
No health data
} - {onRecheck &&
Click to recheck
} -
- ); return ( - - { - e.stopPropagation(); - onRecheck(); - } - : undefined + + { + e.stopPropagation(); + onRecheck(); + } + : undefined + } + > + + {status.charAt(0).toUpperCase() + status.slice(1)} + } - > - - - {status.charAt(0).toUpperCase() + status.slice(1)} - - + /> + +
Health: {status}
+ {lastCheck &&
Last check: {new Date(lastCheck).toLocaleString()}
} + {error && ( +
+
Error
+
{error}
+
+ )} + {!lastCheck && !error &&
No health data
} + {onRecheck &&
Click to recheck
} +
); }; @@ -350,22 +374,22 @@ const ByokRow: FC = ({ connected, onConnect }) => { if (connected) { return (
- BYOK credential + BYOK credential
- - Connected - + + Connected + {onConnect && ( - + )}
@@ -373,20 +397,19 @@ const ByokRow: FC = ({ connected, onConnect }) => { } return (
- BYOK credential + BYOK credential {onConnect ? ( - + ) : ( - — + — )}
); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx index 0aec81fdf4b..d8e153a9699 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Spin } from "antd"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { cn } from "@/lib/cva.config"; import { fetchOpenAPIRegistry } from "@/components/networking"; export interface OpenAPIKeyTool { @@ -49,9 +50,9 @@ const OpenAPIQuickPicker: React.FC = ({ accessToken, se if (loading) { return (
- Popular APIs + Popular APIs
- +
); @@ -61,7 +62,7 @@ const OpenAPIQuickPicker: React.FC = ({ accessToken, se return (
- Popular APIs + Popular APIs
{apis.map((api) => { @@ -73,32 +74,30 @@ const OpenAPIQuickPicker: React.FC = ({ accessToken, se type="button" title={api.description} onClick={() => onSelect(api)} - className={`flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all cursor-pointer - ${ - isSelected - ? "border-blue-500 bg-blue-50 shadow-xs" - : "border-gray-200 hover:border-blue-300 hover:bg-gray-50" - }`} + className={cn( + "flex cursor-pointer flex-col items-center gap-1.5 rounded-lg border p-3 transition-all", + isSelected ? "border-primary bg-accent shadow-xs" : "border-border hover:bg-accent", + )} > {imgFailed ? ( - + {api.title.charAt(0)} ) : ( {api.title} handleImgError(api.name)} /> )} - {api.title} + {api.title} ); })}
-

+

Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter your own spec URL below.

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx index 9c57cbd7d14..b017fc4f1b4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { Alert } from "antd"; +import { TriangleAlert } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { AUTH_TYPE } from "@/components/mcp_tools/types"; /** @@ -10,12 +11,15 @@ import { AUTH_TYPE } from "@/components/mcp_tools/types"; export default function TruePassthroughWarning({ authType }: { authType?: string | null }) { if (authType !== AUTH_TYPE.TRUE_PASSTHROUGH) return null; return ( - + + + True Passthrough disables LiteLLM authentication for this server + + Anyone who can reach the gateway can call this server without a LiteLLM key. The caller's Authorization + header is forwarded to the upstream verbatim, per-key and per-team rate limits and spend tracking do not apply, + and the upstream is fully responsible for authenticating callers. Choose OAuth Delegate instead if callers + should still authenticate to LiteLLM. + + ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx index 469f372409c..cb02fedab66 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx @@ -1,7 +1,10 @@ import React from "react"; -import { Button, Spin, Alert, Collapse } from "antd"; -import { CheckCircleOutlined, ExclamationCircleOutlined, ReloadOutlined, ToolOutlined } from "@ant-design/icons"; -import { Card, Title, Text } from "@tremor/react"; +import { CircleCheck, CircleAlert, RefreshCw, Wrench, Info } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; interface MCPConnectionStatusProps { formValues: Record; @@ -31,27 +34,26 @@ const MCPConnectionStatus: React.FC = ({ } return ( - +
- - Connection Status + +

Connection Status

{!canFetchTools && (formValues.url || formValues.spec_path) && ( -
- - Complete required fields to test connection -
- Fill in URL, Transport, and Authentication to test MCP server connection +
+ +

Complete required fields to test connection

+

Fill in URL, Transport, and Authentication to test MCP server connection

)} {canFetchTools && (
-
+
- +

{isLoadingTools ? "Testing connection to MCP server..." : tools.length > 0 @@ -61,97 +63,84 @@ const MCPConnectionStatus: React.FC = ({ ? "Ready to submit" : "Connection failed" : "Ready to test connection"} - -
- Server: {formValues.url || formValues.spec_path} +

+

Server: {formValues.url || formValues.spec_path}

{isLoadingTools && ( -
- - Connecting... +
+ +

Connecting...

)} {!isLoadingTools && !toolsError && tools.length > 0 && ( -
- - Connected +
+ +

Connected

)} {toolsError && !isPreviewForbidden && ( -
- - Failed +
+ +

Failed

)}
{isLoadingTools && ( -
- - Testing connection and loading tools... +
+ +

Testing connection and loading tools...

)} {toolsError && isPreviewForbidden && ( - + + + Tool preview unavailable + {toolsError} + )} {toolsError && !isPreviewForbidden && ( - -
{toolsError}
- {toolsErrorStackTrace && ( - - {toolsErrorStackTrace} - - ), - }, - ]} - style={{ marginTop: "12px" }} + + + Connection Failed + +
{toolsError}
+ {toolsErrorStackTrace && ( + + + Stack Trace + + } /> - )} -
- } - type="error" - showIcon - action={ - - } - /> +
+ )} {!isLoadingTools && tools.length === 0 && !toolsError && ( -
- - Connection successful! -
- No tools found for this MCP server +
+ +

Connection successful!

+

No tools found for this MCP server

)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx index 6fcff011ba6..b2c181a9112 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx @@ -1,13 +1,15 @@ import React, { useState, useMemo, useEffect } from "react"; -import { Modal, Input, Typography } from "antd"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/cva.config"; import { fetchDiscoverableMCPServers } from "@/components/networking"; import { DiscoverableMCPServer, DiscoverMCPServersResponse } from "@/components/mcp_tools/types"; import { mcpLogoImg } from "./create_mcp_server"; import { resolveLogoSrc } from "@/lib/assetPaths"; -const { Search } = Input; -const { Text } = Typography; - interface MCPDiscoveryProps { isVisible: boolean; onClose: () => void; @@ -16,12 +18,21 @@ interface MCPDiscoveryProps { accessToken: string | null; } -const INITIAL_COLORS = ["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"]; +const INITIAL_COLORS = [ + "bg-blue-500", + "bg-emerald-500", + "bg-amber-500", + "bg-red-500", + "bg-violet-500", + "bg-pink-500", + "bg-cyan-500", + "bg-lime-500", +]; function getInitialAvatar(name: string) { const initial = name.charAt(0).toUpperCase(); const colorIndex = name.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0) % INITIAL_COLORS.length; - return { initial, backgroundColor: INITIAL_COLORS[colorIndex] }; + return { initial, backgroundClass: INITIAL_COLORS[colorIndex] }; } const MCPDiscovery: React.FC = ({ @@ -91,214 +102,126 @@ const MCPDiscovery: React.FC = ({ }, [filteredServers]); return ( - -
- MCP Logo -

Add MCP Server

-
- -
- } - open={isVisible} - onCancel={onClose} - footer={null} - width={1000} - className="top-8" - styles={{ - body: { padding: "24px", maxHeight: "70vh", overflowY: "auto" }, - header: { padding: "24px 24px 0 24px", border: "none" }, - }} - > - {/* Filter pills */} -
- {["All", ...categories].map((cat) => { - const isSelected = selectedCategory === cat; - return ( - - ); - })} -
- - {/* Search */} - setSearchQuery(e.target.value)} - style={{ marginBottom: 16 }} - allowClear - /> - - {/* Loading skeleton */} - {loading && ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- ))} -
- )} - - {error && ( -
- Failed to load servers: {error} -
- )} - - {!loading && !error && filteredServers.length === 0 && ( -
- - No servers found.{" "} - - Add a custom server - - -
- )} - - {/* Server list grouped by category — 2 columns */} - {!loading && - !error && - Object.entries(groupedServers).map(([category, categoryServers]) => ( -
-
- {category} -
-
- {categoryServers.map((server) => { - const avatar = getInitialAvatar(server.title || server.name); - return ( -
onSelectServer(server)} - style={{ - display: "flex", - alignItems: "center", - padding: "8px 10px", - borderRadius: 6, - cursor: "pointer", - transition: "background 0.1s ease", - }} - onMouseEnter={(e) => { - e.currentTarget.style.background = "#f9fafb"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = "transparent"; - }} - > - {server.icon_url ? ( - {server.title} { - const target = e.currentTarget; - target.style.display = "none"; - const next = target.nextElementSibling as HTMLElement; - if (next) next.style.display = "flex"; - }} - /> - ) : null} -
- {avatar.initial} -
- - {server.title || server.name} - - › -
- ); - })} + !open && onClose()}> + + +
+
+ MCP Logo + Add MCP Server
+
- ))} - +
+ +
+ {/* Filter pills */} +
+ {["All", ...categories].map((cat) => { + const isSelected = selectedCategory === cat; + return ( + + ); + })} +
+ + {/* Search */} + + + + + setSearchQuery(e.target.value)} + /> + + + {/* Loading skeleton */} + {loading && ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+ )} + + {error && ( +
+

Failed to load servers: {error}

+
+ )} + + {!loading && !error && filteredServers.length === 0 && ( +
+

+ No servers found.{" "} + +

+
+ )} + + {/* Server list grouped by category — 2 columns */} + {!loading && + !error && + Object.entries(groupedServers).map(([category, categoryServers]) => ( +
+
+ {category} +
+
+ {categoryServers.map((server) => { + const avatar = getInitialAvatar(server.title || server.name); + return ( +
onSelectServer(server)} + className="flex cursor-pointer items-center rounded-md px-2.5 py-2 transition-colors hover:bg-accent" + > + {server.icon_url ? ( + {server.title} { + const target = e.currentTarget; + target.style.display = "none"; + const next = target.nextElementSibling as HTMLElement; + if (next) next.style.display = "flex"; + }} + /> + ) : null} +
+ {avatar.initial} +
+ {server.title || server.name} + › +
+ ); + })} +
+
+ ))} +
+
+
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx index 89c41693a4c..e0d44718ec5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx @@ -1,7 +1,10 @@ -import React from "react"; -import { Tooltip, InputNumber, Collapse, Badge } from "antd"; -import { InfoCircleOutlined, DollarOutlined, ToolOutlined } from "@ant-design/icons"; -import { Card, Title, Text } from "@tremor/react"; +import React, { useState } from "react"; +import { Info, DollarSign, Wrench } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { MCPServerCostInfo } from "@/components/mcp_tools/types"; interface MCPServerCostConfigProps { @@ -11,6 +14,47 @@ interface MCPServerCostConfigProps { disabled?: boolean; } +interface CostInputProps { + value: number | null | undefined; + placeholder: string; + disabled?: boolean; + className?: string; + onChange: (cost: number | null) => void; +} + +/** + * Costs are shown to four decimal places when idle, but the field keeps the raw + * keystrokes while it is being edited so partial input like "0." survives. + */ +const CostInput: React.FC = ({ value, placeholder, disabled, className, onChange }) => { + const [draft, setDraft] = useState(null); + const display = draft ?? (value === null || value === undefined ? "" : value.toFixed(4)); + + const handleChange = (next: string) => { + setDraft(next); + const parsed = Number(next); + onChange(next.trim() === "" || Number.isNaN(parsed) ? null : parsed); + }; + + return ( + + + $ + + setDraft(value === null || value === undefined ? "" : String(value))} + onBlur={() => setDraft(null)} + onChange={(e) => handleChange(e.target.value)} + /> + + ); +}; + const MCPServerCostConfig: React.FC = ({ value = {}, onChange, @@ -37,124 +81,126 @@ const MCPServerCostConfig: React.FC = ({ }; return ( - -
-
- - Cost Configuration - - - -
- -
-
- - - - Set a default cost for all tool calls to this server - + + +
+
+ +

Cost Configuration

+ + } + /> + + Configure costs for this MCP server's tool calls. Set a default rate and per-tool overrides. + +
- {tools.length > 0 && ( -
-
+ + {(value.default_cost_per_query || + (value.tool_name_to_cost_per_query && Object.keys(value.tool_name_to_cost_per_query).length > 0)) && ( +
+

Cost Summary:

+
+ {value.default_cost_per_query && ( +

+ • Default cost: ${value.default_cost_per_query.toFixed(4)} per query +

+ )} + {value.tool_name_to_cost_per_query && + Object.entries(value.tool_name_to_cost_per_query).map( + ([toolName, cost]) => + cost !== null && + cost !== undefined && ( +

+ • {toolName}: ${cost.toFixed(4)} per query +

+ ), + )} +
)}
- - {(value.default_cost_per_query || - (value.tool_name_to_cost_per_query && Object.keys(value.tool_name_to_cost_per_query).length > 0)) && ( -
- Cost Summary: -
- {value.default_cost_per_query && ( - - • Default cost: ${value.default_cost_per_query.toFixed(4)} per query - - )} - {value.tool_name_to_cost_per_query && - Object.entries(value.tool_name_to_cost_per_query).map( - ([toolName, cost]) => - cost !== null && - cost !== undefined && ( - - • {toolName}: ${cost.toFixed(4)} per query - - ), - )} -
-
- )} -
- + + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx index f26f7ba2320..30fd269447e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Text } from "@tremor/react"; import { MCPServerCostInfo } from "@/components/mcp_tools/types"; interface MCPServerCostDisplayProps { @@ -15,12 +14,12 @@ const MCPServerCostDisplay: React.FC = ({ costConfig if (!hasCostConfig) { return ( -
+
-
- +
+

No cost configuration set for this server. Tool calls will be charged at $0.00 per tool call. - +

@@ -28,28 +27,28 @@ const MCPServerCostDisplay: React.FC = ({ costConfig } return ( -
+
{hasDefaultCost && costConfig?.default_cost_per_query !== undefined && costConfig?.default_cost_per_query !== null && (
- Default Cost per Query -
${costConfig.default_cost_per_query.toFixed(4)}
+

Default Cost per Query

+
${costConfig.default_cost_per_query.toFixed(4)}
)} {hasToolCosts && costConfig?.tool_name_to_cost_per_query && (
- Tool-Specific Costs +

Tool-Specific Costs

{Object.entries(costConfig.tool_name_to_cost_per_query).map( ([toolName, cost]) => cost !== null && cost !== undefined && ( -
- {toolName} - ${cost.toFixed(4)} per query +
+

{toolName}

+

${cost.toFixed(4)} per query

), )} @@ -57,20 +56,20 @@ const MCPServerCostDisplay: React.FC = ({ costConfig
)} -
- Cost Summary: +
+

Cost Summary:

{hasDefaultCost && costConfig?.default_cost_per_query !== undefined && costConfig?.default_cost_per_query !== null && ( - +

• Default cost: ${costConfig.default_cost_per_query.toFixed(4)} per query - +

)} {hasToolCosts && costConfig?.tool_name_to_cost_per_query && ( - +

• {Object.keys(costConfig.tool_name_to_cost_per_query).length} tool(s) with custom pricing - +

)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx index 49df8206c8f..736327409d6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx @@ -1,6 +1,9 @@ import React, { useState } from "react"; -import { ArrowLeftIcon, EyeIcon, EyeOffIcon } from "@heroicons/react/outline"; -import { Title, Card, Button, Text, Grid, TabGroup, TabList, TabPanel, TabPanels, Tab, Icon } from "@tremor/react"; +import { ArrowLeft, Eye, EyeOff } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { MCPServer, handleTransport, handleAuth } from "@/components/mcp_tools/types"; // TODO: Move Tools viewer from index file @@ -11,7 +14,6 @@ import MCPServerCostDisplay from "./mcp_server_cost_display"; import { getMaskedAndFullUrl } from "./utils"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; import { CheckIcon, CopyIcon } from "lucide-react"; -import { Button as AntdButton } from "antd"; interface MCPServerViewProps { mcpServer: MCPServer; @@ -86,335 +88,306 @@ export const MCPServerView: React.FC = ({ } }; - const getTransportBadge = (transport: string) => { - const label = transport.toUpperCase(); - return ( - - {label} - - ); - }; + const getTransportBadge = (transport: string) => {transport.toUpperCase()}; - const getAuthBadge = (authType: string) => { - return ( - - {authType} - - ); - }; + const getAuthBadge = (authType: string) => {authType}; return ( -
+
-
- {mcpServer.server_name || mcpServer.alias || "Unnamed Server"} - : } +

{mcpServer.server_name || mcpServer.alias || "Unnamed Server"}

+ {mcpServer.alias && mcpServer.server_name && mcpServer.alias !== mcpServer.server_name && ( - + {mcpServer.alias} - + )}
-
- {mcpServer.server_id} - : } +
+

{mcpServer.server_id}

+
- {mcpServer.description && {mcpServer.description}} + {mcpServer.description &&

{mcpServer.description}

}
- {/* TODO: magic number for index */} - - - {[ - Overview, - MCP Tools, - ...(isProxyAdmin ? [Settings] : []), - ]} - + setSelectedTabIndex(Number(v))}> + + + Overview + + + MCP Tools + + {isProxyAdmin && ( + + Settings + + )} + - - {/* Overview Panel */} - - - - Transport -
- {getTransportBadge( - handleTransport(mcpServer.transport ?? undefined, mcpServer.spec_path ?? undefined), - )} -
-
- - - Authentication -
{getAuthBadge(handleAuth(mcpServer.auth_type ?? undefined))}
-
- - - Host URL -
- - {renderUrlWithToggle(mcpServer.url, showFullUrl)} - - {/* Only proxy admins may reveal the raw URL — non-admins - receive a sanitized server object from the backend - with `url=null`, but hide the toggle anyway as - defense-in-depth in case the URL ever leaks back - into the response. */} - {hasToken && isProxyAdmin && ( - - )} -
-
-
- - Cost Configuration + {/* Overview Panel */} + +
+ +

Transport

- + {getTransportBadge(handleTransport(mcpServer.transport ?? undefined, mcpServer.spec_path ?? undefined))}
- - {/* Tool Panel */} - - - + +

Authentication

+
{getAuthBadge(handleAuth(mcpServer.auth_type ?? undefined))}
+
- {/* Settings Panel */} - - -
- MCP Server Settings - {editing ? null : ( - )}
- {editing ? ( - setEditing(false)} - onSuccess={handleSuccess} - availableAccessGroups={availableAccessGroups} - /> - ) : ( -
-
- Server Name -
- {mcpServer.server_name || —} -
+ +
+ +

Cost Configuration

+
+ +
+
+ + + {/* Tool Panel */} + + + + + {/* Settings Panel */} + + +
+

MCP Server Settings

+ {editing ? null : ( + + )} +
+ {editing ? ( + setEditing(false)} + onSuccess={handleSuccess} + availableAccessGroups={availableAccessGroups} + /> + ) : ( +
+
+

Server Name

+
+ {mcpServer.server_name || —}
-
- Alias -
- {mcpServer.alias || —} -
+
+
+

Alias

+
+ {mcpServer.alias || —}
-
- Description -
- {mcpServer.description || —} -
+
+
+

Description

+
+ {mcpServer.description || —}
-
- URL -
- {renderUrlWithToggle(mcpServer.url, showFullUrl)} - {hasToken && ( - - )} -
+
+
+

URL

+
+ {renderUrlWithToggle(mcpServer.url, showFullUrl)} + {hasToken && ( + + )}
-
- Transport +
+
+

Transport

+
+ {getTransportBadge(handleTransport(mcpServer.transport, mcpServer.spec_path))} +
+
+
+

Authentication

+
{getAuthBadge(handleAuth(mcpServer.auth_type))}
+
+
+

Extra Headers

+
+ {mcpServer.extra_headers && mcpServer.extra_headers.length > 0 ? ( + mcpServer.extra_headers.join(", ") + ) : ( + — + )} +
+
+
+

Allow All Keys

+
+ {mcpServer.allow_all_keys ? ( + + + Enabled + + ) : ( + Disabled + )} +
+
+
+

Network Access

+
+ {mcpServer.available_on_public_internet ? ( + + + Public + + ) : ( + + + Internal only + + )} +
+
+ {handleAuth(mcpServer.auth_type) === "oauth2" && ( +
+

Delegate Auth to Upstream

- {getTransportBadge(handleTransport(mcpServer.transport, mcpServer.spec_path))} -
-
-
- Authentication -
{getAuthBadge(handleAuth(mcpServer.auth_type))}
-
-
- Extra Headers -
- {mcpServer.extra_headers && mcpServer.extra_headers.length > 0 ? ( - mcpServer.extra_headers.join(", ") + {mcpServer.delegate_auth_to_upstream ? ( + + + Enabled (PKCE passthrough) + ) : ( - — + Disabled )}
-
- Allow All Keys -
- {mcpServer.allow_all_keys ? ( - - - Enabled - - ) : ( - - Disabled - - )} -
-
-
- Network Access -
- {mcpServer.available_on_public_internet ? ( - - - Public - - ) : ( - - - Internal only - - )} -
-
- {handleAuth(mcpServer.auth_type) === "oauth2" && ( -
- Delegate Auth to Upstream + )} + {handleAuth(mcpServer.auth_type) !== "oauth2" && + Array.isArray(mcpServer.extra_headers) && + mcpServer.extra_headers.some((h) => typeof h === "string" && h.toLowerCase() === "authorization") && ( +
+

OAuth Pass-through

- {mcpServer.delegate_auth_to_upstream ? ( - - - Enabled (PKCE passthrough) - + {mcpServer.oauth_passthrough ? ( + + + Enabled + ) : ( - - Disabled - + Disabled )}
)} - {handleAuth(mcpServer.auth_type) !== "oauth2" && - Array.isArray(mcpServer.extra_headers) && - mcpServer.extra_headers.some( - (h) => typeof h === "string" && h.toLowerCase() === "authorization", - ) && ( -
- OAuth Pass-through -
- {mcpServer.oauth_passthrough ? ( - - - Enabled - - ) : ( - - Disabled - - )} -
+
+

Access Groups

+
+ {mcpServer.mcp_access_groups && mcpServer.mcp_access_groups.length > 0 ? ( +
+ {mcpServer.mcp_access_groups.map((group: any, index: number) => ( + + {typeof group === "string" ? group : group?.name ?? ""} + + ))}
+ ) : ( + — )} -
- Access Groups -
- {mcpServer.mcp_access_groups && mcpServer.mcp_access_groups.length > 0 ? ( -
- {mcpServer.mcp_access_groups.map((group: any, index: number) => ( - - {typeof group === "string" ? group : group?.name ?? ""} - - ))} -
- ) : ( - — - )} -
-
-
- Allowed Tools -
- {mcpServer.allowed_tools && mcpServer.allowed_tools.length > 0 ? ( -
- {mcpServer.allowed_tools.map((tool: string, index: number) => ( - - {tool} - - ))} -
- ) : ( - - All tools enabled - - )} -
-
-
- Cost -
- -
- )} - - - - +
+

Allowed Tools

+
+ {mcpServer.allowed_tools && mcpServer.allowed_tools.length > 0 ? ( +
+ {mcpServer.allowed_tools.map((tool: string, index: number) => ( + + {tool} + + ))} +
+ ) : ( + All tools enabled + )} +
+
+
+

Cost

+
+ +
+
+
+ )} + + +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx index f186fef22da..79bc6a9bb37 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx @@ -1,8 +1,21 @@ import { isAdminRole } from "@/utils/roles"; -import { QuestionCircleOutlined, SearchOutlined } from "@ant-design/icons"; -import { Button, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; +import { CircleHelp, Search } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import NewBadge from "@/components/common_components/NewBadge"; -import { Descriptions, Empty, Input, Modal, Select, Spin, Tooltip, Typography } from "antd"; import React, { useEffect, useState, useMemo, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; @@ -75,7 +88,6 @@ const compareServers = (a: MCPServer, b: MCPServer, sort: SortKey): number => { } }; -const { Text: AntdText, Title: AntdTitle } = Typography; const EDIT_OAUTH_UI_STATE_KEY = "litellm-mcp-oauth-edit-state"; // Server id stashed by the Tools tab before an OBO OAuth redirect, read once at @@ -95,8 +107,6 @@ const readToolsOAuthServerId = (): string | null => { } }; -const { Option } = Select; - const MCPServers: React.FC = ({ accessToken, userRole, userID }) => { const { data: mcpServers, isLoading: isLoadingServers, refetch } = useMCPServers(); @@ -240,6 +250,15 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) }, [serversWithHealth]); // Get unique MCP access groups from all servers + const teamSelectItems = React.useMemo( + () => ({ + all: isInternalUser ? "All Available Servers" : "All Servers", + personal: "Personal", + ...Object.fromEntries(uniqueTeams.map((team) => [team.team_id, team.team_alias || team.team_id])), + }), + [isInternalUser, uniqueTeams], + ); + const uniqueMcpAccessGroups = React.useMemo(() => { if (!serversWithHealth) return []; return Array.from( @@ -251,6 +270,14 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) ); }, [serversWithHealth]); + const accessGroupSelectItems = React.useMemo( + () => ({ + all: "All Access Groups", + ...Object.fromEntries(uniqueMcpAccessGroups.map((group) => [group, group])), + }), + [uniqueMcpAccessGroups], + ); + // Filtering logic for both team and access group const filterServers = useCallback( (teamId: string, group: string) => { @@ -390,131 +417,135 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) } return ( -
- -
- - This action is permanent and cannot be undone. All associated configurations will be removed. - + +
+ !open && cancelDelete()}> + + + Delete MCP Server? + +
+

+ This action is permanent and cannot be undone. All associated configurations will be removed. +

- {serverToDelete && ( -
- - {serverToDelete.server_name && ( - Name}> - - {serverToDelete.server_name} - - - )} - ID}> - - {serverToDelete.server_id} - - - {serverToDelete.url && ( - URL}> - - {serverToDelete.url} - - - )} - + {serverToDelete && ( +
+ {serverToDelete.server_name && ( +
+
Name
+
{serverToDelete.server_name}
+
+ )} +
+
ID
+
{serverToDelete.server_id}
+
+ {serverToDelete.url && ( +
+
URL
+
{serverToDelete.url}
+
+ )} +
+ )}
- )} -
- - { - setModalVisible(false); - setPrefillData(null); - setDiscoveryVisible(true); - }} - /> -
-
-
- MCP Servers - {filteredServers.length > 0 && ( - - {filteredServers.length} - + + Cancel + + + + + { + setModalVisible(false); + setPrefillData(null); + setDiscoveryVisible(true); + }} + /> +
+
+
+

MCP Servers

+ {filteredServers.length > 0 && {filteredServers.length}} +
+

Configure and manage your MCP servers

+
+
+ {isAdminRole(userRole) && ( + + )} + {!isAdminRole(userRole) && ( + )}
- Configure and manage your MCP servers
-
- {isAdminRole(userRole) && ( - - )} - {!isAdminRole(userRole) && ( - - )} -
-
- setDiscoveryVisible(false)} - onSelectServer={(server: DiscoverableMCPServer) => { - setPrefillData(server); - setDiscoveryVisible(false); - setModalVisible(true); - }} - onCustomServer={() => { - setPrefillData(null); - setDiscoveryVisible(false); - setModalVisible(true); - }} - accessToken={accessToken} - /> - - -
- All Servers - Toolsets - Connect - {isAdminRole(userRole) && Semantic Filter} - {isAdminRole(userRole) && Network Settings} + setDiscoveryVisible(false)} + onSelectServer={(server: DiscoverableMCPServer) => { + setPrefillData(server); + setDiscoveryVisible(false); + setModalVisible(true); + }} + onCustomServer={() => { + setPrefillData(null); + setDiscoveryVisible(false); + setModalVisible(true); + }} + accessToken={accessToken} + /> + + + + All Servers + + + Toolsets + + + Connect + {isAdminRole(userRole) && ( - + + Semantic Filter + + )} + {isAdminRole(userRole) && ( + + Network Settings + + )} + {isAdminRole(userRole) && ( + Submitted MCPs - + )} -
-
- - + + {selectedServerId ? ( = ({ accessToken, userRole, userID })
-
+
- Team - handleTeamChange(v ?? "all")} + > + + + + + {isInternalUser ? "All Available Servers" : "All Servers"} - - - - {uniqueTeams.map((team) => ( - - ))} + + Personal + {uniqueTeams.map((team) => ( + + {team.team_alias || team.team_id} + + ))} +
-
+
- +

Access Group - - + + + } + /> + + An MCP Access Group is a set of users or teams that have permission to access specific MCP + servers. Use access groups to control and organize who can connect to which servers. + - +

- } - placeholder="Search by name, alias, URL, or ID" - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - style={{ maxWidth: 320 }} - /> + + + + + setSearchQuery(e.target.value)} + /> +
- Sort +

Sort

-
+
{displayedServers.length} of {filteredServers.length} servers
{isLoadingServers ? ( -
- +
+ +

Loading MCP servers...

) : displayedServers.length === 0 ? ( -
- +
+

+ {filteredServers.length === 0 + ? "No MCP servers configured. Click '+ Add New MCP Server' to get started." + : "No servers match the current filters or search."} +

) : (
= ({ accessToken, userRole, userID })
)} - - + + - - + + - + {isAdminRole(userRole) && ( - + - + )} {isAdminRole(userRole) && ( - + - + )} {isAdminRole(userRole) && ( - + - + )} - - + - {byokModalServer && ( - setByokModalServer(null)} - onSuccess={(_serverId) => { - refetch(); - setByokModalServer(null); + {byokModalServer && ( + setByokModalServer(null)} + onSuccess={(_serverId) => { + refetch(); + setByokModalServer(null); + }} + /> + )} + + {/* Per-user env-var fill modal — backed by /v1/mcp/server/{id}/user-env-vars */} + { + setEnvVarsModalServer(null); + setDeepLinkServerId(null); + }} + onSaved={() => { + // Refresh the bulk status so the red "N user fields missing" footer + // on each card clears once the user has filled in their values. + refetchEnvVarStatus(); }} /> - )} - - {/* Per-user env-var fill modal — backed by /v1/mcp/server/{id}/user-env-vars */} - { - setEnvVarsModalServer(null); - setDeepLinkServerId(null); - }} - onSaved={() => { - // Refresh the bulk status so the red "N user fields missing" footer - // on each card clears once the user has filled in their values. - refetchEnvVarStatus(); - }} - /> -
+
+ ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx index 60c4c264c3c..274bdf63e32 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx @@ -1,7 +1,14 @@ 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, Radio } from "antd"; +import { Wrench, CircleCheck, Search, Pencil } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { Textarea } from "@/components/ui/textarea"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { cn } from "@/lib/cva.config"; import McpCrudPermissionPanel from "@/components/mcp_tools/McpCrudPermissionPanel"; import { TOOL_DISPLAY_NAME_PATTERN } from "./utils"; @@ -67,86 +74,74 @@ const ToolRow: React.FC = ({ return (
-
onToggle(tool.name)}> +
onToggle(tool.name)}>
- onToggle(tool.name)} /> + onToggle(tool.name)} />
- {toolNameToDisplayName[tool.name] || tool.name} - - {isEnabled ? "Enabled" : "Disabled"} - - {toolNameToDisplayName[tool.name] && ( - - Custom 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 +

Display Name

onDisplayNameChange(tool.name, e.target.value)} - status={isDisplayNameInvalid ? "error" : undefined} + aria-invalid={isDisplayNameInvalid || undefined} /> {isDisplayNameInvalid ? ( - +

Only letters, digits, underscores, and hyphens are allowed (no spaces). - +

) : ( - +

Override how this tool's name appears to users. Leave blank to use original. - +

)}
- Description - Description

+