From e3437795bb40a419836807072eee653f3812e8ee Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:29:25 +0530 Subject: [PATCH] Litellm mcp access group on UI (#12470) * added mcp tools on internal user and divide it by teams * add support for server api call * Added frontend for test key * added tools used output * fix ui for servers * All servers to personal * change columns format * revert ui logic * Added vertical align * fix mapped tests * fix lint * fix lint * remove extra file * fix ui test * comments fixes * change query type * change query type * mcp acces group init * add ability to change server display on ui through access groups * Mcp access group names UI (#12486) * Added ui changes to reflect mcp_access_groups * fix edit mcp page * change to string array (#12491) * change to string array * Remove print * add ability to change server display on ui through access groups * Litellm mcp access groups accesses (#12498) * added mcp access groups for keys and teams * added access groups above servers * fixed ruff * fixed mypy * revert couple changes --- .../litellm_proxy_extras/schema.prisma | 4 +- .../mcp_server/auth/user_api_key_auth_mcp.py | 142 ++++- litellm/proxy/_experimental/mcp_server/db.py | 36 +- .../out/model_hub_table/index.html | 1 + litellm/proxy/_types.py | 7 + .../mcp_management_endpoints.py | 40 ++ litellm/proxy/schema.prisma | 3 + schema.prisma | 26 +- .../common_components/PremiumMCPSelector.tsx | 4 +- .../src/components/create_key_button.tsx | 591 +++++++++--------- .../src/components/key_edit_view.tsx | 26 +- .../MCPServerSelector.tsx | 91 ++- .../mcp_tools/create_mcp_server.tsx | 50 +- .../mcp_tools/mcp_server_columns.tsx | 14 + .../components/mcp_tools/mcp_server_edit.tsx | 41 +- .../components/mcp_tools/mcp_server_view.tsx | 96 +-- .../src/components/mcp_tools/mcp_servers.tsx | 142 +++-- .../src/components/mcp_tools/types.tsx | 1 + .../src/components/networking.tsx | 32 + .../src/components/team/team_info.tsx | 41 +- ui/litellm-dashboard/src/components/teams.tsx | 43 +- 21 files changed, 951 insertions(+), 480 deletions(-) create mode 100644 litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 74fd7157fd..8556d4b9e3 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -155,7 +155,8 @@ model LiteLLM_UserTable { model LiteLLM_ObjectPermissionTable { object_permission_id String @id @default(uuid()) mcp_servers String[] @default([]) - vector_stores String[] @default([]) + vector_stores String[] @default([]) + mcp_access_groups String[] @default([]) teams LiteLLM_TeamTable[] verification_tokens LiteLLM_VerificationToken[] organizations LiteLLM_OrganizationTable[] @@ -176,6 +177,7 @@ model LiteLLM_MCPServerTable { updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? mcp_info Json? @default("{}") + mcp_access_groups String[] } // Generate Tokens for Proxy diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index cfe357709c..0ca2169591 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -220,7 +220,17 @@ class MCPRequestHandler: if key_object_permission is None: return [] - return key_object_permission.mcp_servers or [] + # Get direct MCP servers + direct_mcp_servers = key_object_permission.mcp_servers or [] + + # Get MCP servers from access groups + access_group_servers = await MCPRequestHandler._get_mcp_servers_from_access_groups( + key_object_permission.mcp_access_groups or [] + ) + + # Combine both lists + all_servers = direct_mcp_servers + access_group_servers + return list(set(all_servers)) @staticmethod async def _get_allowed_mcp_servers_for_team( @@ -257,4 +267,132 @@ class MCPRequestHandler: if object_permissions is None: return [] - return object_permissions.mcp_servers or [] \ No newline at end of file + # Get direct MCP servers + direct_mcp_servers = object_permissions.mcp_servers or [] + + # Get MCP servers from access groups + access_group_servers = await MCPRequestHandler._get_mcp_servers_from_access_groups( + object_permissions.mcp_access_groups or [] + ) + + # Combine both lists + all_servers = direct_mcp_servers + access_group_servers + return list(set(all_servers)) + + @staticmethod + async def _get_mcp_servers_from_access_groups( + access_groups: List[str] + ) -> List[str]: + """ + Resolve MCP access groups to server IDs by querying the MCP server table + """ + from litellm.proxy.proxy_server import prisma_client + + if not access_groups or prisma_client is None: + return [] + + try: + # Find all MCP servers that have any of the specified access groups + mcp_servers = await prisma_client.db.litellm_mcpservertable.find_many( + where={ + "mcp_access_groups": { + "hasSome": access_groups + } + } + ) + + # Extract server IDs + server_ids = [server.server_id for server in mcp_servers] + return server_ids + except Exception as e: + verbose_logger.debug(f"Error getting MCP servers from access groups: {e}") + return [] + + @staticmethod + async def get_mcp_access_groups( + user_api_key_auth: Optional[UserAPIKeyAuth] = None, + ) -> List[str]: + """ + Get list of MCP access groups for the given user/key based on permissions + """ + from typing import List + + access_groups: List[str] = [] + access_groups_for_key = ( + await MCPRequestHandler._get_mcp_access_groups_for_key(user_api_key_auth) + ) + access_groups_for_team = ( + await MCPRequestHandler._get_mcp_access_groups_for_team(user_api_key_auth) + ) + + ######################################################### + # If team has access groups, then key must have a subset of the team's access groups + ######################################################### + if len(access_groups_for_team) > 0: + for access_group in access_groups_for_key: + if access_group in access_groups_for_team: + access_groups.append(access_group) + else: + access_groups = access_groups_for_key + + return list(set(access_groups)) + + @staticmethod + async def _get_mcp_access_groups_for_key( + user_api_key_auth: Optional[UserAPIKeyAuth] = None, + ) -> List[str]: + from litellm.proxy.proxy_server import prisma_client + + if user_api_key_auth is None: + return [] + + if user_api_key_auth.object_permission_id is None: + return [] + + if prisma_client is None: + verbose_logger.debug("prisma_client is None") + return [] + + key_object_permission = ( + await prisma_client.db.litellm_objectpermissiontable.find_unique( + where={"object_permission_id": user_api_key_auth.object_permission_id}, + ) + ) + if key_object_permission is None: + return [] + + return key_object_permission.mcp_access_groups or [] + + @staticmethod + async def _get_mcp_access_groups_for_team( + user_api_key_auth: Optional[UserAPIKeyAuth] = None, + ) -> List[str]: + """ + Get MCP access groups for the team + """ + from litellm.proxy.proxy_server import prisma_client + + if user_api_key_auth is None: + return [] + + if user_api_key_auth.team_id is None: + return [] + + if prisma_client is None: + verbose_logger.debug("prisma_client is None") + return [] + + team_obj: Optional[LiteLLM_TeamTable] = ( + await prisma_client.db.litellm_teamtable.find_unique( + where={"team_id": user_api_key_auth.team_id}, + ) + ) + if team_obj is None: + verbose_logger.debug("team_obj is None") + return [] + + object_permissions = team_obj.object_permission + if object_permissions is None: + return [] + + return object_permissions.mcp_access_groups or [] \ No newline at end of file diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 583d3c9033..e9a033c3ee 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -219,18 +219,28 @@ async def create_mcp_server( if data.server_id is None: data.server_id = str(uuid.uuid4()) - # json dumps mcp_info + # Convert model to dict and handle JSON fields + data_dict = data.model_dump() + + # Handle mcp_info serialization mcp_info: Optional[str] = None if data.mcp_info is not None: mcp_info = safe_dumps(data.mcp_info) - del data.mcp_info + del data_dict["mcp_info"] + + # Handle mcp_access_groups - it's already a List[str], no need to serialize + mcp_access_groups: Optional[list] = None + if data.mcp_access_groups is not None: + mcp_access_groups = data.mcp_access_groups + del data_dict["mcp_access_groups"] mcp_server_record = await prisma_client.db.litellm_mcpservertable.create( data={ - **data.model_dump(), + **data_dict, "created_by": touched_by, "updated_by": touched_by, "mcp_info": mcp_info, + "mcp_access_groups": mcp_access_groups, } ) return mcp_server_record @@ -243,22 +253,32 @@ async def update_mcp_server( Update a new mcp server record in the db """ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps - - # json dumps mcp_info + + # Convert model to dict and handle JSON fields + data_dict = data.model_dump() + + # Handle mcp_info serialization mcp_info: Optional[str] = None if data.mcp_info is not None: mcp_info = safe_dumps(data.mcp_info) - del data.mcp_info + del data_dict["mcp_info"] + # Handle mcp_access_groups - it's already a List[str], no need to serialize + mcp_access_groups: Optional[list] = None + if data.mcp_access_groups is not None: + mcp_access_groups = data.mcp_access_groups + del data_dict["mcp_access_groups"] + mcp_server_record = await prisma_client.db.litellm_mcpservertable.update( where={ "server_id": data.server_id, }, data={ - **data.model_dump(), - "mcp_info": mcp_info, + **data_dict, "created_by": touched_by, "updated_by": touched_by, + "mcp_info": mcp_info, + "mcp_access_groups": mcp_access_groups, }, ) return mcp_server_record diff --git a/litellm/proxy/_experimental/out/model_hub_table/index.html b/litellm/proxy/_experimental/out/model_hub_table/index.html new file mode 100644 index 0000000000..492f567313 --- /dev/null +++ b/litellm/proxy/_experimental/out/model_hub_table/index.html @@ -0,0 +1 @@ +LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 22631e507a..edf64584f7 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -669,6 +669,7 @@ class ModelParams(LiteLLMPydanticObjectBase): class LiteLLM_ObjectPermissionBase(LiteLLMPydanticObjectBase): mcp_servers: Optional[List[str]] = None + mcp_access_groups: Optional[List[str]] = None vector_stores: Optional[List[str]] = None @@ -848,6 +849,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): auth_type: Optional[MCPAuthType] = None url: str mcp_info: Optional[MCPInfo] = None + mcp_access_groups: List[str] = Field(default_factory=list) + class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): @@ -859,6 +862,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): auth_type: Optional[MCPAuthType] = None url: str mcp_info: Optional[MCPInfo] = None + mcp_access_groups: List[str] = Field(default_factory=list) + class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): @@ -876,6 +881,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): updated_at: Optional[datetime] = None updated_by: Optional[str] = None teams: List[Dict[str, Optional[str]]] = Field(default_factory=list) + mcp_access_groups: List[str] = Field(default_factory=list) mcp_info: Optional[MCPInfo] = None @@ -1268,6 +1274,7 @@ class LiteLLM_ObjectPermissionTable(LiteLLMPydanticObjectBase): object_permission_id: str mcp_servers: Optional[List[str]] = [] + mcp_access_groups: Optional[List[str]] = [] vector_stores: Optional[List[str]] = [] diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 4eb302b40b..f31612d600 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -10,6 +10,8 @@ Endpoints here: - PUT `/v1/mcp/server` - Edits an existing mcp server. - DELETE `/v1/mcp/server/{server_id}` - Deletes the mcp server given `server_id`. - GET `/v1/mcp/tools - lists all the tools available for a key +- GET `/v1/mcp/access_groups` - lists all available MCP access groups + """ import importlib @@ -109,6 +111,43 @@ if MCP_AVAILABLE: return {"tools": tools} + @router.get( + "/access_groups", + tags=["mcp"], + dependencies=[Depends(user_api_key_auth)], + ) + async def get_mcp_access_groups( + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + ): + """ + Get all available MCP access groups from the database + """ + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "error": "Database not connected. Connect a database to your proxy" + }, + ) + + try: + # Get all MCP servers and extract their access groups + mcp_servers = await prisma_client.db.litellm_mcpservertable.find_many() + # Extract all unique access groups + access_groups = set() + for server in mcp_servers: + if server.mcp_access_groups: + access_groups.update(server.mcp_access_groups) + + # Convert to sorted list + access_groups_list = sorted(list(access_groups)) + + return {"access_groups": access_groups_list} + except Exception as e: + verbose_proxy_logger.debug(f"Error getting MCP access groups: {e}") + return {"access_groups": []} ## FastAPI Routes @router.get( @@ -221,6 +260,7 @@ if MCP_AVAILABLE: created_by=server.created_by, updated_at=server.updated_at, updated_by=server.updated_by, + mcp_access_groups=server.mcp_access_groups if server.mcp_access_groups is not None else [], mcp_info=server.mcp_info, teams=cast(List[Dict[str, str | None]], server_to_teams_map.get(server.server_id, [])) ) diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 74fd7157fd..3b2cf0af16 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -155,6 +155,7 @@ model LiteLLM_UserTable { model LiteLLM_ObjectPermissionTable { object_permission_id String @id @default(uuid()) mcp_servers String[] @default([]) + mcp_access_groups String[] @default([]) vector_stores String[] @default([]) teams LiteLLM_TeamTable[] verification_tokens LiteLLM_VerificationToken[] @@ -176,6 +177,8 @@ model LiteLLM_MCPServerTable { updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? mcp_info Json? @default("{}") + mcp_access_groups String[] + } // Generate Tokens for Proxy diff --git a/schema.prisma b/schema.prisma index 74fd7157fd..be1e84b2fe 100644 --- a/schema.prisma +++ b/schema.prisma @@ -155,6 +155,7 @@ model LiteLLM_UserTable { model LiteLLM_ObjectPermissionTable { object_permission_id String @id @default(uuid()) mcp_servers String[] @default([]) + mcp_access_groups String[] @default([]) vector_stores String[] @default([]) teams LiteLLM_TeamTable[] verification_tokens LiteLLM_VerificationToken[] @@ -164,18 +165,19 @@ model LiteLLM_ObjectPermissionTable { // Holds the MCP server configuration model LiteLLM_MCPServerTable { - server_id String @id @default(uuid()) - alias String? - description String? - url String - transport String @default("sse") - spec_version String @default("2025-03-26") - auth_type String? - created_at DateTime? @default(now()) @map("created_at") - created_by String? - updated_at DateTime? @default(now()) @updatedAt @map("updated_at") - updated_by String? - mcp_info Json? @default("{}") + server_id String @id @default(uuid()) + alias String? + description String? + url String + transport String @default("sse") + spec_version String @default("2025-03-26") + auth_type String? + created_at DateTime? @default(now()) @map("created_at") + created_by String? + updated_at DateTime? @default(now()) @updatedAt @map("updated_at") + updated_by String? + mcp_info Json? @default("{}") + mcp_access_groups String[] } // Generate Tokens for Proxy diff --git a/ui/litellm-dashboard/src/components/common_components/PremiumMCPSelector.tsx b/ui/litellm-dashboard/src/components/common_components/PremiumMCPSelector.tsx index f54d4d31df..6360470c84 100644 --- a/ui/litellm-dashboard/src/components/common_components/PremiumMCPSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PremiumMCPSelector.tsx @@ -3,8 +3,8 @@ import { Text } from "@tremor/react"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; interface PremiumMCPSelectorProps { - onChange: (values: string[]) => void; - value: string[]; + onChange: (value: { servers: string[]; accessGroups: string[] }) => void; + value: { servers: string[]; accessGroups: string[] }; accessToken: string; placeholder?: string; premiumUser?: boolean; diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index 06bba67097..f3e4601b19 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -33,6 +33,7 @@ import { getPossibleUserRoles, userFilterUICall, keyCreateServiceAccountCall, + fetchMCPAccessGroups, } from "./networking"; import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; import PremiumVectorStoreSelector from "./common_components/PremiumVectorStoreSelector"; @@ -178,6 +179,8 @@ const CreateKey: React.FC = ({ >({}); const [userOptions, setUserOptions] = useState([]); const [userSearchLoading, setUserSearchLoading] = useState(false); + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); + const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); const handleOk = () => { setIsModalVisible(false); @@ -199,6 +202,22 @@ const CreateKey: React.FC = ({ } }, [accessToken, userID, userRole]); + const fetchMcpAccessGroups = async () => { + try { + if (accessToken == null) { + return; + } + const groups = await fetchMCPAccessGroups(accessToken); + setMcpAccessGroups(groups); + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + } + }; + + useEffect(() => { + fetchMcpAccessGroups(); + }, [accessToken]); + useEffect(() => { const fetchGuardrails = async () => { @@ -309,6 +328,16 @@ const CreateKey: React.FC = ({ // Remove the original field as it's now part of object_permission delete formValues.allowed_mcp_server_ids; } + + // Transform allowed_mcp_access_groups into object_permission format + if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) { + if (!formValues.object_permission) { + formValues.object_permission = {}; + } + formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups; + // Remove the original field as it's now part of object_permission + delete formValues.allowed_mcp_access_groups; + } let response; if (keyOwner === "service_account") { response = await keyCreateServiceAccountCall(accessToken, formValues); @@ -584,290 +613,290 @@ const CreateKey: React.FC = ({ {/* Section 3: Optional Settings */} {!isFormDisabled && (
- - - Optional Settings - - - - Max Budget (USD){' '} - - - - - } - name="max_budget" - help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if ( - value && - team && - team.max_budget !== null && - value > team.max_budget - ) { - throw new Error( - `Budget cannot exceed team max budget: $${formatNumberWithCommas(team.max_budget, 4)}` - ); - } - }, - }, - ]} - > - - - - Reset Budget{' '} - - - - - } - name="budget_duration" - help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`} - > - form.setFieldValue('budget_duration', value)} /> - - - Tokens per minute Limit (TPM){' '} - - - - - } - name="tpm_limit" - help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if ( - value && - team && - team.tpm_limit !== null && - value > team.tpm_limit - ) { - throw new Error( - `TPM limit cannot exceed team TPM limit: ${team.tpm_limit}` - ); - } - }, - }, - ]} - > - - - - Requests per minute Limit (RPM){' '} - - - - - } - name="rpm_limit" - help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if ( - value && - team && - team.rpm_limit !== null && - value > team.rpm_limit - ) { - throw new Error( - `RPM limit cannot exceed team RPM limit: ${team.rpm_limit}` - ); - } - }, - }, - ]} - > - - - - Expire Key{' '} - - - - - } - name="duration" - className="mt-4" - > - - - - Guardrails{' '} - - e.stopPropagation()} // Prevent accordion from collapsing when clicking link - > - - - - - } - name="guardrails" - className="mt-4" - help="Select existing guardrails or enter new ones" - > - - - - - - Logging Settings - - -
- -
-
-
- - -
- - Advanced Settings - { if (!mcpAccessGroupsLoaded) { fetchMcpAccessGroups(); setMcpAccessGroupsLoaded(true); } }}> + + Optional Settings + + + - Learn more about advanced settings in our{' '} - - documentation - + Max Budget (USD){' '} + + + - }> - - -
-
- - { + if ( + value && + team && + team.max_budget !== null && + value > team.max_budget + ) { + throw new Error( + `Budget cannot exceed team max budget: $${formatNumberWithCommas(team.max_budget, 4)}` + ); + } + }, + }, + ]} + > + + + + Reset Budget{' '} + + + + + } + name="budget_duration" + help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`} + > + form.setFieldValue('budget_duration', value)} /> + + + Tokens per minute Limit (TPM){' '} + + + + + } + name="tpm_limit" + help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`} + rules={[ + { + validator: async (_, value) => { + if ( + value && + team && + team.tpm_limit !== null && + value > team.tpm_limit + ) { + throw new Error( + `TPM limit cannot exceed team TPM limit: ${team.tpm_limit}` + ); + } + }, + }, + ]} + > + + + + Requests per minute Limit (RPM){' '} + + + + + } + name="rpm_limit" + help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`} + rules={[ + { + validator: async (_, value) => { + if ( + value && + team && + team.rpm_limit !== null && + value > team.rpm_limit + ) { + throw new Error( + `RPM limit cannot exceed team RPM limit: ${team.rpm_limit}` + ); + } + }, + }, + ]} + > + + + + Expire Key{' '} + + + + + } + name="duration" + className="mt-4" + > + + + + Guardrails{' '} + + e.stopPropagation()} // Prevent accordion from collapsing when clicking link + > + + + + + } + name="guardrails" + className="mt-4" + help="Select existing guardrails or enter new ones" + > + + + + + + Logging Settings + + +
+ +
+
+
+ + +
+ + Advanced Settings + + Learn more about advanced settings in our{' '} + + documentation + + + }> + + +
+
+ + + +
+
+
+
)}
diff --git a/ui/litellm-dashboard/src/components/key_edit_view.tsx b/ui/litellm-dashboard/src/components/key_edit_view.tsx index a28b2a12a6..d3af198e0d 100644 --- a/ui/litellm-dashboard/src/components/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/key_edit_view.tsx @@ -9,6 +9,7 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; import MCPServerSelector from "./mcp_server_management/MCPServerSelector"; import EditLoggingSettings from "./team/EditLoggingSettings"; import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils"; +import { fetchMCPAccessGroups } from "./networking"; interface KeyEditViewProps { keyData: KeyResponse; @@ -54,6 +55,20 @@ export function KeyEditView({ const [userModels, setUserModels] = useState([]); const team = teams?.find(team => team.team_id === keyData.team_id); const [availableModels, setAvailableModels] = useState([]); + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); + const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); + + const fetchMcpAccessGroups = async () => { + if (!accessToken) return; + if (mcpAccessGroupsLoaded) return; + try { + const groups = await fetchMCPAccessGroups(accessToken); + setMcpAccessGroups(groups); + setMcpAccessGroupsLoaded(true); + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + } + }; useEffect(() => { const fetchModels = async () => { @@ -185,16 +200,15 @@ export function KeyEditView({ /> - + form.setFieldValue('mcp_servers', values)} - value={form.getFieldValue('mcp_servers')} - accessToken={accessToken || ""} - placeholder="Select MCP servers" + onChange={val => form.setFieldValue('mcp_servers_and_groups', val)} + value={form.getFieldValue('mcp_servers_and_groups')} + accessToken={accessToken || ''} + placeholder="Select MCP servers or access groups (optional)" /> - ({ - label: `${server.alias} (${server.server_id})`, - value: server.server_id, - title: server.description || server.alias, - }))} optionFilterProp="label" showSearch style={{ width: '100%' }} disabled={disabled} - /> + > + {options.map(opt => ( + + {opt.isAccessGroup && ( + + )} + {opt.label} + {opt.isAccessGroup && (Access Group)} + + ))} +
); }; 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 e2be2a6c62..c17b871b11 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 @@ -6,8 +6,10 @@ import { Select, message, Button as AntdButton, + Space, + Input, } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; import { Button, TextInput } from "@tremor/react"; import { createMCPServer } from "../networking"; import { MCPServer, MCPServerCostInfo } from "./types"; @@ -33,20 +35,26 @@ const CreateMCPServer: React.FC = ({ const [form] = Form.useForm(); const [isLoading, setIsLoading] = useState(false); const [costConfig, setCostConfig] = useState({}); + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [formValues, setFormValues] = useState>({}); const [tools, setTools] = useState([]); const handleCreate = async (formValues: Record) => { setIsLoading(true); try { + // Transform access groups into objects with name property + + const accessGroups = formValues.mcp_access_groups + // Prepare the payload with cost configuration const payload = { ...formValues, mcp_info: { server_name: formValues.alias || formValues.url, description: formValues.description, - mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null - } + mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null, + }, + mcp_access_groups: accessGroups }; console.log(`Payload: ${JSON.stringify(payload)}`); @@ -252,19 +260,37 @@ const CreateMCPServer: React.FC = ({ 2024-11-05 - - {/* Connection Status Section */} -
- + + MCP Access Groups + + + + + } + name="mcp_access_groups" + className="mb-4" + > + + + + MCP Access Groups + + + + + } + name="mcp_access_groups" + getValueFromEvent={value => value} + > + @@ -304,6 +291,27 @@ const MCPServers: React.FC = ({ ))} + Access Group: +
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index b1ab828e76..11837ad007 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -134,6 +134,7 @@ export interface MCPServer { updated_at: string; updated_by: string; teams?: Team[]; + mcp_access_groups?: string[]; } export interface MCPServerProps { diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 2aab6912c5..1438ff81c0 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -4837,6 +4837,38 @@ export const fetchMCPServers = async (accessToken: string) => { } }; +export const fetchMCPAccessGroups = async (accessToken: string) => { + try { + // Construct base URL + const url = proxyBaseUrl + ? `${proxyBaseUrl}/v1/mcp/access_groups` + : `/v1/mcp/access_groups`; + + console.log("Fetching MCP access groups from:", url); + + const response = await fetch(url, { + method: HTTP_REQUEST.GET, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Fetched MCP access groups:", data); + return data.access_groups || []; + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + throw error; + } +}; + export const createMCPServer = async ( accessToken: string, formValues: Record // Assuming formValues is an object diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 7334cfa1f1..4850086b13 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -40,6 +40,7 @@ import PremiumVectorStoreSelector from "../common_components/PremiumVectorStoreS import { formatNumberWithCommas } from "@/utils/dataUtils"; import EditLoggingSettings from "./EditLoggingSettings"; import LoggingSettingsView from "../logging_settings_view"; +import { fetchMCPAccessGroups } from "../networking"; export interface TeamMembership { user_id: string; @@ -126,6 +127,8 @@ const TeamInfoView: React.FC = ({ const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); const [selectedEditMember, setSelectedEditMember] = useState(null); const [isEditing, setIsEditing] = useState(false); + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); + const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); console.log("userModels in team info", userModels); @@ -149,6 +152,18 @@ const TeamInfoView: React.FC = ({ fetchTeamInfo(); }, [teamId, accessToken]); + const fetchMcpAccessGroups = async () => { + if (!accessToken) return; + if (mcpAccessGroupsLoaded) return; + try { + const groups = await fetchMCPAccessGroups(accessToken); + setMcpAccessGroups(groups); + setMcpAccessGroupsLoaded(true); + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + } + }; + const handleMemberCreate = async (values: any) => { try { if (accessToken == null) return; @@ -288,13 +303,11 @@ const TeamInfoView: React.FC = ({ } // Handle object_permission updates - if (values.vector_stores !== undefined || values.mcp_servers !== undefined) { - updateData.object_permission = { - ...teamData?.team_info.object_permission, - vector_stores: values.vector_stores || [], - mcp_servers: values.mcp_servers || [] - }; - } + const { servers, accessGroups } = values.mcp_servers_and_groups || { servers: [], accessGroups: [] }; + if (!updateData.object_permission) updateData.object_permission = {}; + updateData.object_permission.mcp_servers = servers; + updateData.object_permission.mcp_access_groups = accessGroups; + delete values.mcp_servers_and_groups; const response = await teamUpdateCall(accessToken, updateData); @@ -452,7 +465,9 @@ const TeamInfoView: React.FC = ({ logging_settings: info.metadata?.logging || [], organization_id: info.organization_id, vector_stores: info.object_permission?.vector_stores || [], - mcp_servers: info.object_permission?.mcp_servers || [] + mcp_servers: info.object_permission?.mcp_servers || [], + mcp_access_groups: info.object_permission?.mcp_servers || [], + mcp_servers_and_groups: info.object_permission?.mcp_servers || [] }} layout="vertical" > @@ -547,12 +562,12 @@ const TeamInfoView: React.FC = ({ /> - + form.setFieldValue('mcp_servers', values)} - value={form.getFieldValue('mcp_servers')} - accessToken={accessToken || ""} - placeholder="Select MCP servers" + onChange={val => form.setFieldValue('mcp_servers_and_groups', val)} + value={form.getFieldValue('mcp_servers_and_groups')} + accessToken={accessToken || ''} + placeholder="Select MCP servers or access groups (optional)" /> diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 92458fcc2b..10d8e693a6 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; import { Typography } from "antd"; -import { teamDeleteCall, teamUpdateCall, teamInfoCall, Organization, DEFAULT_ORGANIZATION } from "./networking"; +import { teamDeleteCall, teamUpdateCall, teamInfoCall, Organization, DEFAULT_ORGANIZATION, fetchMCPAccessGroups } from "./networking"; import TeamMemberModal from "@/components/team/edit_membership"; import { fetchTeams } from "./common_components/fetch_teams"; import { @@ -184,6 +184,8 @@ const Teams: React.FC = ({ const [guardrailsList, setGuardrailsList] = useState([]); const [expandedAccordions, setExpandedAccordions] = useState>({}); const [loggingSettings, setLoggingSettings] = useState([]); + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); + const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); useEffect(() => { console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`); @@ -214,6 +216,22 @@ const Teams: React.FC = ({ fetchGuardrails(); }, [accessToken]); + const fetchMcpAccessGroups = async () => { + try { + if (accessToken == null) { + return; + } + const groups = await fetchMCPAccessGroups(accessToken); + setMcpAccessGroups(groups); + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + } + }; + + useEffect(() => { + fetchMcpAccessGroups(); + }, [accessToken]); + useEffect(() => { const fetchTeamInfo = () => { if (!teams) return; @@ -366,6 +384,15 @@ const Teams: React.FC = ({ formValues.object_permission.mcp_servers = formValues.allowed_mcp_server_ids; delete formValues.allowed_mcp_server_ids; } + + // Transform allowed_mcp_access_groups into object_permission + if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) { + if (!formValues.object_permission) { + formValues.object_permission = {}; + } + formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups; + delete formValues.allowed_mcp_access_groups; + } const response: any = await teamCreateCall(accessToken, formValues); if (teams !== null) { setTeams([...teams, response]); @@ -1062,7 +1089,7 @@ const Teams: React.FC = ({ - + { if (!mcpAccessGroupsLoaded) { fetchMcpAccessGroups(); setMcpAccessGroupsLoaded(true); } }}> Additional Settings @@ -1152,20 +1179,20 @@ const Teams: React.FC = ({ label={ Allowed MCP Servers{' '} - + } - name="allowed_mcp_server_ids" + name="allowed_mcp_servers_and_groups" className="mt-8" - help="Select MCP servers this team can access. Leave empty for access to all MCP servers" + help="Select MCP servers or access groups this team can access. Leave empty for access to all." > form.setFieldValue('allowed_mcp_server_ids', values)} - value={form.getFieldValue('allowed_mcp_server_ids')} + onChange={val => form.setFieldValue('allowed_mcp_servers_and_groups', val)} + value={form.getFieldValue('allowed_mcp_servers_and_groups')} accessToken={accessToken || ''} - placeholder="Select MCP servers (optional)" + placeholder="Select MCP servers or access groups (optional)" premiumUser={premiumUser} />