refactor(mcp): move submission rules into Team MCPs tab, grouped free-form UI

Folds the Standards tab into Team MCPs. Submission Rules panel now lives at the
top of the Team MCPs tab — collapsible, shows active rules as chips when closed,
expands to a grouped checkbox editor (Documentation / Source / Connection /
Security). Removes the separate Standards tab from the nav.

MCPStandardsSettings.tsx is now constants-only (FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS,
SETTINGS_KEY) — the UI lives in MCPSubmissionsTab.
This commit is contained in:
Ishaan Jaffer 2026-03-09 16:29:49 -07:00
parent 9acb0636fc
commit ab657fc242
3 changed files with 200 additions and 148 deletions

View file

@ -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<string[]>([]);
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 (
<div className="p-6 max-w-2xl">
<div className="mb-6">
<h2 className="text-base font-semibold text-gray-900">MCP Submission Standards</h2>
<p className="text-sm text-gray-500 mt-1">
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.
</p>
</div>
{isLoading ? (
<div className="text-sm text-gray-400">Loading…</div>
) : (
<div className="space-y-3">
{MCP_REQUIRED_FIELD_DEFS.map((field) => {
const enabled = requiredFields.includes(field.key);
return (
<div
key={field.key}
className={`flex items-center justify-between px-4 py-3 rounded-lg border transition-colors ${
enabled ? "border-blue-200 bg-blue-50" : "border-gray-200 bg-white"
}`}
>
<div className="flex-1 min-w-0 mr-4">
<div className="text-sm font-medium text-gray-900">{field.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{field.description}</div>
</div>
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => toggleField(field.key)}
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
enabled ? "bg-blue-500" : "bg-gray-200"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
enabled ? "translate-x-4" : "translate-x-0"
}`}
/>
</button>
</div>
);
})}
</div>
)}
<div className="mt-6">
<button
type="button"
disabled={isSaving || isLoading}
onClick={handleSave}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors"
>
{isSaving ? "Saving…" : "Save Standards"}
</button>
</div>
</div>
);
}
export const SETTINGS_KEY = "mcp_required_fields";

View file

@ -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<void>;
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 (
<div className="mb-5 border border-gray-200 rounded-lg bg-white overflow-hidden">
{/* Header — always visible */}
<div
className="flex items-center justify-between px-4 py-3 cursor-pointer select-none"
onClick={() => setExpanded((v) => !v)}
>
<div className="flex items-center gap-2">
<SettingsIcon className="h-4 w-4 text-gray-400" />
<span className="text-sm font-semibold text-gray-800">Submission Rules</span>
{activeLabels.length > 0 ? (
<span className="text-xs text-gray-500">
({activeLabels.length} required field{activeLabels.length !== 1 ? "s" : ""})
</span>
) : (
<span className="text-xs text-gray-400 italic">no rules set</span>
)}
</div>
<div className="flex items-center gap-3">
{/* Active rule chips — collapsed view */}
{!expanded && activeLabels.length > 0 && (
<div className="flex flex-wrap gap-1.5 max-w-md">
{activeLabels.map((f) => (
<span
key={f.key}
className="inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full"
>
<CheckIcon className="h-3 w-3" />
{f.label}
</span>
))}
</div>
)}
{expanded ? (
<ChevronUpIcon className="h-4 w-4 text-gray-400" />
) : (
<ChevronDownIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
{/* Expanded editor */}
{expanded && (
<div className="border-t border-gray-100 px-4 pt-4 pb-4">
<p className="text-xs text-gray-500 mb-4">
Select which fields must be filled in before a submission is considered compliant.
LiteLLM will show ✓ / ✗ for each rule on every submission card below.
</p>
<div className="grid grid-cols-2 gap-x-8 gap-y-5">
{FIELD_GROUPS.map((group) => (
<div key={group.label}>
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
{group.label}
</div>
<div className="space-y-2">
{group.fields.map((field) => {
const active = requiredFields.includes(field.key);
return (
<label
key={field.key}
className="flex items-start gap-2.5 cursor-pointer group"
>
<input
type="checkbox"
checked={active}
onChange={() => toggle(field.key)}
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
/>
<div>
<div className="text-sm font-medium text-gray-800 group-hover:text-blue-700 transition-colors">
{field.label}
</div>
<div className="text-xs text-gray-400">{field.description}</div>
</div>
</label>
);
})}
</div>
</div>
))}
</div>
<div className="mt-5 flex items-center gap-3">
<button
type="button"
disabled={isSaving}
onClick={async () => {
await onSave();
setExpanded(false);
}}
className="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors"
>
{isSaving ? "Saving…" : "Save Rules"}
</button>
<button
type="button"
onClick={() => setExpanded(false)}
className="px-4 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
>
Cancel
</button>
</div>
</div>
)}
</div>
);
}
type MCPServerCardProps = {
server: MCPServer;
onApprove: () => void;
@ -273,6 +399,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [requiredFields, setRequiredFields] = useState<string[]>([]);
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 (
<div className="p-6">
{/* Submission Rules panel */}
<SubmissionRulesPanel
requiredFields={requiredFields}
onChange={setRequiredFields}
onSave={handleSaveRules}
isSaving={isSavingRules}
/>
<div className="grid grid-cols-4 gap-4 mb-6">
<StatCard label="Total Submitted" value={summary.total} color="text-gray-900" />
<StatCard label="Pending Review" value={summary.pending_review} color="text-yellow-600" />

View file

@ -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<MCPServerProps> = ({ accessToken, userRole, userID })
<Tab>Semantic Filter</Tab>
<Tab>Network Settings</Tab>
{isAdminRole(userRole) && <Tab><span className="flex items-center gap-2">Team MCPs <NewBadge /></span></Tab>}
{isAdminRole(userRole) && <Tab>Standards</Tab>}
</div>
</TabList>
<TabPanels>
@ -434,11 +432,6 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
<MCPSubmissionsTab accessToken={accessToken} />
</TabPanel>
)}
{isAdminRole(userRole) && (
<TabPanel>
<MCPStandardsSettings accessToken={accessToken} />
</TabPanel>
)}
</TabPanels>
</TabGroup>