🌐 fix: Prevent MCP Body/Header Timeouts at 5-Minute mark (#9476)

* chore: improve error log for tool error

* fix: add undici as fetch method with agent to prevent body/header timeouts at 5-minute mark
This commit is contained in:
Danny Avila 2025-09-05 17:14:39 -04:00 committed by GitHub
parent 4dd2998592
commit 1869854d70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 12 deletions

View file

@ -7,11 +7,12 @@ const {
createRun, createRun,
Tokenizer, Tokenizer,
checkAccess, checkAccess,
logAxiosError,
resolveHeaders, resolveHeaders,
getBalanceConfig, getBalanceConfig,
getTransactionsConfig,
memoryInstructions, memoryInstructions,
formatContentStrings, formatContentStrings,
getTransactionsConfig,
createMemoryProcessor, createMemoryProcessor,
} = require('@librechat/api'); } = require('@librechat/api');
const { const {
@ -88,11 +89,10 @@ function createTokenCounter(encoding) {
} }
function logToolError(graph, error, toolId) { function logToolError(graph, error, toolId) {
logger.error( logAxiosError({
'[api/server/controllers/agents/client.js #chatCompletion] Tool Error',
error, error,
toolId, message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`,
); });
} }
class AgentClient extends BaseClient { class AgentClient extends BaseClient {

View file

@ -280,6 +280,7 @@ Please follow these instructions when using tools from the respective MCP server
CallToolResultSchema, CallToolResultSchema,
{ {
timeout: connection.timeout, timeout: connection.timeout,
resetTimeoutOnProgress: true,
...options, ...options,
}, },
); );

View file

@ -1,4 +1,5 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { fetch as undiciFetch, Agent } from 'undici';
import { import {
StdioClientTransport, StdioClientTransport,
getDefaultEnvironment, getDefaultEnvironment,
@ -11,10 +12,17 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { logger } from '@librechat/data-schemas'; import { logger } from '@librechat/data-schemas';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
import type {
RequestInit as UndiciRequestInit,
RequestInfo as UndiciRequestInfo,
Response as UndiciResponse,
} from 'undici';
import type { MCPOAuthTokens } from './oauth/types'; import type { MCPOAuthTokens } from './oauth/types';
import { mcpConfig } from './mcpConfig'; import { mcpConfig } from './mcpConfig';
import type * as t from './types'; import type * as t from './types';
type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
function isStdioOptions(options: t.MCPOptions): options is t.StdioOptions { function isStdioOptions(options: t.MCPOptions): options is t.StdioOptions {
return 'command' in options; return 'command' in options;
} }
@ -141,11 +149,18 @@ export class MCPConnection extends EventEmitter {
*/ */
private createFetchFunction( private createFetchFunction(
getHeaders: () => Record<string, string> | null | undefined, getHeaders: () => Record<string, string> | null | undefined,
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> { ): (input: UndiciRequestInfo, init?: UndiciRequestInit) => Promise<UndiciResponse> {
return function customFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> { return function customFetch(
input: UndiciRequestInfo,
init?: UndiciRequestInit,
): Promise<UndiciResponse> {
const requestHeaders = getHeaders(); const requestHeaders = getHeaders();
const agent = new Agent({
bodyTimeout: 0,
headersTimeout: 0,
});
if (!requestHeaders) { if (!requestHeaders) {
return fetch(input, init); return undiciFetch(input, { ...init, dispatcher: agent });
} }
let initHeaders: Record<string, string> = {}; let initHeaders: Record<string, string> = {};
@ -159,12 +174,13 @@ export class MCPConnection extends EventEmitter {
} }
} }
return fetch(input, { return undiciFetch(input, {
...init, ...init,
headers: { headers: {
...initHeaders, ...initHeaders,
...requestHeaders, ...requestHeaders,
}, },
dispatcher: agent,
}); });
}; };
} }
@ -235,13 +251,20 @@ export class MCPConnection extends EventEmitter {
eventSourceInit: { eventSourceInit: {
fetch: (url, init) => { fetch: (url, init) => {
const fetchHeaders = new Headers(Object.assign({}, init?.headers, headers)); const fetchHeaders = new Headers(Object.assign({}, init?.headers, headers));
return fetch(url, { const agent = new Agent({
bodyTimeout: 0,
headersTimeout: 0,
});
return undiciFetch(url, {
...init, ...init,
dispatcher: agent,
headers: fetchHeaders, headers: fetchHeaders,
}); });
}, },
}, },
fetch: this.createFetchFunction(this.getRequestHeaders.bind(this)), fetch: this.createFetchFunction(
this.getRequestHeaders.bind(this),
) as unknown as FetchLike,
}); });
transport.onclose = () => { transport.onclose = () => {
@ -279,7 +302,9 @@ export class MCPConnection extends EventEmitter {
headers, headers,
signal: abortController.signal, signal: abortController.signal,
}, },
fetch: this.createFetchFunction(this.getRequestHeaders.bind(this)), fetch: this.createFetchFunction(
this.getRequestHeaders.bind(this),
) as unknown as FetchLike,
}); });
transport.onclose = () => { transport.onclose = () => {