mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(ui): add all-mcp-servers option to MCPServerSelector dropdown
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0d43292707
commit
d31b9ba691
8 changed files with 92 additions and 7 deletions
|
|
@ -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_MCP_SERVERS_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-mcp-servers 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 MCP Servers option by default", () => {
|
||||
renderWithProviders(
|
||||
<MCPServerSelector accessToken="tok" onChange={vi.fn()} value={{ servers: [], accessGroups: [] }} />,
|
||||
);
|
||||
expect(optionByValue(ALL_MCP_SERVERS_SENTINEL)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("emits an exclusive sentinel when All MCP Servers is selected", async () => {
|
||||
const onChange = vi.fn();
|
||||
renderWithProviders(
|
||||
<MCPServerSelector
|
||||
accessToken="tok"
|
||||
allowAllMcpServers
|
||||
onChange={onChange}
|
||||
value={{ servers: ["srv-1"], accessGroups: [] }}
|
||||
/>,
|
||||
);
|
||||
expect(optionByValue(ALL_MCP_SERVERS_SENTINEL)).toBeDefined();
|
||||
|
||||
await userEvent.selectOptions(screen.getByTestId("mcp-select"), [ALL_MCP_SERVERS_SENTINEL]);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ servers: [ALL_MCP_SERVERS_SENTINEL], accessGroups: [], toolsets: [] });
|
||||
});
|
||||
|
||||
it("disables real server options while all-mcp-servers is selected", () => {
|
||||
renderWithProviders(
|
||||
<MCPServerSelector
|
||||
accessToken="tok"
|
||||
allowAllMcpServers
|
||||
onChange={vi.fn()}
|
||||
value={{ servers: [ALL_MCP_SERVERS_SENTINEL], accessGroups: [] }}
|
||||
/>,
|
||||
);
|
||||
expect(optionByValue("srv-1")?.disabled).toBe(true);
|
||||
expect(optionByValue(ALL_MCP_SERVERS_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_MCP_SERVERS_SENTINEL, NO_MCP_SERVERS_SENTINEL } from "@/components/mcp_tools/constants";
|
||||
|
||||
interface MCPServerSelectorProps {
|
||||
onChange: (selected: { servers: string[]; accessGroups: string[]; toolsets: string[] }) => void;
|
||||
|
|
@ -18,6 +18,7 @@ interface MCPServerSelectorProps {
|
|||
disabled?: boolean;
|
||||
teamId?: string | null;
|
||||
allowNoMcpServers?: boolean;
|
||||
allowAllMcpServers?: boolean;
|
||||
}
|
||||
|
||||
const TOOLSET_PREFIX = "toolset:";
|
||||
|
|
@ -31,6 +32,7 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
disabled = false,
|
||||
teamId,
|
||||
allowNoMcpServers = false,
|
||||
allowAllMcpServers = false,
|
||||
}) => {
|
||||
const { data: mcpServers = [], isLoading: serversLoading } = useMCPServers(teamId);
|
||||
const { data: accessGroups = [], isLoading: groupsLoading } = useMCPAccessGroups();
|
||||
|
|
@ -81,10 +83,15 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
];
|
||||
|
||||
const hasNoMcpServersSelected = allowNoMcpServers && selectedValues.includes(NO_MCP_SERVERS_SENTINEL);
|
||||
const hasAllMcpServersSelected = allowAllMcpServers && selectedValues.includes(ALL_MCP_SERVERS_SENTINEL);
|
||||
const hasSentinelSelected = hasNoMcpServersSelected || hasAllMcpServersSelected;
|
||||
|
||||
// Handle selection
|
||||
const handleChange = (selected: string[]) => {
|
||||
// "No MCP Servers" is exclusive: picking it clears everything else.
|
||||
if (allowAllMcpServers && selected.includes(ALL_MCP_SERVERS_SENTINEL)) {
|
||||
onChange({ servers: [ALL_MCP_SERVERS_SENTINEL], accessGroups: [], toolsets: [] });
|
||||
return;
|
||||
}
|
||||
if (allowNoMcpServers && selected.includes(NO_MCP_SERVERS_SENTINEL)) {
|
||||
onChange({ servers: [NO_MCP_SERVERS_SENTINEL], accessGroups: [], toolsets: [] });
|
||||
return;
|
||||
|
|
@ -112,13 +119,31 @@ 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_MCP_SERVERS_SENTINEL) return true;
|
||||
const searchText = options.find((opt) => opt.value === option?.value)?.searchText || "";
|
||||
return searchText.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
>
|
||||
{allowAllMcpServers && (
|
||||
<Select.Option
|
||||
key={ALL_MCP_SERVERS_SENTINEL}
|
||||
value={ALL_MCP_SERVERS_SENTINEL}
|
||||
label="All MCP Servers"
|
||||
disabled={hasNoMcpServersSelected}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span style={{ flex: 1 }}>All MCP Servers</span>
|
||||
<span style={{ color: "#52c41a", fontSize: "12px", fontWeight: 500, opacity: 0.8 }}>Grant all</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
)}
|
||||
{allowNoMcpServers && (
|
||||
<Select.Option key={NO_MCP_SERVERS_SENTINEL} value={NO_MCP_SERVERS_SENTINEL} label="No MCP Servers">
|
||||
<Select.Option
|
||||
key={NO_MCP_SERVERS_SENTINEL}
|
||||
value={NO_MCP_SERVERS_SENTINEL}
|
||||
label="No MCP Servers"
|
||||
disabled={hasAllMcpServersSelected}
|
||||
>
|
||||
<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>
|
||||
|
|
@ -126,7 +151,7 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
|||
</Select.Option>
|
||||
)}
|
||||
{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={hasSentinelSelected}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
// Must match the backend SpecialMCPServerNames.no_mcp_servers enum value.
|
||||
// Must match the backend SpecialMCPServerNames enum values.
|
||||
export const NO_MCP_SERVERS_SENTINEL = "no-mcp-servers";
|
||||
export const ALL_MCP_SERVERS_SENTINEL = "all-mcp-servers";
|
||||
|
|
|
|||
|
|
@ -1402,6 +1402,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
teamId={selectedCreateKeyTeam?.team_id ?? null}
|
||||
placeholder="Select MCP servers or access groups (optional)"
|
||||
allowNoMcpServers
|
||||
allowAllMcpServers
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
|
|
@ -434,6 +434,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
|||
value={form.getFieldValue("mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers and access groups"
|
||||
allowAllMcpServers
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
|
|
@ -510,6 +510,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers and access groups (optional)"
|
||||
allowAllMcpServers
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
allowAllMcpServers
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
|
|
@ -628,6 +628,7 @@ export function KeyEditView({
|
|||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers or access groups (optional)"
|
||||
allowNoMcpServers
|
||||
allowAllMcpServers
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue