mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
fix(mcp): proxy resource templates and fail closed on app config resolution
The host advertises serverResources, and the ext-apps bridge treats resources/read, resources/list, and resources/templates/list as one proxied set. Only the first two were wired, so an app that sent resources/templates/list received a method-not-found. Register an onlistresourcetemplates handler backed by a new MCPManager.listResourceTemplates and a /api/mcp/resources/templates/list route, mirroring the existing resources/list path. Tool listing is left out deliberately: the App Bridge has no app-to-host tools/list request, and serverTools covers only tool calls. Make app follow-up requests fail closed when scoped config resolution errors. resolveConfigServers gains an opt-in throwOnError so the app path rejects instead of degrading to an empty set, which previously let a transient failure fall back to the base config for the same server name and proxy the iframe request to the wrong server.
This commit is contained in:
parent
f31dacac70
commit
acc0befd0b
6 changed files with 128 additions and 5 deletions
|
|
@ -23,10 +23,11 @@ const MCP_INVALID_REQUEST = -32600;
|
|||
*/
|
||||
const resolveAppContext = async (req, serverName) => {
|
||||
const userId = req.user?.id;
|
||||
// Fail closed on config resolution: an app request targets one server by name, so a transient
|
||||
// failure must reject rather than fall back to the base config for that name and proxy to the
|
||||
// wrong server. Auth map resolution may still degrade, since a missing var fails closed downstream.
|
||||
const [configServers, userMCPAuthMap] = await Promise.all([
|
||||
Promise.resolve()
|
||||
.then(() => resolveConfigServers(req))
|
||||
.catch(() => undefined),
|
||||
resolveConfigServers(req, { throwOnError: true }),
|
||||
Promise.resolve()
|
||||
.then(() => getUserMCPAuthMap({ userId, servers: [serverName], findPluginAuthsByKeys }))
|
||||
.catch(() => undefined),
|
||||
|
|
@ -116,6 +117,44 @@ const listMCPResources = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
/** @route POST /api/mcp/resources/templates/list */
|
||||
const listMCPResourceTemplates = 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.listResourceTemplates({
|
||||
userId,
|
||||
serverName,
|
||||
user: req.user,
|
||||
cursor,
|
||||
configServers,
|
||||
customUserVars,
|
||||
flowManager,
|
||||
tokenMethods,
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('[listMCPResourceTemplates] Error:', error);
|
||||
return res.status(500).json({ error: 'Failed to list resource templates' });
|
||||
}
|
||||
};
|
||||
|
||||
/** @route POST /api/mcp/app-tool-call */
|
||||
const appToolCall = async (req, res) => {
|
||||
try {
|
||||
|
|
@ -210,4 +249,10 @@ const serveMCPSandbox = async (_req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
module.exports = { readMCPResource, listMCPResources, appToolCall, serveMCPSandbox };
|
||||
module.exports = {
|
||||
readMCPResource,
|
||||
listMCPResources,
|
||||
listMCPResourceTemplates,
|
||||
appToolCall,
|
||||
serveMCPSandbox,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const {
|
|||
const {
|
||||
readMCPResource,
|
||||
listMCPResources,
|
||||
listMCPResourceTemplates,
|
||||
appToolCall,
|
||||
serveMCPSandbox,
|
||||
} = require('~/server/controllers/mcpApps');
|
||||
|
|
@ -999,6 +1000,17 @@ router.post('/resources/read', requireJwtAuth, checkMCPUsePermissions, readMCPRe
|
|||
*/
|
||||
router.post('/resources/list', requireJwtAuth, checkMCPUsePermissions, listMCPResources);
|
||||
|
||||
/**
|
||||
* List resource templates available on an MCP server
|
||||
* @route POST /api/mcp/resources/templates/list
|
||||
*/
|
||||
router.post(
|
||||
'/resources/templates/list',
|
||||
requireJwtAuth,
|
||||
checkMCPUsePermissions,
|
||||
listMCPResourceTemplates,
|
||||
);
|
||||
|
||||
/**
|
||||
* Proxy tool calls from MCP App iframe to MCP server
|
||||
* @route POST /api/mcp/app-tool-call
|
||||
|
|
|
|||
|
|
@ -115,14 +115,21 @@ async function getAppConfigForUser(userId, user) {
|
|||
* Resolves config-source MCP servers from admin Config overrides for the current
|
||||
* request context. Returns the parsed configs keyed by server name.
|
||||
* @param {import('express').Request} req - Express request with user context
|
||||
* @param {{ throwOnError?: boolean }} [options] - When throwOnError is set, a resolution failure
|
||||
* rejects instead of degrading to an empty set. Callers that route a request to a specific
|
||||
* server (app follow-up requests) must fail closed so a transient error cannot silently fall
|
||||
* back to the base config for the same server name.
|
||||
* @returns {Promise<Record<string, import('@librechat/api').ParsedServerConfig>>}
|
||||
*/
|
||||
async function resolveConfigServers(req) {
|
||||
async function resolveConfigServers(req, { throwOnError = false } = {}) {
|
||||
try {
|
||||
const registry = getMCPServersRegistry();
|
||||
const appConfig = await getAppConfigForRequest(req);
|
||||
return await registry.ensureConfigServers(appConfig?.mcpConfig || {});
|
||||
} catch (error) {
|
||||
if (throwOnError) {
|
||||
throw error;
|
||||
}
|
||||
logger.warn(
|
||||
'[resolveConfigServers] Failed to resolve config servers, degrading to empty:',
|
||||
error,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue