fix(mcp): wait for discovery token before connection preview

This commit is contained in:
Ace Data Cloud Dev 2026-10-18 16:27:00 +08:00
parent c2971654fb
commit 166f54e33c
6 changed files with 69 additions and 4 deletions

View file

@ -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": []
}
]

View file

@ -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(
<CreateMCPServer
{...defaultProps}
@ -1944,6 +1946,7 @@ describe("CreateMCPServer", () => {
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();
});

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 !== "";
@ -894,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"),
},
}}
@ -910,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

@ -156,6 +156,7 @@ describe("MCPDiscovery", () => {
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();
});

View file

@ -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;

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,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;
}