mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-28 06:08:50 +01:00
🔌 feat: MCP OAuth Integration in Chat UI
- **Real-Time Connection Status**: New backend APIs and React Query hooks provide live MCP server connection monitoring with automatic UI updates - **OAuth Flow Components**: Complete MCPConfigDialog, ServerInitializationSection, and CustomUserVarsSection with OAuth URL handling and polling-based completion - **Enhanced Server Selection**: MCPSelect component with connection-aware filtering, visual status indicators, and better credential management UX (still needs a lot of refinement since there is bloat/unused vars and functions leftover from the ideation phase on how to approach OAuth and connection statuses)
This commit is contained in:
parent
b39b60c012
commit
63140237a6
27 changed files with 1760 additions and 286 deletions
|
|
@ -134,6 +134,15 @@ export const plugins = () => '/api/plugins';
|
|||
|
||||
export const mcpReinitialize = (serverName: string) => `/api/mcp/${serverName}/reinitialize`;
|
||||
|
||||
export const mcpReinitializeComplete = (serverName: string) =>
|
||||
`/api/mcp/${serverName}/reinitialize/complete`;
|
||||
|
||||
export const mcpConnectionStatus = () => '/api/mcp/connection/status';
|
||||
|
||||
export const mcpAuthValues = (serverName: string) => `/api/mcp/${serverName}/auth-values`;
|
||||
|
||||
export const mcpOAuthStatus = (flowId: string) => `/api/mcp/oauth/status/${flowId}`;
|
||||
|
||||
export const config = () => '/api/config';
|
||||
|
||||
export const prompts = () => '/api/prompts';
|
||||
|
|
|
|||
|
|
@ -606,6 +606,7 @@ export type TStartupConfig = {
|
|||
description: string;
|
||||
}
|
||||
>;
|
||||
requiresOAuth?: boolean;
|
||||
}
|
||||
>;
|
||||
mcpPlaceholder?: string;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,26 @@ export const reinitializeMCPServer = (serverName: string) => {
|
|||
return request.post(endpoints.mcpReinitialize(serverName));
|
||||
};
|
||||
|
||||
export const completeMCPServerReinitialize = (serverName: string) => {
|
||||
return request.post(endpoints.mcpReinitializeComplete(serverName));
|
||||
};
|
||||
|
||||
export const getMCPConnectionStatus = (): Promise<t.TMCPConnectionStatusResponse> => {
|
||||
return request.get(endpoints.mcpConnectionStatus());
|
||||
};
|
||||
|
||||
export const getMCPAuthValues = (
|
||||
serverName: string,
|
||||
): Promise<{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> }> => {
|
||||
return request.get(endpoints.mcpAuthValues(serverName));
|
||||
};
|
||||
|
||||
export const getMCPOAuthStatus = (
|
||||
flowId: string,
|
||||
): Promise<{ status: string; completed: boolean; failed: boolean; error?: string }> => {
|
||||
return request.get(endpoints.mcpOAuthStatus(flowId));
|
||||
};
|
||||
|
||||
/* Config */
|
||||
|
||||
export const getStartupConfig = (): Promise<
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ export enum QueryKeys {
|
|||
health = 'health',
|
||||
userTerms = 'userTerms',
|
||||
banner = 'banner',
|
||||
mcpConnectionStatus = 'mcpConnectionStatus',
|
||||
mcpAuthValues = 'mcpAuthValues',
|
||||
mcpOAuthStatus = 'mcpOAuthStatus',
|
||||
/* Memories */
|
||||
memories = 'memories',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ const BaseOptionsSchema = z.object({
|
|||
initTimeout: z.number().optional(),
|
||||
/** Controls visibility in chat dropdown menu (MCPSelect) */
|
||||
chatMenu: z.boolean().optional(),
|
||||
/**
|
||||
* Controls whether the MCP server should be initialized on startup
|
||||
* - true: Initialize on startup (default)
|
||||
* - false: Skip initialization on startup (can be initialized later)
|
||||
*/
|
||||
startup: z.boolean().optional(),
|
||||
/**
|
||||
* Controls server instruction behavior:
|
||||
* - undefined/not set: No instructions included (default)
|
||||
|
|
|
|||
|
|
@ -311,13 +311,22 @@ export const useUpdateUserPluginsMutation = (
|
|||
...options,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries([QueryKeys.user]);
|
||||
queryClient.refetchQueries([QueryKeys.tools]);
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useReinitializeMCPServerMutation = (): UseMutationResult<
|
||||
{ success: boolean; message: string; serverName: string },
|
||||
{
|
||||
success: boolean;
|
||||
message: string;
|
||||
serverName: string;
|
||||
oauthRequired?: boolean;
|
||||
oauthCompleted?: boolean;
|
||||
authURL?: string;
|
||||
flowId?: string;
|
||||
},
|
||||
unknown,
|
||||
string,
|
||||
unknown
|
||||
|
|
@ -330,6 +339,54 @@ export const useReinitializeMCPServerMutation = (): UseMutationResult<
|
|||
});
|
||||
};
|
||||
|
||||
export const useCompleteMCPServerReinitializeMutation = (): UseMutationResult<
|
||||
{
|
||||
success: boolean;
|
||||
message: string;
|
||||
serverName: string;
|
||||
},
|
||||
unknown,
|
||||
string,
|
||||
unknown
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(serverName: string) => dataService.completeMCPServerReinitialize(serverName),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries([QueryKeys.tools]);
|
||||
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useMCPOAuthStatusQuery = (
|
||||
flowId: string,
|
||||
config?: UseQueryOptions<
|
||||
{ status: string; completed: boolean; failed: boolean; error?: string },
|
||||
unknown,
|
||||
{ status: string; completed: boolean; failed: boolean; error?: string }
|
||||
>,
|
||||
): QueryObserverResult<
|
||||
{ status: string; completed: boolean; failed: boolean; error?: string },
|
||||
unknown
|
||||
> => {
|
||||
return useQuery<
|
||||
{ status: string; completed: boolean; failed: boolean; error?: string },
|
||||
unknown,
|
||||
{ status: string; completed: boolean; failed: boolean; error?: string }
|
||||
>([QueryKeys.mcpOAuthStatus, flowId], () => dataService.getMCPOAuthStatus(flowId), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: true,
|
||||
staleTime: 1000, // Consider data stale after 1 second for polling
|
||||
enabled: !!flowId,
|
||||
refetchInterval: flowId ? 2000 : false, // Poll every 2 seconds when OAuth is active
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCustomConfigSpeechQuery = (
|
||||
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
|
||||
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ export const tPluginAuthConfigSchema = z.object({
|
|||
authField: z.string(),
|
||||
label: z.string(),
|
||||
description: z.string(),
|
||||
requiresOAuth: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TPluginAuthConfig = z.infer<typeof tPluginAuthConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -632,3 +632,14 @@ export type TBalanceResponse = {
|
|||
lastRefill?: Date;
|
||||
refillAmount?: number;
|
||||
};
|
||||
|
||||
export type TMCPConnectionStatus = {
|
||||
connected: boolean;
|
||||
hasAuthConfig: boolean;
|
||||
hasConnection: boolean;
|
||||
isAppLevel: boolean;
|
||||
isUserLevel: boolean;
|
||||
requiresOAuth: boolean;
|
||||
};
|
||||
|
||||
export type TMCPConnectionStatusResponse = Record<string, TMCPConnectionStatus>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue