diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx index 56d8e16ea48..fb38e392631 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx @@ -1,14 +1,7 @@ "use client"; -import React, { useEffect, useState, useCallback } from "react"; -import { getGeneralSettingsCall, updateConfigFieldSetting } from "../networking"; -import NotificationsManager from "../molecules/notifications_manager"; import { MCPServer } from "./types"; -interface MCPStandardsSettingsProps { - accessToken: string | null; -} - export interface RequiredFieldDef { key: string; label: string; @@ -16,146 +9,64 @@ export interface RequiredFieldDef { check: (server: MCPServer) => boolean; } -export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = [ +export interface FieldGroup { + label: string; + fields: RequiredFieldDef[]; +} + +export const FIELD_GROUPS: FieldGroup[] = [ { - key: "description", - label: "Description", - description: "Server must have a non-empty description.", - check: (s) => !!s.description?.trim(), + label: "Documentation", + fields: [ + { + key: "description", + label: "Description", + description: "Must have a non-empty description", + check: (s) => !!s.description?.trim(), + }, + { + key: "alias", + label: "Alias", + description: "Must have a display alias", + check: (s) => !!s.alias?.trim(), + }, + ], }, { - key: "source_url", - label: "GitHub / Source URL", - description: "Server must have a link to the source repository.", - check: (s) => !!s.source_url?.trim(), + label: "Source", + fields: [ + { + key: "source_url", + label: "GitHub / Source URL", + description: "Must link to a source repository", + check: (s) => !!s.source_url?.trim(), + }, + ], }, { - key: "alias", - label: "Alias", - description: "Server must have a human-readable alias.", - check: (s) => !!s.alias?.trim(), + label: "Connection", + fields: [ + { + key: "url", + label: "Server URL", + description: "Must have a URL configured", + check: (s) => !!s.url?.trim(), + }, + ], }, { - key: "auth_type", - label: "Auth configured", - description: "Server must have an auth type set (not 'none').", - check: (s) => !!s.auth_type && s.auth_type !== "none", - }, - { - key: "url", - label: "Server URL", - description: "Server must have a URL configured.", - check: (s) => !!s.url?.trim(), + label: "Security", + fields: [ + { + key: "auth_type", + label: "Auth configured", + description: "Must use authentication (not 'none')", + check: (s) => !!s.auth_type && s.auth_type !== "none", + }, + ], }, ]; -const SETTINGS_KEY = "mcp_required_fields"; +export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = FIELD_GROUPS.flatMap((g) => g.fields); -export default function MCPStandardsSettings({ accessToken }: MCPStandardsSettingsProps) { - const [requiredFields, setRequiredFields] = useState([]); - const [isSaving, setIsSaving] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const loadSettings = useCallback(async () => { - if (!accessToken) return; - setIsLoading(true); - try { - const settings = await getGeneralSettingsCall(accessToken); - const rows: Array<{ field_name: string; field_value: unknown }> = Array.isArray(settings?.data) - ? settings.data - : []; - const row = rows.find((r) => r.field_name === SETTINGS_KEY); - if (row && Array.isArray(row.field_value)) { - setRequiredFields(row.field_value as string[]); - } - } catch { - // leave defaults - } finally { - setIsLoading(false); - } - }, [accessToken]); - - useEffect(() => { - loadSettings(); - }, [loadSettings]); - - const toggleField = (key: string) => { - setRequiredFields((prev) => - prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key], - ); - }; - - const handleSave = async () => { - if (!accessToken) return; - setIsSaving(true); - try { - await updateConfigFieldSetting(accessToken, SETTINGS_KEY, requiredFields); - NotificationsManager.success("Standards saved"); - } catch { - NotificationsManager.fromBackend("Failed to save standards"); - } finally { - setIsSaving(false); - } - }; - - return ( -
-
-

MCP Submission Standards

-

- Choose which fields are required for a submission to pass your standards. Each submission - card in the Team MCPs tab will show a green ✓ or red ✗ for each requirement. -

-
- - {isLoading ? ( -
Loading…
- ) : ( -
- {MCP_REQUIRED_FIELD_DEFS.map((field) => { - const enabled = requiredFields.includes(field.key); - return ( -
-
-
{field.label}
-
{field.description}
-
- -
- ); - })} -
- )} - -
- -
-
- ); -} +export const SETTINGS_KEY = "mcp_required_fields"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx index 4a639d74f95..74ff0be629c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx @@ -7,15 +7,19 @@ import { XIcon, AlertCircleIcon, ServerIcon, + ChevronDownIcon, + ChevronUpIcon, + SettingsIcon, } from "lucide-react"; import { fetchMCPSubmissions, approveMCPServer, rejectMCPServer, getGeneralSettingsCall, + updateConfigFieldSetting, } from "@/components/networking"; import { MCPServer, MCPSubmissionsSummary } from "./types"; -import { MCP_REQUIRED_FIELD_DEFS } from "./MCPStandardsSettings"; +import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings"; import NotificationsManager from "@/components/molecules/notifications_manager"; type MCPStatus = "active" | "pending_review" | "rejected"; @@ -137,6 +141,128 @@ function ConfirmDialog({ action, serverName, onConfirm, onCancel }: ConfirmDialo ); } +type SubmissionRulesPanelProps = { + requiredFields: string[]; + onChange: (fields: string[]) => void; + onSave: () => Promise; + isSaving: boolean; +}; + +function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: SubmissionRulesPanelProps) { + const [expanded, setExpanded] = useState(false); + const activeLabels = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key)); + + const toggle = (key: string) => { + onChange(requiredFields.includes(key) ? requiredFields.filter((k) => k !== key) : [...requiredFields, key]); + }; + + return ( +
+ {/* Header — always visible */} +
setExpanded((v) => !v)} + > +
+ + Submission Rules + {activeLabels.length > 0 ? ( + + ({activeLabels.length} required field{activeLabels.length !== 1 ? "s" : ""}) + + ) : ( + no rules set + )} +
+
+ {/* Active rule chips — collapsed view */} + {!expanded && activeLabels.length > 0 && ( +
+ {activeLabels.map((f) => ( + + + {f.label} + + ))} +
+ )} + {expanded ? ( + + ) : ( + + )} +
+
+ + {/* Expanded editor */} + {expanded && ( +
+

+ Select which fields must be filled in before a submission is considered compliant. + LiteLLM will show ✓ / ✗ for each rule on every submission card below. +

+
+ {FIELD_GROUPS.map((group) => ( +
+
+ {group.label} +
+
+ {group.fields.map((field) => { + const active = requiredFields.includes(field.key); + return ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+ )} +
+ ); +} + type MCPServerCardProps = { server: MCPServer; onApprove: () => void; @@ -273,6 +399,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [requiredFields, setRequiredFields] = useState([]); + const [isSavingRules, setIsSavingRules] = useState(false); const fetchData = useCallback(async () => { if (!accessToken) { @@ -289,7 +416,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { setSummary(res); if (settings?.data && Array.isArray(settings.data)) { const row = settings.data.find( - (r: { field_name: string; field_value: unknown }) => r.field_name === "mcp_required_fields", + (r: { field_name: string; field_value: unknown }) => r.field_name === SETTINGS_KEY, ); if (row && Array.isArray(row.field_value)) { setRequiredFields(row.field_value as string[]); @@ -306,6 +433,19 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { fetchData(); }, [fetchData]); + const handleSaveRules = async () => { + if (!accessToken) return; + setIsSavingRules(true); + try { + await updateConfigFieldSetting(accessToken, SETTINGS_KEY, requiredFields); + NotificationsManager.success("Submission rules saved"); + } catch { + NotificationsManager.fromBackend("Failed to save submission rules"); + } finally { + setIsSavingRules(false); + } + }; + const filtered = summary.items.filter((s) => { if (statusFilter !== "all" && s.approval_status !== statusFilter) return false; if (search.trim()) { @@ -343,6 +483,14 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { return (
+ {/* Submission Rules panel */} + +
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index fff6b007ce5..856bb824f68 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -19,7 +19,6 @@ import MCPSemanticFilterSettings from "../Settings/AdminSettings/MCPSemanticFilt import MCPNetworkSettings from "./MCPNetworkSettings"; import MCPDiscovery from "./mcp_discovery"; import { ByokCredentialModal } from "./ByokCredentialModal"; -import MCPStandardsSettings from "./MCPStandardsSettings"; const { Text: AntdText, Title: AntdTitle } = Typography; const EDIT_OAUTH_UI_STATE_KEY = "litellm-mcp-oauth-edit-state"; @@ -345,7 +344,6 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) Semantic Filter Network Settings {isAdminRole(userRole) && Team MCPs } - {isAdminRole(userRole) && Standards}
@@ -434,11 +432,6 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) )} - {isAdminRole(userRole) && ( - - - - )}