mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-02 12:22:22 +00:00
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.
213 lines
7.1 KiB
JavaScript
213 lines
7.1 KiB
JavaScript
const path = require('path');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { CacheKeys, Constants } = require('librechat-data-provider');
|
|
const { getUserMCPAuthMap } = require('@librechat/api');
|
|
const { getMCPManager, getFlowStateManager } = require('~/config');
|
|
const { resolveConfigServers } = require('~/server/services/MCP');
|
|
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, 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;
|
|
const [configServers, userMCPAuthMap] = await Promise.all([
|
|
Promise.resolve()
|
|
.then(() => resolveConfigServers(req))
|
|
.catch(() => undefined),
|
|
Promise.resolve()
|
|
.then(() => getUserMCPAuthMap({ userId, servers: [serverName], findPluginAuthsByKeys }))
|
|
.catch(() => undefined),
|
|
]);
|
|
const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`];
|
|
const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS));
|
|
const tokenMethods = { findToken, createToken, updateToken, deleteTokens };
|
|
return { configServers, customUserVars, flowManager, tokenMethods };
|
|
};
|
|
|
|
/** @route POST /api/mcp/resources/read */
|
|
const readMCPResource = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
}
|
|
|
|
const { serverName, uri } = req.body;
|
|
if (!serverName || !uri) {
|
|
return res.status(400).json({ error: 'serverName and uri are required' });
|
|
}
|
|
// The serverResources capability lets an app read any resource the connected MCP server
|
|
// exposes (ui:// templates plus supporting data such as file:// or custom schemes), so the
|
|
// proxy only requires a non-empty string and leaves resource authorization to the server.
|
|
if (typeof uri !== 'string' || uri.length === 0) {
|
|
return res.status(400).json({ error: 'uri must be a non-empty string' });
|
|
}
|
|
|
|
const mcpManager = getMCPManager();
|
|
const { configServers, customUserVars, flowManager, tokenMethods } = await resolveAppContext(
|
|
req,
|
|
serverName,
|
|
);
|
|
const result = await mcpManager.readResource({
|
|
userId,
|
|
serverName,
|
|
uri,
|
|
user: req.user,
|
|
configServers,
|
|
customUserVars,
|
|
flowManager,
|
|
tokenMethods,
|
|
});
|
|
return res.json(result);
|
|
} catch (error) {
|
|
logger.error('[readMCPResource] Error:', error);
|
|
return res.status(500).json({ error: 'Failed to read resource' });
|
|
}
|
|
};
|
|
|
|
/** @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 {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
}
|
|
|
|
const { serverName, toolName, arguments: toolArgs } = req.body;
|
|
if (!serverName || !toolName) {
|
|
return res.status(400).json({ error: 'serverName and toolName are required' });
|
|
}
|
|
if (
|
|
toolArgs !== undefined &&
|
|
toolArgs !== null &&
|
|
(typeof toolArgs !== 'object' || Array.isArray(toolArgs))
|
|
) {
|
|
return res.status(400).json({ error: 'arguments must be an object' });
|
|
}
|
|
|
|
const mcpManager = getMCPManager();
|
|
const { configServers, customUserVars, flowManager, tokenMethods } = await resolveAppContext(
|
|
req,
|
|
serverName,
|
|
);
|
|
const result = await mcpManager.appToolCall({
|
|
userId,
|
|
serverName,
|
|
toolName,
|
|
toolArguments: toolArgs || {},
|
|
user: req.user,
|
|
configServers,
|
|
customUserVars,
|
|
flowManager,
|
|
tokenMethods,
|
|
});
|
|
return res.json(result);
|
|
} catch (error) {
|
|
logger.error('[appToolCall] Error:', error);
|
|
if (error && typeof error === 'object' && error.code === MCP_INVALID_REQUEST) {
|
|
return res.status(400).json({ error: error.message });
|
|
}
|
|
return res.status(500).json({ error: 'Failed to execute tool' });
|
|
}
|
|
};
|
|
|
|
/** @route GET /api/mcp/sandbox */
|
|
const serveMCPSandbox = async (_req, res) => {
|
|
try {
|
|
res.setHeader('Content-Type', 'text/html');
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('Referrer-Policy', 'same-origin');
|
|
|
|
// The MCP Apps spec requires the Host and Sandbox to have different origins for web hosts.
|
|
// Default to same-origin framing; when a dedicated sandbox origin is deployed, the operator
|
|
// lists the allowed host origin(s) so the host page can frame this sandbox cross-origin.
|
|
const allowedParents = (process.env.MCP_SANDBOX_FRAME_ANCESTORS || '').trim();
|
|
if (allowedParents) {
|
|
const ancestors = allowedParents
|
|
.split(/[\s,]+/)
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
res.setHeader('Content-Security-Policy', `frame-ancestors 'self' ${ancestors}`);
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
} else {
|
|
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
}
|
|
|
|
const sandboxPath = path.resolve(
|
|
__dirname,
|
|
'..',
|
|
'..',
|
|
'..',
|
|
'client',
|
|
'public',
|
|
'mcp-sandbox.html',
|
|
);
|
|
return res.sendFile(sandboxPath, (error) => {
|
|
if (error) {
|
|
logger.error('[serveMCPSandbox] Error:', error);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: 'Failed to load MCP sandbox' });
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('[serveMCPSandbox] Error:', error);
|
|
return res.status(500).json({ error: 'Failed to load MCP sandbox' });
|
|
}
|
|
};
|
|
|
|
module.exports = { readMCPResource, listMCPResources, appToolCall, serveMCPSandbox };
|