mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
# 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 []
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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]] = []
|
||||
|
||||
|
||||
|
||||
@@ -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, []))
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
+14
-12
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CreateKeyProps> = ({
|
||||
>({});
|
||||
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||
const [userSearchLoading, setUserSearchLoading] = useState<boolean>(false);
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
@@ -199,6 +202,22 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||
}
|
||||
}, [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<CreateKeyProps> = ({
|
||||
// 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<CreateKeyProps> = ({
|
||||
{/* Section 3: Optional Settings */}
|
||||
{!isFormDisabled && (
|
||||
<div className="mb-8">
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<Title className="m-0">Optional Settings</Title>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Max Budget (USD){' '}
|
||||
<Tooltip title="Maximum amount in USD this key can spend. When reached, the key will be blocked from making further requests">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Reset Budget{' '}
|
||||
<Tooltip title="How often the budget should reset. For example, setting 'daily' will reset the budget every 24 hours">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="budget_duration"
|
||||
help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`}
|
||||
>
|
||||
<BudgetDurationDropdown onChange={(value) => form.setFieldValue('budget_duration', value)} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Tokens per minute Limit (TPM){' '}
|
||||
<Tooltip title="Maximum number of tokens this key can process per minute. Helps control usage and costs">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Requests per minute Limit (RPM){' '}
|
||||
<Tooltip title="Maximum number of API requests this key can make per minute. Helps prevent abuse and manage load">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Expire Key{' '}
|
||||
<Tooltip title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days)">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="duration"
|
||||
className="mt-4"
|
||||
>
|
||||
<TextInput placeholder="e.g., 30d" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Guardrails{' '}
|
||||
<Tooltip title="Apply safety guardrails to this key to filter content or enforce policies">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="guardrails"
|
||||
className="mt-4"
|
||||
help="Select existing guardrails or enter new ones"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select or enter guardrails"
|
||||
options={guardrailsList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Vector Stores{' '}
|
||||
<Tooltip title="Select which vector stores this key can access. If none selected, the key will have access to all available vector stores">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_vector_store_ids"
|
||||
className="mt-4"
|
||||
help="Select vector stores this key can access. Leave empty for access to all vector stores"
|
||||
>
|
||||
<PremiumVectorStoreSelector
|
||||
onChange={(values) => form.setFieldValue('allowed_vector_store_ids', values)}
|
||||
value={form.getFieldValue('allowed_vector_store_ids')}
|
||||
accessToken={accessToken}
|
||||
placeholder="Select vector stores (optional)"
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed MCP Servers{' '}
|
||||
<Tooltip title="Select which MCP servers this key can access. If none selected, the key will have access to all available MCP servers">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_mcp_server_ids"
|
||||
className="mt-4"
|
||||
help="Select MCP servers this key can access. Leave empty for access to all MCP servers"
|
||||
>
|
||||
<PremiumMCPSelector
|
||||
onChange={(values) => form.setFieldValue('allowed_mcp_server_ids', values)}
|
||||
value={form.getFieldValue('allowed_mcp_server_ids')}
|
||||
accessToken={accessToken}
|
||||
placeholder="Select MCP servers (optional)"
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Metadata{' '}
|
||||
<Tooltip title="JSON object with additional information about this key. Used for tracking or custom logic">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="metadata"
|
||||
className="mt-4"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Enter metadata as JSON"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Tags{' '}
|
||||
<Tooltip title="Tags for tracking spend and/or doing tag-based routing. Used for analytics and filtering">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="tags"
|
||||
className="mt-4"
|
||||
help={`Tags for tracking spend and/or doing tag-based routing.`}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Enter tags"
|
||||
tokenSeparators={[',']}
|
||||
options={predefinedTags}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<PremiumLoggingSettings
|
||||
value={loggingSettings}
|
||||
onChange={setLoggingSettings}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<b>Advanced Settings</b>
|
||||
<Tooltip title={
|
||||
<Accordion className="mt-4 mb-4" onClick={() => { if (!mcpAccessGroupsLoaded) { fetchMcpAccessGroups(); setMcpAccessGroupsLoaded(true); } }}>
|
||||
<AccordionHeader>
|
||||
<Title className="m-0">Optional Settings</Title>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Learn more about advanced settings in our{' '}
|
||||
<a
|
||||
href={proxyBaseUrl ? `${proxyBaseUrl}/#/key%20management/generate_key_fn_key_generate_post`: `/#/key%20management/generate_key_fn_key_generate_post`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
Max Budget (USD){' '}
|
||||
<Tooltip title="Maximum amount in USD this key can spend. When reached, the key will be blocked from making further requests">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}>
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-300 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<SchemaFormFields
|
||||
schemaComponent="GenerateKeyRequest"
|
||||
form={form}
|
||||
excludedFields={['key_alias', 'team_id', 'models', 'duration', 'metadata', 'tags', 'guardrails', "max_budget", "budget_duration", "tpm_limit", "rpm_limit"]}
|
||||
}
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Reset Budget{' '}
|
||||
<Tooltip title="How often the budget should reset. For example, setting 'daily' will reset the budget every 24 hours">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="budget_duration"
|
||||
help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`}
|
||||
>
|
||||
<BudgetDurationDropdown onChange={(value) => form.setFieldValue('budget_duration', value)} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Tokens per minute Limit (TPM){' '}
|
||||
<Tooltip title="Maximum number of tokens this key can process per minute. Helps control usage and costs">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Requests per minute Limit (RPM){' '}
|
||||
<Tooltip title="Maximum number of API requests this key can make per minute. Helps prevent abuse and manage load">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Expire Key{' '}
|
||||
<Tooltip title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days)">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="duration"
|
||||
className="mt-4"
|
||||
>
|
||||
<TextInput placeholder="e.g., 30d" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Guardrails{' '}
|
||||
<Tooltip title="Apply safety guardrails to this key to filter content or enforce policies">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="guardrails"
|
||||
className="mt-4"
|
||||
help="Select existing guardrails or enter new ones"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select or enter guardrails"
|
||||
options={guardrailsList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Vector Stores{' '}
|
||||
<Tooltip title="Select which vector stores this key can access. If none selected, the key will have access to all available vector stores">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_vector_store_ids"
|
||||
className="mt-4"
|
||||
help="Select vector stores this key can access. Leave empty for access to all vector stores"
|
||||
>
|
||||
<PremiumVectorStoreSelector
|
||||
onChange={(values) => form.setFieldValue('allowed_vector_store_ids', values)}
|
||||
value={form.getFieldValue('allowed_vector_store_ids')}
|
||||
accessToken={accessToken}
|
||||
placeholder="Select vector stores (optional)"
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed MCP Servers{' '}
|
||||
<Tooltip title="Select which MCP servers or access groups this key can access. Leave empty for access to all.">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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."
|
||||
>
|
||||
<PremiumMCPSelector
|
||||
onChange={val => 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}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Metadata{' '}
|
||||
<Tooltip title="JSON object with additional information about this key. Used for tracking or custom logic">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="metadata"
|
||||
className="mt-4"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Enter metadata as JSON"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Tags{' '}
|
||||
<Tooltip title="Tags for tracking spend and/or doing tag-based routing. Used for analytics and filtering">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="tags"
|
||||
className="mt-4"
|
||||
help={`Tags for tracking spend and/or doing tag-based routing.`}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Enter tags"
|
||||
tokenSeparators={[',']}
|
||||
options={predefinedTags}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<PremiumLoggingSettings
|
||||
value={loggingSettings}
|
||||
onChange={setLoggingSettings}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<b>Advanced Settings</b>
|
||||
<Tooltip title={
|
||||
<span>
|
||||
Learn more about advanced settings in our{' '}
|
||||
<a
|
||||
href={proxyBaseUrl ? `${proxyBaseUrl}/#/key%20management/generate_key_fn_key_generate_post`: `/#/key%20management/generate_key_fn_key_generate_post`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
</span>
|
||||
}>
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-300 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<SchemaFormFields
|
||||
schemaComponent="GenerateKeyRequest"
|
||||
form={form}
|
||||
excludedFields={['key_alias', 'team_id', 'models', 'duration', 'metadata', 'tags', 'guardrails', "max_budget", "budget_duration", "tpm_limit", "rpm_limit"]}
|
||||
/>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
const team = teams?.find(team => team.team_id === keyData.team_id);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
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.Item>
|
||||
|
||||
<Form.Item label="MCP Servers" name="mcp_servers">
|
||||
<Form.Item label="MCP Servers / Access Groups" name="mcp_servers_and_groups">
|
||||
<MCPServerSelector
|
||||
onChange={(values) => 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)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item label="Team ID" name="team_id">
|
||||
<Select
|
||||
placeholder="Select team"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { fetchMCPServers } from '../networking';
|
||||
import { fetchMCPServers, fetchMCPAccessGroups } from '../networking';
|
||||
import { MCPServer } from '../mcp_tools/types';
|
||||
|
||||
|
||||
interface MCPServerSelectorProps {
|
||||
onChange: (selectedMCPServers: string[]) => void;
|
||||
value?: string[];
|
||||
onChange: (selected: { servers: string[]; accessGroups: string[] }) => void;
|
||||
value?: { servers: string[]; accessGroups: string[] };
|
||||
className?: string;
|
||||
accessToken: string;
|
||||
placeholder?: string;
|
||||
@@ -22,51 +21,93 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
||||
disabled = false
|
||||
}) => {
|
||||
const [mcpServers, setMCPServers] = useState<MCPServer[]>([]);
|
||||
const [accessGroups, setAccessGroups] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMCPServerList = async () => {
|
||||
const fetchData = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchMCPServers(accessToken);
|
||||
if (response && Array.isArray(response)) {
|
||||
// Direct array response
|
||||
setMCPServers(response);
|
||||
} else if (response.data && Array.isArray(response.data)) {
|
||||
// Response with data wrapper
|
||||
setMCPServers(response.data);
|
||||
}
|
||||
const [serversRes, groupsRes] = await Promise.all([
|
||||
fetchMCPServers(accessToken),
|
||||
fetchMCPAccessGroups(accessToken)
|
||||
]);
|
||||
let servers = Array.isArray(serversRes) ? serversRes : (serversRes.data || []);
|
||||
let groups = Array.isArray(groupsRes) ? groupsRes : (groupsRes.data || []);
|
||||
setMCPServers(servers);
|
||||
setAccessGroups(groups);
|
||||
} catch (error) {
|
||||
console.error("Error fetching MCP servers:", error);
|
||||
console.error("Error fetching MCP servers or access groups:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMCPServerList();
|
||||
fetchData();
|
||||
}, [accessToken]);
|
||||
|
||||
// Combine options, access groups first
|
||||
const options = [
|
||||
...accessGroups.map(group => ({
|
||||
label: group,
|
||||
value: group,
|
||||
isAccessGroup: true
|
||||
})),
|
||||
...mcpServers.map(server => ({
|
||||
label: `${server.alias || server.server_id} (${server.server_id})`,
|
||||
value: server.server_id,
|
||||
isAccessGroup: false
|
||||
}))
|
||||
];
|
||||
|
||||
// Flatten value for Select
|
||||
const selectedValues = [
|
||||
...(value?.servers || []),
|
||||
...(value?.accessGroups || [])
|
||||
];
|
||||
|
||||
// Handle selection
|
||||
const handleChange = (selected: string[]) => {
|
||||
const servers = selected.filter(v => !accessGroups.includes(v));
|
||||
const accessGroupsSelected = selected.filter(v => accessGroups.includes(v));
|
||||
onChange({ servers, accessGroups: accessGroupsSelected });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
value={selectedValues}
|
||||
loading={loading}
|
||||
className={className}
|
||||
options={mcpServers.map(server => ({
|
||||
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 => (
|
||||
<Select.Option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
>
|
||||
{opt.isAccessGroup && (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
background: '#1890ff',
|
||||
marginRight: 8,
|
||||
verticalAlign: 'middle',
|
||||
}} />
|
||||
)}
|
||||
{opt.label}
|
||||
{opt.isAccessGroup && <span style={{ color: '#1890ff', marginLeft: 8 }}>(Access Group)</span>}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<CreateMCPServerProps> = ({
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [costConfig, setCostConfig] = useState<MCPServerCostInfo>({});
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
const [tools, setTools] = useState<any[]>([]);
|
||||
|
||||
const handleCreate = async (formValues: Record<string, any>) => {
|
||||
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<CreateMCPServerProps> = ({
|
||||
<Select.Option value="2024-11-05">2024-11-05</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Connection Status Section */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<MCPConnectionStatus
|
||||
accessToken={accessToken}
|
||||
formValues={formValues}
|
||||
onToolsLoaded={setTools}
|
||||
/>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
MCP Access Groups
|
||||
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="mcp_access_groups"
|
||||
className="mb-4"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[',']}
|
||||
options={mcpAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group
|
||||
}))}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Cost Configuration Section */}
|
||||
<div className="mt-6">
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<MCPServerCostConfig
|
||||
value={costConfig}
|
||||
onChange={setCostConfig}
|
||||
|
||||
@@ -56,6 +56,20 @@ export const mcpServerColumns = (
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 <span>{groups.join(", ")}</span>;
|
||||
}
|
||||
}
|
||||
return <span className="text-gray-400 italic">None</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
|
||||
@@ -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<MCPServerEditProps> = ({ 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<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
const handleSave = async (values: Record<string, any>) => {
|
||||
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<MCPServerEditProps> = ({ 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<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
<Select.Option value="2024-11-05">2024-11-05</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
MCP Access Groups
|
||||
<Tooltip title="Define access groups for this MCP server. Each group represents a set of permissions.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="mcp_access_groups"
|
||||
getValueFromEvent={value => value}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Add or select access groups"
|
||||
tokenSeparators={[',']}
|
||||
// Ensure value is always an array of strings
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<AntdButton onClick={onCancel}>Cancel</AntdButton>
|
||||
<Button type="submit">Save Changes</Button>
|
||||
|
||||
@@ -153,50 +153,64 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Server Name</Text>
|
||||
<div>{mcpServer.alias}</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Server Name</Text>
|
||||
<div>{mcpServer.alias}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Description</Text>
|
||||
<div>{mcpServer.description}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">URL</Text>
|
||||
<div className="font-mono break-all overflow-wrap-anywhere max-w-full flex items-center gap-2">
|
||||
{renderUrlWithToggle(mcpServer.url, showFullUrl)}
|
||||
{hasToken && (
|
||||
<button
|
||||
onClick={() => setShowFullUrl(!showFullUrl)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<Icon
|
||||
icon={showFullUrl ? EyeOffIcon : EyeIcon}
|
||||
size="sm"
|
||||
className="text-gray-500"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Transport</Text>
|
||||
<div>{handleTransport(mcpServer.transport)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Auth Type</Text>
|
||||
<div>{handleAuth(mcpServer.auth_type)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Spec Version</Text>
|
||||
<div>{mcpServer.spec_version}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Access Groups</Text>
|
||||
<div>
|
||||
<Text className="font-medium">Description</Text>
|
||||
<div>{mcpServer.description}</div>
|
||||
{mcpServer.mcp_access_groups && mcpServer.mcp_access_groups.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mcpServer.mcp_access_groups.map((group: any, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-gray-100 rounded-md text-sm"
|
||||
>
|
||||
{typeof group === 'string' ? group : group?.name ?? ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No access groups defined</Text>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">URL</Text>
|
||||
<div className="font-mono break-all overflow-wrap-anywhere max-w-full flex items-center gap-2">
|
||||
{renderUrlWithToggle(mcpServer.url, showFullUrl)}
|
||||
{hasToken && (
|
||||
<button
|
||||
onClick={() => setShowFullUrl(!showFullUrl)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<Icon
|
||||
icon={showFullUrl ? EyeOffIcon : EyeIcon}
|
||||
size="sm"
|
||||
className="text-gray-500"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Transport</Text>
|
||||
<div>{handleTransport(mcpServer.transport)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Auth Type</Text>
|
||||
<div>{handleAuth(mcpServer.auth_type)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Spec Version</Text>
|
||||
<div>{mcpServer.spec_version}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cost Configuration Section */}
|
||||
<MCPServerCostDisplay costConfig={mcpServer.mcp_info?.mcp_server_cost_info} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -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<DeleteModalProps> = ({
|
||||
}> = ({
|
||||
isModalOpen,
|
||||
title,
|
||||
confirmDelete,
|
||||
cancelDelete,
|
||||
}) => {
|
||||
if (!isModalOpen) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
@@ -82,7 +70,6 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
userRole,
|
||||
userID,
|
||||
}) => {
|
||||
// Query to fetch MCP tools
|
||||
const {
|
||||
data: mcpServers,
|
||||
isLoading: isLoadingServers,
|
||||
@@ -96,26 +83,21 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
enabled: !!accessToken,
|
||||
}) as { data: MCPServer[]; isLoading: boolean; refetch: () => void };
|
||||
|
||||
const createMCPServer = (newMcpServer: any) => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// state
|
||||
const [serverIdToDelete, setServerToDelete] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedServerId, setSelectedServerId] = useState<string | null>(null);
|
||||
const [editServer, setEditServer] = useState(false);
|
||||
const [selectedTeam, setSelectedTeam] = useState<string>("all");
|
||||
const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState<string>("all");
|
||||
const [filteredServers, setFilteredServers] = useState<MCPServer[]>([]);
|
||||
const [currentTeam, setCurrentTeam] = useState<string>("all");
|
||||
const [modelViewMode, setModelViewMode] = useState<string>("all");
|
||||
|
||||
|
||||
// Get unique teams from all servers
|
||||
const uniqueTeams = React.useMemo(() => {
|
||||
if (!mcpServers) return [];
|
||||
const teamsSet = new Set<string>();
|
||||
const uniqueTeamsArray: Team[] = [];
|
||||
|
||||
mcpServers.forEach((server: MCPServer) => {
|
||||
if (server.teams) {
|
||||
server.teams.forEach((team: Team) => {
|
||||
@@ -130,48 +112,63 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
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<MCPServerProps> = ({
|
||||
);
|
||||
|
||||
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<MCPServerProps> = ({
|
||||
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 (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
@@ -228,7 +215,7 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
selectedServerId ? (
|
||||
<MCPServerView
|
||||
mcpServer={
|
||||
mcpServers.find(
|
||||
filteredServers.find(
|
||||
(server: MCPServer) => server.server_id === selectedServerId
|
||||
) || {
|
||||
server_id: '',
|
||||
@@ -267,7 +254,7 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
<CreateMCPServer
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
onCreateSuccess={createMCPServer}
|
||||
onCreateSuccess={refetch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -277,7 +264,7 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
<div className="flex items-center gap-4">
|
||||
<Text className="text-lg font-semibold text-gray-900">Current Team:</Text>
|
||||
<Select
|
||||
value={currentTeam}
|
||||
value={selectedTeam}
|
||||
onChange={handleTeamChange}
|
||||
style={{ width: 300 }}
|
||||
>
|
||||
@@ -304,6 +291,27 @@ const MCPServers: React.FC<MCPServerProps> = ({
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Text className="text-lg font-semibold text-gray-900 ml-6">Access Group:</Text>
|
||||
<Select
|
||||
value={selectedMcpAccessGroup}
|
||||
onChange={handleMcpAccessGroupChange}
|
||||
style={{ width: 300 }}
|
||||
>
|
||||
<Option value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">All Access Groups</span>
|
||||
</div>
|
||||
</Option>
|
||||
{uniqueMcpAccessGroups.map((group) => (
|
||||
<Option key={group} value={group}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">{group}</span>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface MCPServer {
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
teams?: Team[];
|
||||
mcp_access_groups?: string[];
|
||||
}
|
||||
|
||||
export interface MCPServerProps {
|
||||
|
||||
@@ -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<string, any> // Assuming formValues is an object
|
||||
|
||||
@@ -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<TeamInfoProps> = ({
|
||||
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
|
||||
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
|
||||
console.log("userModels in team info", userModels);
|
||||
|
||||
@@ -149,6 +152,18 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
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<TeamInfoProps> = ({
|
||||
}
|
||||
|
||||
// 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<TeamInfoProps> = ({
|
||||
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<TeamInfoProps> = ({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="MCP Servers" name="mcp_servers">
|
||||
<Form.Item label="MCP Servers / Access Groups" name="mcp_servers_and_groups">
|
||||
<MCPServerSelector
|
||||
onChange={(values) => 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)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Organization ID" name="organization_id">
|
||||
|
||||
@@ -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<TeamProps> = ({
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
|
||||
@@ -214,6 +216,22 @@ const Teams: React.FC<TeamProps> = ({
|
||||
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<TeamProps> = ({
|
||||
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<TeamProps> = ({
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
|
||||
<Accordion className="mt-20 mb-8">
|
||||
<Accordion className="mt-20 mb-8" onClick={() => { if (!mcpAccessGroupsLoaded) { fetchMcpAccessGroups(); setMcpAccessGroupsLoaded(true); } }}>
|
||||
<AccordionHeader>
|
||||
<b>Additional Settings</b>
|
||||
</AccordionHeader>
|
||||
@@ -1152,20 +1179,20 @@ const Teams: React.FC<TeamProps> = ({
|
||||
label={
|
||||
<span>
|
||||
Allowed MCP Servers{' '}
|
||||
<Tooltip title="Select which MCP servers this team can access by default. Leave empty for access to all MCP servers">
|
||||
<Tooltip title="Select which MCP servers or access groups this team can access by default. Leave empty for access to all.">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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."
|
||||
>
|
||||
<PremiumMCPSelector
|
||||
onChange={(values) => 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}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user