diff --git a/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx b/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx
index 8ea6a99ad6..f43e78ed46 100644
--- a/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx
+++ b/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx
@@ -7,6 +7,8 @@ import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { HeadersTable } from "@/components/ui/headersTable";
import { Input } from "@/components/ui/input";
+import { MultiSelect } from "@/components/ui/multiSelect";
+import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -14,13 +16,14 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { TriStateCheckbox } from "@/components/ui/tristateCheckbox";
import { useToast } from "@/hooks/use-toast";
import { MCP_STATUS_COLORS } from "@/lib/constants/config";
-import { getErrorMessage, useGetCoreConfigQuery, useUpdateMCPClientMutation } from "@/lib/store";
-import { MCPClient } from "@/lib/types/mcp";
+import { getErrorMessage, useGetCoreConfigQuery, useGetVirtualKeysQuery, useUpdateMCPClientMutation } from "@/lib/store";
+import { MCPClient, MCPVKConfig } from "@/lib/types/mcp";
import { mcpClientUpdateSchema, type MCPClientUpdateSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
-import { ChevronDown, ChevronRight, Info } from "lucide-react";
-import { useEffect, useState } from "react";
+import { ChevronDown, ChevronRight, Info, Plus, Trash2 } from "lucide-react";
+import { useDebouncedValue } from "@/hooks/useDebounce";
+import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { CodeEditor } from "@/components/ui/codeEditor";
@@ -47,6 +50,73 @@ export default function MCPClientSheet({ mcpClient, onClose, onSubmitSuccess }:
const { toast } = useToast();
const [expandedTools, setExpandedTools] = useState
>(new Set());
+ // VK access management — search-based dropdown (limit 20), no pagination issue
+ const [vkSearch, setVKSearch] = useState("");
+ const [vkSelectValue, setVKSelectValue] = useState("");
+ const debouncedVkSearch = useDebouncedValue(vkSearch, 300);
+ const { data: vksData } = useGetVirtualKeysQuery({ limit: 20, search: debouncedVkSearch || undefined });
+ const allToolNames = useMemo(() => mcpClient.tools?.map((t) => t.name) ?? [], [mcpClient.tools]);
+
+ // Initial VK configs come directly from the MCP client response — always complete, no pagination issue.
+ const initialVKConfigs = useMemo(
+ () => (mcpClient.vk_configs ?? []).map((vc) => ({ virtual_key_id: vc.virtual_key_id, tools_to_execute: vc.tools_to_execute })),
+ [mcpClient.vk_configs],
+ );
+
+ const [vkConfigs, setVKConfigs] = useState([]);
+ const [vkConfigsDirty, setVKConfigsDirty] = useState(false);
+ // Persists names for newly added VKs so they survive search result changes
+ const [localVKNames, setLocalVKNames] = useState>({});
+
+ // Sync vkConfigs when mcpClient changes
+ useEffect(() => {
+ setVKConfigs(initialVKConfigs);
+ setVKConfigsDirty(false);
+ setLocalVKNames({});
+ }, [initialVKConfigs]);
+
+ // Name lookup: server response names → search results → locally cached names (highest priority)
+ const vkNameByID = useMemo>(() => {
+ const m: Record = {};
+ for (const vc of mcpClient.vk_configs ?? []) m[vc.virtual_key_id] = vc.virtual_key_name;
+ for (const vk of vksData?.virtual_keys ?? []) m[vk.id] = vk.name;
+ Object.assign(m, localVKNames);
+ return m;
+ }, [mcpClient.vk_configs, vksData, localVKNames]);
+
+ const vkOptions = useMemo(
+ () =>
+ (vksData?.virtual_keys ?? [])
+ .filter((vk) => !vkConfigs.some((vc) => vc.virtual_key_id === vk.id))
+ .map((vk) => ({ value: vk.id, label: vk.name })),
+ [vksData, vkConfigs],
+ );
+
+ const toolOptions = useMemo(
+ () => [
+ { value: "*", label: "Allow All Tools", description: "Allow all current and future tools" },
+ ...allToolNames.map((n) => ({ value: n, label: n })),
+ ],
+ [allToolNames],
+ );
+
+ const addVKConfig = (vkId: string) => {
+ const name = vksData?.virtual_keys?.find((vk) => vk.id === vkId)?.name;
+ if (name) setLocalVKNames((prev) => ({ ...prev, [vkId]: name }));
+ setVKConfigs((prev) => [...prev, { virtual_key_id: vkId, tools_to_execute: ["*"] }]);
+ setVKConfigsDirty(true);
+ };
+
+ const removeVKConfig = (vkId: string) => {
+ setVKConfigs((prev) => prev.filter((vc) => vc.virtual_key_id !== vkId));
+ setVKConfigsDirty(true);
+ };
+
+ const updateVKConfigTools = (vkId: string, tools: string[]) => {
+ setVKConfigs((prev) => prev.map((vc) => (vc.virtual_key_id === vkId ? { ...vc, tools_to_execute: tools } : vc)));
+ setVKConfigsDirty(true);
+ };
+
const toggleToolExpanded = (toolName: string) => {
setExpandedTools((prev) => {
const next = new Set(prev);
@@ -104,6 +174,7 @@ export default function MCPClientSheet({ mcpClient, onClose, onSubmitSuccess }:
tool_pricing: data.tool_pricing,
tool_sync_interval: data.tool_sync_interval ?? 0,
allowed_extra_headers: data.allowed_extra_headers,
+ vk_configs: vkConfigsDirty ? vkConfigs : undefined,
},
}).unwrap();
@@ -238,7 +309,7 @@ export default function MCPClientSheet({ mcpClient, onClose, onSubmitSuccess }:
)}
+
+ {mcpClient.tools && mcpClient.tools.length > 0 && (
+