From 8b1d804391ccd74559a84894c17cd91247d0cab5 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:02:42 +0200 Subject: [PATCH] feat: Add OAuth token revocation endpoint and related functionality --- api/server/services/MCP.js | 8 ++-- client/src/components/MCP/MCPConfigDialog.tsx | 4 +- .../components/MCP/MCPServerStatusIcon.tsx | 6 +-- .../MCP/ServerInitializationSection.tsx | 1 + client/src/hooks/MCP/useMCPServerManager.ts | 48 ++++++++++++++++++- packages/data-provider/src/api-endpoints.ts | 4 ++ packages/data-provider/src/data-service.ts | 4 ++ .../src/react-query/react-query-service.ts | 15 ++++++ packages/data-provider/src/types/mutations.ts | 6 +++ 9 files changed, 85 insertions(+), 11 deletions(-) diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 7c26763164..9d237323a4 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -399,10 +399,10 @@ async function getServerConnectionStatus( } } - // return { - // requiresOAuth: oauthServers.has(serverName), - // connectionState: finalConnectionState, - // }; + return { + requiresOAuth, + connectionState: finalConnectionState, + }; } module.exports = { diff --git a/client/src/components/MCP/MCPConfigDialog.tsx b/client/src/components/MCP/MCPConfigDialog.tsx index 5bcd590ec7..04fadc525b 100644 --- a/client/src/components/MCP/MCPConfigDialog.tsx +++ b/client/src/components/MCP/MCPConfigDialog.tsx @@ -128,11 +128,11 @@ export default function MCPConfigDialog({ /> - {/* Server Initialization Section */} + {/* Server Initialization Section - Always show for OAuth servers or when custom vars exist */} 0} + hasCustomUserVars={hasFields} /> diff --git a/client/src/components/MCP/MCPServerStatusIcon.tsx b/client/src/components/MCP/MCPServerStatusIcon.tsx index 69f328a0c8..6ae1d452bc 100644 --- a/client/src/components/MCP/MCPServerStatusIcon.tsx +++ b/client/src/components/MCP/MCPServerStatusIcon.tsx @@ -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 ( ); } - return null; // No config button for connected servers without customUserVars + return null; // No config button for connected servers without customUserVars or OAuth } return null; diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index 0623ba1a21..59b3a6bc41 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -22,6 +22,7 @@ export default function ServerInitializationSection({ initializeServer, connectionStatus, cancelOAuthFlow, + revokeOAuthFlow, isInitializing, isCancellable, getOAuthUrl, diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index 74e9dd7c77..e46b72e75d 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -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, diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index ba0c89d94f..0d0323774a 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -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'; diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index faa62a37da..520d9a5544 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -163,6 +163,10 @@ export function cancelMCPOAuth(serverName: string): Promise { + return request.post(endpoints.revokeMCPOAuth(serverName), {}); +} + /* Config */ export const getStartupConfig = (): Promise< diff --git a/packages/data-provider/src/react-query/react-query-service.ts b/packages/data-provider/src/react-query/react-query-service.ts index a88fb0b4e1..b8dc3cb899 100644 --- a/packages/data-provider/src/react-query/react-query-service.ts +++ b/packages/data-provider/src/react-query/react-query-service.ts @@ -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, ): QueryObserverResult => { diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index 48e34b5d8c..06a012bd7d 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -378,3 +378,9 @@ export interface CancelMCPOAuthResponse { success: boolean; message: string; } + +export interface RevokeMCPOAuthResponse { + success: boolean; + message: string; + serverName: string; +}