This commit is contained in:
Ace Data Cloud Dev 2026-10-27 02:21:48 +00:00 • committed by GitHub
commit ceadc24200
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 185 additions and 0 deletions

View file

@ -428,6 +428,71 @@
{"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",
"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": []
},
{
"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",
"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": []
},
{
"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",
"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": []
},
{
"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",
"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": []
},
{
"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",
"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": []
}
]
}

View file

@ -1932,6 +1932,62 @@ describe("CreateMCPServer", () => {
});
describe("when prefillData is provided", () => {
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(
<CreateMCPServer
{...defaultProps}
prefillData={{
name: "suno_acedatacloud",
title: "Suno by Ace Data Cloud",
description: "Music generation with your own API token",
category: "Media Generation",
transport: "http",
url: "https://suno.mcp.acedata.cloud/mcp",
auth_type: "bearer_token",
credential_url: credentialUrl,
}}
/>,
);
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");
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();
});
it("should populate form fields from discovery data", async () => {
const prefillData = {
name: "github-mcp",

View file

@ -149,6 +149,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
// 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<CreateMCPServerProps> = ({
oauthAccessToken,
formValues,
enabled: true,
requiresAuthValue: requiresDiscoveryToken,
});
const authSectionMounted = transportType !== "stdio" && transportType !== "";
@ -384,6 +388,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
alias: sanitizedName,
description: prefillData.description || "",
transport: transport,
...(prefillData.auth_type === AUTH_TYPE.BEARER_TOKEN ? { auth_type: AUTH_TYPE.BEARER_TOKEN } : {}),
};
if (transport === "stdio") {
@ -893,8 +898,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</span>
}
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"),
},
}}
@ -909,6 +918,22 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</MountedFormField>
)}
{requiresDiscoveryToken && (
<p className="text-sm text-muted-foreground">
Enter your API token to test this connection. Copy only the token, without Bearer or quotes.{" "}
{prefillData?.credential_url && (
<a
href={prefillData.credential_url}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Get your API token
</a>
)}
</p>
)}
{isOAuthAuthType && (
<OAuthFormFields
isM2M={isM2MFlow}

View file

@ -131,6 +131,35 @@ 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(<MCPDiscovery {...defaultProps} onSelectServer={onSelectServer} />);
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(server.credential_url).toContain("https://platform.acedata.cloud/console/applications?");
expect(screen.queryByText("GitHub")).not.toBeInTheDocument();
});
it("offers a custom-server escape hatch", async () => {
const onCustomServer = vi.fn();
render(<MCPDiscovery {...defaultProps} onCustomServer={onCustomServer} />);

View file

@ -562,6 +562,8 @@ export interface DiscoverableMCPServer {
category: string;
registry_url?: string | null;
transport: string;
auth_type?: "bearer_token" | null;
credential_url?: string | null;
url?: string | null;
command?: string | null;
args?: string[] | null;

View file

@ -27,6 +27,7 @@ interface UseTestMCPConnectionProps {
oauthAccessToken?: string | null;
formValues: Record<string, any>;
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<any[]>([]);
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,6 +82,9 @@ export const useTestMCPConnection = ({
const credentialsKey = JSON.stringify(formValues.credentials ?? {});
const fetchTools = async () => {
if (!hasRequiredAuthValue) {
return;
}
if (!accessToken || (!formValues.url && !formValues.spec_path)) {
return;
}