diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 47d15f416a0..34b07ff2475 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1312,7 +1312,7 @@ "count": 1 }, "react-hooks/set-state-in-effect": { - "count": 5 + "count": 6 } }, "src/components/mcp_tools/mcp_connect.tsx": { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 7af8e9bf00a..dbafe41f994 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -32,6 +32,7 @@ interface CreateMCPServerProps { setModalVisible: (visible: boolean) => void; availableAccessGroups: string[]; prefillData?: DiscoverableMCPServer | null; + duplicateServer?: MCPServer | null; onBackToDiscovery?: () => void; } @@ -57,6 +58,7 @@ const CreateMCPServer: React.FC = ({ setModalVisible, availableAccessGroups, prefillData, + duplicateServer, onBackToDiscovery, }) => { const [form] = Form.useForm(); @@ -284,6 +286,98 @@ const CreateMCPServer: React.FC = ({ setAliasManuallyEdited(false); }, [isModalVisible, prefillData, form]); + React.useEffect(() => { + if (!isModalVisible || !duplicateServer) { + return; + } + + const transport = duplicateServer.spec_path + ? TRANSPORT.OPENAPI + : duplicateServer.transport || ""; + setTransportType(transport); + + const copySuffix = "_copy"; + const baseName = duplicateServer.server_name || duplicateServer.alias || ""; + const newName = baseName + copySuffix; + const newAlias = (duplicateServer.alias || baseName) + copySuffix; + + const prefillValues: Record = { + server_name: newName, + alias: newAlias, + description: duplicateServer.description || "", + transport: transport, + auth_type: duplicateServer.auth_type || "none", + mcp_access_groups: duplicateServer.mcp_access_groups || [], + allow_all_keys: duplicateServer.allow_all_keys ?? false, + available_on_public_internet: duplicateServer.available_on_public_internet ?? false, + delegate_auth_to_upstream: duplicateServer.delegate_auth_to_upstream ?? false, + oauth_passthrough: duplicateServer.oauth_passthrough ?? false, + source_url: duplicateServer.source_url || "", + }; + + if (transport === "stdio") { + const stdioObj: Record = {}; + if (duplicateServer.command) stdioObj.command = duplicateServer.command; + if (duplicateServer.args && duplicateServer.args.length > 0) stdioObj.args = duplicateServer.args; + if (duplicateServer.env && Object.keys(duplicateServer.env).length > 0) { + stdioObj.env = duplicateServer.env; + } + if (Object.keys(stdioObj).length > 0) { + prefillValues.stdio_config = JSON.stringify(stdioObj, null, 2); + } + } else if (transport === TRANSPORT.OPENAPI && duplicateServer.spec_path) { + prefillValues.spec_path = duplicateServer.spec_path; + } else if (duplicateServer.url) { + prefillValues.url = duplicateServer.url; + } + + if (duplicateServer.static_headers && Object.keys(duplicateServer.static_headers).length > 0) { + prefillValues.static_headers = Object.entries(duplicateServer.static_headers).map( + ([header, value]) => ({ header, value }) + ); + } + + if (duplicateServer.is_byok) { + prefillValues.is_byok = true; + prefillValues.byok_description = duplicateServer.byok_description || []; + prefillValues.byok_api_key_help_url = duplicateServer.byok_api_key_help_url || ""; + } + + if (duplicateServer.authorization_url) { + prefillValues.authorization_url = duplicateServer.authorization_url; + } + if (duplicateServer.token_url) { + prefillValues.token_url = duplicateServer.token_url; + } + if (duplicateServer.registration_url) { + prefillValues.registration_url = duplicateServer.registration_url; + } + + if (duplicateServer.mcp_info?.logo_url) { + setLogoUrl(duplicateServer.mcp_info.logo_url); + } + + if (duplicateServer.allowed_tools && duplicateServer.allowed_tools.length > 0) { + setAllowedTools(duplicateServer.allowed_tools); + setHasToolAllowlistInteraction(true); + } + + if (duplicateServer.tool_name_to_display_name) { + setToolNameToDisplayName(duplicateServer.tool_name_to_display_name); + } + if (duplicateServer.tool_name_to_description) { + setToolNameToDescription(duplicateServer.tool_name_to_description); + } + + if (duplicateServer.mcp_info?.mcp_server_cost_info) { + setCostConfig(duplicateServer.mcp_info.mcp_server_cost_info); + } + + form.setFieldsValue(prefillValues); + setFormValues(prefillValues); + setAliasManuallyEdited(false); + }, [isModalVisible, duplicateServer, form]); + const handleCreate = async (values: Record) => { setIsLoading(true); try { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx index bc13ed72a8e..c6275f6f04d 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { ColumnDef } from "@tanstack/react-table"; import { MCPServer } from "./types"; import { Icon } from "@tremor/react"; -import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; +import { DuplicateIcon, PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; import { getMaskedAndFullUrl } from "./utils"; import { Tooltip } from "antd"; import { CheckOutlined } from "@ant-design/icons"; @@ -90,6 +90,7 @@ export const mcpServerColumns = ( onByokConnect?: (server: MCPServer) => void, onRecheckHealth?: (serverId: string) => void, recheckingServerIds?: Set, + onDuplicate?: (server: MCPServer) => void, ): ColumnDef[] => [ { accessorKey: "server_id", @@ -307,6 +308,17 @@ export const mcpServerColumns = ( + {onDuplicate && ( + + + + )}