From 393a9472e734a209e95d2783df903160c4b1133f Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 18 Feb 2026 23:36:07 -0800 Subject: [PATCH] feat: Add MCP server approval workflow UI (mock) This commit implements a mock/demo version of the MCP server approval workflow UI to demonstrate the admin approval process. Changes: - Created approvals page with pending/approved/rejected tabs - Added MCPApprovalTable component with view/approve/reject actions - Added ApprovalDetailModal for viewing full request details - Created mock data with 10 sample MCP approval requests - Updated navigation to add "Approvals" menu item with badge count Features: - Admin-only access control - Tab-based view of requests by status - Approve/reject actions with reason capture - Detailed view of all request metadata - Badge showing pending request count in navigation Tech stack: - Next.js App Router (dashboard route) - Ant Design components (Table, Modal, Tabs, Badge) - TypeScript with proper type definitions - Client-side state management (mock data) Note: This is a prototype using mock data. Backend integration and database schema will be added in future PRs. Co-Authored-By: Claude Opus 4.6 --- .../src/app/(dashboard)/approvals/page.tsx | 149 +++++++++++ .../approvals/ApprovalDetailModal.tsx | 152 +++++++++++ .../components/approvals/MCPApprovalTable.tsx | 231 ++++++++++++++++ .../src/components/approvals/mockData.ts | 252 ++++++++++++++++++ .../src/components/leftnav.tsx | 14 +- 5 files changed, 797 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx create mode 100644 ui/litellm-dashboard/src/components/approvals/ApprovalDetailModal.tsx create mode 100644 ui/litellm-dashboard/src/components/approvals/MCPApprovalTable.tsx create mode 100644 ui/litellm-dashboard/src/components/approvals/mockData.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx new file mode 100644 index 00000000000..ef48fa1677d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, Tabs, Badge, message } from "antd"; +import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined } from "@ant-design/icons"; +import MCPApprovalTable from "@/components/approvals/MCPApprovalTable"; +import { isAdminRole } from "@/components/utils/roles"; +import { mockMCPApprovalRequests, MCPApprovalRequest } from "@/components/approvals/mockData"; + +export default function ApprovalsPage() { + const [userRole, setUserRole] = useState(null); + const [approvalRequests, setApprovalRequests] = useState(mockMCPApprovalRequests); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Get user role from localStorage/session + const role = localStorage.getItem("user_role") || null; + setUserRole(role); + setLoading(false); + }, []); + + // Check if user is admin + if (loading) { + return ( +
+ Loading... +
+ ); + } + + if (!isAdminRole(userRole)) { + return ( +
+ +
+ +

Access Denied

+

Admin role required to access this page.

+
+
+
+ ); + } + + const pendingCount = approvalRequests.filter(r => r.status === "pending").length; + const approvedCount = approvalRequests.filter(r => r.status === "approved").length; + const rejectedCount = approvalRequests.filter(r => r.status === "rejected").length; + + const handleApprove = (approvalId: string) => { + setApprovalRequests(prev => + prev.map(req => + req.approval_id === approvalId + ? { + ...req, + status: "approved" as const, + reviewed_at: new Date().toISOString(), + reviewed_by: "current-admin" + } + : req + ) + ); + message.success("MCP server request approved successfully"); + }; + + const handleReject = (approvalId: string, reason: string) => { + setApprovalRequests(prev => + prev.map(req => + req.approval_id === approvalId + ? { + ...req, + status: "rejected" as const, + rejection_reason: reason, + reviewed_at: new Date().toISOString(), + reviewed_by: "current-admin" + } + : req + ) + ); + message.success("MCP server request rejected"); + }; + + const tabItems = [ + { + key: "pending", + label: ( + + Pending{" "} + {pendingCount > 0 && } + + ), + children: ( + r.status === "pending")} + onApprove={handleApprove} + onReject={handleReject} + /> + ), + }, + { + key: "approved", + label: ( + + Approved{" "} + {approvedCount > 0 && } + + ), + children: ( + r.status === "approved")} + onApprove={handleApprove} + onReject={handleReject} + readOnly={true} + /> + ), + }, + { + key: "rejected", + label: ( + + Rejected{" "} + {rejectedCount > 0 && } + + ), + children: ( + r.status === "rejected")} + onApprove={handleApprove} + onReject={handleReject} + readOnly={true} + /> + ), + }, + ]; + + return ( +
+ + {pendingCount} pending approval{pendingCount !== 1 ? "s" : ""} +
+ } + > + + + + ); +} diff --git a/ui/litellm-dashboard/src/components/approvals/ApprovalDetailModal.tsx b/ui/litellm-dashboard/src/components/approvals/ApprovalDetailModal.tsx new file mode 100644 index 00000000000..1c16735b2d6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/approvals/ApprovalDetailModal.tsx @@ -0,0 +1,152 @@ +import { Modal, Descriptions, Tag, Button, Space, Divider } from "antd"; +import { MCPApprovalRequest } from "./mockData"; + +interface ApprovalDetailModalProps { + request: MCPApprovalRequest; + visible: boolean; + onClose: () => void; + onApprove?: (approvalId: string) => void; + onReject?: (approvalId: string) => void; +} + +export default function ApprovalDetailModal({ + request, + visible, + onClose, + onApprove, + onReject +}: ApprovalDetailModalProps) { + const getStatusColor = (status: string) => { + switch (status) { + case "pending": + return "gold"; + case "approved": + return "green"; + case "rejected": + return "red"; + default: + return "default"; + } + }; + + return ( + + + {onApprove && request.status === "pending" && ( + <> + + + + )} + + } + > + + + + {request.status.toUpperCase()} + + + + {request.server_name} + {request.alias || "-"} + {request.description || "-"} + {request.url} + {request.transport} + {request.auth_type || "None"} + + + {request.mcp_access_groups.length > 0 ? ( + request.mcp_access_groups.map(group => ( + + {group} + + )) + ) : ( + "-" + )} + + + + {request.allowed_tools.length > 0 ? ( +
+ {request.allowed_tools.map(tool => ( + + {tool} + + ))} +
+ ) : ( + "-" + )} +
+
+ + Requester Information + + + {request.requester_email} + {request.team_name || "-"} + + {new Date(request.created_at).toLocaleString()} + + + + {request.request_metadata && ( + <> + Request Metadata + + {request.request_metadata.business_justification && ( + + {request.request_metadata.business_justification} + + )} + {request.request_metadata.expected_usage && ( + + {request.request_metadata.expected_usage} + + )} + + + )} + + {request.status !== "pending" && ( + <> + Review Information + + + {request.reviewed_at ? new Date(request.reviewed_at).toLocaleString() : "-"} + + {request.rejection_reason && ( + +
{request.rejection_reason}
+
+ )} +
+ + )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/approvals/MCPApprovalTable.tsx b/ui/litellm-dashboard/src/components/approvals/MCPApprovalTable.tsx new file mode 100644 index 00000000000..896ddfa71db --- /dev/null +++ b/ui/litellm-dashboard/src/components/approvals/MCPApprovalTable.tsx @@ -0,0 +1,231 @@ +import { useState } from "react"; +import { Table, Button, Space, Tag, Tooltip, Modal, Input, Badge } from "antd"; +import { EyeOutlined, CheckOutlined, CloseOutlined } from "@ant-design/icons"; +import type { ColumnsType } from "antd/es/table"; +import { MCPApprovalRequest } from "./mockData"; +import ApprovalDetailModal from "./ApprovalDetailModal"; + +interface MCPApprovalTableProps { + requests: MCPApprovalRequest[]; + onApprove: (approvalId: string) => void; + onReject: (approvalId: string, reason: string) => void; + readOnly?: boolean; +} + +export default function MCPApprovalTable({ + requests, + onApprove, + onReject, + readOnly = false +}: MCPApprovalTableProps) { + const [selectedRequest, setSelectedRequest] = useState(null); + const [showDetailModal, setShowDetailModal] = useState(false); + const [showRejectModal, setShowRejectModal] = useState(false); + const [rejectReason, setRejectReason] = useState(""); + const [rejectingId, setRejectingId] = useState(null); + + const handleViewDetails = (record: MCPApprovalRequest) => { + setSelectedRequest(record); + setShowDetailModal(true); + }; + + const handleApproveClick = (approvalId: string) => { + onApprove(approvalId); + }; + + const handleRejectClick = (approvalId: string) => { + setRejectingId(approvalId); + setShowRejectModal(true); + }; + + const handleRejectConfirm = () => { + if (rejectingId && rejectReason.trim()) { + onReject(rejectingId, rejectReason); + setShowRejectModal(false); + setRejectReason(""); + setRejectingId(null); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "pending": + return "gold"; + case "approved": + return "green"; + case "rejected": + return "red"; + default: + return "default"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "pending": + return "Pending"; + case "approved": + return "Approved"; + case "rejected": + return "Rejected"; + default: + return status; + } + }; + + const columns: ColumnsType = [ + { + title: "Status", + dataIndex: "status", + key: "status", + width: 120, + render: (status: string) => ( + {getStatusText(status)} + ) + }, + { + title: "Server Name", + dataIndex: "server_name", + key: "server_name", + width: 200, + render: (text: string, record: MCPApprovalRequest) => ( +
+
{text}
+ {record.alias && ( +
Alias: {record.alias}
+ )} +
+ ) + }, + { + title: "Description", + dataIndex: "description", + key: "description", + ellipsis: true, + render: (text: string) => ( + + {text || "-"} + + ) + }, + { + title: "Requested By", + key: "requester", + width: 200, + render: (record: MCPApprovalRequest) => ( +
+
{record.requester_email}
+ {record.team_name && ( + + )} +
+ ) + }, + { + title: "Requested Date", + dataIndex: "created_at", + key: "created_at", + width: 150, + render: (date: string) => new Date(date).toLocaleDateString() + }, + { + title: "Access Groups", + dataIndex: "mcp_access_groups", + key: "mcp_access_groups", + width: 150, + render: (groups: string[]) => ( + + + + ) + }, + { + title: "Actions", + key: "actions", + width: 200, + fixed: "right", + render: (record: MCPApprovalRequest) => ( + + + {!readOnly && record.status === "pending" && ( + <> + + + + )} + + ) + } + ]; + + return ( + <> + + + {selectedRequest && ( + setShowDetailModal(false)} + onApprove={!readOnly ? handleApproveClick : undefined} + onReject={!readOnly ? handleRejectClick : undefined} + /> + )} + + { + setShowRejectModal(false); + setRejectReason(""); + }} + okText="Confirm Rejection" + okButtonProps={{ danger: true, disabled: !rejectReason.trim() }} + > +
+ Rejection Reason * +
+ setRejectReason(e.target.value)} + required + /> +
+ The requester will be notified with this reason. +
+
+ + ); +} diff --git a/ui/litellm-dashboard/src/components/approvals/mockData.ts b/ui/litellm-dashboard/src/components/approvals/mockData.ts new file mode 100644 index 00000000000..9b67be554a5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/approvals/mockData.ts @@ -0,0 +1,252 @@ +// Mock data for MCP approval requests +// This file contains sample data for demonstrating the approval workflow + +export interface MCPApprovalRequest { + approval_id: string; + server_name: string; + alias?: string; + description?: string; + url: string; + transport: string; + auth_type?: string; + mcp_access_groups: string[]; + allowed_tools: string[]; + status: 'pending' | 'approved' | 'rejected'; + requested_by: string; + requested_by_team_id?: string; + team_name?: string; + requester_email: string; + created_at: string; + reviewed_by?: string; + reviewed_at?: string; + rejection_reason?: string; + request_metadata?: { + business_justification?: string; + expected_usage?: string; + }; +} + +export const mockMCPApprovalRequests: MCPApprovalRequest[] = [ + { + approval_id: "req-001", + server_name: "team-engineering-mcp", + alias: "eng-mcp", + description: "Engineering team's MCP server for code analysis and automated review", + url: "https://mcp.engineering.example.com", + transport: "sse", + auth_type: "bearer", + mcp_access_groups: ["engineering", "backend-team"], + allowed_tools: ["code-review", "lint-check", "test-runner", "coverage-analysis"], + status: "pending", + requested_by: "user-123", + requested_by_team_id: "team-eng", + team_name: "Engineering Team", + requester_email: "john.smith@example.com", + created_at: "2026-02-15T10:30:00Z", + request_metadata: { + business_justification: "Need automated code review for PR workflows and continuous integration. Will help reduce manual code review time by ~40%.", + expected_usage: "~500 requests per day across 50 engineers" + } + }, + { + approval_id: "req-002", + server_name: "data-science-ml-mcp", + alias: "ds-ml", + description: "Data Science team's MCP server for ML model training and evaluation", + url: "https://mcp.datascience.example.com", + transport: "sse", + auth_type: "oauth2", + mcp_access_groups: ["data-science", "ml-team"], + allowed_tools: ["train-model", "evaluate-model", "feature-engineering", "data-viz"], + status: "pending", + requested_by: "user-456", + requested_by_team_id: "team-ds", + team_name: "Data Science Team", + requester_email: "sarah.jones@example.com", + created_at: "2026-02-16T14:20:00Z", + request_metadata: { + business_justification: "Required for automated ML pipeline. Will enable faster model iteration and A/B testing.", + expected_usage: "~200 training jobs per week, ~1000 evaluation requests per day" + } + }, + { + approval_id: "req-003", + server_name: "security-audit-mcp", + alias: "sec-audit", + description: "Security team's MCP server for vulnerability scanning and compliance checks", + url: "https://mcp.security.example.com", + transport: "sse", + auth_type: "bearer", + mcp_access_groups: ["security", "compliance-team"], + allowed_tools: ["vulnerability-scan", "dependency-check", "secrets-detection", "compliance-audit"], + status: "pending", + requested_by: "user-789", + requested_by_team_id: "team-sec", + team_name: "Security Team", + requester_email: "mike.chen@example.com", + created_at: "2026-02-17T09:15:00Z", + request_metadata: { + business_justification: "Critical for SOC 2 compliance. Automated security scanning for all production deployments.", + expected_usage: "~100 scans per day, triggered on every deployment" + } + }, + { + approval_id: "req-004", + server_name: "devops-infrastructure-mcp", + alias: "devops-infra", + description: "DevOps team's MCP server for infrastructure provisioning and monitoring", + url: "https://mcp.devops.example.com", + transport: "stdio", + auth_type: "api_key", + mcp_access_groups: ["devops", "sre-team"], + allowed_tools: ["provision-server", "deploy-service", "health-check", "log-analysis"], + status: "approved", + requested_by: "user-111", + requested_by_team_id: "team-devops", + team_name: "DevOps Team", + requester_email: "alex.rodriguez@example.com", + created_at: "2026-02-10T11:00:00Z", + reviewed_by: "admin-001", + reviewed_at: "2026-02-11T15:30:00Z", + request_metadata: { + business_justification: "Essential for automated infrastructure management. Reduces manual toil and improves reliability.", + expected_usage: "~300 infrastructure operations per day" + } + }, + { + approval_id: "req-005", + server_name: "product-analytics-mcp", + alias: "prod-analytics", + description: "Product team's MCP server for user analytics and metrics tracking", + url: "https://mcp.product.example.com", + transport: "sse", + auth_type: "bearer", + mcp_access_groups: ["product", "analytics-team"], + allowed_tools: ["track-event", "query-metrics", "generate-report", "create-dashboard"], + status: "approved", + requested_by: "user-222", + requested_by_team_id: "team-product", + team_name: "Product Team", + requester_email: "lisa.thompson@example.com", + created_at: "2026-02-12T13:45:00Z", + reviewed_by: "admin-002", + reviewed_at: "2026-02-13T10:00:00Z", + request_metadata: { + business_justification: "Required for product analytics and user behavior tracking. Enables data-driven product decisions.", + expected_usage: "~1000 events per minute, ~50 report generations per day" + } + }, + { + approval_id: "req-006", + server_name: "marketing-automation-mcp", + alias: "marketing-auto", + description: "Marketing team's MCP server for campaign automation and email marketing", + url: "https://mcp.marketing.example.com", + transport: "sse", + auth_type: "oauth2", + mcp_access_groups: ["marketing"], + allowed_tools: ["send-campaign", "segment-users", "track-conversion", "ab-test"], + status: "rejected", + requested_by: "user-333", + requested_by_team_id: "team-marketing", + team_name: "Marketing Team", + requester_email: "david.kim@example.com", + created_at: "2026-02-14T16:00:00Z", + reviewed_by: "admin-001", + reviewed_at: "2026-02-15T09:20:00Z", + rejection_reason: "Security concerns: OAuth2 configuration incomplete. Missing required scopes and callback URLs. Please resubmit with proper authentication setup.", + request_metadata: { + business_justification: "Need automated marketing campaigns for user engagement and retention.", + expected_usage: "~10 campaigns per week, ~100K emails per month" + } + }, + { + approval_id: "req-007", + server_name: "customer-support-mcp", + alias: "support", + description: "Customer Support team's MCP server for ticket management and chatbot", + url: "https://mcp.support.example.com", + transport: "sse", + auth_type: "bearer", + mcp_access_groups: ["support", "customer-success"], + allowed_tools: ["create-ticket", "update-ticket", "search-knowledge-base", "chatbot-response"], + status: "pending", + requested_by: "user-444", + requested_by_team_id: "team-support", + team_name: "Customer Support Team", + requester_email: "emma.wilson@example.com", + created_at: "2026-02-18T08:00:00Z", + request_metadata: { + business_justification: "Enhance customer support response times. Automated ticket routing and AI-powered responses.", + expected_usage: "~500 tickets per day, ~2000 chatbot interactions per day" + } + }, + { + approval_id: "req-008", + server_name: "finance-reporting-mcp", + alias: "fin-reports", + description: "Finance team's MCP server for financial reporting and analysis", + url: "https://mcp.finance.example.com", + transport: "stdio", + auth_type: "mtls", + mcp_access_groups: ["finance", "accounting"], + allowed_tools: ["generate-financial-report", "calculate-metrics", "budget-analysis", "forecast"], + status: "rejected", + requested_by: "user-555", + requested_by_team_id: "team-finance", + team_name: "Finance Team", + requester_email: "robert.anderson@example.com", + created_at: "2026-02-13T10:30:00Z", + reviewed_by: "admin-002", + reviewed_at: "2026-02-14T11:45:00Z", + rejection_reason: "Insufficient access controls. Finance data requires additional encryption and audit logging. Please add detailed access policies and data retention policies.", + request_metadata: { + business_justification: "Automate monthly financial reports and forecasting. Reduce manual work by 60%.", + expected_usage: "~50 reports per month, daily metric calculations" + } + }, + { + approval_id: "req-009", + server_name: "hr-recruitment-mcp", + alias: "hr-recruit", + description: "HR team's MCP server for recruitment automation and candidate screening", + url: "https://mcp.hr.example.com", + transport: "sse", + auth_type: "bearer", + mcp_access_groups: ["hr", "recruitment-team"], + allowed_tools: ["parse-resume", "screen-candidate", "schedule-interview", "generate-offer"], + status: "pending", + requested_by: "user-666", + requested_by_team_id: "team-hr", + team_name: "HR Team", + requester_email: "jessica.brown@example.com", + created_at: "2026-02-17T15:30:00Z", + request_metadata: { + business_justification: "Streamline recruitment process. Automated resume screening and interview scheduling to reduce time-to-hire.", + expected_usage: "~200 applications per week, ~50 interviews per month" + } + }, + { + approval_id: "req-010", + server_name: "legal-contract-mcp", + alias: "legal-docs", + description: "Legal team's MCP server for contract management and document review", + url: "https://mcp.legal.example.com", + transport: "sse", + auth_type: "mtls", + mcp_access_groups: ["legal"], + allowed_tools: ["review-contract", "extract-terms", "compliance-check", "redline-document"], + status: "approved", + requested_by: "user-777", + requested_by_team_id": "team-legal", + team_name: "Legal Team", + requester_email: "william.martinez@example.com", + created_at: "2026-02-11T09:00:00Z", + reviewed_by: "admin-001", + reviewed_at: "2026-02-12T14:15:00Z", + request_metadata: { + business_justification: "Automated contract review and compliance checking. Speeds up legal review process by 50%.", + expected_usage: "~30 contracts per week, ongoing compliance monitoring" + } + } +]; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 6233d2463fc..2e4915224b9 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -27,7 +27,7 @@ import { UserOutlined, } from "@ant-design/icons"; import type { MenuProps } from "antd"; -import { ConfigProvider, Layout, Menu } from "antd"; +import { Badge, ConfigProvider, Layout, Menu } from "antd"; import { useMemo } from "react"; import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles"; import NewBadge from "./common_components/NewBadge"; @@ -117,6 +117,18 @@ const menuGroups: MenuGroup[] = [ icon: , roles: all_admin_roles, }, + { + key: "approvals", + page: "approvals", + label: ( + + Approvals + + + ), + icon: , + roles: all_admin_roles, + }, { key: "tools", page: "tools",