fix: Defer MCP Connection for Request-Scoped Placeholders on Reinitialize (#14148)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

Co-authored-by: fe2131-art <232701181+fe2131-art@users.noreply.github.com>
This commit is contained in:
Danny Avila 2026-07-07 07:54:42 -04:00 committed by GitHub
parent dcdaaeac67
commit f3692b5d43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 1 deletions

View file

@ -1,5 +1,9 @@
const { logger } = require('@librechat/data-schemas');
const { getMissingCustomUserVars, requiresEphemeralUserConnection } = require('@librechat/api');
const {
getMissingCustomUserVars,
requiresEphemeralUserConnection,
getMissingRuntimeBodyPlaceholderFields,
} = require('@librechat/api');
const { CacheKeys, Constants } = require('librechat-data-provider');
const { getMCPManager, getMCPServersRegistry, getFlowStateManager } = require('~/config');
const { findToken, createToken, updateToken, deleteTokens } = require('~/models');
@ -121,6 +125,28 @@ async function reinitMCPServer({
};
}
/** `{{LIBRECHAT_BODY_*}}` placeholders only resolve during a chat turn; connecting
* without them would fail, so defer the connection instead of reporting a failure. */
const missingBodyFields = serverConfig
? getMissingRuntimeBodyPlaceholderFields(serverConfig, requestBody)
: [];
if (missingBodyFields.length > 0) {
logger.info(
`[MCP Reinitialize] Server '${serverName}' requires request body field(s) [${missingBodyFields.join(
', ',
)}] for runtime placeholders; connection deferred to first use in a chat turn`,
);
return {
availableTools: null,
success: true,
message: `MCP server '${serverName}' uses request-scoped placeholders; connection will be established on first use in a chat turn`,
oauthRequired: false,
serverName,
oauthUrl: null,
tools: null,
};
}
const flowManager = _flowManager ?? getFlowStateManager(getLogStores(CacheKeys.FLOWS));
const mcpManager = getMCPManager();
const tokenMethods = { findToken, updateToken, createToken, deleteTokens };

View file

@ -184,3 +184,100 @@ describe('reinitMCPServer — customUserVars gating (issue #10969)', () => {
expect(mockGetConnection).toHaveBeenCalledTimes(1);
});
});
describe('reinitMCPServer — runtime BODY placeholder pre-check (issue #14074)', () => {
const user = { id: 'user-123' };
const serverName = 'Thingy';
const serverConfig = {
type: 'streamable-http',
url: 'https://thingy.example.com/mcp',
source: 'yaml',
headers: { 'X-Conversation-Id': '{{LIBRECHAT_BODY_CONVERSATIONID}}' },
};
beforeEach(() => {
jest.clearAllMocks();
mockUpdateMCPServerTools.mockResolvedValue({});
});
it('defers connection without failing when body placeholders cannot resolve outside a chat turn', async () => {
const result = await reinitMCPServer({
user,
serverName,
serverConfig,
userMCPAuthMap: undefined,
});
expect(mockGetConnection).not.toHaveBeenCalled();
expect(mockDiscoverServerTools).not.toHaveBeenCalled();
expect(result).toMatchObject({
availableTools: null,
success: true,
tools: null,
oauthRequired: false,
serverName,
});
expect(result.message).toContain('first use in a chat turn');
});
it('treats an empty-string body field as missing', async () => {
const result = await reinitMCPServer({
user,
serverName,
serverConfig,
requestBody: { conversationId: ' ' },
userMCPAuthMap: undefined,
});
expect(mockGetConnection).not.toHaveBeenCalled();
expect(result.success).toBe(true);
});
it('connects normally when the request body provides the placeholder fields', async () => {
const disconnect = jest.fn().mockResolvedValue(undefined);
mockGetConnection.mockResolvedValue({
disconnect,
fetchTools: jest.fn().mockResolvedValue([]),
});
await reinitMCPServer({
user,
serverName,
serverConfig,
requestBody: { conversationId: 'convo-1' },
userMCPAuthMap: undefined,
});
expect(mockGetConnection).toHaveBeenCalledTimes(1);
});
it('reports missing customUserVars before deferring on body placeholders', async () => {
const result = await reinitMCPServer({
user,
serverName,
serverConfig: {
...serverConfig,
customUserVars: { THINGY_TOKEN: { title: 'Thingy Access Token' } },
},
userMCPAuthMap: undefined,
});
expect(result.success).toBe(false);
expect(result.message).toContain('THINGY_TOKEN');
});
it('still treats unrelated connection errors as real failures', async () => {
mockGetConnection.mockRejectedValue(new Error('ECONNREFUSED'));
const result = await reinitMCPServer({
user,
serverName,
serverConfig: { type: 'streamable-http', url: 'https://thingy.example.com/mcp' },
userMCPAuthMap: undefined,
});
expect(mockDiscoverServerTools).not.toHaveBeenCalled();
expect(result.success).toBe(false);
expect(result.message).toBe(`Failed to reinitialize MCP server '${serverName}'`);
});
});