From c2971654fb8bcf703d4c6a9b18afb01ef2015699 Mon Sep 17 00:00:00 2001 From: Ace Data Cloud Dev Date: Sun, 18 Oct 2026 02:45:21 +0800 Subject: [PATCH 1/3] feat(mcp): discover Ace media services by name --- litellm/proxy/mcp_registry.json | 60 +++++++++++++++++++ .../CreateMCPServer.integration.test.tsx | 27 +++++++++ .../_components/CreateMCPServer.tsx | 1 + .../mcp_discovery.integration.test.tsx | 28 +++++++++ .../src/components/mcp_tools/types.tsx | 1 + 5 files changed, 117 insertions(+) diff --git a/litellm/proxy/mcp_registry.json b/litellm/proxy/mcp_registry.json index 70e81f127c9..ed8c5a28ecf 100644 --- a/litellm/proxy/mcp_registry.json +++ b/litellm/proxy/mcp_registry.json @@ -428,6 +428,66 @@ {"name": "SUPABASE_URL", "description": "Supabase Project URL", "secret": false}, {"name": "SUPABASE_SERVICE_ROLE_KEY", "description": "Supabase Service Role Key", "secret": true} ] + }, + { + "name": "suno_acedatacloud", + "title": "Suno by Ace Data Cloud", + "description": "Generate Suno music, lyrics and covers through Ace Data Cloud. Retrieve the same task until final audio is ready. Requires your own Ace API token and service access.", + "icon_url": "https://raw.githubusercontent.com/AceDataCloud/MCPs/4a2abb23a6f19ea8460c4883dabb5ad378a428b7/suno/vscode/icon.png", + "category": "Media Generation", + "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.AceDataCloud%2Fmcp-suno", + "transport": "http", + "url": "https://suno.mcp.acedata.cloud/mcp", + "auth_type": "bearer_token", + "env_vars": [] + }, + { + "name": "seedance_acedatacloud", + "title": "Seedance by Ace Data Cloud", + "description": "Generate Seedance videos from text or images through Ace Data Cloud. Query the same task for terminal video results. Requires your own Ace API token and service access.", + "icon_url": "https://raw.githubusercontent.com/AceDataCloud/MCPs/4a2abb23a6f19ea8460c4883dabb5ad378a428b7/seedance/vscode/icon.png", + "category": "Media Generation", + "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.AceDataCloud%2Fmcp-seedance", + "transport": "http", + "url": "https://seedance.mcp.acedata.cloud/mcp", + "auth_type": "bearer_token", + "env_vars": [] + }, + { + "name": "happyhorse_acedatacloud", + "title": "HappyHorse by Ace Data Cloud", + "description": "Generate and edit Happy Horse videos through Ace Data Cloud. Query the same task for final video results. Requires your own Ace API token and service access.", + "icon_url": "https://cdn.acedata.cloud/e58fce2811.jpg", + "category": "Media Generation", + "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.AceDataCloud%2Fmcp-happyhorse", + "transport": "http", + "url": "https://happyhorse.mcp.acedata.cloud/mcp", + "auth_type": "bearer_token", + "env_vars": [] + }, + { + "name": "gpt_image_acedatacloud", + "title": "GPT Image by Ace Data Cloud", + "description": "Generate and edit GPT Image images through the Ace Data Cloud OpenAI MCP server, which also exposes chat, embeddings and audio tools. Requires your own Ace API token and service access.", + "icon_url": "https://raw.githubusercontent.com/AceDataCloud/MCPs/4a2abb23a6f19ea8460c4883dabb5ad378a428b7/openai/vscode/icon.png", + "category": "Media Generation", + "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.AceDataCloud%2Fmcp-openai", + "transport": "http", + "url": "https://openai.mcp.acedata.cloud/mcp", + "auth_type": "bearer_token", + "env_vars": [] + }, + { + "name": "nano_banana_acedatacloud", + "title": "Nano Banana by Ace Data Cloud", + "description": "Generate and edit NanoBanana images through Ace Data Cloud. Retrieve the same task for final images. Requires your own Ace API token and service access.", + "icon_url": "https://raw.githubusercontent.com/AceDataCloud/MCPs/4a2abb23a6f19ea8460c4883dabb5ad378a428b7/nanobanana/vscode/icon.png", + "category": "Media Generation", + "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.AceDataCloud%2Fmcp-nanobanana-pro", + "transport": "http", + "url": "https://nanobanana.mcp.acedata.cloud/mcp", + "auth_type": "bearer_token", + "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 65656a053ce..fb8cbfaf2ab 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,6 +1932,33 @@ describe("CreateMCPServer", () => { }); describe("when prefillData is provided", () => { + it("opens the credential field for an authenticated discovery entry without supplying a token", async () => { + render( + , + ); + + expect(await screen.findByText("Authentication Value")).toBeInTheDocument(); + expect(screen.getByRole("combobox", { name: "Authentication", exact: true })).toHaveTextContent("Bearer Token"); + expect(screen.getByPlaceholderText("https://your-mcp-server.com")).toHaveValue( + "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"); + expect(networking.createMCPServer).not.toHaveBeenCalled(); + }); + it("should populate form fields from discovery data", async () => { const prefillData = { name: "github-mcp", 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 07618abb9a4..ea3782552eb 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 @@ -384,6 +384,7 @@ const CreateMCPServer: React.FC = ({ alias: sanitizedName, description: prefillData.description || "", transport: transport, + ...(prefillData.auth_type === AUTH_TYPE.BEARER_TOKEN ? { auth_type: AUTH_TYPE.BEARER_TOKEN } : {}), }; if (transport === "stdio") { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.integration.test.tsx index 02e04a0aa97..4eb3cee4a6f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.integration.test.tsx @@ -131,6 +131,34 @@ describe("MCPDiscovery", () => { expect(onSelectServer).toHaveBeenCalledWith(githubServer); }); + it.each([ + ["suno", "Suno by Ace Data Cloud"], + ["seedance", "Seedance by Ace Data Cloud"], + ["happy horse", "HappyHorse by Ace Data Cloud"], + ["gpt image", "GPT Image by Ace Data Cloud"], + ["nanobanana", "Nano Banana by Ace Data Cloud"], + ])("finds %s from the bundled catalog and selects its authenticated connection", async (query, title) => { + const testDirectory = dirname(fileURLToPath(import.meta.url)); + const registry = JSON.parse( + readFileSync(resolve(testDirectory, "../../../../../../../litellm/proxy/mcp_registry.json"), "utf8"), + ) as { servers: DiscoverableMCPServer[] }; + vi.mocked(fetchDiscoverableMCPServers).mockResolvedValue({ + servers: registry.servers, + categories: [...new Set(registry.servers.map((server) => server.category))], + }); + const onSelectServer = vi.fn(); + renderWithProviders(); + await screen.findByText(title); + + fireEvent.change(screen.getByPlaceholderText("Search servers..."), { target: { value: query } }); + await userEvent.click(await screen.findByText(title)); + + const server = registry.servers.find((entry) => entry.title === title)!; + expect(onSelectServer).toHaveBeenCalledWith(server); + expect(server.auth_type).toBe("bearer_token"); + expect(screen.queryByText("GitHub")).not.toBeInTheDocument(); + }); + it("offers a custom-server escape hatch", async () => { const onCustomServer = vi.fn(); render(); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 22fd64f10d8..ea109c7a883 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -562,6 +562,7 @@ export interface DiscoverableMCPServer { category: string; registry_url?: string | null; transport: string; + auth_type?: "bearer_token" | null; url?: string | null; command?: string | null; args?: string[] | null; 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 2/3] 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; } From 6ed14e006a030cdff76c84564ff4fd4d83d40a01 Mon Sep 17 00:00:00 2001 From: Ace Data Cloud Dev Date: Sun, 18 Oct 2026 16:38:51 +0800 Subject: [PATCH 3/3] fix(mcp): keep token guard within lint condition budget --- ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx index 0a771dcf24f..8ecbb63dcd1 100644 --- a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx +++ b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx @@ -82,7 +82,10 @@ export const useTestMCPConnection = ({ const credentialsKey = JSON.stringify(formValues.credentials ?? {}); const fetchTools = async () => { - if (!accessToken || !hasRequiredAuthValue || (!formValues.url && !formValues.spec_path)) { + if (!hasRequiredAuthValue) { + return; + } + if (!accessToken || (!formValues.url && !formValues.spec_path)) { return; }