diff --git a/mocks/mcp-server.ts b/mocks/mcp-server.ts index 24a5325..e685102 100644 --- a/mocks/mcp-server.ts +++ b/mocks/mcp-server.ts @@ -63,7 +63,10 @@ export function createMcpApp(options: MockMcpServerOptions): HonoApp { app.all('/mcp', async (c) => { // Get account ID from header const accountId = c.req.header('x-account-id') ?? 'default'; - const tools = accountTools[accountId] ?? accountTools.default ?? []; + const accountSpecificTools = accountTools[accountId] ?? accountTools.default ?? []; + // The real MCP server registers submit_feedback unconditionally for every account, so mirror + // it here — the SDK inherits the feedback tool straight from the MCP catalog. + const tools = [...accountSpecificTools, submitFeedbackMcpTool]; // Create a new MCP server instance per request const mcp = new McpServer({ name: 'test-mcp-server', version: '1.0.0' }); @@ -95,6 +98,26 @@ export function createMcpApp(options: MockMcpServerOptions): HonoApp { // Pre-defined tool sets for common test scenarios +/** + * The global feedback tool the real MCP server always registers. Mirrored by the mock so tests + * exercise the same "feedback inherited from MCP" path as production. + */ +export const submitFeedbackMcpTool = { + name: 'submit_feedback', + description: 'Record a structured verdict on how well the tools served this session.', + inputSchema: { + type: 'object', + properties: { + rating: { type: 'string' }, + source: { type: 'string' }, + tool_names: { type: 'array', items: { type: 'string' } }, + feedback: { type: 'string' }, + session_id: { type: 'string' }, + }, + required: ['rating', 'source', 'tool_names'], + }, +} as const satisfies McpToolDefinition; + export const defaultMcpTools = [ { name: 'default_tool_1', diff --git a/src/feedback.test.ts b/src/feedback.test.ts deleted file mode 100644 index 54c4c35..0000000 --- a/src/feedback.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { http, HttpResponse } from 'msw'; -import { server } from '../mocks/node'; -import { TEST_BASE_URL } from '../mocks/constants'; -import { createFeedbackTool } from './feedback'; -import { StackOneError } from './utils/error-stackone'; - -interface FeedbackResultItem { - account_id: string; - status: string; - result?: unknown; - error?: string; -} - -interface FeedbackResult { - message: string; - total_accounts: number; - successful: number; - failed: number; - results: FeedbackResultItem[]; -} - -describe('tool_feedback', () => { - describe('validation tests', () => { - it('test_missing_required_fields', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - - // Test missing account_id - await expect( - tool.execute({ feedback: 'Great tools!', tool_names: ['test_tool'] }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test missing tool_names - await expect( - tool.execute({ feedback: 'Great tools!', account_id: 'acc_123456' }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test missing feedback - await expect( - tool.execute({ account_id: 'acc_123456', tool_names: ['test_tool'] }), - ).rejects.toBeInstanceOf(StackOneError); - }); - - it('test_empty_and_whitespace_validation', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - - // Test empty feedback - await expect( - tool.execute({ feedback: '', account_id: 'acc_123456', tool_names: ['test_tool'] }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test whitespace-only feedback - await expect( - tool.execute({ feedback: ' ', account_id: 'acc_123456', tool_names: ['test_tool'] }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test empty account_id - await expect( - tool.execute({ feedback: 'Great tools!', account_id: '', tool_names: ['test_tool'] }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test empty tool_names list - await expect( - tool.execute({ feedback: 'Great tools!', account_id: 'acc_123456', tool_names: [] }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test tool_names with only whitespace - await expect( - tool.execute({ - feedback: 'Great tools!', - account_id: 'acc_123456', - tool_names: [' ', ' '], - }), - ).rejects.toBeInstanceOf(StackOneError); - }); - - it('test_multiple_account_ids_validation', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - - // Test empty account ID list - await expect( - tool.execute({ - feedback: 'Great tools!', - account_id: [], - tool_names: ['test_tool'], - }), - ).rejects.toBeInstanceOf(StackOneError); - - // Test list with only empty strings - await expect( - tool.execute({ - feedback: 'Great tools!', - account_id: ['', ' '], - tool_names: ['test_tool'], - }), - ).rejects.toBeInstanceOf(StackOneError); - }); - - it('test_json_string_input', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - const recordedRequests: Request[] = []; - const listener = ({ request }: { request: Request }) => { - recordedRequests.push(request); - }; - server.events.on('request:start', listener); - - // Test JSON string input - const jsonInput = JSON.stringify({ - feedback: 'Great tools!', - account_id: 'acc_123456', - tool_names: ['test_tool'], - }); - - const result = await tool.execute(jsonInput); - - expect(recordedRequests).toHaveLength(1); - expect(result).toMatchObject({ - message: 'Feedback sent to 1 account(s)', - total_accounts: 1, - successful: 1, - failed: 0, - }); - server.events.removeListener('request:start', listener); - }); - }); - - describe('execution tests', () => { - it('test_single_account_execution', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - const recordedRequests: Request[] = []; - const listener = ({ request }: { request: Request }) => { - recordedRequests.push(request); - }; - server.events.on('request:start', listener); - - const result = await tool.execute({ - feedback: 'Great tools!', - account_id: 'acc_123456', - tool_names: ['data_export', 'analytics'], - }); - - expect(recordedRequests).toHaveLength(1); - expect(recordedRequests[0]?.url).toBe(`${TEST_BASE_URL}/ai/tool-feedback`); - expect(recordedRequests[0]?.method).toBe('POST'); - // TODO: Remove type assertion once createFeedbackTool returns properly typed result instead of JsonDict - const feedbackResult = result as unknown as FeedbackResult; - expect(feedbackResult).toMatchObject({ - message: 'Feedback sent to 1 account(s)', - total_accounts: 1, - successful: 1, - failed: 0, - }); - expect(feedbackResult.results[0]).toMatchObject({ - account_id: 'acc_123456', - status: 'success', - }); - expect(feedbackResult.results[0].result).toHaveProperty( - 'message', - 'Feedback successfully stored', - ); - server.events.removeListener('request:start', listener); - }); - - it('test_call_method_interface', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - const recordedRequests: Request[] = []; - const listener = ({ request }: { request: Request }) => { - recordedRequests.push(request); - }; - server.events.on('request:start', listener); - - // Test using the tool directly (equivalent to .call() in Python) - const result = await tool.execute({ - feedback: 'Great tools!', - account_id: 'acc_123456', - tool_names: ['test_tool'], - }); - - expect(recordedRequests).toHaveLength(1); - expect(result).toMatchObject({ - message: 'Feedback sent to 1 account(s)', - total_accounts: 1, - successful: 1, - failed: 0, - }); - server.events.removeListener('request:start', listener); - }); - - it('test_api_error_handling', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - - // Override the default handler to return an error - server.use( - http.post(`${TEST_BASE_URL}/ai/tool-feedback`, () => { - return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); - }), - ); - - await expect( - tool.execute({ - feedback: 'Great tools!', - account_id: 'acc_123456', - tool_names: ['test_tool'], - }), - ).rejects.toBeInstanceOf(StackOneError); - }); - - it('test_multiple_account_ids_execution', async () => { - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - - // Test all accounts succeed - const recordedRequests: Request[] = []; - const listener = ({ request }: { request: Request }) => { - recordedRequests.push(request); - }; - server.events.on('request:start', listener); - - const result = await tool.execute({ - feedback: 'Great tools!', - account_id: ['acc_123456', 'acc_789012', 'acc_345678'], - tool_names: ['test_tool'], - }); - - expect(recordedRequests).toHaveLength(3); - expect(result).toMatchObject({ - message: 'Feedback sent to 3 account(s)', - total_accounts: 3, - successful: 3, - failed: 0, - }); - server.events.removeListener('request:start', listener); - - // Test mixed success/error scenario - let callCount = 0; - server.use( - http.post(`${TEST_BASE_URL}/ai/tool-feedback`, () => { - callCount++; - if (callCount === 1) { - return HttpResponse.json({ message: 'Success' }); - } - return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); - }), - ); - - const mixedResult = await tool.execute({ - feedback: 'Great tools!', - account_id: ['acc_123456', 'acc_789012'], - tool_names: ['test_tool'], - }); - - expect(callCount).toBe(2); - // TODO: Remove type assertion once createFeedbackTool returns properly typed result instead of JsonDict - const mixedFeedbackResult = mixedResult as unknown as FeedbackResult; - expect(mixedFeedbackResult).toMatchObject({ - message: 'Feedback sent to 2 account(s)', - total_accounts: 2, - successful: 1, - failed: 1, - }); - - const successResult = mixedFeedbackResult.results.find((r) => r.account_id === 'acc_123456'); - const errorResult = mixedFeedbackResult.results.find((r) => r.account_id === 'acc_789012'); - - expect(successResult).toMatchObject({ - account_id: 'acc_123456', - status: 'success', - result: { message: 'Success' }, - }); - expect(errorResult).toMatchObject({ - account_id: 'acc_789012', - status: 'error', - error: '{"error":"Unauthorized"}', - }); - }); - - it('test_tool_integration', async () => { - // Test tool properties - const tool = createFeedbackTool(undefined, undefined, TEST_BASE_URL); - expect(tool.name).toBe('tool_feedback'); - expect(tool.description).toContain('Collects user feedback'); - expect(tool.parameters).toBeDefined(); - - // Test OpenAI function format conversion - const openaiFormat = tool.toOpenAI(); - expect(openaiFormat).toMatchObject({ - type: 'function', - function: { - name: 'tool_feedback', - description: expect.stringContaining('Collects user feedback'), - parameters: expect.objectContaining({ - type: 'object', - properties: expect.objectContaining({ - feedback: expect.any(Object), - account_id: expect.any(Object), - tool_names: expect.any(Object), - }), - }), - }, - }); - }); - }); -}); diff --git a/src/feedback.ts b/src/feedback.ts deleted file mode 100644 index 97b2b5c..0000000 --- a/src/feedback.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { z } from 'zod/v4'; -import { DEFAULT_BASE_URL } from './consts'; -import { BaseTool } from './tool'; -import type { ExecuteConfig, ExecuteOptions, JsonObject, JsonValue, ToolParameters } from './types'; -import { StackOneError } from './utils/error-stackone'; - -interface FeedbackToolOptions { - baseUrl?: string; - apiKey?: string; - accountId?: string; -} - -const createNonEmptyTrimmedStringSchema = (fieldName: string) => - z - .string() - .transform((value) => value.trim()) - .refine((value) => value.length > 0, { - message: `${fieldName} must be a non-empty string.`, - }); - -const feedbackInputSchema = z.object({ - feedback: createNonEmptyTrimmedStringSchema('Feedback'), - account_id: z - .union([ - createNonEmptyTrimmedStringSchema('Account ID'), - z - .array(createNonEmptyTrimmedStringSchema('Account ID')) - .min(1, 'At least one account ID is required'), - ]) - .transform((value) => (Array.isArray(value) ? value : [value])), - tool_names: z - .array(z.string()) - .min(1, 'At least one tool name is required') - .transform((value) => value.map((item) => item.trim()).filter((item) => item.length > 0)) - .refine((value) => value.length > 0, { - message: 'Tool names must contain at least one non-empty string', - }), -}); - -export function createFeedbackTool( - apiKey?: string, - accountId?: string, - baseUrl = DEFAULT_BASE_URL, -): BaseTool { - const options: FeedbackToolOptions = { - apiKey, - accountId, - baseUrl, - }; - const name = 'tool_feedback' as const; - const description = - 'Collects user feedback on StackOne tool performance. First ask the user, "Are you ok with sending feedback to StackOne?" and mention that the LLM will take care of sending it. Call this tool only when the user explicitly answers yes.'; - const parameters = { - type: 'object', - properties: { - account_id: { - oneOf: [ - { - type: 'string', - description: 'Single account identifier (e.g., "acc_123456")', - }, - { - type: 'array', - items: { - type: 'string', - }, - description: 'Array of account identifiers (e.g., ["acc_123456", "acc_789012"])', - }, - ], - description: 'Account identifier(s) - can be a single string or array of strings', - }, - feedback: { - type: 'string', - description: 'Verbatim feedback from the user about their experience with StackOne tools.', - }, - tool_names: { - type: 'array', - items: { - type: 'string', - }, - description: 'Array of tool names being reviewed', - }, - }, - required: ['feedback', 'account_id', 'tool_names'], - } as const satisfies ToolParameters; - - const executeConfig = { - kind: 'http', - method: 'POST', - url: '/ai/tool-feedback', - bodyType: 'json', - params: [], - } as const satisfies ExecuteConfig; - - // Get API key from environment or options - const resolvedApiKey = options.apiKey || process.env.STACKONE_API_KEY; - - // Create authentication headers - const authHeaders: Record = {}; - if (resolvedApiKey) { - const authString = Buffer.from(`${resolvedApiKey}:`).toString('base64'); - authHeaders.Authorization = `Basic ${authString}`; - } - - const tool = new BaseTool(name, description, parameters, executeConfig, authHeaders); - const resolvedBaseUrl = options.baseUrl ?? DEFAULT_BASE_URL; - - tool.execute = async function ( - this: BaseTool, - inputParams?: JsonObject | string, - executeOptions?: ExecuteOptions, - ): Promise { - try { - const rawParams = - typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; - const parsedParams = feedbackInputSchema.parse(rawParams); - - const headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...this.getHeaders(), - }; - - // Handle dry run - show what would be sent to each account - if (executeOptions?.dryRun) { - const dryRunResults = parsedParams.account_id.map((accountId: string) => ({ - url: `${resolvedBaseUrl}${executeConfig.url}`, - method: executeConfig.method, - headers, - body: { - feedback: parsedParams.feedback, - account_id: accountId, - tool_names: parsedParams.tool_names, - }, - })); - - return { - multiple_requests: dryRunResults, - total_accounts: parsedParams.account_id.length, - } satisfies JsonObject; - } - - // Send feedback to each account individually - const results: Array<{ account_id: string; status: number; response: JsonValue }> = []; - const errors: Array<{ account_id: string; status?: number; error: string }> = []; - - for (const accountId of parsedParams.account_id) { - try { - const requestBody = { - feedback: parsedParams.feedback, - account_id: accountId, - tool_names: parsedParams.tool_names, - }; - - const response = await fetch(`${resolvedBaseUrl}${executeConfig.url}`, { - method: executeConfig.method satisfies 'POST', - headers, - body: JSON.stringify(requestBody), - }); - - const text = await response.text(); - let parsed: JsonValue; - try { - parsed = text ? (JSON.parse(text) satisfies JsonValue) : {}; - } catch { - parsed = { raw: text }; - } - - if (!response.ok) { - errors.push({ - account_id: accountId, - status: response.status, - error: - typeof parsed === 'object' && parsed !== null - ? JSON.stringify(parsed) - : String(parsed), - }); - } else { - results.push({ - account_id: accountId, - status: response.status, - response: parsed, - }); - } - } catch (error) { - errors.push({ - account_id: accountId, - error: error instanceof Error ? error.message : String(error), - }); - } - } - - // Return summary of all submissions in Python SDK format - const response = { - message: `Feedback sent to ${parsedParams.account_id.length} account(s)`, - total_accounts: parsedParams.account_id.length, - successful: results.length, - failed: errors.length, - results: [ - ...results.map((r) => ({ - account_id: r.account_id, - status: 'success', - result: r.response, - })), - ...errors.map((e) => ({ - account_id: e.account_id, - status: 'error', - error: e.error, - })), - ], - } satisfies JsonObject; - - // If all submissions failed, throw an error - if (errors.length > 0 && results.length === 0) { - throw new StackOneError( - `Failed to submit feedback to any account. Errors: ${JSON.stringify(errors)}`, - ); - } - - return response; - } catch (error) { - if (error instanceof StackOneError) { - throw error; - } - throw new StackOneError('Error executing tool', { cause: error }); - } - }; - - return tool; -} diff --git a/src/index.ts b/src/index.ts index fa54c85..a280fc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ */ export { BaseTool, StackOneTool, Tools } from './tool'; -export { createFeedbackTool } from './feedback'; export { isBinaryDownloadResult, type BinaryDownloadResult } from './utils/binary-response'; export { StackOneError } from './utils/error-stackone'; export { StackOneAPIError } from './utils/error-stackone-api'; diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index 74c0f3b..aa38eca 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -283,7 +283,7 @@ describe('StackOneToolSet', () => { const toolNames = tools.toArray().map((t) => t.name); expect(toolNames).toContain('default_tool_1'); expect(toolNames).toContain('default_tool_2'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('uses x-account-id header when fetching tools with accountIds', async () => { @@ -299,7 +299,7 @@ describe('StackOneToolSet', () => { const toolNames = tools.toArray().map((t) => t.name); expect(toolNames).toContain('acc1_tool_1'); expect(toolNames).toContain('acc1_tool_2'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('uses setAccounts when no accountIds provided in fetchTools', async () => { @@ -322,7 +322,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('acc1_tool_2'); expect(toolNames).toContain('acc2_tool_1'); expect(toolNames).toContain('acc2_tool_2'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('uses accountIds from constructor when no accountIds provided in fetchTools', async () => { @@ -343,7 +343,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('acc1_tool_2'); expect(toolNames).toContain('acc2_tool_1'); expect(toolNames).toContain('acc2_tool_2'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('setAccounts overrides constructor accountIds', async () => { @@ -367,7 +367,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('acc2_tool_1'); expect(toolNames).toContain('acc2_tool_2'); expect(toolNames).toContain('acc3_tool_1'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('overrides setAccounts when accountIds provided in fetchTools', async () => { @@ -386,7 +386,7 @@ describe('StackOneToolSet', () => { expect(tools.length).toBe(2); const toolNames = tools.toArray().map((t) => t.name); expect(toolNames).toContain('acc3_tool_1'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); // Regression for issue #365: tools must carry the x-account-id of the @@ -786,7 +786,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('bamboohr_list_employees'); expect(toolNames).toContain('bamboohr_get_employee'); expect(toolNames).not.toContain('workday_list_employees'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('filters tools by actions with exact match', async () => { @@ -806,7 +806,7 @@ describe('StackOneToolSet', () => { const toolNames = tools.toArray().map((t) => t.name); expect(toolNames).toContain('hibob_list_employees'); expect(toolNames).toContain('hibob_create_employees'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('filters tools by actions with glob pattern', async () => { @@ -827,7 +827,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('workday_list_employees'); expect(toolNames).not.toContain('hibob_create_employees'); expect(toolNames).not.toContain('bamboohr_get_employee'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('combines accountIds and actions filters', async () => { @@ -902,7 +902,7 @@ describe('StackOneToolSet', () => { expect(toolNames).toContain('bamboohr_list_employees'); expect(toolNames).not.toContain('hibob_create_employees'); expect(toolNames).not.toContain('bamboohr_get_employee'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); }); it('combines all filters: accountIds, providers, and actions', async () => { @@ -962,7 +962,57 @@ describe('StackOneToolSet', () => { expect(tools.length).toBe(2); const toolNames = tools.toArray().map((t) => t.name); expect(toolNames).toContain('hibob_list_employees'); - expect(toolNames).toContain('tool_feedback'); + expect(toolNames).toContain('submit_feedback'); + }); + }); + + describe('feedback tool (inherited from MCP)', () => { + it('is included by default', async () => { + const toolset = new StackOneToolSet({ + baseUrl: TEST_BASE_URL, + apiKey: 'test-key', + accountId: 'mixed', + }); + + const tools = await toolset.fetchTools(); + expect(tools.toArray().map((t) => t.name)).toContain('submit_feedback'); + }); + + it('is excluded when feedback is disabled', async () => { + const toolset = new StackOneToolSet({ + baseUrl: TEST_BASE_URL, + apiKey: 'test-key', + accountId: 'mixed', + }); + + const tools = await toolset.fetchTools({ feedback: false }); + expect(tools.toArray().map((t) => t.name)).not.toContain('submit_feedback'); + }); + + it('stays out even when other filters would otherwise keep tools', async () => { + const toolset = new StackOneToolSet({ + baseUrl: TEST_BASE_URL, + apiKey: 'test-key', + accountId: 'mixed', + }); + + const tools = await toolset.fetchTools({ providers: ['hibob'], feedback: false }); + const toolNames = tools.toArray().map((t) => t.name); + expect(toolNames).toContain('hibob_list_employees'); + expect(toolNames).not.toContain('submit_feedback'); + }); + + it('appears exactly once across multiple accounts', async () => { + const toolset = new StackOneToolSet({ + baseUrl: TEST_BASE_URL, + apiKey: 'test-key', + }); + + const tools = await toolset.fetchTools({ accountIds: ['acc1', 'acc2'] }); + const feedbackCount = tools + .toArray() + .filter((t) => t.name === 'submit_feedback').length; + expect(feedbackCount).toBe(1); }); }); diff --git a/src/toolsets.ts b/src/toolsets.ts index 3e0dd09..2e3c69a 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -2,7 +2,6 @@ import { defu } from 'defu'; import type { MergeExclusive, SimplifyDeep } from 'type-fest'; import { z } from 'zod/v4'; import { DEFAULT_BASE_URL } from './consts'; -import { createFeedbackTool } from './feedback'; import { type StackOneHeaders, normalizeHeaders, stackOneHeadersSchema } from './headers'; import { ToolIndex } from './local-search'; import { createMCPClient } from './mcp-client'; @@ -191,6 +190,9 @@ export type StackOneToolSetConfig = StackOneToolSetBaseConfig & Partial tool.name !== FEEDBACK_TOOL_NAME); + const feedbackTool = tools.toArray().find((tool) => tool.name === FEEDBACK_TOOL_NAME); + const finalTools = + options?.feedback === false || !feedbackTool + ? new Tools(nonFeedbackTools) + : new Tools([...nonFeedbackTools, feedbackTool]); + + this.catalogCache.set(cacheKey, finalTools); + return finalTools; } /**