🧭 fix: Fail Closed When Expected MCP Tools Are Unavailable (#14646)

* fix: fail closed when expected mcp tools are unavailable

* test: strengthen MCP handoff coverage

* fix: clarify unavailable MCP tool guidance

* fix: preserve MCP discovery for empty catalogs
This commit is contained in:
Danny Avila 2026-08-05 17:30:57 -04:00 committed by GitHub
parent 7775f25b0d
commit 489bc02d4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1112 additions and 44 deletions

View file

@ -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);
@ -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', () => {

View file

@ -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);
@ -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 = {

View file

@ -49,6 +49,7 @@ const {
loadAgentTools,
loadToolsForExecution,
getAccessibleMcpServerNames,
isExpectedMCPToolsUnavailableError,
} = require('~/server/services/ToolService');
const {
findAccessibleResources,
@ -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;
}
}
};
}

View file

@ -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,
@ -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;
}
}
};
}

View file

@ -11,6 +11,7 @@ const { isEphemeralAgentId } = require('librechat-data-provider');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const { getMCPServerTools } = require('~/server/services/Config');
const { getAccessibleMcpServerNames } = require('~/server/services/MCP');
const { isExpectedMCPToolsUnavailableError } = require('~/server/services/ToolService');
const { getSkillDbMethods, canAuthorSkillFiles } = require('./skillDeps');
const db = require('~/models');
@ -226,6 +227,9 @@ const processAddedConvo = async ({
return { userMCPAuthMap };
} catch (err) {
if (isExpectedMCPToolsUnavailableError(err)) {
throw err;
}
logger.error('[processAddedConvo] Error processing addedConvo for parallel agent', err);
return { userMCPAuthMap };
}

View file

@ -41,6 +41,11 @@ jest.mock('~/server/services/MCP', () => ({
getAccessibleMcpServerNames: jest.fn(async () => []),
}));
jest.mock('~/server/services/ToolService', () => ({
isExpectedMCPToolsUnavailableError: (error) =>
error?.code === 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
}));
jest.mock('./skillDeps', () => ({
canAuthorSkillFiles: (...args) => mockCanAuthorSkillFiles(...args),
getSkillDbMethods: () => mockGetSkillDbMethods(),
@ -138,6 +143,16 @@ describe('processAddedConvo', () => {
);
});
it('propagates an expected-MCP-tools failure from an added parallel agent', async () => {
const toolError = Object.assign(new Error('Added agent expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
});
mockInitializeAgent.mockRejectedValueOnce(toolError);
await expect(processAddedConvo(baseParams())).rejects.toBe(toolError);
});
it('keeps deployment-aware skill metadata on a persisted added-agent config', async () => {
const deploymentSkillId = { toString: () => 'deployment-skill' };
const agentConfigs = new Map();

View file

@ -40,6 +40,7 @@ const {
loadAgentTools,
loadToolsForExecution,
getAccessibleMcpServerNames,
isExpectedMCPToolsUnavailableError,
} = require('~/server/services/ToolService');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const {
@ -109,6 +110,9 @@ function createToolLoader(signal, streamId = null, definitionsOnly = false, jobC
});
} catch (error) {
logger.error('Error loading tools for agent ' + agentId, error);
if (isExpectedMCPToolsUnavailableError(error)) {
throw error;
}
}
};
}
@ -800,6 +804,9 @@ const initializeClient = async ({
agentToolContexts.set(agentId, buildAgentToolContext({ agent, config }));
return config;
} catch (err) {
if (isExpectedMCPToolsUnavailableError(err)) {
throw err;
}
logger.error(`[processAgent] Error processing subagent ${agentId}:`, err);
skippedAgentIds.add(agentId);
return null;

View file

@ -58,6 +58,8 @@ const mockLoadToolsForExecution = jest.fn();
jest.mock('~/server/services/ToolService', () => ({
loadAgentTools: jest.fn(),
loadToolsForExecution: (...args) => mockLoadToolsForExecution(...args),
isExpectedMCPToolsUnavailableError: (error) =>
error?.code === 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
}));
jest.mock('~/server/controllers/ModelController', () => ({
@ -82,6 +84,7 @@ jest.mock('~/cache', () => ({
const { initializeClient } = require('./initialize');
const { getSkillDbMethods, getSkillToolDeps } = require('./skillDeps');
const { loadAgentTools } = require('~/server/services/ToolService');
const { getModelsConfig } = require('~/server/controllers/ModelController');
const { logger } = require('@librechat/data-schemas');
const { User, AclEntry } = require('~/db/models');
@ -199,6 +202,83 @@ describe('initializeClient — processAgent ACL gate', () => {
);
});
it('propagates an expected-MCP-tools failure from the runtime tool loader', async () => {
const toolError = Object.assign(new Error('Expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
});
loadAgentTools.mockRejectedValueOnce(toolError);
mockInitializeAgent.mockImplementationOnce(async ({ req, res, loadTools, agent }) => {
await loadTools({
req,
res,
tools: ['run_query_mcp_warehouse'],
model: agent.model,
agentId: agent.id,
provider: agent.provider,
});
return makePrimaryConfig([]);
});
await expect(
initializeClient({
req: makeReq(),
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
}),
).rejects.toBe(toolError);
});
it('aborts the run when a handoff target resolves none of its expected MCP tools', async () => {
const target = await createAgent({
id: AUTHORIZED_ID,
name: 'Target Agent',
provider: 'openai',
model: 'gpt-4',
author: new mongoose.Types.ObjectId(),
tools: ['run_query_mcp_warehouse'],
});
await AclEntry.create({
principalType: PrincipalType.USER,
principalId: testUser._id,
principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT,
resourceId: target._id,
permBits: PermissionBits.VIEW,
grantedBy: testUser._id,
});
const toolError = Object.assign(new Error('Target Agent expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
});
loadAgentTools.mockRejectedValueOnce(toolError);
mockInitializeAgent
.mockResolvedValueOnce(
makePrimaryConfig([{ from: PRIMARY_ID, to: AUTHORIZED_ID, edgeType: 'handoff' }]),
)
.mockImplementationOnce(async ({ req, res, loadTools, agent }) => {
await loadTools({
req,
res,
tools: agent.tools,
model: agent.model,
agentId: agent.id,
provider: agent.provider,
});
});
await expect(
initializeClient({
req: makeReq(),
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
}),
).rejects.toBe(toolError);
});
it('should skip handoff agent and filter its edge when user lacks VIEW access', async () => {
await createAgent({
id: TARGET_ID,
@ -588,6 +668,49 @@ describe('initializeClient — subagent loading', () => {
return agent;
};
it('aborts the run when a pure subagent resolves none of its expected MCP tools', async () => {
const subAgent = await createAgent({
id: SUBAGENT_ID,
name: 'Data Subagent',
provider: 'openai',
model: 'gpt-4',
author: new mongoose.Types.ObjectId(),
tools: ['run_query_mcp_warehouse'],
});
await grantView(subAgent);
const toolError = Object.assign(new Error('Subagent expected MCP tools are unavailable'), {
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
});
loadAgentTools.mockRejectedValueOnce(toolError);
mockInitializeAgent
.mockResolvedValueOnce(
makePrimaryConfig({
subagents: { enabled: true, allowSelf: true, agent_ids: [SUBAGENT_ID] },
}),
)
.mockImplementationOnce(async ({ req, res, loadTools, agent }) => {
await loadTools({
req,
res,
tools: agent.tools,
model: agent.model,
agentId: agent.id,
provider: agent.provider,
});
});
await expect(
initializeClient({
req: makeSubagentReq(),
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
}),
).rejects.toBe(toolError);
});
it('loads a configured subagent, populates `subagentAgentConfigs`, and keeps it out of `agentConfigs`', async () => {
const subAgent = await createAgent({
id: SUBAGENT_ID,

View file

@ -524,6 +524,29 @@ const nativeTools = new Set([
Tools.memory,
]);
const mcpServerPinPrefix = `${Constants.mcp_server}${Constants.mcp_delimiter}`;
const isExpectedMCPTool = (toolName) =>
toolName?.includes(Constants.mcp_delimiter) &&
!toolName.startsWith(mcpServerPinPrefix) &&
!isActionTool(toolName);
const expectedMCPToolsUnavailableCode = 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE';
const isExpectedMCPToolsUnavailableError = (error) =>
error?.code === expectedMCPToolsUnavailableCode;
const createExpectedMCPToolsUnavailableError = (agentName, cause) => {
const subject = agentName ? `Agent "${agentName}"` : 'The agent';
const error = new Error(
`${subject} is configured to use MCP tools, but none are available. Verify that the MCP server is connected and this agent can access its selected tools, then try again.`,
);
error.name = 'AgentToolInitializationError';
error.code = expectedMCPToolsUnavailableCode;
error.status = 503;
error.statusCode = 503;
if (cause != null) {
error.cause = cause;
}
return error;
};
/** Checks if a tool name is a known built-in tool */
const isBuiltInTool = (toolName) =>
Boolean(
@ -573,6 +596,7 @@ async function loadToolDefinitionsWrapper({
}
const appConfig = req.config;
const hasExpectedMCPTools = agent.tools.some(isExpectedMCPTool);
const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent.id);
const checkCapability = (capability) => enabledCapabilities.has(capability);
@ -616,6 +640,9 @@ async function loadToolDefinitionsWrapper({
});
if (!filteredTools || filteredTools.length === 0) {
if (hasExpectedMCPTools) {
throw createExpectedMCPToolsUnavailableError(agent.name);
}
return { toolDefinitions: [] };
}
@ -893,6 +920,33 @@ async function loadToolDefinitionsWrapper({
return result?.availableTools || null;
};
const refreshMCPServerTools = async (_userId, serverName) => {
if (pendingOAuthServers.has(serverName)) {
return null;
}
const oauthStart = async (authURL, options) => {
pendingOAuthServers.add(serverName);
if (typeof authURL === 'string' && authURL.length > 0) {
pendingOAuthStarts.set(serverName, { authURL, options });
}
};
const result = await reinitMCPServer({
user: req.user,
forceNew: true,
oauthStart,
flowManager,
serverName,
configServers,
userMCPAuthMap,
requestBody: req.body,
requestScopedConnections,
});
rememberMCPAvailableTools(serverName, result?.availableTools);
return result?.availableTools || null;
};
const getActionToolDefinitions = async (agentId, actionToolNames) => {
const actionSets = (await loadActionSets({ agent_id: agentId })) ?? [];
if (actionSets.length === 0) {
@ -952,26 +1006,28 @@ async function loadToolDefinitionsWrapper({
return definitions;
};
let { toolDefinitions, toolRegistry, hasDeferredTools } = await loadToolDefinitions(
{
userId: req.user.id,
agentId: agent.id,
tools: defsFilteredTools,
toolOptions: agent.tool_options,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
provider: agent.provider,
mcpServerNames,
rawServerNames: mcpRawServerNames,
accessibleServerNames: defsAccessibleServerNames,
},
{
isBuiltInTool,
getOrFetchMCPServerTools,
getActionToolDefinitions,
},
);
let { toolDefinitions, toolRegistry, hasDeferredTools, mcpResolution } =
await loadToolDefinitions(
{
userId: req.user.id,
agentId: agent.id,
tools: defsFilteredTools,
toolOptions: agent.tool_options,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
provider: agent.provider,
mcpServerNames,
rawServerNames: mcpRawServerNames,
accessibleServerNames: defsAccessibleServerNames,
},
{
isBuiltInTool,
getOrFetchMCPServerTools,
refreshMCPServerTools,
getActionToolDefinitions,
},
);
/** OAuth discovery must not reconnect (or prompt for) a server whose
* definitions the collision filter deliberately rejected. */
@ -1056,15 +1112,21 @@ async function loadToolDefinitionsWrapper({
{
isBuiltInTool,
getOrFetchMCPServerTools,
refreshMCPServerTools,
getActionToolDefinitions,
},
);
toolDefinitions = reloadResult.toolDefinitions;
toolRegistry = reloadResult.toolRegistry;
hasDeferredTools = reloadResult.hasDeferredTools;
mcpResolution = reloadResult.mcpResolution;
}
}
if (hasExpectedMCPTools && mcpResolution?.resolvedToolCount === 0) {
throw createExpectedMCPToolsUnavailableError(agent.name);
}
/** @type {Record<string, string>} */
const toolContextMap = {};
/** @type {Record<string, string>} */
@ -1195,16 +1257,23 @@ async function loadAgentTools({
accessibleMcpServerNames,
}) {
if (definitionsOnly) {
return loadToolDefinitionsWrapper({
req,
res,
agent,
agentResourceType,
streamId,
jobCreatedAt,
tool_resources,
accessibleMcpServerNames,
});
try {
return await loadToolDefinitionsWrapper({
req,
res,
agent,
agentResourceType,
streamId,
jobCreatedAt,
tool_resources,
accessibleMcpServerNames,
});
} catch (error) {
if (isExpectedMCPToolsUnavailableError(error) || !agent.tools?.some(isExpectedMCPTool)) {
throw error;
}
throw createExpectedMCPToolsUnavailableError(agent.name, error);
}
}
if (!agent.tools || agent.tools.length === 0) {
@ -1971,6 +2040,7 @@ module.exports = {
loadToolsForExecution,
processRequiredActions,
resolveAgentCapabilities,
isExpectedMCPToolsUnavailableError,
/** Re-exported for controllers that already depend on (and mock) this
* module, avoiding a fresh heavy `services/MCP` require chain there. */
getAccessibleMcpServerNames,

View file

@ -255,7 +255,7 @@ async function reinitMCPServer({
tools = await connection.fetchTools();
}
if (tools && tools.length > 0) {
if (tools) {
availableTools = await updateMCPServerTools({
userId: user.id,
serverName,

View file

@ -102,6 +102,24 @@ describe('reinitMCPServer — customUserVars gating (issue #10969)', () => {
);
});
it('updates the cache with an empty catalog after a successful connection', async () => {
mockGetConnection.mockResolvedValue({ fetchTools: jest.fn().mockResolvedValue([]) });
await reinitMCPServer({
user,
serverName,
serverConfig: { type: 'streamable-http', url: 'https://thingy.example.com/mcp' },
userMCPAuthMap: undefined,
});
expect(mockUpdateMCPServerTools).toHaveBeenCalledWith({
userId: user.id,
serverName,
tools: [],
serverConfig: { type: 'streamable-http', url: 'https://thingy.example.com/mcp' },
});
});
it('passes request body and Graph resolver into connection creation', async () => {
mockGetConnection.mockResolvedValue({ fetchTools: jest.fn().mockResolvedValue([]) });
const requestBody = { conversationId: 'conv-123', messageId: 'msg-123' };

View file

@ -359,6 +359,113 @@ describe('ToolService - Action Capability Gating', () => {
expect(callArgs.tools).toContain(regularTool);
});
it('fails initialization when an explicitly selected MCP tool cannot be resolved', async () => {
const mcpTool = `search${Constants.mcp_delimiter}warehouse`;
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockResolveConfigServers.mockResolvedValue({
warehouse: {
type: 'streamable-http',
url: 'https://mcp.example.com/warehouse',
},
});
mockLoadToolDefinitions.mockResolvedValueOnce({
toolDefinitions: [],
toolRegistry: new Map(),
hasDeferredTools: false,
mcpResolution: { expectedToolCount: 1, resolvedToolCount: 0 },
});
await expect(
loadAgentTools({
req,
res: {},
agent: { id: 'agent_123', name: 'Target Agent', tools: [mcpTool] },
definitionsOnly: true,
}),
).rejects.toMatchObject({
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
message: expect.stringContaining('can access its selected tools'),
});
});
it('fails closed when MCP definition loading throws before resolution completes', async () => {
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolDefinitions.mockRejectedValueOnce(new Error('MCP registry unavailable'));
await expect(
loadAgentTools({
req,
res: {},
agent: {
id: 'agent_123',
name: 'Target Agent',
tools: [`run_query${Constants.mcp_delimiter}warehouse`],
},
definitionsOnly: true,
}),
).rejects.toMatchObject({
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
cause: expect.objectContaining({ message: 'MCP registry unavailable' }),
});
});
it('allows a server pin with no explicitly selected MCP tools', async () => {
const serverPin = `${Constants.mcp_server}${Constants.mcp_delimiter}warehouse`;
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolDefinitions.mockResolvedValueOnce({
toolDefinitions: [],
toolRegistry: new Map(),
hasDeferredTools: false,
mcpResolution: { expectedToolCount: 0, resolvedToolCount: 0 },
});
await expect(
loadAgentTools({
req,
res: {},
agent: { id: 'agent_123', tools: [serverPin] },
definitionsOnly: true,
}),
).resolves.toMatchObject({ toolDefinitions: [] });
});
it('allows partial MCP resolution when at least one expected tool is available', async () => {
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolDefinitions.mockResolvedValueOnce({
toolDefinitions: [{ name: 'list_sources_mcp_warehouse', toolType: 'mcp' }],
toolRegistry: new Map(),
hasDeferredTools: false,
mcpResolution: { expectedToolCount: 2, resolvedToolCount: 1 },
});
await expect(
loadAgentTools({
req,
res: {},
agent: {
id: 'agent_123',
tools: [
`list_sources${Constants.mcp_delimiter}warehouse`,
`run_query${Constants.mcp_delimiter}warehouse`,
],
},
definitionsOnly: true,
}),
).resolves.toMatchObject({
toolDefinitions: [expect.objectContaining({ name: 'list_sources_mcp_warehouse' })],
});
});
it('should filter MCP tool definitions when user lacks MCP server use permission', async () => {
const { userCanUseMCPServers } = require('~/server/services/MCP');
userCanUseMCPServers.mockResolvedValueOnce(false);
@ -381,6 +488,31 @@ describe('ToolService - Action Capability Gating', () => {
expect(callArgs.tools).not.toContain(mcpTool);
});
it('fails explicitly when MCP permission filtering removes every expected tool', async () => {
const { userCanUseMCPServers } = require('~/server/services/MCP');
userCanUseMCPServers.mockResolvedValueOnce(false);
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
await expect(
loadAgentTools({
req,
res: {},
agent: {
id: 'agent_123',
tools: [`run_query${Constants.mcp_delimiter}warehouse`],
},
definitionsOnly: true,
}),
).rejects.toMatchObject({
code: 'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
statusCode: 503,
});
expect(mockLoadToolDefinitions).not.toHaveBeenCalled();
});
it('should return actionsEnabled in the result', async () => {
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);