mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
⏳ 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
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:
parent
dcdaaeac67
commit
f3692b5d43
2 changed files with 124 additions and 1 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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}'`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue