feat: Add OAuth token revocation endpoint and related functionality

This commit is contained in:
Marco Beretta 2025-07-30 16:02:42 +02:00
parent f25407768e
commit 8b1d804391
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
9 changed files with 85 additions and 11 deletions

View file

@ -399,10 +399,10 @@ async function getServerConnectionStatus(
}
}
// return {
// requiresOAuth: oauthServers.has(serverName),
// connectionState: finalConnectionState,
// };
return {
requiresOAuth,
connectionState: finalConnectionState,
};
}
module.exports = {

View file

@ -128,11 +128,11 @@ export default function MCPConfigDialog({
/>
</div>
{/* Server Initialization Section */}
{/* Server Initialization Section - Always show for OAuth servers or when custom vars exist */}
<ServerInitializationSection
serverName={serverName}
requiresOAuth={serverStatus?.requiresOAuth || false}
hasCustomUserVars={fieldsSchema && Object.keys(fieldsSchema).length > 0}
hasCustomUserVars={hasFields}
/>
</OGDialogContent>
</OGDialog>

View file

@ -73,8 +73,8 @@ export default function MCPServerStatusIcon({
}
if (connectionState === 'connected') {
// Only show config button if there are customUserVars to configure
if (hasCustomUserVars) {
// Show config button if there are customUserVars to configure OR if it's an OAuth server (for revoke functionality)
if (hasCustomUserVars || requiresOAuth) {
const isAuthenticated = tool?.authenticated || requiresOAuth;
return (
<AuthenticatedStatusIcon
@ -84,7 +84,7 @@ export default function MCPServerStatusIcon({
/>
);
}
return null; // No config button for connected servers without customUserVars
return null; // No config button for connected servers without customUserVars or OAuth
}
return null;

View file

@ -22,6 +22,7 @@ export default function ServerInitializationSection({
initializeServer,
connectionStatus,
cancelOAuthFlow,
revokeOAuthFlow,
isInitializing,
isCancellable,
getOAuthUrl,

View file

@ -1,7 +1,7 @@
import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
import { useToastContext } from '@librechat/client';
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys, dataService } from 'librechat-data-provider';
import {
useCancelMCPOAuthMutation,
useUpdateUserPluginsMutation,
@ -48,6 +48,17 @@ export function useMCPServerManager() {
const reinitializeMutation = useReinitializeMCPServerMutation();
const cancelOAuthMutation = useCancelMCPOAuthMutation();
// Create OAuth revoke mutation using the data service (which includes authentication)
const revokeOAuthMutation = useMutation(
(serverName: string) => dataService.revokeMCPOAuth(serverName),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
queryClient.invalidateQueries([QueryKeys.tools]);
},
},
);
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
onSuccess: async () => {
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
@ -295,6 +306,38 @@ export function useMCPServerManager() {
[queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation],
);
const revokeOAuthFlow = useCallback(
(serverName: string) => {
revokeOAuthMutation.mutate(serverName, {
onSuccess: () => {
cleanupServerState(serverName);
// Remove server from selected values since OAuth tokens are revoked
const currentValues = mcpValues ?? [];
const filteredValues = currentValues.filter((name) => name !== serverName);
setMCPValues(filteredValues);
// Force refresh of connection status to reflect the revoked state
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
queryClient.invalidateQueries([QueryKeys.tools]);
showToast({
message: `OAuth tokens revoked for ${serverName}. You can now re-authenticate.`,
status: 'success',
});
},
onError: (error) => {
console.error(`[MCP Manager] Failed to revoke OAuth for ${serverName}:`, error);
showToast({
message: `Failed to revoke OAuth tokens for ${serverName}`,
status: 'error',
});
},
});
},
[revokeOAuthMutation, cleanupServerState, mcpValues, setMCPValues, showToast, queryClient],
);
const isInitializing = useCallback(
(serverName: string) => {
return serverStates[serverName]?.isInitializing || false;
@ -536,6 +579,7 @@ export function useMCPServerManager() {
connectionStatus,
initializeServer,
cancelOAuthFlow,
revokeOAuthFlow,
isInitializing,
isCancellable,
getOAuthUrl,

View file

@ -144,6 +144,10 @@ export const cancelMCPOAuth = (serverName: string) => {
return `/api/mcp/oauth/cancel/${serverName}`;
};
export const revokeMCPOAuth = (serverName: string) => {
return `/api/mcp/${serverName}/oauth/revoke`;
};
export const config = () => '/api/config';
export const prompts = () => '/api/prompts';

View file

@ -163,6 +163,10 @@ export function cancelMCPOAuth(serverName: string): Promise<m.CancelMCPOAuthResp
return request.post(endpoints.cancelMCPOAuth(serverName), {});
}
export function revokeMCPOAuth(serverName: string): Promise<m.RevokeMCPOAuthResponse> {
return request.post(endpoints.revokeMCPOAuth(serverName), {});
}
/* Config */
export const getStartupConfig = (): Promise<

View file

@ -351,6 +351,21 @@ export const useCancelMCPOAuthMutation = (): UseMutationResult<
});
};
export const useRevokeMCPOAuthMutation = (): UseMutationResult<
m.RevokeMCPOAuthResponse,
unknown,
string,
unknown
> => {
const queryClient = useQueryClient();
return useMutation((serverName: string) => dataService.revokeMCPOAuth(serverName), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
queryClient.invalidateQueries([QueryKeys.tools]);
},
});
};
export const useGetCustomConfigSpeechQuery = (
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {

View file

@ -378,3 +378,9 @@ export interface CancelMCPOAuthResponse {
success: boolean;
message: string;
}
export interface RevokeMCPOAuthResponse {
success: boolean;
message: string;
serverName: string;
}