mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): preview tools with a staged interactive OAuth token in the edit form
For authorization_code the edit preview listed tools by server_id only, relying on the stored per-user DB credential, so a token authorized in the edit session gave an empty preview until the admin saved; the create form previews the identical state through the config-based preview endpoint, which takes the token explicitly. The edit fetch now routes through that same endpoint when a staged interactive token is held, built from the form values with the saved record as fallback, and keeps the by-server_id listing for every other case
This commit is contained in:
parent
b304620311
commit
71e0491d37
2 changed files with 77 additions and 1 deletions
|
|
@ -10,6 +10,7 @@ vi.mock("../networking", () => ({
|
|||
updateMCPServer: vi.fn(),
|
||||
listMCPTools: vi.fn().mockResolvedValue({ tools: [], error: null }),
|
||||
storeMCPOAuthUserCredential: vi.fn().mockResolvedValue({}),
|
||||
testMCPToolsListRequest: vi.fn().mockResolvedValue({ tools: [], error: null }),
|
||||
}));
|
||||
|
||||
vi.mock("../molecules/notifications_manager", () => ({
|
||||
|
|
@ -511,6 +512,30 @@ describe("MCPServerEdit OAuth token invalidation", () => {
|
|||
expect(mockRemoveToken).toHaveBeenCalledWith("oauth_server_1", undefined);
|
||||
});
|
||||
|
||||
it("previews tools with a staged interactive OAuth token before it is saved", async () => {
|
||||
// Regression: for authorization_code the fetch went by server_id only, relying on the stored DB
|
||||
// credential, so a token authorized in this edit session gave an empty preview until the admin
|
||||
// saved; the create form previews the identical state via the config-based preview endpoint.
|
||||
mockOauth.tokenResponse = { access_token: "staged-obo-tok" };
|
||||
|
||||
renderOAuthEdit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(networking.testMCPToolsListRequest)).toHaveBeenCalledWith(
|
||||
"access-token",
|
||||
expect.objectContaining({ server_id: "oauth_server_1", url: "https://example.com/mcp" }),
|
||||
"staged-obo-tok",
|
||||
);
|
||||
});
|
||||
expect(networking.listMCPTools).not.toHaveBeenCalled();
|
||||
// Previewing must stay stateless: the staged token is committed only by an explicit Save
|
||||
// (storeMCPOAuthUserCredential for authorization_code, setToken for the client-forwarded modes).
|
||||
expect(networking.storeMCPOAuthUserCredential).not.toHaveBeenCalled();
|
||||
expect(mockSetToken).not.toHaveBeenCalled();
|
||||
expect(networking.updateMCPServer).not.toHaveBeenCalled();
|
||||
mockOauth.tokenResponse = null;
|
||||
});
|
||||
|
||||
it("keeps the admin's in-flight endpoint edits when the token is invalidated", async () => {
|
||||
// Regression: invalidation used to form.resetFields the endpoint fields; with the edit Form's
|
||||
// initialValues that silently reverted an admin-corrected token_url back to the saved (wrong)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
getMcpOAuthMode,
|
||||
oauth2FlowToFormValue,
|
||||
} from "./types";
|
||||
import { updateMCPServer, listMCPTools, storeMCPOAuthUserCredential } from "../networking";
|
||||
import { updateMCPServer, listMCPTools, storeMCPOAuthUserCredential, testMCPToolsListRequest } from "../networking";
|
||||
import { getToken, isTokenValid, removeToken, setToken } from "@/utils/mcpTokenStore";
|
||||
import { buildMcpPassthroughAuthHeader } from "@/utils/mcpHeaderUtils";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
|
|
@ -421,6 +421,53 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// A token authorized in this edit session for interactive OAuth (authorization_code) is only
|
||||
// committed to the DB on save, so a plain by-server_id listing cannot use it and the preview would
|
||||
// stay empty until the admin saves; the create form previews the identical state through the
|
||||
// config-based preview endpoint, which takes the staged token explicitly. Returns false when there
|
||||
// is no staged interactive token so fetchTools falls through to the by-server_id listing.
|
||||
const previewWithStagedInteractiveToken = async (
|
||||
isPassthrough: boolean,
|
||||
isBrowserHeldTokenMode: boolean,
|
||||
): Promise<boolean> => {
|
||||
const stagedToken =
|
||||
!isPassthrough && !isBrowserHeldTokenMode && getEffectiveAuthType() === AUTH_TYPE.OAUTH2
|
||||
? oauthTokenResponse?.access_token
|
||||
: undefined;
|
||||
if (!stagedToken) {
|
||||
return false;
|
||||
}
|
||||
setIsLoadingTools(true);
|
||||
setToolsError(null);
|
||||
try {
|
||||
const values = form.getFieldsValue(true);
|
||||
const rawTransport = values.transport || mcpServer.transport;
|
||||
const previewConfig = {
|
||||
server_id: mcpServer.server_id,
|
||||
server_name: values.server_name || mcpServer.server_name || mcpServer.alias,
|
||||
url: values.url || mcpServer.url,
|
||||
transport: rawTransport === TRANSPORT.OPENAPI ? TRANSPORT.HTTP : rawTransport,
|
||||
auth_type: AUTH_TYPE.OAUTH2,
|
||||
authorization_url: values.authorization_url,
|
||||
token_url: values.token_url,
|
||||
registration_url: values.registration_url,
|
||||
};
|
||||
const toolsResponse = await testMCPToolsListRequest(accessToken, previewConfig, stagedToken);
|
||||
if (toolsResponse.tools && !toolsResponse.error) {
|
||||
setTools(toolsResponse.tools);
|
||||
} else {
|
||||
setTools([]);
|
||||
setToolsError(toolsResponse.message || "Failed to load tools");
|
||||
}
|
||||
} catch (error) {
|
||||
setTools([]);
|
||||
setToolsError(error instanceof Error ? error.message : "Failed to load tools");
|
||||
} finally {
|
||||
setIsLoadingTools(false);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const fetchTools = async () => {
|
||||
if (!accessToken || !mcpServer.server_id) return;
|
||||
|
||||
|
|
@ -436,6 +483,10 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
delegate_auth_to_upstream: mcpServer.delegate_auth_to_upstream,
|
||||
}) === "passthrough";
|
||||
const isBrowserHeldTokenMode = isClientForwardedTokenMode(getEffectiveAuthType());
|
||||
|
||||
if (await previewWithStagedInteractiveToken(isPassthrough, isBrowserHeldTokenMode)) {
|
||||
return;
|
||||
}
|
||||
if (isPassthrough || isBrowserHeldTokenMode) {
|
||||
const token =
|
||||
oauthTokenResponse?.access_token ??
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue