mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
fix(mcp): wait for discovery token before connection preview
This commit is contained in:
parent
c2971654fb
commit
166f54e33c
6 changed files with 69 additions and 4 deletions
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue