mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(mcp): add all-proxy-mcps grant for teams
Teams can grant access to every MCP server on the proxy by storing the all-proxy-mcps sentinel in object_permission.mcp_servers instead of enumerating each server one by one. The sentinel expands against the live registry at resolution time, so servers registered later are covered without re-editing the team The expansion lives in MCPServerManager.expand_permission_list, the single chokepoint every key/team/org/end-user permission list flows through, so it works uniformly wherever object_permission.mcp_servers is set, mirroring the existing all-proxy-models pattern for models The dashboard MCP selector gains an exclusive "All Proxy MCPs" option on the team create and edit forms, and the permissions view renders the grant
This commit is contained in:
parent
17bfd415ae
commit
5d02c8f9b4
11 changed files with 277 additions and 19 deletions
|
|
@ -4144,10 +4144,16 @@ class MCPServerManager:
|
|||
emitted so admins can diagnose stale/typo permission entries — the
|
||||
downstream access-check denies them when compared against the
|
||||
concrete request server_id.
|
||||
|
||||
The ``all-proxy-mcps`` sentinel expands to every server in the live
|
||||
registry, so an entity granted it stays in sync as servers are added
|
||||
or removed without re-editing its permission list.
|
||||
"""
|
||||
if not identifiers:
|
||||
return []
|
||||
registry = self.get_registry()
|
||||
if SpecialMCPServerNames.all_proxy_mcp_servers.value in identifiers:
|
||||
return list(registry.keys())
|
||||
expanded: Set[str] = set()
|
||||
for identifier in identifiers:
|
||||
if identifier in registry:
|
||||
|
|
|
|||
|
|
@ -2985,6 +2985,7 @@ class SpecialModelNames(enum.Enum):
|
|||
|
||||
class SpecialMCPServerNames(enum.Enum):
|
||||
no_mcp_servers = "no-mcp-servers"
|
||||
all_proxy_mcp_servers = "all-proxy-mcps"
|
||||
|
||||
|
||||
class SpecialProxyStrings(enum.Enum):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, call as mock_call, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -16,6 +17,8 @@ from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
|
|||
MCPRequestHandler,
|
||||
)
|
||||
from litellm.proxy._types import (
|
||||
LiteLLM_MCPServerTable,
|
||||
MCPTransport,
|
||||
SpecialHeaders,
|
||||
SpecialMCPServerNames,
|
||||
UserAPIKeyAuth,
|
||||
|
|
@ -100,6 +103,66 @@ class TestMCPRequestHandler:
|
|||
mock_key_servers.assert_called_once_with(mock_user_auth)
|
||||
mock_team_servers.assert_called_once_with(mock_user_auth)
|
||||
|
||||
async def test_get_allowed_mcp_servers_for_team_all_proxy_mcps(self):
|
||||
"""A team scoped to the all-proxy-mcps sentinel resolves to every
|
||||
server in the live registry — the smoothest path requested in the
|
||||
ticket, exercised through the real expansion logic."""
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
MCPServerManager,
|
||||
)
|
||||
|
||||
manager = MCPServerManager()
|
||||
for server_id in ("srv-a", "srv-b", "srv-c"):
|
||||
await manager.add_server(
|
||||
LiteLLM_MCPServerTable(
|
||||
server_id=server_id,
|
||||
alias=server_id,
|
||||
description="",
|
||||
url=None,
|
||||
transport=MCPTransport.stdio,
|
||||
command="python",
|
||||
args=["-m", "server"],
|
||||
env={},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
object_permission = MagicMock()
|
||||
object_permission.mcp_servers = [
|
||||
SpecialMCPServerNames.all_proxy_mcp_servers.value
|
||||
]
|
||||
object_permission.mcp_access_groups = []
|
||||
object_permission.mcp_tool_permissions = None
|
||||
|
||||
team_obj = MagicMock()
|
||||
team_obj.object_permission = object_permission
|
||||
team_obj.access_group_ids = []
|
||||
|
||||
user_auth = UserAPIKeyAuth(api_key="k", user_id="u", team_id="t")
|
||||
|
||||
with patch(
|
||||
"litellm.proxy.proxy_server.prisma_client", MagicMock()
|
||||
), patch(
|
||||
"litellm.proxy.auth.auth_checks.get_team_object",
|
||||
AsyncMock(return_value=team_obj),
|
||||
), patch(
|
||||
"litellm.proxy.auth.auth_checks._get_mcp_server_ids_from_access_groups",
|
||||
AsyncMock(return_value=[]),
|
||||
), patch(
|
||||
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager",
|
||||
manager,
|
||||
), patch.object(
|
||||
MCPRequestHandler,
|
||||
"_get_mcp_servers_from_access_groups",
|
||||
AsyncMock(return_value=[]),
|
||||
):
|
||||
result = await MCPRequestHandler._get_allowed_mcp_servers_for_team(
|
||||
user_auth
|
||||
)
|
||||
|
||||
assert sorted(result) == ["srv-a", "srv-b", "srv-c"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"team_servers,key_servers,expected_servers,scenario",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ from litellm.proxy._types import (
|
|||
MCPEnvVar,
|
||||
MCPEnvVarScope,
|
||||
MCPTransport,
|
||||
SpecialMCPServerNames,
|
||||
)
|
||||
from litellm.types.mcp import MCPAuth
|
||||
from litellm.types.mcp_server.mcp_server_manager import MCPOAuthMetadata, MCPServer
|
||||
|
|
@ -111,6 +112,54 @@ class TestMCPServerManager:
|
|||
assert added_server.args == ["-m", "server"]
|
||||
assert added_server.env == {"DEBUG": "1", "TEST": "1"}
|
||||
|
||||
async def test_expand_permission_list_all_proxy_mcps_sentinel(self):
|
||||
"""The all-proxy-mcps sentinel expands to every server in the live
|
||||
registry and re-expands against servers registered later, so a grant
|
||||
stays current without the permission list being re-edited."""
|
||||
manager = MCPServerManager()
|
||||
|
||||
async def _register(server_id: str):
|
||||
await manager.add_server(
|
||||
LiteLLM_MCPServerTable(
|
||||
server_id=server_id,
|
||||
alias=server_id,
|
||||
description="",
|
||||
url=None,
|
||||
transport=MCPTransport.stdio,
|
||||
command="python",
|
||||
args=["-m", "server"],
|
||||
env={},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
await _register("srv-a")
|
||||
await _register("srv-b")
|
||||
|
||||
sentinel = SpecialMCPServerNames.all_proxy_mcp_servers.value
|
||||
|
||||
assert set(manager.expand_permission_list([sentinel])) == {"srv-a", "srv-b"}
|
||||
|
||||
# The sentinel dominates: mixing it with a concrete id still grants all.
|
||||
assert set(manager.expand_permission_list([sentinel, "srv-a"])) == {
|
||||
"srv-a",
|
||||
"srv-b",
|
||||
}
|
||||
|
||||
# A server registered after the grant is included with no re-edit.
|
||||
await _register("srv-c")
|
||||
assert set(manager.expand_permission_list([sentinel])) == {
|
||||
"srv-a",
|
||||
"srv-b",
|
||||
"srv-c",
|
||||
}
|
||||
|
||||
# Without the sentinel, only the named server resolves — proving the
|
||||
# full-registry result above comes from the sentinel branch, not a
|
||||
# blanket "return everything".
|
||||
assert manager.expand_permission_list(["srv-a"]) == ["srv-a"]
|
||||
|
||||
async def test_create_mcp_client_stdio(self):
|
||||
"""Test creating MCP client for stdio transport"""
|
||||
manager = MCPServerManager()
|
||||
|
|
|
|||
|
|
@ -1479,6 +1479,7 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers or access groups (optional)"
|
||||
allowAllProxyMcps
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import MCPServerSelector from "./MCPServerSelector";
|
||||
import { NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants";
|
||||
import { ALL_PROXY_MCPS_SENTINEL, NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/mcpServers/useMCPServers", () => ({
|
||||
useMCPServers: vi.fn(),
|
||||
|
|
@ -98,3 +98,57 @@ describe("MCPServerSelector no-mcp-servers option", () => {
|
|||
expect(optionByValue(NO_MCP_SERVERS_SENTINEL)?.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCPServerSelector all-proxy-mcps option", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseMCPServers.mockReturnValue({
|
||||
data: [{ server_id: "srv-1", server_name: "Server One" }],
|
||||
isLoading: false,
|
||||
} as any);
|
||||
mockUseMCPAccessGroups.mockReturnValue({ data: [], isLoading: false } as any);
|
||||
mockUseMCPToolsets.mockReturnValue({ data: [], isLoading: false } as any);
|
||||
});
|
||||
|
||||
const optionByValue = (value: string) =>
|
||||
Array.from(screen.getByTestId("mcp-select").querySelectorAll("option")).find(
|
||||
(o) => (o as HTMLOptionElement).value === value,
|
||||
) as HTMLOptionElement | undefined;
|
||||
|
||||
it("hides the All Proxy MCPs option by default", () => {
|
||||
renderWithProviders(
|
||||
<MCPServerSelector accessToken="tok" onChange={vi.fn()} value={{ servers: [], accessGroups: [] }} />,
|
||||
);
|
||||
expect(optionByValue(ALL_PROXY_MCPS_SENTINEL)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("emits an exclusive sentinel when All Proxy MCPs is selected", async () => {
|
||||
const onChange = vi.fn();
|
||||
renderWithProviders(
|
||||
<MCPServerSelector
|
||||
accessToken="tok"
|
||||
allowAllProxyMcps
|
||||
onChange={onChange}
|
||||
value={{ servers: ["srv-1"], accessGroups: [] }}
|
||||
/>,
|
||||
);
|
||||
expect(optionByValue(ALL_PROXY_MCPS_SENTINEL)).toBeDefined();
|
||||
|
||||
await userEvent.selectOptions(screen.getByTestId("mcp-select"), [ALL_PROXY_MCPS_SENTINEL]);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ servers: [ALL_PROXY_MCPS_SENTINEL], accessGroups: [], toolsets: [] });
|
||||
});
|
||||
|
||||
it("disables real server options while the sentinel is selected", () => {
|
||||
renderWithProviders(
|
||||
<MCPServerSelector
|
||||
accessToken="tok"
|
||||
allowAllProxyMcps
|
||||
onChange={vi.fn()}
|
||||
value={{ servers: [ALL_PROXY_MCPS_SENTINEL], accessGroups: [] }}
|
||||
/>,
|
||||
);
|
||||
expect(optionByValue("srv-1")?.disabled).toBe(true);
|
||||
expect(optionByValue(ALL_PROXY_MCPS_SENTINEL)?.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"
|
|||
import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolsets";
|
||||
import { Select } from "antd";
|
||||
import React from "react";
|
||||
import { NO_MCP_SERVERS_SENTINEL } from "@/components/mcp_tools/constants";
|
||||
import { ALL_PROXY_MCPS_SENTINEL, NO_MCP_SERVERS_SENTINEL } from "@/components/mcp_tools/constants";
|
||||
|
||||
interface MCPServerSelectorProps {
|
||||
onChange: (selected: { servers: string[]; accessGroups: string[]; toolsets: string[] }) => void;
|
||||
|
|
@ -18,10 +18,30 @@ interface MCPServerSelectorProps {
|
|||
disabled?: boolean;
|
||||
teamId?: string | null;
|
||||
allowNoMcpServers?: boolean;
|
||||
allowAllProxyMcps?: boolean;
|
||||
}
|
||||
|
||||
const TOOLSET_PREFIX = "toolset:";
|
||||
|
||||
type MCPSelection = { servers: string[]; accessGroups: string[]; toolsets: string[] };
|
||||
|
||||
// "No MCP Servers" and "All Proxy MCPs" are mutually exclusive with everything
|
||||
// else. The most restrictive ("block all") wins, mirroring the backend
|
||||
// precedence where the no-mcp-servers sentinel overrides every grant.
|
||||
const resolveExclusiveSelection = (
|
||||
selected: string[],
|
||||
allowNoMcpServers: boolean,
|
||||
allowAllProxyMcps: boolean,
|
||||
): MCPSelection | null => {
|
||||
if (allowNoMcpServers && selected.includes(NO_MCP_SERVERS_SENTINEL)) {
|
||||
return { servers: [NO_MCP_SERVERS_SENTINEL], accessGroups: [], toolsets: [] };
|
||||
}
|
||||
if (allowAllProxyMcps && selected.includes(ALL_PROXY_MCPS_SENTINEL)) {
|
||||
return { servers: [ALL_PROXY_MCPS_SENTINEL], accessGroups: [], toolsets: [] };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
|
|
@ -31,6 +51,7 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
disabled = false,
|
||||
teamId,
|
||||
allowNoMcpServers = false,
|
||||
allowAllProxyMcps = false,
|
||||
}) => {
|
||||
const { data: mcpServers = [], isLoading: serversLoading } = useMCPServers(teamId);
|
||||
const { data: accessGroups = [], isLoading: groupsLoading } = useMCPAccessGroups();
|
||||
|
|
@ -81,12 +102,14 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
];
|
||||
|
||||
const hasNoMcpServersSelected = allowNoMcpServers && selectedValues.includes(NO_MCP_SERVERS_SENTINEL);
|
||||
const hasAllProxyMcpsSelected = allowAllProxyMcps && selectedValues.includes(ALL_PROXY_MCPS_SENTINEL);
|
||||
const hasExclusiveSelected = hasNoMcpServersSelected || hasAllProxyMcpsSelected;
|
||||
|
||||
// Handle selection
|
||||
const handleChange = (selected: string[]) => {
|
||||
// "No MCP Servers" is exclusive: picking it clears everything else.
|
||||
if (allowNoMcpServers && selected.includes(NO_MCP_SERVERS_SENTINEL)) {
|
||||
onChange({ servers: [NO_MCP_SERVERS_SENTINEL], accessGroups: [], toolsets: [] });
|
||||
const exclusive = resolveExclusiveSelection(selected, allowNoMcpServers, allowAllProxyMcps);
|
||||
if (exclusive) {
|
||||
onChange(exclusive);
|
||||
return;
|
||||
}
|
||||
const toolsetsSelected = selected
|
||||
|
|
@ -98,6 +121,39 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
onChange({ servers, accessGroups: accessGroupsSelected, toolsets: toolsetsSelected });
|
||||
};
|
||||
|
||||
const renderExclusiveOptions = () => [
|
||||
...(allowAllProxyMcps
|
||||
? [
|
||||
<Select.Option
|
||||
key={ALL_PROXY_MCPS_SENTINEL}
|
||||
value={ALL_PROXY_MCPS_SENTINEL}
|
||||
label="All Proxy MCPs"
|
||||
disabled={hasNoMcpServersSelected}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ flex: 1 }}>All Proxy MCPs</span>
|
||||
<span style={{ color: "#1890ff", fontSize: "12px", fontWeight: 500, opacity: 0.8 }}>Every server</span>
|
||||
</div>
|
||||
</Select.Option>,
|
||||
]
|
||||
: []),
|
||||
...(allowNoMcpServers
|
||||
? [
|
||||
<Select.Option
|
||||
key={NO_MCP_SERVERS_SENTINEL}
|
||||
value={NO_MCP_SERVERS_SENTINEL}
|
||||
label="No MCP Servers"
|
||||
disabled={hasAllProxyMcpsSelected}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ flex: 1 }}>No MCP Servers</span>
|
||||
<span style={{ color: "#8c8c8c", fontSize: "12px", fontWeight: 500, opacity: 0.8 }}>Block all</span>
|
||||
</div>
|
||||
</Select.Option>,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
|
|
@ -112,21 +168,14 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
style={{ width: "100%" }}
|
||||
disabled={disabled}
|
||||
filterOption={(input, option) => {
|
||||
if (option?.value === NO_MCP_SERVERS_SENTINEL) return true;
|
||||
if (option?.value === NO_MCP_SERVERS_SENTINEL || option?.value === ALL_PROXY_MCPS_SENTINEL) return true;
|
||||
const searchText = options.find((opt) => opt.value === option?.value)?.searchText || "";
|
||||
return searchText.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
>
|
||||
{allowNoMcpServers && (
|
||||
<Select.Option key={NO_MCP_SERVERS_SENTINEL} value={NO_MCP_SERVERS_SENTINEL} label="No MCP Servers">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ flex: 1 }}>No MCP Servers</span>
|
||||
<span style={{ color: "#8c8c8c", fontSize: "12px", fontWeight: 500, opacity: 0.8 }}>Block all</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
)}
|
||||
{renderExclusiveOptions()}
|
||||
{options.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value} label={opt.label} disabled={hasNoMcpServersSelected}>
|
||||
<Select.Option key={opt.value} value={opt.value} label={opt.label} disabled={hasExclusiveSelected}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
// Must match the backend SpecialMCPServerNames.no_mcp_servers enum value.
|
||||
export const NO_MCP_SERVERS_SENTINEL = "no-mcp-servers";
|
||||
|
||||
// Must match the backend SpecialMCPServerNames.all_proxy_mcp_servers enum value.
|
||||
export const ALL_PROXY_MCPS_SENTINEL = "all-proxy-mcps";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|||
import userEvent from "@testing-library/user-event";
|
||||
import MCPServerPermissions from "./MCPServerPermissions";
|
||||
import * as networking from "../networking";
|
||||
import { ALL_PROXY_MCPS_SENTINEL } from "../mcp_tools/constants";
|
||||
|
||||
vi.mock("../networking");
|
||||
|
||||
|
|
@ -248,6 +249,28 @@ describe("MCPServerPermissions", () => {
|
|||
expect(screen.getByText("0")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the all-proxy-mcps grant instead of listing the sentinel as a server", async () => {
|
||||
/**
|
||||
* Tests that the all-proxy-mcps sentinel renders the "every server" grant
|
||||
* state rather than a literal server row, so an admin sees that the team
|
||||
* has all proxy MCPs.
|
||||
*/
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([]);
|
||||
|
||||
render(
|
||||
<MCPServerPermissions
|
||||
mcpServers={[ALL_PROXY_MCPS_SENTINEL]}
|
||||
mcpAccessGroups={[]}
|
||||
mcpToolPermissions={{}}
|
||||
accessToken={mockAccessToken}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/All proxy MCP servers/)).toBeInTheDocument();
|
||||
// The sentinel must not leak into the list as a fake server id.
|
||||
expect(screen.queryByText(ALL_PROXY_MCPS_SENTINEL)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle multiple servers with different tool permissions", async () => {
|
||||
/**
|
||||
* Tests that multiple servers can each have their own tool permissions
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ServerIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/
|
|||
import { Tooltip } from "antd";
|
||||
import { fetchMCPServers, fetchMCPToolsets } from "../networking";
|
||||
import { MCPServer, MCPToolset } from "../mcp_tools/types";
|
||||
import { NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants";
|
||||
import { ALL_PROXY_MCPS_SENTINEL, NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants";
|
||||
|
||||
interface MCPServerPermissionsProps {
|
||||
mcpServers: string[];
|
||||
|
|
@ -96,11 +96,12 @@ export function MCPServerPermissions({
|
|||
};
|
||||
|
||||
const blocksAllMcpServers = mcpServers.includes(NO_MCP_SERVERS_SENTINEL);
|
||||
const grantsAllProxyMcps = mcpServers.includes(ALL_PROXY_MCPS_SENTINEL);
|
||||
|
||||
// Merge servers and access groups into one list
|
||||
const mergedItems = [
|
||||
...mcpServers
|
||||
.filter((server) => server !== NO_MCP_SERVERS_SENTINEL)
|
||||
.filter((server) => server !== NO_MCP_SERVERS_SENTINEL && server !== ALL_PROXY_MCPS_SENTINEL)
|
||||
.map((server) => ({ type: "server", value: server })),
|
||||
...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
|
||||
];
|
||||
|
|
@ -111,8 +112,8 @@ export function MCPServerPermissions({
|
|||
<div className="flex items-center gap-2">
|
||||
<ServerIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">MCP Servers</Text>
|
||||
<Badge color={blocksAllMcpServers ? "red" : "blue"} size="xs">
|
||||
{blocksAllMcpServers ? "Blocked" : totalCount}
|
||||
<Badge color={blocksAllMcpServers ? "red" : grantsAllProxyMcps ? "green" : "blue"} size="xs">
|
||||
{blocksAllMcpServers ? "Blocked" : grantsAllProxyMcps ? "All" : totalCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
|
|
@ -123,6 +124,13 @@ export function MCPServerPermissions({
|
|||
No MCP servers — this key is blocked from all MCP servers, including its team's servers
|
||||
</Text>
|
||||
</div>
|
||||
) : grantsAllProxyMcps ? (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-green-50 border border-green-200">
|
||||
<ServerIcon className="h-4 w-4 text-green-500" />
|
||||
<Text className="text-green-700 text-sm">
|
||||
All proxy MCP servers — access to every MCP server registered on the proxy, including ones added later
|
||||
</Text>
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-1">
|
||||
{mergedItems.map((item, index) => {
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
value={form.getFieldValue("mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers or access groups (optional)"
|
||||
allowAllProxyMcps
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue