Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import type { McpToolSchema } from '@/lib/mcp/types'
import { getProviderIdFromServiceId, type OAuthProvider, type OAuthService } from '@/lib/oauth'
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
import { resolveStoredToolName } from '@/lib/workflows/subblocks/display'
import { buildToolSubBlockId } from '@/lib/workflows/tool-input/synthetic-subblocks'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { McpServerFormModal } from '@/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal'
Expand Down Expand Up @@ -59,7 +60,8 @@ import {
ActiveSearchTargetProvider,
useActiveSearchTarget,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import { getAllBlocks } from '@/blocks'
import { getAllBlocks, getBlock } from '@/blocks'
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
import { getTileIconColorClass } from '@/blocks/icon-color'
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
import { BUILT_IN_TOOL_TYPES } from '@/blocks/utils'
Expand Down Expand Up @@ -543,6 +545,13 @@ export const ToolInput = memo(function ToolInput({
const { data: customTools = [] } = useCustomTools(shouldFetchCustomTools ? workspaceId : '')

const { mcpTools, isLoading: mcpLoading } = useMcpTools(workspaceId)
const mcpToolNamesById = useMemo(() => {
const names = new Map<string, string>()
for (const t of mcpTools) {
if (!names.has(t.id)) names.set(t.id, t.name)
}
return names
}, [mcpTools])

const { data: mcpServers = [], isLoading: mcpServersLoading } = useMcpServers(workspaceId)
const { data: storedMcpTools = [] } = useStoredMcpTools(workspaceId)
Expand Down Expand Up @@ -642,6 +651,7 @@ export const ToolInput = memo(function ToolInput({

const { filterBlocks, config: permissionConfig } = usePermissionConfig()

const customBlockOverlayVersion = useCustomBlockOverlayVersion()
const toolBlocks = useMemo(() => {
const allToolBlocks = getAllBlocks().filter(
(block) =>
Expand All @@ -659,7 +669,7 @@ export const ToolInput = memo(function ToolInput({
block.type !== 'file'
)
return filterBlocks(allToolBlocks)
}, [filterBlocks])
}, [filterBlocks, customBlockOverlayVersion])

const hasBackfilledRef = useRef(false)
useEffect(() => {
Expand Down Expand Up @@ -1662,9 +1672,11 @@ export const ToolInput = memo(function ToolInput({
const isCustomTool = tool.type === 'custom-tool'
const isMcpTool = tool.type === 'mcp'
const isWorkflowTool = tool.type === 'workflow'
// Fall back to the unfiltered registry so chips for types hidden
// from the picker (permissions, hideFromToolbar) keep their chrome.
const toolBlock =
!isCustomTool && !isMcpTool
? toolBlocks.find((block) => block.type === tool.type)
? (toolBlocks.find((block) => block.type === tool.type) ?? getBlock(tool.type))
: null

const currentToolId =
Expand Down Expand Up @@ -1713,9 +1725,6 @@ export const ToolInput = memo(function ToolInput({
? resolveCustomToolFromReference(tool, customTools)
: null

const customToolTitle = isCustomTool
? tool.title || resolvedCustomTool?.title || 'Unknown Tool'
: null
const customToolSchema = isCustomTool ? tool.schema || resolvedCustomTool?.schema : null
const customToolParams =
isCustomTool && customToolSchema?.function?.parameters?.properties
Expand Down Expand Up @@ -1747,6 +1756,11 @@ export const ToolInput = memo(function ToolInput({
)
: []

// Canonical name wins; stored title only when nothing resolves
// (same policy as the canvas summary — see resolveStoredToolName).
const toolDisplayName =
resolveStoredToolName(tool, { customTools, mcpToolNamesById }) ?? 'Unknown Tool'

const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length
const displayParams: ToolParameterConfig[] = isCustomTool
? customToolParams
Expand Down Expand Up @@ -1854,7 +1868,7 @@ export const ToolInput = memo(function ToolInput({
)}
</div>
<span className='truncate font-medium text-[var(--text-primary)] text-small'>
{formatDisplayText((isCustomTool ? customToolTitle : tool.title) ?? '', {
{formatDisplayText(toolDisplayName ?? '', {
workflowSearchHighlight: getToolTitleSearchHighlight(toolIndex),
})}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import { getBlock } from '@/blocks'
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useCustomTools } from '@/hooks/queries/custom-tools'
import { useFolderMap } from '@/hooks/queries/folders'
import { isWorkflowEffectivelyLocked } from '@/hooks/queries/utils/folder-tree'
import { useWorkflowMap } from '@/hooks/queries/workflows'
Expand Down Expand Up @@ -170,6 +173,15 @@ export function WorkflowSearchReplace() {
const prevIsOpenRef = useRef(false)
const afterReplaceIndexRef = useRef<number | null>(null)
const { data: workspaceCredentials } = useWorkspaceCredentials({ workspaceId, enabled: isOpen })
const { data: customTools = [] } = useCustomTools(isOpen && workspaceId ? workspaceId : '')
const { mcpTools } = useMcpTools(isOpen && workspaceId ? workspaceId : '')
const mcpToolNamesById = useMemo(() => {
const names = new Map<string, string>()
for (const t of mcpTools) {
if (!names.has(t.id)) names.set(t.id, t.name)
}
return names
}, [mcpTools])

useRegisterGlobalCommands([
createCommand({
Expand Down Expand Up @@ -202,6 +214,8 @@ export function WorkflowSearchReplace() {
[workspaceCredentials]
)

/** Overlay version invalidates getBlock-resolved tool names in the index. */
const customBlockOverlayVersion = useCustomBlockOverlayVersion()
const matches = useMemo(
() =>
indexWorkflowSearchMatches({
Expand All @@ -215,10 +229,15 @@ export function WorkflowSearchReplace() {
workspaceId,
workflowId,
credentialTypeById,
customTools,
mcpToolNamesById,
}),
[
currentWorkflow.isSnapshotView,
credentialTypeById,
customBlockOverlayVersion,
customTools,
mcpToolNamesById,
Comment thread
cursor[bot] marked this conversation as resolved.
query,
readonlyReason,
searchBlocks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
import { getDependsOnFields } from '@/blocks/utils'
import { useKnowledgeBase } from '@/hooks/kb/use-knowledge'
Expand All @@ -68,6 +69,9 @@ const logger = createLogger('WorkflowBlock')
/** Stable empty object to avoid creating new references */
const EMPTY_SUBBLOCK_VALUES = {} as Record<string, any>

/** Stable empty map for rows that never resolve MCP tool names */
const EMPTY_MCP_TOOL_NAMES: ReadonlyMap<string, string> = new Map()

interface SubBlockRowProps {
title: string
value?: string
Expand Down Expand Up @@ -274,17 +278,23 @@ const SubBlockRow = memo(function SubBlockRow({
}, [subBlock?.type, rawValue, mcpServers])

const { data: mcpToolsData = [] } = useMcpToolsQuery(workspaceId || '')
const mcpToolNamesById = useMemo(() => {
if (subBlock?.type !== 'mcp-tool-selector' && subBlock?.type !== 'tool-input') {
return EMPTY_MCP_TOOL_NAMES
}
const names = new Map<string, string>()
for (const t of mcpToolsData) {
const toolId = createMcpToolId(t.serverId, t.name)
if (!names.has(toolId)) names.set(toolId, t.name)
}
return names
}, [subBlock?.type, mcpToolsData])
const mcpToolDisplayName = useMemo(() => {
if (subBlock?.type !== 'mcp-tool-selector' || typeof rawValue !== 'string') {
return null
}

const tool = mcpToolsData.find((t) => {
const toolId = createMcpToolId(t.serverId, t.name)
return toolId === rawValue
})
return tool?.name ?? null
}, [subBlock?.type, rawValue, mcpToolsData])
return mcpToolNamesById.get(rawValue) ?? null
}, [subBlock?.type, rawValue, mcpToolNamesById])

const { data: tables = [] } = useTablesList(workspaceId || '')
const tableDisplayName = useMemo(() => {
Expand Down Expand Up @@ -329,11 +339,16 @@ const SubBlockRow = memo(function SubBlockRow({
[subBlock, rawValue, workflowVariables]
)

/** Hydrates tool references to display names. */
/**
* Hydrates tool references to display names. The overlay version is a dep
* because resolveToolsLabel reads getBlock, whose custom-block results
* change when the client overlay hydrates (see client-overlay.ts).
*/
const { data: customTools = [] } = useCustomTools(workspaceId || '')
const customBlockOverlayVersion = useCustomBlockOverlayVersion()
const toolsDisplayValue = useMemo(
() => resolveToolsLabel(subBlock, rawValue, customTools),
[subBlock, rawValue, customTools]
() => resolveToolsLabel(subBlock, rawValue, customTools, mcpToolNamesById),
[subBlock, rawValue, customTools, mcpToolNamesById, customBlockOverlayVersion]
)

const filterDisplayValue = useMemo(
Expand Down
51 changes: 50 additions & 1 deletion apps/sim/lib/workflows/search-replace/indexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,10 @@ describe('indexWorkflowSearchMatches', () => {
custom: {
subBlocks: [{ id: 'tools', title: 'Tools', type: 'tool-input' }],
},
native: {
name: 'Customer Mailer',
subBlocks: [],
},
},
}).filter((match) => match.blockId === 'tool-input-1')

Expand All @@ -1395,7 +1399,7 @@ describe('indexWorkflowSearchMatches', () => {
expect.objectContaining({
subBlockId: 'tools',
valuePath: [0, 'title'],
searchText: 'Customer notifier',
searchText: 'Customer Mailer',
}),
expect.objectContaining({
subBlockId: 'tools',
Expand All @@ -1404,6 +1408,7 @@ describe('indexWorkflowSearchMatches', () => {
}),
])
)
expect(matches.some((match) => match.searchText === 'Customer notifier')).toBe(false)
expect(matches.some((match) => match.valuePath.includes('toolId'))).toBe(false)
expect(matches.some((match) => match.valuePath.includes('operation'))).toBe(false)
expect(matches.some((match) => match.valuePath.includes('credentialId'))).toBe(false)
Expand All @@ -1418,6 +1423,50 @@ describe('indexWorkflowSearchMatches', () => {
expect(matches.some((match) => match.valuePath.includes('schema'))).toBe(false)
})

it('indexes canonical MCP and custom-tool names over mutated stored titles', () => {
const workflow = createSearchReplaceWorkflowFixture()
workflow.blocks['tool-input-1'] = {
id: 'tool-input-1',
type: 'custom',
name: 'Tool Input Block',
position: { x: 0, y: 0 },
enabled: true,
outputs: {},
subBlocks: {
tools: {
id: 'tools',
type: 'tool-input',
value: [
{ type: 'mcp', toolId: 'mcp-tool-1', title: 'Mutated Title', params: {} },
{ type: 'custom-tool', customToolId: 'ct-1', title: 'Mutated Title', params: {} },
],
},
},
}

const matches = indexWorkflowSearchMatches({
workflow,
query: 'customer',
mode: 'text',
blockConfigs: {
...SEARCH_REPLACE_BLOCK_CONFIGS,
custom: {
subBlocks: [{ id: 'tools', title: 'Tools', type: 'tool-input' }],
},
},
customTools: [{ id: 'ct-1', title: 'Customer Records' }],
mcpToolNamesById: new Map([['mcp-tool-1', 'customer_lookup']]),
}).filter((match) => match.blockId === 'tool-input-1')

expect(matches).toEqual(
expect.arrayContaining([
expect.objectContaining({ valuePath: [0, 'title'], searchText: 'customer_lookup' }),
expect.objectContaining({ valuePath: [1, 'title'], searchText: 'Customer Records' }),
])
)
expect(matches.some((match) => match.searchText === 'Mutated Title')).toBe(false)
})

it('indexes explicit secret tool params for intentional replacement', () => {
const workflow = createSearchReplaceWorkflowFixture()
workflow.blocks['tool-input-1'] = {
Expand Down
20 changes: 18 additions & 2 deletions apps/sim/lib/workflows/search-replace/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from '@/lib/workflows/search-replace/types'
import { pathToKey, walkStringValues } from '@/lib/workflows/search-replace/value-walker'
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
import { resolveStoredToolName } from '@/lib/workflows/subblocks/display'
import {
buildCanonicalIndex,
buildSubBlockValues,
Expand Down Expand Up @@ -931,6 +932,8 @@ function addToolInputMatches({
workflowId,
credentialTypeById,
blockConfigs,
customTools,
mcpToolNamesById,
}: {
matches: WorkflowSearchMatch[]
block: WorkflowSearchBlockState
Expand All @@ -950,11 +953,20 @@ function addToolInputMatches({
workflowId?: string
credentialTypeById?: Record<string, string | undefined>
blockConfigs?: WorkflowSearchIndexerOptions['blockConfigs']
customTools?: WorkflowSearchIndexerOptions['customTools']
mcpToolNamesById?: WorkflowSearchIndexerOptions['mcpToolNamesById']
}) {
const parentCanonicalModes = getSearchCanonicalModes(block)

parseStoredToolInputValue(value).forEach((tool, toolIndex) => {
if (mode !== 'resource' && tool.title) {
// Index the resolved display name (not the stored mutable title) so
// search text and highlights match what the tool chip actually renders.
const toolDisplayName = resolveStoredToolName(tool, {
customTools,
mcpToolNamesById,
getBlockConfig: (type) => blockConfigs?.[type] ?? getBlock(type),
})
Comment thread
j15z marked this conversation as resolved.
if (mode !== 'resource' && toolDisplayName) {
addTextMatches({
matches,
idPrefix: 'tool-input-title',
Expand All @@ -963,7 +975,7 @@ function addToolInputMatches({
canonicalSubBlockId,
subBlockType: 'tool-input',
fieldTitle: 'Tool',
value: tool.title,
value: toolDisplayName,
valuePath: [toolIndex, 'title'],
target: { kind: 'subblock' },
query,
Expand Down Expand Up @@ -1228,6 +1240,8 @@ export function indexWorkflowSearchMatches(
workflowId,
blockConfigs = {},
credentialTypeById,
customTools,
mcpToolNamesById,
} = options

const matches: WorkflowSearchMatch[] = []
Expand Down Expand Up @@ -1363,6 +1377,8 @@ export function indexWorkflowSearchMatches(
workflowId,
credentialTypeById,
blockConfigs,
customTools,
mcpToolNamesById,
})
continue
}
Expand Down
12 changes: 11 additions & 1 deletion apps/sim/lib/workflows/search-replace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
WorkflowSearchSubflowEditableValue,
WorkflowSearchSubflowFieldId,
} from '@/lib/workflows/search-replace/subflow-fields'
import type { StoredCustomToolRecord } from '@/lib/workflows/subblocks/display'
import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext } from '@/hooks/selectors/types'
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
Expand Down Expand Up @@ -96,10 +97,19 @@ export interface WorkflowSearchIndexerOptions {
workflowId?: string
blockConfigs?: Record<
string,
| { subBlocks?: SubBlockConfig[]; triggers?: { enabled?: boolean }; category?: string }
| {
name?: string
subBlocks?: SubBlockConfig[]
triggers?: { enabled?: boolean }
category?: string
}
| undefined
>
credentialTypeById?: Record<string, string | undefined>
/** Custom-tool records so indexed tool names match the rendered chips. */
customTools?: StoredCustomToolRecord[]
/** Live MCP tool names keyed by composite tool id, same as the chip renderers. */
mcpToolNamesById?: ReadonlyMap<string, string>
}

export interface WorkflowSearchReplacementOption {
Expand Down
Loading
Loading