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:
Krrish Dholakia 2026-07-01 02:26:27 +00:00
parent 0d43292707
commit d31b9ba691
8 changed files with 92 additions and 7 deletions

View file

@ -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);
});
});

View file

@ -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={{

View file

@ -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";

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -628,6 +628,7 @@ export function KeyEditView({
accessToken={accessToken || ""}
placeholder="Select MCP servers or access groups (optional)"
allowNoMcpServers
allowAllMcpServers
/>
</Form.Item>