From ff39323fff5536e784dbe52fe85c6666a7f53e3e Mon Sep 17 00:00:00 2001
From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
Date: Thu, 25 Jun 2026 07:12:46 -0700
Subject: [PATCH] fix(mcp): OAuth-aware app connections, list proxy, unique
result ids, laid-out iframes
Plumbs OAuth context into app follow-up requests. The app controllers now build a
flowManager and tokenMethods and pass them through readResource, listResources, and
appToolCall to getAppConnection and getConnection, so a cold-recreated connection
(idle timeout, restart, reload) for an OAuth-backed server reuses the user's stored
tokens instead of failing for lack of OAuth context.
Backs the advertised serverResources capability with resource listing. Apps that
feature-detect serverResources can call resources/list, which had no handler. A new
listResources manager method, a POST /api/mcp/resources/list route, and an
onlistresources bridge handler proxy listing the same way reads are proxied.
Makes synthetic and embedded app resource ids unique per result snapshot. The id now
mixes in the tool result content, _meta, and error state alongside the resourceUri,
structuredContent, and arguments, so repeated calls that differ only in those fields
no longer collide and overwrite earlier conversation resources.
Keeps app iframes laid out while waiting for size. The frame is rendered transparent
until a positive size event instead of display:none, with the loading state overlaid,
so an app whose initial auto-resize reports zero is not stuck behind the spinner.
---
api/server/controllers/mcpApps.js | 77 +++++++++++++++---
api/server/routes/mcp.js | 13 ++-
.../Chat/Messages/Content/ToolCall.tsx | 8 +-
.../Messages/Content/UIResourceCarousel.tsx | 6 +-
.../MCPUIResource/MCPUIResource.tsx | 6 +-
client/src/hooks/MCP/useAppBridge.ts | 10 ++-
client/src/utils/mcpApps.ts | 4 +
packages/api/src/mcp/MCPManager.ts | 73 +++++++++++++++++
.../api/src/mcp/__tests__/MCPManager.test.ts | 79 +++++++++++++++++++
packages/api/src/mcp/parsers.ts | 27 +++++--
10 files changed, 274 insertions(+), 29 deletions(-)
diff --git a/api/server/controllers/mcpApps.js b/api/server/controllers/mcpApps.js
index 8a05227ea7..027064123e 100644
--- a/api/server/controllers/mcpApps.js
+++ b/api/server/controllers/mcpApps.js
@@ -1,18 +1,25 @@
const path = require('path');
const { logger } = require('@librechat/data-schemas');
-const { Constants } = require('librechat-data-provider');
+const { CacheKeys, Constants } = require('librechat-data-provider');
const { getUserMCPAuthMap } = require('@librechat/api');
-const { getMCPManager } = require('~/config');
+const { getMCPManager, getFlowStateManager } = require('~/config');
const { resolveConfigServers } = require('~/server/services/MCP');
-const { findPluginAuthsByKeys } = require('~/models');
+const {
+ findPluginAuthsByKeys,
+ findToken,
+ createToken,
+ updateToken,
+ deleteTokens,
+} = require('~/models');
+const { getLogStores } = require('~/cache');
// MCP SDK ErrorCode.InvalidRequest = -32600
const MCP_INVALID_REQUEST = -32600;
/**
- * Resolves the request-scoped config and the user's custom variables for a server so app
- * follow-up requests can connect to config-sourced servers and re-resolve credentialed headers
- * even when the original tool-call connection is gone.
+ * Resolves the request-scoped config, the user's custom variables, and the OAuth flow/token
+ * context for a server so app follow-up requests can connect to config-sourced servers and
+ * re-resolve credentialed or OAuth connections even when the original tool-call connection is gone.
*/
const resolveAppContext = async (req, serverName) => {
const userId = req.user?.id;
@@ -25,7 +32,9 @@ const resolveAppContext = async (req, serverName) => {
.catch(() => undefined),
]);
const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`];
- return { configServers, customUserVars };
+ const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS));
+ const tokenMethods = { findToken, createToken, updateToken, deleteTokens };
+ return { configServers, customUserVars, flowManager, tokenMethods };
};
/** @route POST /api/mcp/resources/read */
@@ -48,7 +57,10 @@ const readMCPResource = async (req, res) => {
}
const mcpManager = getMCPManager();
- const { configServers, customUserVars } = await resolveAppContext(req, serverName);
+ const { configServers, customUserVars, flowManager, tokenMethods } = await resolveAppContext(
+ req,
+ serverName,
+ );
const result = await mcpManager.readResource({
userId,
serverName,
@@ -56,6 +68,8 @@ const readMCPResource = async (req, res) => {
user: req.user,
configServers,
customUserVars,
+ flowManager,
+ tokenMethods,
});
return res.json(result);
} catch (error) {
@@ -64,6 +78,44 @@ const readMCPResource = async (req, res) => {
}
};
+/** @route POST /api/mcp/resources/list */
+const listMCPResources = async (req, res) => {
+ try {
+ const userId = req.user?.id;
+ if (!userId) {
+ return res.status(401).json({ error: 'Unauthorized' });
+ }
+
+ const { serverName, cursor } = req.body;
+ if (!serverName) {
+ return res.status(400).json({ error: 'serverName is required' });
+ }
+ if (cursor !== undefined && typeof cursor !== 'string') {
+ return res.status(400).json({ error: 'cursor must be a string' });
+ }
+
+ const mcpManager = getMCPManager();
+ const { configServers, customUserVars, flowManager, tokenMethods } = await resolveAppContext(
+ req,
+ serverName,
+ );
+ const result = await mcpManager.listResources({
+ userId,
+ serverName,
+ user: req.user,
+ cursor,
+ configServers,
+ customUserVars,
+ flowManager,
+ tokenMethods,
+ });
+ return res.json(result);
+ } catch (error) {
+ logger.error('[listMCPResources] Error:', error);
+ return res.status(500).json({ error: 'Failed to list resources' });
+ }
+};
+
/** @route POST /api/mcp/app-tool-call */
const appToolCall = async (req, res) => {
try {
@@ -85,7 +137,10 @@ const appToolCall = async (req, res) => {
}
const mcpManager = getMCPManager();
- const { configServers, customUserVars } = await resolveAppContext(req, serverName);
+ const { configServers, customUserVars, flowManager, tokenMethods } = await resolveAppContext(
+ req,
+ serverName,
+ );
const result = await mcpManager.appToolCall({
userId,
serverName,
@@ -94,6 +149,8 @@ const appToolCall = async (req, res) => {
user: req.user,
configServers,
customUserVars,
+ flowManager,
+ tokenMethods,
});
return res.json(result);
} catch (error) {
@@ -153,4 +210,4 @@ const serveMCPSandbox = async (_req, res) => {
}
};
-module.exports = { readMCPResource, appToolCall, serveMCPSandbox };
+module.exports = { readMCPResource, listMCPResources, appToolCall, serveMCPSandbox };
diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js
index d4b821388c..3ca4af5c87 100644
--- a/api/server/routes/mcp.js
+++ b/api/server/routes/mcp.js
@@ -31,7 +31,12 @@ const {
getMCPServerById,
getMCPTools,
} = require('~/server/controllers/mcp');
-const { readMCPResource, appToolCall, serveMCPSandbox } = require('~/server/controllers/mcpApps');
+const {
+ readMCPResource,
+ listMCPResources,
+ appToolCall,
+ serveMCPSandbox,
+} = require('~/server/controllers/mcpApps');
const mcpAppToolCallLimiter = require('~/server/middleware/limiters/mcpAppToolCallLimiter');
const {
getOAuthReconnectionManager,
@@ -988,6 +993,12 @@ router.delete(
*/
router.post('/resources/read', requireJwtAuth, checkMCPUsePermissions, readMCPResource);
+/**
+ * List resources available on an MCP server
+ * @route POST /api/mcp/resources/list
+ */
+router.post('/resources/list', requireJwtAuth, checkMCPUsePermissions, listMCPResources);
+
/**
* Proxy tool calls from MCP App iframe to MCP server
* @route POST /api/mcp/app-tool-call
diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx
index a5c6b96e6e..d1015217c1 100644
--- a/client/src/components/Chat/Messages/Content/ToolCall.tsx
+++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx
@@ -76,9 +76,9 @@ const MCPAppView = React.memo(function MCPAppView({
}
return (
-
+
{!loaded && !timedOut && (
-
+