From 166f54e33cbb9bcdd977fb889c782f20cafef6b9 Mon Sep 17 00:00:00 2001 From: Ace Data Cloud Dev Date: Sun, 18 Oct 2026 16:27:00 +0800 Subject: [PATCH] fix(mcp): wait for discovery token before connection preview --- litellm/proxy/mcp_registry.json | 5 +++ .../CreateMCPServer.integration.test.tsx | 35 +++++++++++++++++-- .../_components/CreateMCPServer.tsx | 24 +++++++++++++ .../mcp_discovery.integration.test.tsx | 1 + .../src/components/mcp_tools/types.tsx | 1 + .../src/hooks/useTestMCPConnection.tsx | 7 +++- 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/mcp_registry.json b/litellm/proxy/mcp_registry.json index ed8c5a28ecf..f12551e798f 100644 --- a/litellm/proxy/mcp_registry.json +++ b/litellm/proxy/mcp_registry.json @@ -439,6 +439,7 @@ "transport": "http", "url": "https://suno.mcp.acedata.cloud/mcp", "auth_type": "bearer_token", + "credential_url": "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_suno_discovery_api_key", "env_vars": [] }, { @@ -451,6 +452,7 @@ "transport": "http", "url": "https://seedance.mcp.acedata.cloud/mcp", "auth_type": "bearer_token", + "credential_url": "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_seedance_discovery_api_key", "env_vars": [] }, { @@ -463,6 +465,7 @@ "transport": "http", "url": "https://happyhorse.mcp.acedata.cloud/mcp", "auth_type": "bearer_token", + "credential_url": "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_happyhorse_discovery_api_key", "env_vars": [] }, { @@ -475,6 +478,7 @@ "transport": "http", "url": "https://openai.mcp.acedata.cloud/mcp", "auth_type": "bearer_token", + "credential_url": "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_gpt_image_discovery_api_key", "env_vars": [] }, { @@ -487,6 +491,7 @@ "transport": "http", "url": "https://nanobanana.mcp.acedata.cloud/mcp", "auth_type": "bearer_token", + "credential_url": "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_nano_banana_discovery_api_key", "env_vars": [] } ] diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx index fb8cbfaf2ab..1e3f5da2508 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx @@ -1932,7 +1932,9 @@ describe("CreateMCPServer", () => { }); describe("when prefillData is provided", () => { - it("opens the credential field for an authenticated discovery entry without supplying a token", async () => { + it("waits for a required discovery token before previewing or adding the server", async () => { + const credentialUrl = + "https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=litellm_suno_discovery_api_key"; render( { transport: "http", url: "https://suno.mcp.acedata.cloud/mcp", auth_type: "bearer_token", + credential_url: credentialUrl, }} />, ); @@ -1954,8 +1957,34 @@ describe("CreateMCPServer", () => { "https://suno.mcp.acedata.cloud/mcp", ); expect(getServerNameInput()).toHaveValue("suno_acedatacloud"); - expect(screen.getByPlaceholderText("Enter token or secret")).toHaveValue(""); - expect(screen.getByPlaceholderText("Enter token or secret")).toHaveAttribute("type", "password"); + const tokenInput = screen.getByPlaceholderText("Enter token or secret"); + expect(tokenInput).toHaveValue(""); + expect(tokenInput).toHaveAttribute("type", "password"); + expect(screen.getByRole("link", { name: "Get your API token" })).toHaveAttribute("href", credentialUrl); + expect(networking.testMCPToolsListRequest).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" })); + expect(await screen.findByText("Enter your API token before adding this server")).toBeInTheDocument(); + expect(networking.createMCPServer).not.toHaveBeenCalled(); + expect(networking.testMCPToolsListRequest).not.toHaveBeenCalled(); + + fireEvent.change(tokenInput, { target: { value: " " } }); + expect(networking.testMCPToolsListRequest).not.toHaveBeenCalled(); + fireEvent.change(tokenInput, { target: { value: "local-test-token" } }); + await waitFor(() => expect(networking.testMCPToolsListRequest).toHaveBeenCalledTimes(1)); + expect(networking.testMCPToolsListRequest).toHaveBeenCalledWith( + "test-token", + expect.objectContaining({ + url: "https://suno.mcp.acedata.cloud/mcp", + auth_type: "bearer_token", + credentials: { auth_value: "local-test-token" }, + }), + null, + ); + + fireEvent.change(tokenInput, { target: { value: "" } }); + expect(tokenInput).toHaveValue(""); + expect(networking.testMCPToolsListRequest).toHaveBeenCalledTimes(1); expect(networking.createMCPServer).not.toHaveBeenCalled(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index ea3782552eb..1347e72a733 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -149,6 +149,9 @@ const CreateMCPServer: React.FC = ({ // section can warn that the saved app may not match the new upstream (the app is kept, not wiped). const [appMayNotMatchUpstream, setAppMayNotMatchUpstream] = useState(false); + const requiresDiscoveryToken = + prefillData?.auth_type === AUTH_TYPE.BEARER_TOKEN && formValues.auth_type === AUTH_TYPE.BEARER_TOKEN; + // Single hook call shared by MCPConnectionStatus and MCPToolConfiguration to avoid duplicate requests. const { tools, @@ -164,6 +167,7 @@ const CreateMCPServer: React.FC = ({ oauthAccessToken, formValues, enabled: true, + requiresAuthValue: requiresDiscoveryToken, }); const authSectionMounted = transportType !== "stdio" && transportType !== ""; @@ -894,8 +898,12 @@ const CreateMCPServer: React.FC = ({ } name={["credentials", "auth_value"]} + required={requiresDiscoveryToken} rules={{ validate: { + ...(requiresDiscoveryToken + ? { required: requiredRule("Enter your API token before adding this server") } + : {}), notWhitespace: notOnlyWhitespace("Authentication value cannot be empty whitespace"), }, }} @@ -910,6 +918,22 @@ const CreateMCPServer: React.FC = ({ )} + {requiresDiscoveryToken && ( +

+ Enter your API token to test this connection. Copy only the token, without Bearer or quotes.{" "} + {prefillData?.credential_url && ( + + Get your API token + + )} +

+ )} + {isOAuthAuthType && ( { const server = registry.servers.find((entry) => entry.title === title)!; expect(onSelectServer).toHaveBeenCalledWith(server); expect(server.auth_type).toBe("bearer_token"); + expect(server.credential_url).toContain("https://platform.acedata.cloud/console/applications?"); expect(screen.queryByText("GitHub")).not.toBeInTheDocument(); }); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index ea109c7a883..2974b106c0c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -563,6 +563,7 @@ export interface DiscoverableMCPServer { registry_url?: string | null; transport: string; auth_type?: "bearer_token" | null; + credential_url?: string | null; url?: string | null; command?: string | null; args?: string[] | null; diff --git a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx index d27e1ca8bf9..0a771dcf24f 100644 --- a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx +++ b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx @@ -27,6 +27,7 @@ interface UseTestMCPConnectionProps { oauthAccessToken?: string | null; formValues: Record; enabled?: boolean; // Optional flag to enable/disable auto-fetching + requiresAuthValue?: boolean; } interface UseTestMCPConnectionReturn { @@ -46,6 +47,7 @@ export const useTestMCPConnection = ({ oauthAccessToken, formValues, enabled = true, + requiresAuthValue = false, }: UseTestMCPConnectionProps): UseTestMCPConnectionReturn => { const [tools, setTools] = useState([]); const [isLoadingTools, setIsLoadingTools] = useState(false); @@ -60,6 +62,8 @@ export const useTestMCPConnection = ({ const requiresOAuthToken = (formValues.auth_type === AUTH_TYPE.OAUTH2 && !isM2MOAuth) || isBrowserHeldTokenMode; const isOpenAPITransport = formValues.transport === TRANSPORT.OPENAPI; const hasEndpoint = isOpenAPITransport ? !!formValues.spec_path : !!formValues.url; + const authValue = formValues.credentials?.auth_value; + const hasRequiredAuthValue = !requiresAuthValue || (typeof authValue === "string" && authValue.trim().length > 0); // For OpenAPI: tools are derived from the spec itself — no auth needed to load them. // For other transports: auth_type is required, and OAuth interactive flows need a token. @@ -67,6 +71,7 @@ export const useTestMCPConnection = ({ ? !!(hasEndpoint && accessToken) : !!( hasEndpoint && + hasRequiredAuthValue && formValues.transport && formValues.auth_type && accessToken && @@ -77,7 +82,7 @@ export const useTestMCPConnection = ({ const credentialsKey = JSON.stringify(formValues.credentials ?? {}); const fetchTools = async () => { - if (!accessToken || (!formValues.url && !formValues.spec_path)) { + if (!accessToken || !hasRequiredAuthValue || (!formValues.url && !formValues.spec_path)) { return; }