🔌 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:
Dustin Healy 2025-07-21 01:29:33 -07:00
parent b39b60c012
commit 63140237a6
27 changed files with 1760 additions and 286 deletions

View file

@ -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';

View file

@ -606,6 +606,7 @@ export type TStartupConfig = {
description: string;
}
>;
requiresOAuth?: boolean;
}
>;
mcpPlaceholder?: string;

View file

@ -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<

View file

@ -46,6 +46,9 @@ export enum QueryKeys {
health = 'health',
userTerms = 'userTerms',
banner = 'banner',
mcpConnectionStatus = 'mcpConnectionStatus',
mcpAuthValues = 'mcpAuthValues',
mcpOAuthStatus = 'mcpOAuthStatus',
/* Memories */
memories = 'memories',
}

View file

@ -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)

View file

@ -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> => {

View file

@ -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>;

View file

@ -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>;