mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix: inline import, add approval workflow tests, rename Submitted MCPs
This commit is contained in:
parent
3aedc99698
commit
fbdff99d41
3 changed files with 249 additions and 4 deletions
|
|
@ -18,7 +18,7 @@ import importlib
|
|||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Iterable, List, Literal, Optional
|
||||
|
||||
from fastapi import (
|
||||
|
|
@ -712,8 +712,6 @@ if MCP_AVAILABLE:
|
|||
Creates the server with approval_status=pending_review.
|
||||
Requires a team-scoped API key.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
if not user_api_key_dict.team_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
|
|||
|
|
@ -1512,3 +1512,250 @@ class TestManagementPayloadValidation:
|
|||
assert len(result) == 1
|
||||
assert result[0]["server_id"] == "server-1"
|
||||
assert result[0]["status"] == "healthy"
|
||||
|
||||
|
||||
class TestMCPApprovalWorkflow:
|
||||
"""Tests for BYOM submission: register, list submissions, approve, reject."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_mcp_server_requires_team_key(self):
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
register_mcp_server,
|
||||
)
|
||||
|
||||
payload = NewMCPServerRequest(
|
||||
alias="My Server",
|
||||
url="https://example.com/mcp",
|
||||
transport=MCPTransport.sse,
|
||||
)
|
||||
# No team_id → should raise 400
|
||||
user_auth = generate_mock_user_api_key_auth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
team_id=None,
|
||||
)
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await register_mcp_server(payload=payload, user_api_key_dict=user_auth)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "team" in str(exc_info.value.detail).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_mcp_server_sets_pending_review(self):
|
||||
from litellm.proxy._types import MCPApprovalStatus
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
register_mcp_server,
|
||||
)
|
||||
|
||||
payload = NewMCPServerRequest(
|
||||
alias="My Server",
|
||||
url="https://example.com/mcp",
|
||||
transport=MCPTransport.sse,
|
||||
)
|
||||
user_auth = generate_mock_user_api_key_auth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
team_id="team-123",
|
||||
user_id="user-abc",
|
||||
)
|
||||
created_record = generate_mock_mcp_server_db_record(
|
||||
alias="My Server",
|
||||
url="https://example.com/mcp",
|
||||
)
|
||||
created_record.approval_status = MCPApprovalStatus.pending_review
|
||||
created_record.submitted_by = "user-abc"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.validate_and_normalize_mcp_server_payload",
|
||||
MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.create_mcp_server",
|
||||
AsyncMock(return_value=created_record),
|
||||
) as mock_create,
|
||||
):
|
||||
result = await register_mcp_server(
|
||||
payload=payload, user_api_key_dict=user_auth
|
||||
)
|
||||
|
||||
# Endpoint sets pending_review before calling create_mcp_server
|
||||
call_payload = mock_create.call_args[0][1]
|
||||
assert call_payload.approval_status == MCPApprovalStatus.pending_review
|
||||
assert call_payload.submitted_by == "user-abc"
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_submissions_non_admin_forbidden(self):
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
get_mcp_server_submissions,
|
||||
)
|
||||
|
||||
non_admin = generate_mock_user_api_key_auth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
)
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_mcp_server_submissions(user_api_key_dict=non_admin)
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_submissions_admin_returns_summary(self):
|
||||
from litellm.proxy._types import MCPSubmissionsSummary
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
get_mcp_server_submissions,
|
||||
)
|
||||
|
||||
admin = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
pending = generate_mock_mcp_server_db_record(alias="Pending")
|
||||
pending.approval_status = "pending_review"
|
||||
summary = MCPSubmissionsSummary(
|
||||
total=1, pending_review=1, active=0, rejected=0, items=[pending]
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_submissions",
|
||||
AsyncMock(return_value=summary),
|
||||
),
|
||||
):
|
||||
result = await get_mcp_server_submissions(user_api_key_dict=admin)
|
||||
|
||||
assert result.total == 1
|
||||
assert result.pending_review == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_non_pending_server_raises_400(self):
|
||||
from litellm.proxy._types import MCPApprovalStatus
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
approve_mcp_server_submission,
|
||||
)
|
||||
|
||||
admin = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
active_server = generate_mock_mcp_server_db_record()
|
||||
active_server.approval_status = MCPApprovalStatus.active
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server",
|
||||
AsyncMock(return_value=active_server),
|
||||
),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await approve_mcp_server_submission(
|
||||
server_id="server-1", user_api_key_dict=admin
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_pending_server_loads_into_registry(self):
|
||||
from litellm.proxy._types import MCPApprovalStatus
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
approve_mcp_server_submission,
|
||||
)
|
||||
|
||||
admin = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
pending_server = generate_mock_mcp_server_db_record()
|
||||
pending_server.approval_status = MCPApprovalStatus.pending_review
|
||||
approved_server = generate_mock_mcp_server_db_record()
|
||||
approved_server.approval_status = MCPApprovalStatus.active
|
||||
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.reload_servers_from_database = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server",
|
||||
AsyncMock(return_value=pending_server),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.approve_mcp_server",
|
||||
AsyncMock(return_value=approved_server),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.global_mcp_server_manager",
|
||||
mock_manager,
|
||||
),
|
||||
):
|
||||
result = await approve_mcp_server_submission(
|
||||
server_id=pending_server.server_id, user_api_key_dict=admin
|
||||
)
|
||||
|
||||
mock_manager.reload_servers_from_database.assert_awaited_once()
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reject_already_rejected_raises_400(self):
|
||||
from litellm.proxy._types import MCPApprovalStatus, RejectMCPServerRequest
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
reject_mcp_server_submission,
|
||||
)
|
||||
|
||||
admin = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
rejected_server = generate_mock_mcp_server_db_record()
|
||||
rejected_server.approval_status = MCPApprovalStatus.rejected
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server",
|
||||
AsyncMock(return_value=rejected_server),
|
||||
),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await reject_mcp_server_submission(
|
||||
server_id="server-1",
|
||||
payload=RejectMCPServerRequest(review_notes="duplicate"),
|
||||
user_api_key_dict=admin,
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reject_active_server_allowed(self):
|
||||
"""Admin can deactivate an already-approved server via the reject endpoint."""
|
||||
from litellm.proxy._types import MCPApprovalStatus, RejectMCPServerRequest
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import (
|
||||
reject_mcp_server_submission,
|
||||
)
|
||||
|
||||
admin = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
active_server = generate_mock_mcp_server_db_record()
|
||||
active_server.approval_status = MCPApprovalStatus.active
|
||||
now_rejected = generate_mock_mcp_server_db_record()
|
||||
now_rejected.approval_status = MCPApprovalStatus.rejected
|
||||
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server",
|
||||
AsyncMock(return_value=active_server),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.mcp_management_endpoints.reject_mcp_server",
|
||||
AsyncMock(return_value=now_rejected),
|
||||
),
|
||||
):
|
||||
result = await reject_mcp_server_submission(
|
||||
server_id=active_server.server_id,
|
||||
payload=RejectMCPServerRequest(review_notes="policy violation"),
|
||||
user_api_key_dict=admin,
|
||||
)
|
||||
assert result is not None
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
<Tab>Connect</Tab>
|
||||
<Tab>Semantic Filter</Tab>
|
||||
<Tab>Network Settings</Tab>
|
||||
{isAdminRole(userRole) && <Tab><span className="flex items-center gap-2">Submitted Tools <NewBadge /></span></Tab>}
|
||||
{isAdminRole(userRole) && <Tab><span className="flex items-center gap-2">Submitted MCPs <NewBadge /></span></Tab>}
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue