Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b807292
📉 perf: Bound Early Event Buffering for Detached Generations (#14612)
danny-avila Aug 3, 2026
ccf4301
🚦 feat: Configurable Circuit Breakers for Runaway Streamed Tool Args …
danny-avila Aug 5, 2026
22642df
🧹 fix: Exclude Mongo ID From Conversation Updates (#14631)
danny-avila Aug 5, 2026
26ba2c2
♿️ a11y: improve keyboard operability, focus retention, and accessibl…
berry-13 Aug 5, 2026
557c22d
🚏 fix: Localized Guidance for Model-Not-Found Errors
danny-avila Aug 5, 2026
1bbe7df
📦 chore: npm audit (#14640)
danny-avila Aug 5, 2026
58cdd9c
⚡ feat: Coalesce Redis Streaming Delta Publications into Windowed Bat…
danny-avila Aug 5, 2026
3f0a1ec
🛡️ fix: Run message-filter PII patterns on a linear-time regex engine…
danny-avila Aug 5, 2026
4cec1a6
🔒 feat: Add allowedAddresses Exemption to Speech (STT/TTS) and OCR Co…
danny-avila Aug 5, 2026
7775f25
🪢 fix: disable central fanout media uploads for langfuse (#14642)
upman Aug 5, 2026
489bc02
🧭 fix: Fail Closed When Expected MCP Tools Are Unavailable (#14646)
danny-avila Aug 5, 2026
9b7a307
🛂 fix: Honor Claude Document Limits Through Inference Gateways (#14650)
danny-avila Aug 5, 2026
56175af
🎟️ fix: Reconcile MCP OAuth Readiness Across Pods (#14629)
danny-avila Aug 5, 2026
3051079
🌊 fix: Preserve Custom Endpoint `streamRate` When `endpoints.all` Is …
danny-avila Aug 5, 2026
45cc53c
🛰️ chore: bump `@librechat/agents` to v3.4.0, cover streamed subagent…
danny-avila Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ NODE_MAX_OLD_SPACE_SIZE=6144
# See otel/langfuse-fanout/README.md.
# LANGFUSE_FANOUT_ENABLED=false
# LANGFUSE_FANOUT_COLLECTOR_URL=http://langfuse-fanout-collector:4318
# App-side switch: set true to tell the Langfuse SDK not to create media uploads
# for central/fallback collector traces. Tenant-routed media uploads are unchanged.
# LANGFUSE_FANOUT_CENTRAL_MEDIA_UPLOAD_DISABLED=false
# Emergency switch: unset/false defaults enabled; set true to keep central fanout export but skip tenant trace/score export.
# LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED=false
# Langfuse Cloud base URL options: https://cloud.langfuse.com (EU),
Expand Down Expand Up @@ -992,6 +995,14 @@ HELP_AND_FAQ_URL=https://librechat.ai
# pre-v2 build while v2 generations remain active in Redis.
# GENERATION_PROTOCOL_VERSION=2

# Coalesce streamed model/tool-argument deltas into windowed Redis publications (ms).
# Unset or 0 (default) publishes per delta. 25 is recommended: it batches the publish
# EVAL and the durable append across the window (fewer Redis round trips and lower
# Redis CPU at high token rates) at the cost of up to one window of added delivery
# latency. Enable only after EVERY replica runs a build with batch-frame support;
# older subscribers drop coalesced frames. Values are capped at 1000.
# STREAM_DELTA_COALESCE_MS=25

# Single Redis instance
# REDIS_URI=redis://127.0.0.1:6379

Expand Down
2 changes: 2 additions & 0 deletions api/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function getFlowStateManager(flowsCache) {
if (!flowManager) {
flowManager = new FlowStateManager(flowsCache, {
ttl: mcpConfig.OAUTH_FLOW_TTL,
monitorTimeout: mcpConfig.OAUTH_HANDLING_TIMEOUT,
retainedFailureTypes: ['mcp_oauth'],
});
}
return flowManager;
Expand Down
5 changes: 3 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "^4.3.3",
"@librechat/agents": "^3.3.11",
"@librechat/agents": "^3.4.0",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down Expand Up @@ -122,10 +122,11 @@
"pdfjs-dist": "^5.4.624",
"prom-client": "^15.1.3",
"rate-limit-redis": "^4.2.0",
"re2js": "^2.8.6",
"sanitize-html": "^2.17.6",
"sharp": "^0.35.3",
"ua-parser-js": "^1.0.36",
"undici": "^7.24.1",
"undici": "^7.29.0",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^5.0.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
Expand Down
27 changes: 27 additions & 0 deletions api/server/controllers/agents/__tests__/openai.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ jest.mock('~/cache', () => ({
jest.mock('~/server/services/ToolService', () => ({
loadAgentTools: jest.fn().mockResolvedValue([]),
loadToolsForExecution: jest.fn().mockResolvedValue([]),
isExpectedMCPToolsUnavailableError: (error) =>
error?.code === 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
}));

const mockGetMultiplier = jest.fn().mockReturnValue(1);
Expand Down Expand Up @@ -393,6 +395,31 @@ describe('OpenAIChatCompletionController', () => {
expect.objectContaining({ agentResourceType: ResourceType.REMOTE_AGENT }),
);
});

it('returns 503 when an agent expects MCP tools but resolves none', async () => {
const { initializeAgent } = require('@librechat/api');
const { loadAgentTools } = require('~/server/services/ToolService');
const toolError = Object.assign(new Error('Expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
status: 503,
statusCode: 503,
});
loadAgentTools.mockRejectedValueOnce(toolError);
initializeAgent.mockImplementationOnce(async ({ req, res, loadTools, agent }) => {
await loadTools({
req,
res,
tools: ['run_query_mcp_warehouse'],
model: agent.model,
agentId: agent.id,
provider: agent.provider,
});
});

await OpenAIChatCompletionController(req, res);

expect(res.status).toHaveBeenCalledWith(503);
});
});

describe('execution envelope', () => {
Expand Down
32 changes: 32 additions & 0 deletions api/server/controllers/agents/__tests__/responses.unit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ jest.mock('@librechat/api', () => ({
jest.mock('~/server/services/ToolService', () => ({
loadAgentTools: jest.fn().mockResolvedValue([]),
loadToolsForExecution: jest.fn().mockResolvedValue([]),
isExpectedMCPToolsUnavailableError: (error) =>
error?.code === 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
}));

const mockGetMultiplier = jest.fn().mockReturnValue(1);
Expand Down Expand Up @@ -343,6 +345,36 @@ describe('createResponse controller', () => {
};
});

it('returns 503 when an agent expects MCP tools but resolves none', async () => {
const { initializeAgent, sendResponsesErrorResponse } = require('@librechat/api');
const { loadAgentTools } = require('~/server/services/ToolService');
const toolError = Object.assign(new Error('Expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
status: 503,
statusCode: 503,
});
loadAgentTools.mockRejectedValueOnce(toolError);
initializeAgent.mockImplementationOnce(async ({ req, res, loadTools, agent }) => {
await loadTools({
req,
res,
tools: ['run_query_mcp_warehouse'],
model: agent.model,
agentId: agent.id,
provider: agent.provider,
});
});

await createResponse(req, res);

expect(sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
503,
'Expected MCP tools are unavailable',
'server_error',
);
});

describe('execution envelope', () => {
it('creates the portable run input before agent initialization', async () => {
req.user = {
Expand Down
8 changes: 7 additions & 1 deletion api/server/controllers/agents/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const {
loadAgentTools,
loadToolsForExecution,
getAccessibleMcpServerNames,
isExpectedMCPToolsUnavailableError,
} = require('~/server/services/ToolService');
const {
findAccessibleResources,
Expand Down Expand Up @@ -103,6 +104,9 @@ function createToolLoader(signal, definitionsOnly = true) {
});
} catch (error) {
logger.error('Error loading tools for agent ' + agentId, error);
if (isExpectedMCPToolsUnavailableError(error)) {
throw error;
}
}
};
}
Expand Down Expand Up @@ -189,7 +193,9 @@ const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
return sendErrorResponse(
res,
400,
`Message contains a ${piiHit.label}. Remove it and try again.`,
piiHit.misconfigured
? 'Message filtering is misconfigured; contact your administrator.'
: `Message contains a ${piiHit.label}. Remove it and try again.`,
'invalid_request_error',
'message_filter_pii_block',
);
Expand Down
13 changes: 11 additions & 2 deletions api/server/controllers/agents/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ const {
createToolEndCallback,
agentLogHandlerObj,
} = require('~/server/controllers/agents/callbacks');
const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService');
const {
loadAgentTools,
loadToolsForExecution,
isExpectedMCPToolsUnavailableError,
} = require('~/server/services/ToolService');
const {
findAccessibleResources,
getEffectivePermissions,
Expand Down Expand Up @@ -113,6 +117,9 @@ function createToolLoader(signal, definitionsOnly = true) {
});
} catch (error) {
logger.error('Error loading tools for agent ' + agentId, error);
if (isExpectedMCPToolsUnavailableError(error)) {
throw error;
}
}
};
}
Expand Down Expand Up @@ -622,7 +629,9 @@ const executeResponse = async (envelope, { req, res }) => {
return sendResponsesErrorResponse(
res,
400,
`Message contains a ${piiHit.label}. Remove it and try again.`,
piiHit.misconfigured
? 'Message filtering is misconfigured; contact your administrator.'
: `Message contains a ${piiHit.label}. Remove it and try again.`,
'invalid_request',
'message_filter_pii_block',
);
Expand Down
22 changes: 15 additions & 7 deletions api/server/controllers/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
* @import { MCPServerRegistry } from '@librechat/api'
* @import { MCPServerDocument } from 'librechat-data-provider'
*/
const { randomUUID } = require('crypto');
const { logger, SystemCapabilities } = require('@librechat/data-schemas');
const {
checkAccess,
isUserSourced,
MCPConnection,
MCPErrorCodes,
splitMCPToolKey,
normalizeServerName,
Expand Down Expand Up @@ -403,13 +405,19 @@ const createMCPServerController = async (req, res) => {
const reservedServerNames = [
...new Set([...configNames, ...configNames.map(normalizeServerName)]),
];
const result = await getMCPServersRegistry().addServer(
'temp_server_name',
validation.data,
'DB',
userId,
reservedServerNames,
);
const inspectionServerName = `temp_server_${randomUUID()}`;
let result;
try {
result = await getMCPServersRegistry().addServer(
inspectionServerName,
validation.data,
'DB',
userId,
reservedServerNames,
);
} finally {
MCPConnection.clearCooldown(inspectionServerName);
}
res.status(201).json({
serverName: result.serverName,
...redactServerSecrets(result.config, { canEdit: true }),
Expand Down
4 changes: 4 additions & 0 deletions api/server/experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
maybeInjectQueryDevtoolsBootstrap,
preAuthTenantMiddleware,
configureServerTimeouts,
configureMessageFilterRegexValidator,
} = require('@librechat/api');
const { connectDb, indexSync } = require('~/db');
const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
Expand All @@ -49,6 +50,9 @@ const optionalJwtAuth = require('./middleware/optionalJwtAuth');
const noIndex = require('./middleware/noIndex');
const routes = require('./routes');

/** Reject messageFilter PII patterns the RE2 runtime engine cannot compile, at config load. */
configureMessageFilterRegexValidator();

const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};

/** Allow PORT=0 to be used for automatic free port assignment */
Expand Down
4 changes: 4 additions & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const {
configureServerTimeouts,
setupGracefulShutdown,
updateInterfacePermissions,
configureMessageFilterRegexValidator,
} = require('@librechat/api');
const { connectDb, indexSync } = require('~/db');
const {
Expand All @@ -56,6 +57,9 @@ const staticCache = require('./utils/staticCache');
const noIndex = require('./middleware/noIndex');
const routes = require('./routes');

/** Reject messageFilter PII patterns the RE2 runtime engine cannot compile, at config load. */
configureMessageFilterRegexValidator();

const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};

// Allow PORT=0 to be used for automatic free port assignment
Expand Down
Loading
Loading