From 9acb0636fca12ca74af2d4b98d86a3e129cc5a65 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 9 Mar 2026 16:22:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20add=20MCP=20Standards=20=E2=80=94?= =?UTF-8?q?=20required=20fields=20config=20+=20CI-style=20checks=20on=20su?= =?UTF-8?q?bmissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Standards" tab (admin-only) to MCP Servers where admins define which server fields are required for a submission to pass. Each submission card in Team MCPs then shows a green ✓ or red ✗ for each required field, with a summary "N/M checks" badge in the header — like GitHub CI status rows. Also adds a `source_url` field (GitHub / Source URL) to the MCP server schema so non-admins can link to the source repo when submitting a server. - schema.prisma: add `source_url String?` to LiteLLM_MCPServerTable - migration: 20260309000001_add_mcp_source_url - _types.py: source_url on NewMCPServerRequest, UpdateMCPServerRequest, LiteLLM_MCPServerTable - types.tsx: source_url on MCPServer interface - create_mcp_server.tsx: GitHub/Source URL form field - MCPStandardsSettings.tsx: new — toggle which fields are required (stored in general settings as mcp_required_fields) - mcp_servers.tsx: Standards tab (admin-only) - MCPSubmissionsTab.tsx: load required fields + CI-style check pills on each card --- .../migration.sql | 3 + litellm/proxy/_types.py | 3 + litellm/proxy/schema.prisma | 1 + .../mcp_tools/MCPStandardsSettings.tsx | 161 ++++++++++++++++++ .../mcp_tools/MCPSubmissionsTab.tsx | 56 +++++- .../mcp_tools/create_mcp_server.tsx | 10 ++ .../src/components/mcp_tools/mcp_servers.tsx | 7 + .../src/components/mcp_tools/types.tsx | 3 + 8 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260309000001_add_mcp_source_url/migration.sql create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260309000001_add_mcp_source_url/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260309000001_add_mcp_source_url/migration.sql new file mode 100644 index 00000000000..dc468b82061 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260309000001_add_mcp_source_url/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable: Add source_url field to LiteLLM_MCPServerTable for GitHub/docs link +ALTER TABLE "LiteLLM_MCPServerTable" + ADD COLUMN IF NOT EXISTS "source_url" TEXT; diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index fe407456077..97c8b20bcad 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1123,6 +1123,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): is_byok: bool = False byok_description: List[str] = Field(default_factory=list) byok_api_key_help_url: Optional[str] = None + source_url: Optional[str] = None # BYOM submission fields (set by endpoint, not by caller) approval_status: Optional[str] = None submitted_by: Optional[str] = None @@ -1186,6 +1187,7 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): is_byok: bool = False byok_description: List[str] = Field(default_factory=list) byok_api_key_help_url: Optional[str] = None + source_url: Optional[str] = None @model_validator(mode="before") @classmethod @@ -1249,6 +1251,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): byok_description: List[str] = Field(default_factory=list) byok_api_key_help_url: Optional[str] = None has_user_credential: Optional[bool] = None + source_url: Optional[str] = None # BYOM submission fields approval_status: Optional[str] = Field( default="active", diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 36fba408b35..721c3e404d2 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -315,6 +315,7 @@ model LiteLLM_MCPServerTable { is_byok Boolean @default(false) byok_description String[] @default([]) byok_api_key_help_url String? + source_url String? // BYOM submission lifecycle approval_status String? @default("active") submitted_by String? diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx new file mode 100644 index 00000000000..56d8e16ea48 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx @@ -0,0 +1,161 @@ +"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; + description: string; + check: (server: MCPServer) => boolean; +} + +export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = [ + { + key: "description", + label: "Description", + description: "Server must have a non-empty description.", + check: (s) => !!s.description?.trim(), + }, + { + key: "source_url", + label: "GitHub / Source URL", + description: "Server must have a link to the 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(), + }, + { + 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(), + }, +]; + +const SETTINGS_KEY = "mcp_required_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}
+
+ +
+ ); + })} +
+ )} + +
+ +
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx index f620a159a9c..4a639d74f95 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx @@ -12,8 +12,10 @@ import { fetchMCPSubmissions, approveMCPServer, rejectMCPServer, + getGeneralSettingsCall, } from "@/components/networking"; import { MCPServer, MCPSubmissionsSummary } from "./types"; +import { MCP_REQUIRED_FIELD_DEFS } from "./MCPStandardsSettings"; import NotificationsManager from "@/components/molecules/notifications_manager"; type MCPStatus = "active" | "pending_review" | "rejected"; @@ -139,12 +141,21 @@ type MCPServerCardProps = { server: MCPServer; onApprove: () => void; onReject: () => void; + requiredFields: string[]; }; -function MCPServerCard({ server, onApprove, onReject }: MCPServerCardProps) { +function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServerCardProps) { const approvalStatus = (server.approval_status ?? "active") as MCPStatus; const statusCfg = STATUS_CONFIG[approvalStatus] ?? STATUS_CONFIG["active"]; + const checks = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key)).map((f) => ({ + key: f.key, + label: f.label, + passed: f.check(server), + })); + const passCount = checks.filter((c) => c.passed).length; + const allPassed = checks.length > 0 && passCount === checks.length; + return (
@@ -156,6 +167,15 @@ function MCPServerCard({ server, onApprove, onReject }: MCPServerCardProps) { {statusCfg.label} + {checks.length > 0 && ( + + {passCount}/{checks.length} checks + + )}

{server.alias ?? server.server_name ?? server.server_id} @@ -188,6 +208,25 @@ function MCPServerCard({ server, onApprove, onReject }: MCPServerCardProps) { Rejection reason: {server.review_notes}

)} + {checks.length > 0 && ( +
+ {checks.map((c) => ( + + {c.passed ? ( + + ) : ( + + )} + {c.label} + + ))} +
+ )}

{approvalStatus === "pending_review" && (
@@ -233,6 +272,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { } | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [requiredFields, setRequiredFields] = useState([]); const fetchData = useCallback(async () => { if (!accessToken) { @@ -242,8 +282,19 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { setIsLoading(true); setError(null); try { - const res: MCPSubmissionsSummary = await fetchMCPSubmissions(accessToken); + const [res, settings] = await Promise.all([ + fetchMCPSubmissions(accessToken), + getGeneralSettingsCall(accessToken).catch(() => null), + ]); 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", + ); + if (row && Array.isArray(row.field_value)) { + setRequiredFields(row.field_value as string[]); + } + } } catch (err) { setError(err instanceof Error ? err.message : "Failed to load submissions"); } finally { @@ -340,6 +391,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) { setConfirmAction({ serverId: server.server_id, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 0e5fcb16818..ae57b10b21e 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -574,6 +574,16 @@ const CreateMCPServer: React.FC = ({ /> + GitHub / Source URL} + name="source_url" + > + + + Transport Type} name="transport" 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 856bb824f68..fff6b007ce5 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -19,6 +19,7 @@ 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"; @@ -344,6 +345,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) Semantic Filter Network Settings {isAdminRole(userRole) && Team MCPs } + {isAdminRole(userRole) && Standards}
@@ -432,6 +434,11 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) )} + {isAdminRole(userRole) && ( + + + + )} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 1c0db246b0b..cea535caa4e 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -185,6 +185,9 @@ export interface MCPServer { byok_api_key_help_url?: string | null; has_user_credential?: boolean | null; + /** GitHub / source repository URL */ + source_url?: string | null; + /** BYOM (Bring Your Own MCP) submission fields */ approval_status?: "active" | "pending_review" | "rejected" | null; submitted_by?: string | null;