diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js index c2ca150228..1e331fc40d 100644 --- a/api/server/services/Endpoints/agents/initialize.spec.js +++ b/api/server/services/Endpoints/agents/initialize.spec.js @@ -539,6 +539,48 @@ describe('initializeClient — subagent loading', () => { expect(arg.actionsEnabled).toBe(true); }); + it('threads run-scoped MCP tool definitions into ON_TOOL_EXECUTE loading', async () => { + /** Regression guard for the request-scoped MCP/PTC handoff: the + * `mcpAvailableTools` discovered at run start must survive + * `buildAgentToolContext` and reach `loadToolsForExecution`, otherwise + * request-scoped servers reinitialize on every programmatic tool call + * and can trip the MCP circuit breaker under parallel calls. */ + const mcpTool = 'list_tables_mcp_ClickHouse'; + const mcpAvailableTools = { + ClickHouse: { + [mcpTool]: { + type: 'function', + function: { + name: mcpTool, + description: 'List tables', + parameters: { type: 'object', properties: {} }, + }, + }, + }, + }; + const primaryConfig = { + ...makePrimaryConfig({}), + toolRegistry: new Map([[mcpTool, { name: mcpTool }]]), + mcpAvailableTools, + }; + mockInitializeAgent.mockResolvedValue(primaryConfig); + + await initializeClient({ + req: makeSubagentReq(), + res: {}, + signal: new AbortController().signal, + endpointOption: makeEndpointOption(), + }); + + expect(capturedToolExecuteOptions?.loadTools).toBeInstanceOf(Function); + await capturedToolExecuteOptions.loadTools([mcpTool], PRIMARY_ID); + + expect(mockLoadToolsForExecution).toHaveBeenCalledTimes(1); + expect(mockLoadToolsForExecution).toHaveBeenCalledWith( + expect.objectContaining({ mcpAvailableTools }), + ); + }); + it('deduplicates repeated ids in subagents.agent_ids', async () => { const subAgent = await createAgent({ id: DUPLICATE_SUBAGENT_ID, diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts index 857638f59a..1bec88f837 100644 --- a/packages/api/src/agents/__tests__/initialize.test.ts +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -2068,3 +2068,54 @@ describe('initializeAgent — code-generated file thread filter (regression)', ( expect(getUserCodeFiles).not.toHaveBeenCalled(); }); }); + +describe('initializeAgent — run-scoped MCP tool definitions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('carries mcpAvailableTools from the loadTools result onto the initialized agent', async () => { + /** Regression guard for the request-scoped MCP/PTC handoff: dropping this + * field at the destructure boundary forces per-call reinitialization + * downstream and can storm the MCP circuit breaker. */ + const { agent, req, res, loadTools, db } = createMocks(); + const mcpTool = 'list_tables_mcp_ClickHouse'; + const mcpAvailableTools = { + ClickHouse: { + [mcpTool]: { + type: 'function' as const, + function: { + name: mcpTool, + description: 'List tables', + parameters: { type: 'object' as const, properties: {} }, + }, + }, + }, + }; + loadTools.mockResolvedValue({ + tools: [], + toolContextMap: {}, + dynamicToolContextMap: {}, + userMCPAuthMap: undefined, + toolRegistry: undefined, + toolDefinitions: [], + hasDeferredTools: false, + mcpAvailableTools, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { endpoint: EModelEndpoint.agents }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + expect(result.mcpAvailableTools).toEqual(mcpAvailableTools); + }); +});