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"
- >
-
-
- Allowed Vector Stores{' '}
-
-
-
-
- }
- name="allowed_vector_store_ids"
- className="mt-4"
- help="Select vector stores this key can access. Leave empty for access to all vector stores"
- >
- form.setFieldValue('allowed_vector_store_ids', values)}
- value={form.getFieldValue('allowed_vector_store_ids')}
- accessToken={accessToken}
- placeholder="Select vector stores (optional)"
- premiumUser={premiumUser}
- />
-
-
-
- Allowed MCP Servers{' '}
-
-
-
-
- }
- name="allowed_mcp_server_ids"
- className="mt-4"
- help="Select MCP servers this key can access. Leave empty for access to all MCP servers"
- >
- form.setFieldValue('allowed_mcp_server_ids', values)}
- value={form.getFieldValue('allowed_mcp_server_ids')}
- accessToken={accessToken}
- placeholder="Select MCP servers (optional)"
- premiumUser={premiumUser}
- />
-
-
-
- Metadata{' '}
-
-
-
-
- }
- name="metadata"
- className="mt-4"
- >
-
-
-
- Tags{' '}
-
-
-
-
- }
- name="tags"
- className="mt-4"
- help={`Tags for tracking spend and/or doing tag-based routing.`}
- >
-
-
-
-
-
- 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"
+ >
+
-
-
-
-
+
+
+ Allowed Vector Stores{' '}
+
+
+
+
+ }
+ name="allowed_vector_store_ids"
+ className="mt-4"
+ help="Select vector stores this key can access. Leave empty for access to all vector stores"
+ >
+ form.setFieldValue('allowed_vector_store_ids', values)}
+ value={form.getFieldValue('allowed_vector_store_ids')}
+ accessToken={accessToken}
+ placeholder="Select vector stores (optional)"
+ premiumUser={premiumUser}
+ />
+
+
+
+ Allowed MCP Servers{' '}
+
+
+
+
+ }
+ name="allowed_mcp_servers_and_groups"
+ className="mt-4"
+ help="Select MCP servers or access groups this key can access. Leave empty for access to all."
+ >
+ form.setFieldValue('allowed_mcp_servers_and_groups', val)}
+ value={form.getFieldValue('allowed_mcp_servers_and_groups')}
+ accessToken={accessToken}
+ placeholder="Select MCP servers or access groups (optional)"
+ premiumUser={premiumUser}
+ />
+
+
+
+ Metadata{' '}
+
+
+
+
+ }
+ name="metadata"
+ className="mt-4"
+ >
+
+
+
+ Tags{' '}
+
+
+
+
+ }
+ name="tags"
+ className="mt-4"
+ help={`Tags for tracking spend and/or doing tag-based routing.`}
+ >
+
+
+
+
+
+ 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)"
/>
-
-
- {/* Connection Status Section */}
-
-
+
+ MCP Access Groups
+
+
+
+
+ }
+ name="mcp_access_groups"
+ className="mb-4"
+ >
+ ({
+ value: group,
+ label: group
+ }))}
+ maxTagCount="responsive"
+ allowClear
+ />
+
{/* Cost Configuration Section */}
-
+
),
},
+ {
+ id: "mcp_access_groups",
+ header: "Access Groups",
+ cell: ({ row }) => {
+ const groups = row.original.mcp_access_groups;
+ if (Array.isArray(groups) && groups.length > 0) {
+ // If string array
+ if (typeof groups[0] === "string") {
+ return {groups.join(", ")};
+ }
+ }
+ return None;
+ },
+ },
{
header: "Created At",
accessorKey: "created_at",
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
index 88201ea83b..a6a4f922a3 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -1,9 +1,10 @@
import React, { useState, useEffect } from "react";
-import { Form, Select, Button as AntdButton, message } from "antd";
+import { Form, Select, Button as AntdButton, message, Input, Space, Tooltip } from "antd";
import { Button, TextInput, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { MCPServer, MCPServerCostInfo } from "./types";
import { updateMCPServer, testMCPToolsListRequest } from "../networking";
import MCPServerCostConfig from "./mcp_server_cost_config";
+import { MinusCircleOutlined, PlusOutlined, InfoCircleOutlined } from "@ant-design/icons";
interface MCPServerEditProps {
mcpServer: MCPServer;
@@ -25,6 +26,15 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o
}
}, [mcpServer]);
+ // Transform string array to object array for initial form values
+ useEffect(() => {
+ if (mcpServer.mcp_access_groups) {
+ // If access groups are objects, extract the name property; if strings, use as is
+ const groupNames = mcpServer.mcp_access_groups.map((g: any) => typeof g === 'string' ? g : g.name || String(g));
+ form.setFieldValue('mcp_access_groups', groupNames);
+ }
+ }, [mcpServer]);
+
// Fetch tools when component mounts
useEffect(() => {
fetchTools();
@@ -68,6 +78,9 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o
const handleSave = async (values: Record) => {
if (!accessToken) return;
try {
+ // Ensure access groups is always a string array
+ const accessGroups = (values.mcp_access_groups || []).map((g: any) => typeof g === 'string' ? g : g.name || String(g));
+
// Prepare the payload with cost configuration
const payload = {
...values,
@@ -76,7 +89,8 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o
server_name: values.alias || values.url,
description: values.description,
mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null
- }
+ },
+ mcp_access_groups: accessGroups
};
const updated = await updateMCPServer(accessToken, payload);
@@ -125,6 +139,29 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o
2024-11-05
+
+
+ MCP Access Groups
+
+
+
+
+ }
+ name="mcp_access_groups"
+ getValueFromEvent={value => value}
+ >
+ trigger.parentNode}
+ />
+
+
Cancel
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
index 2a31ae0494..8aca98a97f 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
@@ -153,50 +153,64 @@ export const MCPServerView: React.FC
= ({
onSuccess={handleSuccess}
/>
) : (
-
-
-
-
Server Name
-
{mcpServer.alias}
+
+
+
Server Name
+
{mcpServer.alias}
+
+
+
Description
+
{mcpServer.description}
+
+
+
URL
+
+ {renderUrlWithToggle(mcpServer.url, showFullUrl)}
+ {hasToken && (
+
+ )}
+
+
+
Transport
+
{handleTransport(mcpServer.transport)}
+
+
+
Auth Type
+
{handleAuth(mcpServer.auth_type)}
+
+
+
Spec Version
+
{mcpServer.spec_version}
+
+
+
Access Groups
-
Description
-
{mcpServer.description}
+ {mcpServer.mcp_access_groups && mcpServer.mcp_access_groups.length > 0 ? (
+
+ {mcpServer.mcp_access_groups.map((group: any, index: number) => (
+
+ {typeof group === 'string' ? group : group?.name ?? ''}
+
+ ))}
+
+ ) : (
+
No access groups defined
+ )}
-
-
URL
-
- {renderUrlWithToggle(mcpServer.url, showFullUrl)}
- {hasToken && (
-
- )}
-
-
-
-
Transport
-
{handleTransport(mcpServer.transport)}
-
-
-
Auth Type
-
{handleAuth(mcpServer.auth_type)}
-
-
-
Spec Version
-
{mcpServer.spec_version}
-
-
-
- {/* Cost Configuration Section */}
-
+
)}
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 8835ce12b3..5b89c8fc44 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx
@@ -1,13 +1,9 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
-
import {
Modal,
- Tooltip,
message,
Select,
- Space,
- Typography
} from "antd";
import {
TabPanel,
@@ -16,8 +12,6 @@ import {
TabList,
Tab,
} from "@tremor/react";
-import { LinkIcon } from "lucide-react";
-
import {
Grid,
Col,
@@ -26,7 +20,6 @@ import {
} from "@tremor/react";
import { DataTable } from "../view_logs/table";
import { mcpServerColumns } from "./mcp_server_columns";
-
import {
deleteMCPServer,
fetchMCPServers,
@@ -35,8 +28,6 @@ import {
MCPServer,
MCPServerProps,
Team,
- handleAuth,
- handleTransport,
} from "./types";
import { isAdminRole } from "@/utils/roles";
import { MCPServerView } from "./mcp_server_view";
@@ -45,21 +36,18 @@ import MCPConnect from "./mcp_connect";
const { Option } = Select;
-interface DeleteModalProps {
+const DeleteModal: React.FC<{
isModalOpen: boolean;
title: string;
confirmDelete: () => void;
cancelDelete: () => void;
-}
-
-const DeleteModal: React.FC
= ({
+}> = ({
isModalOpen,
title,
confirmDelete,
cancelDelete,
}) => {
if (!isModalOpen) return null;
-
return (
= ({
userRole,
userID,
}) => {
- // Query to fetch MCP tools
const {
data: mcpServers,
isLoading: isLoadingServers,
@@ -96,26 +83,21 @@ const MCPServers: React.FC = ({
enabled: !!accessToken,
}) as { data: MCPServer[]; isLoading: boolean; refetch: () => void };
- const createMCPServer = (newMcpServer: any) => {
- refetch();
- };
-
// state
const [serverIdToDelete, setServerToDelete] = useState(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedServerId, setSelectedServerId] = useState(null);
const [editServer, setEditServer] = useState(false);
const [selectedTeam, setSelectedTeam] = useState("all");
+ const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState("all");
const [filteredServers, setFilteredServers] = useState([]);
- const [currentTeam, setCurrentTeam] = useState("all");
- const [modelViewMode, setModelViewMode] = useState("all");
+
// Get unique teams from all servers
const uniqueTeams = React.useMemo(() => {
if (!mcpServers) return [];
const teamsSet = new Set();
const uniqueTeamsArray: Team[] = [];
-
mcpServers.forEach((server: MCPServer) => {
if (server.teams) {
server.teams.forEach((team: Team) => {
@@ -130,48 +112,63 @@ const MCPServers: React.FC = ({
return uniqueTeamsArray;
}, [mcpServers]);
+ // Get unique MCP access groups from all servers
+ const uniqueMcpAccessGroups = React.useMemo(() => {
+ if (!mcpServers) return [];
+ return Array.from(new Set(
+ mcpServers.flatMap(server => server.mcp_access_groups)
+ ));
+ }, [mcpServers]);
+
// Handle team filter change
const handleTeamChange = (teamId: string) => {
setSelectedTeam(teamId);
- setCurrentTeam(teamId);
-
- if (!mcpServers) return;
-
- if (teamId === "all") {
- setFilteredServers(mcpServers);
- } else if (teamId === "personal") {
- // For now, show empty list for personal servers
- setFilteredServers([]);
- } else {
- const filtered = mcpServers.filter(server =>
- server.teams?.some(team => team.team_id === teamId)
- );
- setFilteredServers(filtered);
- }
+ filterServers(teamId, selectedMcpAccessGroup);
};
- React.useEffect(() => {
- if (mcpServers) {
- // Filter servers based on current team selection
- if (currentTeam === "all") {
- setFilteredServers(mcpServers);
- } else if (currentTeam === "personal") {
- // For now, show empty list for personal servers
- setFilteredServers([]);
- } else {
- const filtered = mcpServers.filter(server =>
- server.teams?.some(team => team.team_id === currentTeam)
- );
- setFilteredServers(filtered);
- }
+ // Handle MCP access group filter change
+ const handleMcpAccessGroupChange = (group: string) => {
+ setSelectedMcpAccessGroup(group);
+ filterServers(selectedTeam, group);
+ };
+
+ // Filtering logic for both team and access group
+ const filterServers = (teamId: string, group: string) => {
+ if (!mcpServers) return setFilteredServers([]);
+ let filtered = mcpServers;
+ if (teamId === "personal") {
+ setFilteredServers([]);
+ return;
}
- }, [mcpServers, currentTeam]);
+ if (teamId !== "all") {
+ filtered = filtered.filter(server =>
+ server.teams?.some(team => team.team_id === teamId)
+ );
+ }
+ if (group !== "all") {
+ filtered = filtered.filter(server =>
+ server.mcp_access_groups?.some((g: any) =>
+ typeof g === 'string' ? g === group : g && g.name === group
+ )
+ );
+ }
+ setFilteredServers(filtered);
+ };
+
+ // Initial and effect-based filtering
+ useEffect(() => {
+ filterServers(selectedTeam, selectedMcpAccessGroup);
+ // eslint-disable-next-line
+ }, [mcpServers]);
const columns = React.useMemo(
() =>
mcpServerColumns(
userRole ?? "",
- (serverId: string) => setSelectedServerId(serverId),
+ (serverId: string) => {
+ setSelectedServerId(serverId);
+ setEditServer(false);
+ },
(serverId: string) => {
setSelectedServerId(serverId);
setEditServer(true);
@@ -182,7 +179,6 @@ const MCPServers: React.FC = ({
);
function handleDelete(server_id: string) {
- // Set the team to delete and open the confirmation modal
setServerToDelete(server_id);
setIsDeleteModalOpen(true);
}
@@ -191,31 +187,22 @@ const MCPServers: React.FC = ({
if (serverIdToDelete == null || accessToken == null) {
return;
}
-
try {
await deleteMCPServer(accessToken, serverIdToDelete);
- // Successfully completed the deletion. Update the state to trigger a rerender.
message.success("Deleted MCP Server successfully");
refetch();
} catch (error) {
console.error("Error deleting the mcp server:", error);
- // Handle any error situations, such as displaying an error message to the user.
}
-
- // Close the confirmation modal and reset the serverToDelete
setIsDeleteModalOpen(false);
setServerToDelete(null);
};
const cancelDelete = () => {
- // Close the confirmation modal and reset the serverToDelete
setIsDeleteModalOpen(false);
setServerToDelete(null);
};
- // Check if user is internal or admin
- const canAccessMCPServers = isAdminRole(userRole!) || (userRole && userRole.includes('internal'));
-
if (!accessToken || !userRole || !userID) {
return (
@@ -228,7 +215,7 @@ const MCPServers: React.FC = ({
selectedServerId ? (
server.server_id === selectedServerId
) || {
server_id: '',
@@ -267,7 +254,7 @@ const MCPServers: React.FC = ({
)}
@@ -277,7 +264,7 @@ const MCPServers: React.FC = ({
Current Team:
@@ -304,6 +291,27 @@ const MCPServers: React.FC = ({
))}
+
Access Group:
+
+
+ {uniqueMcpAccessGroups.map((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}
/>