diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index c7cb74eeeb7..93a797c7e2b 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -455,6 +455,7 @@ export type SimplifiedNodeType = Pick< | 'outputs' > & { tag?: NodeCreatorTag; + isNew?: boolean; }; export interface SubcategoryItemProps { description?: string; diff --git a/packages/frontend/editor-ui/src/app/constants/nodeCreator.ts b/packages/frontend/editor-ui/src/app/constants/nodeCreator.ts index 29cae15bb14..1b1dc7315c2 100644 --- a/packages/frontend/editor-ui/src/app/constants/nodeCreator.ts +++ b/packages/frontend/editor-ui/src/app/constants/nodeCreator.ts @@ -63,4 +63,4 @@ export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fB export const RECOMMENDED_NODES: string[] = [DATA_TABLE_NODE_TYPE, DATA_TABLE_TOOL_NODE_TYPE]; export const BETA_NODES: string[] = ['@n8n/n8n-nodes-langchain.microsoftAgent365Trigger']; -export const NEW_TOOL_CATEGORIES: string[] = [AI_CATEGORY_HUMAN_IN_THE_LOOP]; +export const NEW_TOOL_CATEGORIES: string[] = [AI_CATEGORY_MCP_NODES]; diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/ItemTypes/NodeItem.vue b/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/ItemTypes/NodeItem.vue index 8bba204242d..cdd82216036 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/ItemTypes/NodeItem.vue +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/ItemTypes/NodeItem.vue @@ -150,6 +150,10 @@ const tag = computed(() => { return undefined; }); +// Only surface the "new" badge in search results — under the category itself +// the parent subcategory tile already carries the badge. +const showNewBadge = computed(() => Boolean(props.nodeType.isNew && activeViewStack.search)); + function onDragStart(event: DragEvent): void { if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'copy'; @@ -190,6 +194,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) { :is-official="isOfficial" :data-test-id="dataTestId" :tag="tag" + :is-new="showNewBadge" @dragstart="onDragStart" @dragend="onDragEnd" > diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.test.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.test.ts index 144bf39bdd8..856f6ddb9fd 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.test.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.test.ts @@ -5,6 +5,7 @@ import type { SimplifiedNodeType, } from '@/Interface'; import { + finalizeItems, formatTriggerActionName, filterAndSearchNodes, groupItemsInSections, @@ -33,6 +34,9 @@ import { AI_CATEGORY_OTHER_TOOLS, AI_CATEGORY_VECTOR_STORES, AI_CATEGORY_HUMAN_IN_THE_LOOP, + AI_CATEGORY_MCP_NODES, + AI_CATEGORY_ROOT_NODES, + AI_SUBCATEGORY, } from '@/app/constants'; vi.mock('@/app/stores/settings.store', () => ({ @@ -705,6 +709,35 @@ describe('NodeCreator - utils', () => { }); }); + describe('finalizeItems - MCP registry tool isNew flag', () => { + const makeMcpNode = (subcategoriesAi: string[]) => + mockNodeCreateElement(undefined, { + name: 'mcpRegistryNode', + codex: { + categories: ['AI'], + subcategories: { [AI_SUBCATEGORY]: subcategoriesAi }, + }, + }); + + it('should flag registry-generated MCP tools as new', () => { + const node = makeMcpNode([AI_CATEGORY_MCP_NODES]); + const [result] = finalizeItems([node]) as NodeCreateElement[]; + expect(result.properties.isNew).toBe(true); + }); + + it('should not flag MCP nodes that are also Root Nodes (e.g. McpTrigger)', () => { + const node = makeMcpNode([AI_CATEGORY_ROOT_NODES, AI_CATEGORY_MCP_NODES]); + const [result] = finalizeItems([node]) as NodeCreateElement[]; + expect(result.properties.isNew).toBeUndefined(); + }); + + it('should not flag tools that are not in the MCP subcategory', () => { + const node = makeMcpNode(['Tools']); + const [result] = finalizeItems([node]) as NodeCreateElement[]; + expect(result.properties.isNew).toBeUndefined(); + }); + }); + describe('mapToolSubcategoryIcon', () => { it('should return "globe" for AI_CATEGORY_OTHER_TOOLS', () => { expect(mapToolSubcategoryIcon(AI_CATEGORY_OTHER_TOOLS)).toBe('globe'); diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.ts b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.ts index dc579d0ffb0..0fa4582072b 100644 --- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.ts +++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/nodeCreator.utils.ts @@ -15,6 +15,7 @@ import { AI_CATEGORY_HUMAN_IN_THE_LOOP, AI_CATEGORY_MCP_NODES, AI_CATEGORY_OTHER_TOOLS, + AI_CATEGORY_ROOT_NODES, AI_CATEGORY_VECTOR_STORES, AI_SUBCATEGORY, AI_TRANSFORM_NODE_TYPE, @@ -288,7 +289,17 @@ export const removePreviewToken = (key: string) => export const isNodePreviewKey = (key = '') => key.includes(COMMUNITY_NODE_TYPE_PREVIEW_TOKEN); function applyNodeTags(element: INodeCreateElement): INodeCreateElement { - if (element.type !== 'node' || element.properties.tag) return element; + if (element.type !== 'node') return element; + + const aiSubcategories = element.properties.codex?.subcategories?.[AI_SUBCATEGORY] ?? []; + if ( + aiSubcategories.includes(AI_CATEGORY_MCP_NODES) && + !aiSubcategories.includes(AI_CATEGORY_ROOT_NODES) + ) { + element.properties.isNew = true; + } + + if (element.properties.tag) return element; if (RECOMMENDED_NODES.includes(element.properties.name)) { element.properties.tag = {