diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.test.ts new file mode 100644 index 00000000000..dfc5e33557a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { resolveMCPConnectBaseUrl } from "./mcpConnectBaseUrl"; + +describe("resolveMCPConnectBaseUrl", () => { + const fallbackBaseUrl = "https://runtime.litellm.test"; + + it("uses LITELLM_UI_API_DOC_BASE_URL when provided", () => { + expect( + resolveMCPConnectBaseUrl( + { + LITELLM_UI_API_DOC_BASE_URL: "https://docs.litellm.test", + }, + fallbackBaseUrl, + ), + ).toBe("https://docs.litellm.test"); + }); + + it("prefers LITELLM_UI_API_DOC_BASE_URL over PROXY_BASE_URL", () => { + expect( + resolveMCPConnectBaseUrl( + { + LITELLM_UI_API_DOC_BASE_URL: "https://docs.litellm.test", + PROXY_BASE_URL: "https://proxy.litellm.test", + }, + fallbackBaseUrl, + ), + ).toBe("https://docs.litellm.test"); + }); + + it("falls back to PROXY_BASE_URL when the docs url is missing", () => { + expect( + resolveMCPConnectBaseUrl( + { + PROXY_BASE_URL: "https://proxy.litellm.test", + }, + fallbackBaseUrl, + ), + ).toBe("https://proxy.litellm.test"); + }); + + it("falls back to the runtime base url when settings are empty", () => { + expect( + resolveMCPConnectBaseUrl( + { + LITELLM_UI_API_DOC_BASE_URL: " ", + PROXY_BASE_URL: "", + }, + fallbackBaseUrl, + ), + ).toBe(fallbackBaseUrl); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.ts b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.ts new file mode 100644 index 00000000000..5e250c36db8 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcpConnectBaseUrl.ts @@ -0,0 +1,21 @@ +export interface MCPConnectProxySettings { + PROXY_BASE_URL?: string | null; + LITELLM_UI_API_DOC_BASE_URL?: string | null; +} + +export function resolveMCPConnectBaseUrl( + proxySettings: MCPConnectProxySettings | undefined, + fallbackBaseUrl: string, +): string { + const customDocBaseUrl = proxySettings?.LITELLM_UI_API_DOC_BASE_URL; + if (customDocBaseUrl && customDocBaseUrl.trim()) { + return customDocBaseUrl.trim(); + } + + const proxyBaseUrl = proxySettings?.PROXY_BASE_URL; + if (proxyBaseUrl && proxyBaseUrl.trim()) { + return proxyBaseUrl.trim(); + } + + return fallbackBaseUrl; +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx new file mode 100644 index 00000000000..1e770359242 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx @@ -0,0 +1,31 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import MCPConnect from "./mcp_connect"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => "https://runtime.litellm.test"), +})); + +vi.mock("@/utils/dataUtils", () => ({ + copyToClipboard: vi.fn(), +})); + +describe("MCPConnect", () => { + it("renders connection snippets with the API docs base url when configured", () => { + const apiDocBaseUrl = "https://docs.litellm.test"; + const proxyBaseUrl = "https://proxy.litellm.test"; + + const { container } = render( + , + ); + + expect(container.textContent).toContain(`${apiDocBaseUrl}/mcp`); + expect(container.textContent).toContain(`${apiDocBaseUrl}/v1/responses`); + expect(container.textContent).not.toContain(`${proxyBaseUrl}/mcp`); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx index 7bdfd9c6b8f..9fee32d118b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx @@ -6,6 +6,7 @@ import { TabPanel, TabPanels, TabGroup, TabList, Tab, Title as TremorTitle, Text import { CopyIcon, Code, Terminal, Globe, CheckIcon, ExternalLinkIcon, KeyIcon, ServerIcon, Zap } from "lucide-react"; import { getProxyBaseUrl } from "@/components/networking"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; +import { resolveMCPConnectBaseUrl, type MCPConnectProxySettings } from "./mcpConnectBaseUrl"; const { Title, Text } = Typography; const { Panel } = Collapse; @@ -112,10 +113,11 @@ const FeatureCard: React.FC = ({ interface MCPConnectProps { currentServerAccessGroups?: string[]; + proxySettings?: MCPConnectProxySettings; } -const MCPConnect: React.FC = ({ currentServerAccessGroups = [] }) => { - const proxyBaseUrl = getProxyBaseUrl(); +const MCPConnect: React.FC = ({ currentServerAccessGroups = [], proxySettings }) => { + const proxyBaseUrl = resolveMCPConnectBaseUrl(proxySettings, getProxyBaseUrl()); const [copiedStates, setCopiedStates] = useState>({}); const [serverHeaders, setServerHeaders] = useState>({ openai: [], diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx index f9f3d20ca15..a5cb485fefe 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx @@ -27,6 +27,15 @@ vi.mock("@/components/molecules/notifications_manager", () => ({ }, })); +vi.mock("@/app/(dashboard)/hooks/proxySettings/useProxySettings", () => ({ + __esModule: true, + default: vi.fn(() => ({ + PROXY_BASE_URL: "", + PROXY_LOGOUT_URL: "", + LITELLM_UI_API_DOC_BASE_URL: null, + })), +})); + const createQueryClient = () => new QueryClient({ defaultOptions: { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx index 193246aab3c..a99cc936e9f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx @@ -20,6 +20,7 @@ import React, { useEffect, useState, useMemo, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useMCPServerHealth } from "@/app/(dashboard)/hooks/mcpServers/useMCPServerHealth"; +import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings"; import NotificationsManager from "@/components/molecules/notifications_manager"; import { deleteMCPServer } from "@/components/networking"; import { MCPSubmissionsTab } from "./MCPSubmissionsTab"; @@ -109,6 +110,7 @@ const readToolsOAuthServerId = (): string | null => { const MCPServers: React.FC = ({ accessToken, userRole, userID }) => { const { data: mcpServers, isLoading: isLoadingServers, refetch } = useMCPServers(); + const proxySettings = useProxySettings(accessToken); // Fetch health status for all servers const { @@ -707,7 +709,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) - + {isAdminRole(userRole) && (