Compare commits
7 Commits
fix/mcp-va
...
feat/revok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b1d804391 | ||
|
|
f25407768e | ||
|
|
8a1a38f346 | ||
|
|
32081245da | ||
|
|
6fd3b569ac | ||
|
|
6671fcb714 | ||
|
|
c4677ab3fb |
@@ -1,10 +1,11 @@
|
||||
const express = require('express');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider');
|
||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
||||
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
||||
const { getProjectByName } = require('~/models/Project');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { getMCPManager } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -102,11 +103,16 @@ router.get('/', async function (req, res) {
|
||||
payload.mcpServers = {};
|
||||
const config = await getCustomConfig();
|
||||
if (config?.mcpServers != null) {
|
||||
const mcpManager = getMCPManager();
|
||||
const oauthServers = mcpManager.getOAuthServers();
|
||||
|
||||
for (const serverName in config.mcpServers) {
|
||||
const serverConfig = config.mcpServers[serverName];
|
||||
payload.mcpServers[serverName] = {
|
||||
customUserVars: serverConfig?.customUserVars || {},
|
||||
chatMenu: serverConfig?.chatMenu,
|
||||
isOAuth: oauthServers.has(serverName),
|
||||
startup: serverConfig?.startup,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
return res.redirect('/oauth/error?error=missing_state');
|
||||
}
|
||||
|
||||
// Extract flow ID from state
|
||||
const flowId = state;
|
||||
logger.debug('[MCP OAuth] Using flow ID from state', { flowId });
|
||||
|
||||
@@ -116,22 +115,17 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
hasCodeVerifier: !!flowState.codeVerifier,
|
||||
});
|
||||
|
||||
// Complete the OAuth flow
|
||||
logger.debug('[MCP OAuth] Completing OAuth flow');
|
||||
const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager);
|
||||
logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route');
|
||||
|
||||
// Try to establish the MCP connection with the new tokens
|
||||
try {
|
||||
const mcpManager = getMCPManager(flowState.userId);
|
||||
logger.debug(`[MCP OAuth] Attempting to reconnect ${serverName} with new OAuth tokens`);
|
||||
|
||||
// For user-level OAuth, try to establish the connection
|
||||
if (flowState.userId !== 'system') {
|
||||
// We need to get the user object - in this case we'll need to reconstruct it
|
||||
const user = { id: flowState.userId };
|
||||
|
||||
// Try to establish connection with the new tokens
|
||||
const userConnection = await mcpManager.getUserConnection({
|
||||
user,
|
||||
serverName,
|
||||
@@ -148,10 +142,8 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
`[MCP OAuth] Successfully reconnected ${serverName} for user ${flowState.userId}`,
|
||||
);
|
||||
|
||||
// Fetch and cache tools now that we have a successful connection
|
||||
const userTools = (await getCachedTools({ userId: flowState.userId })) || {};
|
||||
|
||||
// Remove any old tools from this server in the user's cache
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
@@ -159,7 +151,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new tools from this server
|
||||
const tools = await userConnection.fetchTools();
|
||||
for (const tool of tools) {
|
||||
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||
@@ -173,7 +164,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Save the updated user tool cache
|
||||
await setCachedTools(userTools, { userId: flowState.userId });
|
||||
|
||||
logger.debug(
|
||||
@@ -183,7 +173,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't fail the OAuth callback if reconnection fails - the tokens are still saved
|
||||
logger.warn(
|
||||
`[MCP OAuth] Failed to reconnect ${serverName} after OAuth, but tokens are saved:`,
|
||||
error,
|
||||
@@ -219,7 +208,6 @@ router.get('/oauth/tokens/:flowId', requireJwtAuth, async (req, res) => {
|
||||
return res.status(401).json({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
// Allow system flows or user-owned flows
|
||||
if (!flowId.startsWith(`${user.id}:`) && !flowId.startsWith('system:')) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
@@ -287,11 +275,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
|
||||
|
||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||
const flowManager = getFlowStateManager(flowsCache);
|
||||
|
||||
// Generate the flow ID for this user/server combination
|
||||
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
|
||||
|
||||
// Check if flow exists
|
||||
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
|
||||
|
||||
if (!flowState) {
|
||||
@@ -302,8 +286,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel the flow by marking it as failed
|
||||
await flowManager.completeFlow(flowId, 'mcp_oauth', null, 'User cancelled OAuth flow');
|
||||
await flowManager.failFlow(flowId, 'mcp_oauth', 'User cancelled OAuth flow');
|
||||
|
||||
logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`);
|
||||
|
||||
@@ -317,6 +300,120 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Revoke OAuth tokens for an MCP server
|
||||
* This endpoint revokes OAuth tokens and disconnects the server for a fresh start
|
||||
*/
|
||||
router.post('/:serverName/oauth/revoke', requireJwtAuth, async (req, res) => {
|
||||
try {
|
||||
const { serverName } = req.params;
|
||||
const user = req.user;
|
||||
|
||||
if (!user?.id) {
|
||||
return res.status(401).json({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
logger.info(`[MCP OAuth Revoke] Revoking OAuth tokens for ${serverName} by user ${user.id}`);
|
||||
|
||||
const printConfig = false;
|
||||
const config = await loadCustomConfig(printConfig);
|
||||
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
|
||||
return res.status(404).json({
|
||||
error: `MCP server '${serverName}' not found in configuration`,
|
||||
});
|
||||
}
|
||||
|
||||
const mcpManager = getMCPManager(user.id);
|
||||
|
||||
// Delete OAuth access and refresh tokens
|
||||
const baseIdentifier = `mcp:${serverName}`;
|
||||
|
||||
try {
|
||||
await deleteTokens({ identifier: baseIdentifier });
|
||||
await deleteTokens({ identifier: `${baseIdentifier}:refresh` });
|
||||
logger.info(`[MCP OAuth Revoke] Successfully cleared OAuth tokens for ${serverName}`);
|
||||
} catch (error) {
|
||||
logger.warn(`[MCP OAuth Revoke] Failed to clear OAuth tokens for ${serverName}:`, error);
|
||||
}
|
||||
|
||||
// Disconnect the server and clear all connection state
|
||||
try {
|
||||
await mcpManager.disconnectServer(serverName);
|
||||
logger.info(`[MCP OAuth Revoke] Disconnected server: ${serverName}`);
|
||||
|
||||
// Clear the server from OAuth servers set to prevent it from showing as requiring OAuth
|
||||
if (mcpManager.removeOAuthServer) {
|
||||
mcpManager.removeOAuthServer(serverName);
|
||||
logger.info(`[MCP OAuth Revoke] Removed ${serverName} from OAuth servers set`);
|
||||
} else {
|
||||
// Fallback: clear the OAuth servers set entry manually (should not be needed now)
|
||||
const oauthServers = mcpManager.getOAuthServers();
|
||||
if (oauthServers && oauthServers.has(serverName)) {
|
||||
oauthServers.delete(serverName);
|
||||
logger.info(`[MCP OAuth Revoke] Manually removed ${serverName} from OAuth servers set`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear connection state from both app and user connection maps
|
||||
const appConnections = mcpManager.getAllConnections();
|
||||
const userConnections = mcpManager.getUserConnections(user.id);
|
||||
|
||||
if (appConnections && appConnections.has(serverName)) {
|
||||
appConnections.delete(serverName);
|
||||
logger.info(`[MCP OAuth Revoke] Cleared ${serverName} from app connections`);
|
||||
}
|
||||
|
||||
if (userConnections && userConnections.has(serverName)) {
|
||||
userConnections.delete(serverName);
|
||||
logger.info(`[MCP OAuth Revoke] Cleared ${serverName} from user connections`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[MCP OAuth Revoke] Failed to disconnect server ${serverName}:`, error);
|
||||
}
|
||||
|
||||
// Clear cached tools for this server
|
||||
try {
|
||||
const userTools = (await getCachedTools({ userId: user.id })) || {};
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
delete userTools[key];
|
||||
}
|
||||
}
|
||||
|
||||
await setCachedTools(userTools, { userId: user.id });
|
||||
logger.info(`[MCP OAuth Revoke] Cleared cached tools for ${serverName}`);
|
||||
} catch (error) {
|
||||
logger.warn(`[MCP OAuth Revoke] Failed to clear cached tools for ${serverName}:`, error);
|
||||
}
|
||||
|
||||
// Cancel any pending OAuth flows
|
||||
try {
|
||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||
const flowManager = getFlowStateManager(flowsCache);
|
||||
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
|
||||
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
|
||||
|
||||
if (flowState && flowState.status === 'PENDING') {
|
||||
await flowManager.failFlow(flowId, 'mcp_oauth', 'OAuth tokens revoked by user');
|
||||
logger.info(`[MCP OAuth Revoke] Cancelled pending OAuth flow for ${serverName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[MCP OAuth Revoke] Failed to cancel OAuth flow for ${serverName}:`, error);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `OAuth tokens revoked for ${serverName}. Server can now be re-authenticated.`,
|
||||
serverName,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MCP OAuth Revoke] Unexpected error', error);
|
||||
res.status(500).json({ error: 'Failed to revoke OAuth tokens' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Reinitialize MCP server
|
||||
* This endpoint allows reinitializing a specific MCP server
|
||||
@@ -379,8 +476,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
createToken,
|
||||
deleteTokens,
|
||||
},
|
||||
returnOnOAuth: true, // Return immediately when OAuth is initiated
|
||||
// Add OAuth handlers to capture the OAuth URL when needed
|
||||
returnOnOAuth: true,
|
||||
oauthStart: async (authURL) => {
|
||||
logger.info(`[MCP Reinitialize] OAuth URL received: ${authURL}`);
|
||||
oauthUrl = authURL;
|
||||
@@ -395,7 +491,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[MCP Reinitialize] OAuth state - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
// Check if this is an OAuth error - if so, the flow state should be set up now
|
||||
const isOAuthError =
|
||||
err.message?.includes('OAuth') ||
|
||||
err.message?.includes('authentication') ||
|
||||
@@ -408,7 +503,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[MCP Reinitialize] OAuth required for ${serverName} (isOAuthError: ${isOAuthError}, oauthRequired: ${oauthRequired}, isOAuthFlowInitiated: ${isOAuthFlowInitiated})`,
|
||||
);
|
||||
oauthRequired = true;
|
||||
// Don't return error - continue so frontend can handle OAuth
|
||||
} else {
|
||||
logger.error(
|
||||
`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`,
|
||||
@@ -418,11 +512,9 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch and cache tools if we successfully connected (no OAuth required)
|
||||
if (userConnection && !oauthRequired) {
|
||||
const userTools = (await getCachedTools({ userId: user.id })) || {};
|
||||
|
||||
// Remove any old tools from this server in the user's cache
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
@@ -430,7 +522,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new tools from this server
|
||||
const tools = await userConnection.fetchTools();
|
||||
for (const tool of tools) {
|
||||
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||
@@ -444,7 +535,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Save the updated user tool cache
|
||||
await setCachedTools(userTools, { userId: user.id });
|
||||
}
|
||||
|
||||
@@ -452,11 +542,19 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const getResponseMessage = () => {
|
||||
if (oauthRequired) {
|
||||
return `MCP server '${serverName}' ready for OAuth authentication`;
|
||||
}
|
||||
if (userConnection) {
|
||||
return `MCP server '${serverName}' reinitialized successfully`;
|
||||
}
|
||||
return `Failed to reinitialize MCP server '${serverName}'`;
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: oauthRequired
|
||||
? `MCP server '${serverName}' ready for OAuth authentication`
|
||||
: `MCP server '${serverName}' reinitialized successfully`,
|
||||
success: (userConnection && !oauthRequired) || (oauthRequired && oauthUrl),
|
||||
message: getResponseMessage(),
|
||||
serverName,
|
||||
oauthRequired,
|
||||
oauthUrl,
|
||||
@@ -585,19 +683,16 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
|
||||
const pluginKey = `${Constants.mcp_prefix}${serverName}`;
|
||||
const authValueFlags = {};
|
||||
|
||||
// Check existence of saved values for each custom user variable (don't fetch actual values)
|
||||
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
|
||||
for (const varName of Object.keys(serverConfig.customUserVars)) {
|
||||
try {
|
||||
const value = await getUserPluginAuthValue(user.id, varName, false, pluginKey);
|
||||
// Only store boolean flag indicating if value exists
|
||||
authValueFlags[varName] = !!(value && value.length > 0);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[MCP Auth Value Flags] Error checking ${varName} for user ${user.id}:`,
|
||||
err,
|
||||
);
|
||||
// Default to false if we can't check
|
||||
authValueFlags[varName] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,14 +287,26 @@ async function checkOAuthFlowStatus(userId, serverName) {
|
||||
const flowTTL = flowState.ttl || 180000; // Default 3 minutes
|
||||
|
||||
if (flowState.status === 'FAILED' || flowAge > flowTTL) {
|
||||
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
flowAge,
|
||||
flowTTL,
|
||||
timedOut: flowAge > flowTTL,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: true };
|
||||
const wasCancelled = flowState.error && flowState.error.includes('cancelled');
|
||||
|
||||
if (wasCancelled) {
|
||||
logger.debug(`[MCP Connection Status] Found cancelled OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
error: flowState.error,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: false };
|
||||
} else {
|
||||
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
flowAge,
|
||||
flowTTL,
|
||||
timedOut: flowAge > flowTTL,
|
||||
error: flowState.error,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (flowState.status === 'PENDING') {
|
||||
@@ -327,7 +339,7 @@ async function getServerConnectionStatus(
|
||||
serverName,
|
||||
appConnections,
|
||||
userConnections,
|
||||
oauthServers,
|
||||
_oauthServers,
|
||||
) {
|
||||
const getConnectionState = () =>
|
||||
appConnections.get(serverName)?.connectionState ??
|
||||
@@ -337,7 +349,36 @@ async function getServerConnectionStatus(
|
||||
const baseConnectionState = getConnectionState();
|
||||
let finalConnectionState = baseConnectionState;
|
||||
|
||||
if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) {
|
||||
const mcpManager = getMCPManager(userId);
|
||||
|
||||
const hasOAuthConfig = mcpManager.serverRequiresOAuth(serverName);
|
||||
|
||||
if (hasOAuthConfig) {
|
||||
const baseIdentifier = `mcp:${serverName}`;
|
||||
try {
|
||||
const accessToken = await findToken({ identifier: baseIdentifier });
|
||||
|
||||
if (!accessToken) {
|
||||
// No tokens found, server should be considered disconnected regardless of in-memory state
|
||||
finalConnectionState = 'disconnected';
|
||||
logger.debug(
|
||||
`[Connection Status] No OAuth tokens found for ${serverName}, marking as disconnected`,
|
||||
);
|
||||
} else if (baseConnectionState === 'disconnected') {
|
||||
// Tokens exist but connection shows disconnected, check OAuth flow status
|
||||
const { hasActiveFlow, hasFailedFlow } = await checkOAuthFlowStatus(userId, serverName);
|
||||
|
||||
if (hasFailedFlow) {
|
||||
finalConnectionState = 'error';
|
||||
} else if (hasActiveFlow) {
|
||||
finalConnectionState = 'connecting';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[Connection Status] Error checking tokens for ${serverName}:`, error);
|
||||
finalConnectionState = 'disconnected';
|
||||
}
|
||||
} else if (baseConnectionState === 'disconnected') {
|
||||
const { hasActiveFlow, hasFailedFlow } = await checkOAuthFlowStatus(userId, serverName);
|
||||
|
||||
if (hasFailedFlow) {
|
||||
@@ -347,8 +388,19 @@ async function getServerConnectionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
let requiresOAuth = hasOAuthConfig;
|
||||
if (hasOAuthConfig) {
|
||||
try {
|
||||
const baseIdentifier = `mcp:${serverName}`;
|
||||
const accessToken = await findToken({ identifier: baseIdentifier });
|
||||
requiresOAuth = !accessToken;
|
||||
} catch (_error) {
|
||||
requiresOAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requiresOAuth: oauthServers.has(serverName),
|
||||
requiresOAuth,
|
||||
connectionState: finalConnectionState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ function MCPSelect() {
|
||||
batchToggleServers,
|
||||
getServerStatusIconProps,
|
||||
getConfigDialogProps,
|
||||
isInitializing,
|
||||
localize,
|
||||
} = useMCPServerManager();
|
||||
|
||||
@@ -32,14 +33,20 @@ function MCPSelect() {
|
||||
const renderItemContent = useCallback(
|
||||
(serverName: string, defaultContent: React.ReactNode) => {
|
||||
const statusIconProps = getServerStatusIconProps(serverName);
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
|
||||
// Common wrapper for the main content (check mark + text)
|
||||
// Ensures Check & Text are adjacent and the group takes available space.
|
||||
/**
|
||||
Common wrapper for the main content (check mark + text).
|
||||
Ensures Check & Text are adjacent and the group takes available space.
|
||||
*/
|
||||
const mainContentWrapper = (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-grow items-center rounded bg-transparent p-0 text-left transition-colors focus:outline-none"
|
||||
className={`flex flex-grow items-center rounded bg-transparent p-0 text-left transition-colors focus:outline-none ${
|
||||
isServerInitializing ? 'opacity-50' : ''
|
||||
}`}
|
||||
tabIndex={0}
|
||||
disabled={isServerInitializing}
|
||||
>
|
||||
{defaultContent}
|
||||
</button>
|
||||
@@ -58,15 +65,13 @@ function MCPSelect() {
|
||||
|
||||
return mainContentWrapper;
|
||||
},
|
||||
[getServerStatusIconProps],
|
||||
[getServerStatusIconProps, isInitializing],
|
||||
);
|
||||
|
||||
// Don't render if no servers are selected and not pinned
|
||||
if ((!mcpValues || mcpValues.length === 0) && !isPinned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render if no MCP servers are configured
|
||||
if (!configuredServers || configuredServers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
getConfigDialogProps,
|
||||
isInitializing,
|
||||
} = useMCPServerManager();
|
||||
|
||||
const menuStore = Ariakit.useMenuStore({
|
||||
@@ -86,6 +87,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
{configuredServers.map((serverName) => {
|
||||
const statusIconProps = getServerStatusIconProps(serverName);
|
||||
const isSelected = mcpValues?.includes(serverName) ?? false;
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
|
||||
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
|
||||
|
||||
@@ -96,12 +98,15 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
event.preventDefault();
|
||||
toggleServerSelection(serverName);
|
||||
}}
|
||||
disabled={isServerInitializing}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer',
|
||||
'scroll-m-1 outline-none transition-colors',
|
||||
'hover:bg-black/[0.075] dark:hover:bg-white/10',
|
||||
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
|
||||
'w-full min-w-0 justify-between text-sm',
|
||||
isServerInitializing &&
|
||||
'opacity-50 hover:bg-transparent dark:hover:bg-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
|
||||
@@ -110,13 +110,15 @@ export default function CustomUserVarsSection({
|
||||
|
||||
const handleRevokeClick = () => {
|
||||
onRevoke();
|
||||
// Reset form after revoke
|
||||
reset();
|
||||
};
|
||||
|
||||
// Don't render if no fields to configure
|
||||
if (!fields || Object.keys(fields).length === 0) {
|
||||
return null;
|
||||
return (
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
{localize('com_sidepanel_mcp_no_custom_vars', { '0': serverName })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -128,10 +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={hasFields}
|
||||
/>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,11 +7,13 @@ import { useLocalize } from '~/hooks';
|
||||
interface ServerInitializationSectionProps {
|
||||
serverName: string;
|
||||
requiresOAuth: boolean;
|
||||
hasCustomUserVars?: boolean;
|
||||
}
|
||||
|
||||
export default function ServerInitializationSection({
|
||||
serverName,
|
||||
requiresOAuth,
|
||||
hasCustomUserVars = false,
|
||||
}: ServerInitializationSectionProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
@@ -20,6 +22,7 @@ export default function ServerInitializationSection({
|
||||
initializeServer,
|
||||
connectionStatus,
|
||||
cancelOAuthFlow,
|
||||
revokeOAuthFlow,
|
||||
isInitializing,
|
||||
isCancellable,
|
||||
getOAuthUrl,
|
||||
@@ -32,15 +35,14 @@ export default function ServerInitializationSection({
|
||||
const serverOAuthUrl = getOAuthUrl(serverName);
|
||||
|
||||
const handleInitializeClick = useCallback(() => {
|
||||
initializeServer(serverName);
|
||||
initializeServer(serverName, false);
|
||||
}, [initializeServer, serverName]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
cancelOAuthFlow(serverName);
|
||||
}, [cancelOAuthFlow, serverName]);
|
||||
|
||||
// Show subtle reinitialize option if connected
|
||||
if (isConnected) {
|
||||
if (isConnected && (requiresOAuth || hasCustomUserVars)) {
|
||||
return (
|
||||
<div className="flex justify-start">
|
||||
<button
|
||||
@@ -55,11 +57,15 @@ export default function ServerInitializationSection({
|
||||
);
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[#991b1b] bg-[#2C1315] p-4">
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
{requiresOAuth
|
||||
? localize('com_ui_mcp_not_authenticated', { 0: serverName })
|
||||
: localize('com_ui_mcp_not_initialized', { 0: serverName })}
|
||||
@@ -70,7 +76,7 @@ export default function ServerInitializationSection({
|
||||
<Button
|
||||
onClick={handleInitializeClick}
|
||||
disabled={isServerInitializing}
|
||||
className="flex items-center gap-2 bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
|
||||
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
>
|
||||
{isServerInitializing ? (
|
||||
<>
|
||||
@@ -103,7 +109,7 @@ export default function ServerInitializationSection({
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => window.open(serverOAuthUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="flex-1 bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
|
||||
className="flex-1 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
{localize('com_ui_continue_oauth')}
|
||||
</Button>
|
||||
|
||||
@@ -141,29 +141,31 @@ function MCPPanelContent() {
|
||||
{localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })}
|
||||
</h3>
|
||||
|
||||
{/* Server Initialization Section */}
|
||||
<div className="mb-4">
|
||||
<ServerInitializationSection
|
||||
<CustomUserVarsSection
|
||||
serverName={selectedServerNameForEditing}
|
||||
requiresOAuth={serverStatus?.requiresOAuth || false}
|
||||
fields={serverBeingEdited.config.customUserVars}
|
||||
onSave={(authData) => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigSave(selectedServerNameForEditing, authData);
|
||||
}
|
||||
}}
|
||||
onRevoke={() => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigRevoke(selectedServerNameForEditing);
|
||||
}
|
||||
}}
|
||||
isSubmitting={updateUserPluginsMutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom User Variables Section */}
|
||||
<CustomUserVarsSection
|
||||
<ServerInitializationSection
|
||||
serverName={selectedServerNameForEditing}
|
||||
fields={serverBeingEdited.config.customUserVars}
|
||||
onSave={(authData) => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigSave(selectedServerNameForEditing, authData);
|
||||
}
|
||||
}}
|
||||
onRevoke={() => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigRevoke(selectedServerNameForEditing);
|
||||
}
|
||||
}}
|
||||
isSubmitting={updateUserPluginsMutation.isLoading}
|
||||
requiresOAuth={serverStatus?.requiresOAuth || false}
|
||||
hasCustomUserVars={
|
||||
serverBeingEdited.config.customUserVars &&
|
||||
Object.keys(serverBeingEdited.config.customUserVars).length > 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -55,9 +55,10 @@ export const useUpdateConversationMutation = (
|
||||
return useMutation(
|
||||
(payload: t.TUpdateConversationRequest) => dataService.updateConversation(payload),
|
||||
{
|
||||
onSuccess: (updatedConvo) => {
|
||||
queryClient.setQueryData([QueryKeys.conversation, id], updatedConvo);
|
||||
updateConvoInAllQueries(queryClient, id, () => updatedConvo);
|
||||
onSuccess: (updatedConvo, payload) => {
|
||||
const targetId = payload.conversationId || id;
|
||||
queryClient.setQueryData([QueryKeys.conversation, targetId], updatedConvo);
|
||||
updateConvoInAllQueries(queryClient, targetId, () => updatedConvo);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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,
|
||||
useReinitializeMCPServerMutation,
|
||||
useCancelMCPOAuthMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||
import type { ConfigFieldDetail } from '~/components/MCP/MCPConfigDialog';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import { useBadgeRowContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface ServerState {
|
||||
isInitializing: boolean;
|
||||
@@ -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' });
|
||||
@@ -171,6 +182,7 @@ export function useMCPServerManager() {
|
||||
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
@@ -180,10 +192,15 @@ export function useMCPServerManager() {
|
||||
message: localize('com_ui_mcp_init_failed'),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Error polling server ${serverName}:`, error);
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
}, 3500);
|
||||
|
||||
@@ -201,7 +218,7 @@ export function useMCPServerManager() {
|
||||
);
|
||||
|
||||
const initializeServer = useCallback(
|
||||
async (serverName: string) => {
|
||||
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
||||
updateServerState(serverName, { isInitializing: true });
|
||||
|
||||
try {
|
||||
@@ -216,7 +233,9 @@ export function useMCPServerManager() {
|
||||
isInitializing: true,
|
||||
});
|
||||
|
||||
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
||||
if (autoOpenOAuth) {
|
||||
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
startServerPolling(serverName);
|
||||
} else {
|
||||
@@ -234,6 +253,12 @@ export function useMCPServerManager() {
|
||||
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
} else {
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
||||
@@ -259,18 +284,60 @@ export function useMCPServerManager() {
|
||||
|
||||
const cancelOAuthFlow = useCallback(
|
||||
(serverName: string) => {
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
cleanupServerState(serverName);
|
||||
cancelOAuthMutation.mutate(serverName);
|
||||
cancelOAuthMutation.mutate(serverName, {
|
||||
onSuccess: () => {
|
||||
cleanupServerState(serverName);
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
|
||||
status: 'warning',
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
|
||||
status: 'warning',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(`[MCP Manager] Failed to cancel OAuth for ${serverName}:`, error);
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[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;
|
||||
@@ -303,6 +370,10 @@ export function useMCPServerManager() {
|
||||
const disconnectedServers: string[] = [];
|
||||
|
||||
serverNames.forEach((serverName) => {
|
||||
if (isInitializing(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverStatus = connectionStatus[serverName];
|
||||
if (serverStatus?.connectionState === 'connected') {
|
||||
connectedServers.push(serverName);
|
||||
@@ -317,11 +388,15 @@ export function useMCPServerManager() {
|
||||
initializeServer(serverName);
|
||||
});
|
||||
},
|
||||
[connectionStatus, setMCPValues, initializeServer],
|
||||
[connectionStatus, setMCPValues, initializeServer, isInitializing],
|
||||
);
|
||||
|
||||
const toggleServerSelection = useCallback(
|
||||
(serverName: string) => {
|
||||
if (isInitializing(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValues = mcpValues ?? [];
|
||||
const isCurrentlySelected = currentValues.includes(serverName);
|
||||
|
||||
@@ -337,7 +412,7 @@ export function useMCPServerManager() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[mcpValues, setMCPValues, connectionStatus, initializeServer],
|
||||
[mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing],
|
||||
);
|
||||
|
||||
const handleConfigSave = useCallback(
|
||||
@@ -504,6 +579,7 @@ export function useMCPServerManager() {
|
||||
connectionStatus,
|
||||
initializeServer,
|
||||
cancelOAuthFlow,
|
||||
revokeOAuthFlow,
|
||||
isInitializing,
|
||||
isCancellable,
|
||||
getOAuthUrl,
|
||||
|
||||
@@ -155,7 +155,10 @@ export default function useSideNavLinks({
|
||||
if (
|
||||
startupConfig?.mcpServers &&
|
||||
Object.values(startupConfig.mcpServers).some(
|
||||
(server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
|
||||
(server: any) =>
|
||||
(server.customUserVars && Object.keys(server.customUserVars).length > 0) ||
|
||||
server.isOAuth ||
|
||||
server.startup === false,
|
||||
)
|
||||
) {
|
||||
links.push({
|
||||
|
||||
@@ -506,6 +506,7 @@
|
||||
"com_sidepanel_hide_panel": "Hide Panel",
|
||||
"com_sidepanel_manage_files": "Manage Files",
|
||||
"com_sidepanel_mcp_no_servers_with_vars": "No MCP servers with configurable variables.",
|
||||
"com_sidepanel_mcp_no_custom_vars": "No custom user variables set for {{0}}",
|
||||
"com_sidepanel_mcp_variables_for": "MCP Variables for {{0}}",
|
||||
"com_sidepanel_parameters": "Parameters",
|
||||
"com_sources_image_alt": "Search result image",
|
||||
@@ -851,7 +852,6 @@
|
||||
"com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully",
|
||||
"com_ui_mcp_dialog_desc": "Please enter the necessary information below.",
|
||||
"com_ui_mcp_enter_var": "Enter value for {{0}}",
|
||||
"com_ui_mcp_init_cancelled": "MCP server '{{0}}' initialization was cancelled due to simultaneous request",
|
||||
"com_ui_mcp_init_failed": "Failed to initialize MCP server",
|
||||
"com_ui_mcp_initialize": "Initialize",
|
||||
"com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully",
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -51804,7 +51804,7 @@
|
||||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.902",
|
||||
"version": "0.7.903",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
|
||||
@@ -494,6 +494,9 @@ export class MCPManager {
|
||||
connection.on('oauthRequired', async (data) => {
|
||||
logger.info(`[MCP][User: ${userId}][${serverName}] oauthRequired event received`);
|
||||
|
||||
// Add server to OAuth servers set
|
||||
this.oauthServers.add(serverName);
|
||||
|
||||
// If we just want to initiate OAuth and return, handle it differently
|
||||
if (returnOnOAuth) {
|
||||
try {
|
||||
@@ -1140,4 +1143,17 @@ ${logPrefix} Flow ID: ${newFlowId}
|
||||
public getOAuthServers(): Set<string> {
|
||||
return this.oauthServers;
|
||||
}
|
||||
|
||||
/** Remove a server from OAuth servers set */
|
||||
public removeOAuthServer(serverName: string): void {
|
||||
this.oauthServers.delete(serverName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a server requires OAuth based on configuration
|
||||
*/
|
||||
public serverRequiresOAuth(serverName: string): boolean {
|
||||
const config = this.mcpConfigs[serverName];
|
||||
return !!config?.oauth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import {
|
||||
discoverOAuthMetadata,
|
||||
registerClient,
|
||||
startAuthorization,
|
||||
exchangeAuthorization,
|
||||
discoverAuthorizationServerMetadata,
|
||||
discoverOAuthProtectedResourceMetadata,
|
||||
} from '@modelcontextprotocol/sdk/client/auth.js';
|
||||
import { OAuthMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js';
|
||||
@@ -61,7 +61,7 @@ export class MCPOAuthHandler {
|
||||
|
||||
// Discover OAuth metadata
|
||||
logger.debug(`[MCPOAuth] Discovering OAuth metadata from ${authServerUrl}`);
|
||||
const rawMetadata = await discoverOAuthMetadata(authServerUrl);
|
||||
const rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
||||
|
||||
if (!rawMetadata) {
|
||||
logger.error(`[MCPOAuth] Failed to discover OAuth metadata from ${authServerUrl}`);
|
||||
@@ -466,7 +466,10 @@ export class MCPOAuthHandler {
|
||||
throw new Error('No token URL available for refresh');
|
||||
} else {
|
||||
/** Auto-discover OAuth configuration for refresh */
|
||||
const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl);
|
||||
const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl);
|
||||
if (!oauthMetadata) {
|
||||
throw new Error('Failed to discover OAuth metadata for token refresh');
|
||||
}
|
||||
if (!oauthMetadata.token_endpoint) {
|
||||
throw new Error('No token endpoint found in OAuth metadata');
|
||||
}
|
||||
@@ -584,7 +587,7 @@ export class MCPOAuthHandler {
|
||||
}
|
||||
|
||||
/** Auto-discover OAuth configuration for refresh */
|
||||
const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl);
|
||||
const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl);
|
||||
|
||||
if (!oauthMetadata.token_endpoint) {
|
||||
throw new Error('No token endpoint found in OAuth metadata');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.902",
|
||||
"version": "0.7.903",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -613,6 +613,8 @@ export type TStartupConfig = {
|
||||
}
|
||||
>;
|
||||
chatMenu?: boolean;
|
||||
isOAuth?: boolean;
|
||||
startup?: boolean;
|
||||
}
|
||||
>;
|
||||
mcpPlaceholder?: string;
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -378,3 +378,9 @@ export interface CancelMCPOAuthResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RevokeMCPOAuthResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
serverName: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user