From 977148907e63f4b9326171abea4ef5a33d7f9f63 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 11:58:28 -0800 Subject: [PATCH 01/17] feat(agent): add MCP server discovery mode for agent tool input --- .../components/tool-input/tool-input.tsx | 276 ++++++++++++++---- .../sub-block/components/tool-input/types.ts | 19 +- .../sub-block/components/tool-input/utils.ts | 9 + .../emcn/components/combobox/combobox.tsx | 30 +- .../executor/handlers/agent/agent-handler.ts | 90 +++++- apps/sim/executor/handlers/agent/types.ts | 25 ++ 6 files changed, 372 insertions(+), 77 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 9df538fb0d..43af25fc34 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Loader2, WrenchIcon, XIcon } from 'lucide-react' +import { ChevronRight, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -41,6 +41,7 @@ import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowI import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' import { isCustomToolAlreadySelected, + isMcpServerAlreadySelected, isMcpToolAlreadySelected, isWorkflowAlreadySelected, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils' @@ -481,6 +482,7 @@ export const ToolInput = memo(function ToolInput({ const [draggedIndex, setDraggedIndex] = useState(null) const [dragOverIndex, setDragOverIndex] = useState(null) const [usageControlPopoverIndex, setUsageControlPopoverIndex] = useState(null) + const [expandedMcpServers, setExpandedMcpServers] = useState>(new Set()) const canonicalModeOverrides = useWorkflowStore( useCallback( @@ -702,6 +704,35 @@ export const ToolInput = memo(function ToolInput({ return selectedTools.some((tool) => tool.toolId === toolId) } + /** + * Groups MCP tools by their parent server. + */ + const mcpToolsByServer = useMemo(() => { + const grouped = new Map() + for (const tool of availableMcpTools) { + if (!grouped.has(tool.serverId)) { + grouped.set(tool.serverId, []) + } + grouped.get(tool.serverId)!.push(tool) + } + return grouped + }, [availableMcpTools]) + + /** + * Toggles the expanded state of an MCP server in the dropdown. + */ + const toggleMcpServerExpanded = useCallback((serverId: string) => { + setExpandedMcpServers((prev) => { + const next = new Set(prev) + if (next.has(serverId)) { + next.delete(serverId) + } else { + next.add(serverId) + } + return next + }) + }, []) + const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { if (isPreview || disabled) return @@ -1283,40 +1314,123 @@ export const ToolInput = memo(function ToolInput({ }) } - if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) { - groups.push({ - section: 'MCP Tools', - items: availableMcpTools.map((mcpTool) => { - const server = mcpServers.find((s) => s.id === mcpTool.serverId) - const alreadySelected = isMcpToolAlreadySelected(selectedTools, mcpTool.id) - return { - label: mcpTool.name, - value: `mcp-${mcpTool.id}`, - iconElement: createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon), + // MCP Servers section - grouped by server with expandable folders + if (!permissionConfig.disableMcpTools && mcpToolsByServer.size > 0) { + const serverItems: ComboboxOption[] = [] + + for (const [serverId, tools] of mcpToolsByServer) { + const server = mcpServers.find((s) => s.id === serverId) + const serverName = tools[0]?.serverName || server?.name || 'Unknown Server' + const isExpanded = expandedMcpServers.has(serverId) + const serverAlreadySelected = isMcpServerAlreadySelected(selectedTools, serverId) + const toolCount = tools.length + + // Server folder header (clickable to expand/collapse) + serverItems.push({ + label: serverName, + value: `mcp-server-folder-${serverId}`, + iconElement: ( +
+ +
+ +
+
+ ), + onSelect: () => { + toggleMcpServerExpanded(serverId) + }, + disabled: false, + keepOpen: true, + }) + + // If expanded, show "Use all tools" option and individual tools + if (isExpanded) { + serverItems.push({ + label: `Use all ${toolCount} tools`, + value: `mcp-server-all-${serverId}`, + iconElement: ( +
+ +
+ ), onSelect: () => { - if (alreadySelected) return + if (serverAlreadySelected) return + const filteredTools = selectedTools.filter( + (tool) => !(tool.type === 'mcp' && tool.params?.serverId === serverId) + ) const newTool: StoredTool = { - type: 'mcp', - title: mcpTool.name, - toolId: mcpTool.id, + type: 'mcp-server', + title: `${serverName} (all tools)`, + toolId: `mcp-server-${serverId}`, params: { - serverId: mcpTool.serverId, + serverId, ...(server?.url && { serverUrl: server.url }), - toolName: mcpTool.name, - serverName: mcpTool.serverName, + serverName, + toolCount: String(toolCount), }, - isExpanded: true, + isExpanded: false, usageControl: 'auto', - schema: { - ...mcpTool.inputSchema, - description: mcpTool.description, - }, } - handleMcpToolSelect(newTool, true) + setStoreValue([ + ...filteredTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + setOpen(false) }, - disabled: isPreview || disabled || alreadySelected, + disabled: isPreview || disabled || serverAlreadySelected, + }) + + // Individual tools from this server + for (const mcpTool of tools) { + const alreadySelected = + isMcpToolAlreadySelected(selectedTools, mcpTool.id) || serverAlreadySelected + serverItems.push({ + label: mcpTool.name, + value: `mcp-${mcpTool.id}`, + iconElement: ( +
+ {createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon)} +
+ ), + onSelect: () => { + if (alreadySelected) return + const newTool: StoredTool = { + type: 'mcp', + title: mcpTool.name, + toolId: mcpTool.id, + params: { + serverId: mcpTool.serverId, + ...(server?.url && { serverUrl: server.url }), + toolName: mcpTool.name, + serverName: mcpTool.serverName, + }, + isExpanded: true, + usageControl: 'auto', + schema: { + ...mcpTool.inputSchema, + description: mcpTool.description, + }, + } + handleMcpToolSelect(newTool, true) + }, + disabled: isPreview || disabled || alreadySelected, + }) } - }), + } + } + + groups.push({ + section: 'MCP Servers', + items: serverItems, }) } @@ -1396,6 +1510,8 @@ export const ToolInput = memo(function ToolInput({ customTools, availableMcpTools, mcpServers, + mcpToolsByServer, + expandedMcpServers, toolBlocks, isPreview, disabled, @@ -1407,6 +1523,7 @@ export const ToolInput = memo(function ToolInput({ permissionConfig.disableMcpTools, availableWorkflows, isToolAlreadySelected, + toggleMcpServerExpanded, ]) return ( @@ -1427,19 +1544,20 @@ export const ToolInput = memo(function ToolInput({ selectedTools.map((tool, toolIndex) => { const isCustomTool = tool.type === 'custom-tool' const isMcpTool = tool.type === 'mcp' + const isMcpServer = tool.type === 'mcp-server' const isWorkflowTool = tool.type === 'workflow' const toolBlock = - !isCustomTool && !isMcpTool + !isCustomTool && !isMcpTool && !isMcpServer ? toolBlocks.find((block) => block.type === tool.type) : null const currentToolId = - !isCustomTool && !isMcpTool + !isCustomTool && !isMcpTool && !isMcpServer ? getToolIdForOperation(tool.type, tool.operation) || tool.toolId || '' : tool.toolId || '' const toolParams = - !isCustomTool && !isMcpTool && currentToolId + !isCustomTool && !isMcpTool && !isMcpServer && currentToolId ? getToolParametersConfig(currentToolId, tool.type, { operation: tool.operation, ...tool.params, @@ -1449,7 +1567,7 @@ export const ToolInput = memo(function ToolInput({ const toolScopedOverrides = scopeCanonicalOverrides(canonicalModeOverrides, tool.type) const subBlocksResult: SubBlocksForToolInput | null = - !isCustomTool && !isMcpTool && currentToolId + !isCustomTool && !isMcpTool && !isMcpServer && currentToolId ? getSubBlocksForToolInput( currentToolId, tool.type, @@ -1512,21 +1630,26 @@ export const ToolInput = memo(function ToolInput({ ) : [] - const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length + const useSubBlocks = + !isCustomTool && !isMcpTool && !isMcpServer && subBlocksResult?.subBlocks?.length const displayParams: ToolParameterConfig[] = isCustomTool ? customToolParams : isMcpTool ? mcpToolParams - : toolParams?.userInputParameters || [] + : isMcpServer + ? [] // MCP servers have no user-configurable params + : toolParams?.userInputParameters || [] const displaySubBlocks: BlockSubBlockConfig[] = useSubBlocks ? subBlocksResult!.subBlocks : [] - const hasOperations = !isCustomTool && !isMcpTool && hasMultipleOperations(tool.type) + const hasOperations = + !isCustomTool && !isMcpTool && !isMcpServer && hasMultipleOperations(tool.type) const hasParams = useSubBlocks ? displaySubBlocks.length > 0 : displayParams.filter((param) => evaluateParameterCondition(param, tool)).length > 0 - const hasToolBody = hasOperations || hasParams + // MCP servers are expandable to show tool list + const hasToolBody = isMcpServer ? true : hasOperations || hasParams const isExpandedForDisplay = hasToolBody ? isPreview @@ -1534,6 +1657,11 @@ export const ToolInput = memo(function ToolInput({ : !!tool.isExpanded : false + // For MCP servers, get the list of tools for display + const mcpServerTools = isMcpServer + ? availableMcpTools.filter((t) => t.serverId === tool.params?.serverId) + : [] + return (
{isCustomTool ? ( ) : isMcpTool ? ( + ) : isMcpServer ? ( + ) : isWorkflowTool ? ( ) : ( @@ -1593,6 +1725,11 @@ export const ToolInput = memo(function ToolInput({ {isCustomTool ? customToolTitle : tool.title} + {isMcpServer && ( + + {tool.params?.toolCount || mcpServerTools.length} tools + + )} {isMcpTool && !mcpDataLoading && (() => { @@ -1698,30 +1835,53 @@ export const ToolInput = memo(function ToolInput({ {!isCustomTool && isExpandedForDisplay && (
- {(() => { - const hasOperations = hasMultipleOperations(tool.type) - const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] + {/* MCP Server tool list (read-only) */} + {isMcpServer && mcpServerTools.length > 0 && ( +
+
+ Available tools: +
+
+ {mcpServerTools.map((serverTool) => ( + + {serverTool.name} + + ))} +
+
+ )} - return hasOperations && operationOptions.length > 0 ? ( -
-
- Operation + {/* Operation dropdown for tools with multiple operations */} + {!isMcpServer && + (() => { + const hasOperations = hasMultipleOperations(tool.type) + const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] + + return hasOperations && operationOptions.length > 0 ? ( +
+
+ Operation +
+ option.id !== '') + .map((option) => ({ + label: option.label, + value: option.id, + }))} + value={tool.operation || operationOptions[0].id} + onChange={(value) => handleOperationChange(toolIndex, value)} + placeholder='Select operation' + disabled={disabled} + />
- option.id !== '') - .map((option) => ({ - label: option.label, - value: option.id, - }))} - value={tool.operation || operationOptions[0].id} - onChange={(value) => handleOperationChange(toolIndex, value)} - placeholder='Select operation' - disabled={disabled} - /> -
- ) : null - })()} + ) : null + })()} {(() => { const renderedElements: React.ReactNode[] = [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts index 138b6a5621..d87661afa5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts @@ -2,18 +2,33 @@ * Represents a tool selected and configured in the workflow * * @remarks + * Valid types include: + * - Standard block types (e.g., 'api', 'search', 'function') + * - 'custom-tool': User-defined tools with custom code + * - 'mcp': Individual MCP tool from a connected server + * - 'mcp-server': All tools from an MCP server (agent discovery mode). + * At execution time, this expands into individual tool definitions for + * all tools available on the server. + * * For custom tools (new format), we only store: type, customToolId, usageControl, isExpanded. * Everything else (title, schema, code) is loaded dynamically from the database. * Legacy custom tools with inline schema/code are still supported for backwards compatibility. */ export interface StoredTool { - /** Block type identifier */ + /** + * Block type identifier. + * 'mcp-server' enables server-level selection where all tools from + * the server are made available to the LLM at execution time. + */ type: string /** Display title for the tool (optional for new custom tool format) */ title?: string /** Direct tool ID for execution (optional for new custom tool format) */ toolId?: string - /** Parameter values configured by the user (optional for new custom tool format) */ + /** + * Parameter values configured by the user. + * For 'mcp-server' type, includes: serverId, serverUrl, serverName, toolCount + */ params?: Record /** Whether the tool details are expanded in UI */ isExpanded?: boolean diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts index 1110a5808b..d47a745bdb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts @@ -7,6 +7,15 @@ export function isMcpToolAlreadySelected(selectedTools: StoredTool[], mcpToolId: return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) } +/** + * Checks if an MCP server is already selected (all tools mode). + */ +export function isMcpServerAlreadySelected(selectedTools: StoredTool[], serverId: string): boolean { + return selectedTools.some( + (tool) => tool.type === 'mcp-server' && tool.params?.serverId === serverId + ) +} + /** * Checks if a custom tool is already selected. */ diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index 3940b24313..2c98353c46 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -53,6 +53,8 @@ export type ComboboxOption = { onSelect?: () => void /** Whether this option is disabled */ disabled?: boolean + /** When true, keep the dropdown open after selecting this option */ + keepOpen?: boolean } /** @@ -254,13 +256,15 @@ const Combobox = memo( * Handles selection of an option */ const handleSelect = useCallback( - (selectedValue: string, customOnSelect?: () => void) => { + (selectedValue: string, customOnSelect?: () => void, keepOpen?: boolean) => { // If option has custom onSelect, use it instead if (customOnSelect) { customOnSelect() - setOpen(false) - setHighlightedIndex(-1) - setSearchQuery('') + if (!keepOpen) { + setOpen(false) + setHighlightedIndex(-1) + setSearchQuery('') + } return } @@ -272,11 +276,13 @@ const Combobox = memo( onMultiSelectChange(newValues) } else { onChange?.(selectedValue) - setOpen(false) - setHighlightedIndex(-1) - setSearchQuery('') - if (editable && inputRef.current) { - inputRef.current.blur() + if (!keepOpen) { + setOpen(false) + setHighlightedIndex(-1) + setSearchQuery('') + if (editable && inputRef.current) { + inputRef.current.blur() + } } } }, @@ -345,7 +351,7 @@ const Combobox = memo( e.preventDefault() const selectedOption = filteredOptions[highlightedIndex] if (selectedOption && !selectedOption.disabled) { - handleSelect(selectedOption.value, selectedOption.onSelect) + handleSelect(selectedOption.value, selectedOption.onSelect, selectedOption.keepOpen) } } else if (!editable) { e.preventDefault() @@ -671,7 +677,7 @@ const Combobox = memo( e.preventDefault() e.stopPropagation() if (!option.disabled) { - handleSelect(option.value, option.onSelect) + handleSelect(option.value, option.onSelect, option.keepOpen) } }} onMouseEnter={() => @@ -746,7 +752,7 @@ const Combobox = memo( e.preventDefault() e.stopPropagation() if (!option.disabled) { - handleSelect(option.value, option.onSelect) + handleSelect(option.value, option.onSelect, option.keepOpen) } }} onMouseEnter={() => !option.disabled && setHighlightedIndex(index)} diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index c76ab48c0d..d7c0e92a2f 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -164,7 +164,7 @@ export class AgentBlockHandler implements BlockHandler { private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise { if (!Array.isArray(tools) || tools.length === 0) return - const hasMcpTools = tools.some((t) => t.type === 'mcp') + const hasMcpTools = tools.some((t) => t.type === 'mcp' || t.type === 'mcp-server') const hasCustomTools = tools.some((t) => t.type === 'custom-tool') if (hasMcpTools) { @@ -182,7 +182,7 @@ export class AgentBlockHandler implements BlockHandler { ): Promise { if (!Array.isArray(tools) || tools.length === 0) return tools - const mcpTools = tools.filter((t) => t.type === 'mcp') + const mcpTools = tools.filter((t) => t.type === 'mcp' || t.type === 'mcp-server') if (mcpTools.length === 0) return tools const serverIds = [...new Set(mcpTools.map((t) => t.params?.serverId).filter(Boolean))] @@ -216,7 +216,7 @@ export class AgentBlockHandler implements BlockHandler { } return tools.filter((tool) => { - if (tool.type !== 'mcp') return true + if (tool.type !== 'mcp' && tool.type !== 'mcp-server') return true const serverId = tool.params?.serverId if (!serverId) return false return availableServerIds.has(serverId) @@ -236,11 +236,14 @@ export class AgentBlockHandler implements BlockHandler { }) const mcpTools: ToolInput[] = [] + const mcpServers: ToolInput[] = [] const otherTools: ToolInput[] = [] for (const tool of filtered) { if (tool.type === 'mcp') { mcpTools.push(tool) + } else if (tool.type === 'mcp-server') { + mcpServers.push(tool) } else { otherTools.push(tool) } @@ -249,7 +252,12 @@ export class AgentBlockHandler implements BlockHandler { const otherResults = await Promise.all( otherTools.map(async (tool) => { try { - if (tool.type && tool.type !== 'custom-tool' && tool.type !== 'mcp') { + if ( + tool.type && + tool.type !== 'custom-tool' && + tool.type !== 'mcp' && + tool.type !== 'mcp-server' + ) { await validateBlockType(ctx.userId, tool.type, ctx) } if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { @@ -265,12 +273,84 @@ export class AgentBlockHandler implements BlockHandler { const mcpResults = await this.processMcpToolsBatched(ctx, mcpTools) - const allTools = [...otherResults, ...mcpResults] + // Process MCP servers (all tools from server mode) + const mcpServerResults = await this.processMcpServerSelections(ctx, mcpServers) + + const allTools = [...otherResults, ...mcpResults, ...mcpServerResults] return allTools.filter( (tool): tool is NonNullable => tool !== null && tool !== undefined ) } + /** + * Process MCP server selections by discovering and formatting all tools from each server. + * This enables "agent discovery" mode where the LLM can call any tool from the server. + */ + private async processMcpServerSelections( + ctx: ExecutionContext, + mcpServerSelections: ToolInput[] + ): Promise { + if (mcpServerSelections.length === 0) return [] + + const results: any[] = [] + + for (const serverSelection of mcpServerSelections) { + const serverId = serverSelection.params?.serverId + const serverName = serverSelection.params?.serverName + const usageControl = serverSelection.usageControl || 'auto' + + if (!serverId) { + logger.error('MCP server selection missing serverId:', serverSelection) + continue + } + + try { + // Discover all tools from this server + const discoveredTools = await this.discoverMcpToolsForServer(ctx, serverId) + + // Create tool definitions for each discovered tool + for (const mcpTool of discoveredTools) { + const created = await this.createMcpToolFromDiscoveredServerTool( + mcpTool, + serverId, + serverName || serverId, + usageControl + ) + if (created) results.push(created) + } + + logger.info( + `[AgentHandler] Expanded MCP server ${serverName} into ${discoveredTools.length} tools` + ) + } catch (error) { + logger.error(`[AgentHandler] Failed to process MCP server selection:`, { serverId, error }) + } + } + + return results + } + + /** + * Create an MCP tool from server discovery for the "all tools" mode. + * Delegates to buildMcpTool so server-discovered tools use the same + * execution pipeline as individually-selected MCP tools. + */ + private async createMcpToolFromDiscoveredServerTool( + mcpTool: any, + serverId: string, + serverName: string, + usageControl: string + ): Promise { + return this.buildMcpTool({ + serverId, + toolName: mcpTool.name, + description: mcpTool.description || `MCP tool ${mcpTool.name} from ${serverName}`, + schema: mcpTool.inputSchema || { type: 'object', properties: {} }, + userProvidedParams: {}, + usageControl, + }) + } + private async createCustomTool(ctx: ExecutionContext, tool: ToolInput): Promise { const userProvidedParams = tool.params || {} diff --git a/apps/sim/executor/handlers/agent/types.ts b/apps/sim/executor/handlers/agent/types.ts index fc36d0e6f4..3c69548fa2 100644 --- a/apps/sim/executor/handlers/agent/types.ts +++ b/apps/sim/executor/handlers/agent/types.ts @@ -39,11 +39,36 @@ export interface AgentInputs { thinkingLevel?: string } +/** + * Represents a tool input for the agent block. + * + * @remarks + * Valid types include: + * - Standard block types (e.g., 'api', 'search', 'function') + * - 'custom-tool': User-defined tools with custom code + * - 'mcp': Individual MCP tool from a connected server + * - 'mcp-server': All tools from an MCP server (agent discovery mode). + * At execution time, this is expanded into individual tool definitions + * for all tools available on the server. This enables dynamic capability + * discovery where the LLM can call any tool from the server. + */ export interface ToolInput { + /** + * Tool type identifier. + * 'mcp-server' enables server-level selection where all tools from + * the server are made available to the LLM at execution time. + */ type?: string schema?: any title?: string code?: string + /** + * Tool parameters. For 'mcp-server' type, includes: + * - serverId: The MCP server ID + * - serverUrl: The server URL (optional) + * - serverName: Human-readable server name + * - toolCount: Number of tools available (for display) + */ params?: Record timeout?: number usageControl?: 'auto' | 'force' | 'none' From 0feb7abcaf40792e58b195f1ca798e136ffa6447 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 12:09:54 -0800 Subject: [PATCH 02/17] fix(tool-input): use type variant for MCP server tool count badge --- .../components/sub-block/components/tool-input/tool-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 43af25fc34..cfbc26f08d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1726,7 +1726,7 @@ export const ToolInput = memo(function ToolInput({ {isCustomTool ? customToolTitle : tool.title} {isMcpServer && ( - + {tool.params?.toolCount || mcpServerTools.length} tools )} From cad1803a91b0f5d74a6ba7c3c4934affb88b1319 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 12:23:20 -0800 Subject: [PATCH 03/17] fix(mcp-dynamic-args): align label styling with standard subblock labels --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx index 5271ecb33f..67bd115a69 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import { Combobox, Label, Slider, Switch } from '@/components/emcn/components' -import { cn } from '@/lib/core/utils/cn' import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input' import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -147,7 +146,7 @@ export function McpDynamicArgs({ /> @@ -351,15 +350,14 @@ export function McpDynamicArgs({
{showLabel && ( - +
+ +
)} {renderParameterInput(paramName, paramSchema as any)}
From 6267ba3a6d6c5dd27924e4debf989b6bfa48daca Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 26 Feb 2026 12:35:01 -0800 Subject: [PATCH 04/17] standardized inp format UI --- .../components/general/components/api-info-modal.tsx | 2 +- .../deploy/components/deploy-modal/components/mcp/mcp.tsx | 2 +- .../components/document-tag-entry/document-tag-entry.tsx | 2 +- .../filter-builder/components/filter-rule-row.tsx | 2 +- .../sub-block/components/input-mapping/input-mapping.tsx | 2 +- .../knowledge-tag-filters/knowledge-tag-filters.tsx | 2 +- .../components/sort-builder/components/sort-rule-row.tsx | 2 +- .../sub-block/components/starter/input-format.tsx | 2 +- .../components/variables-input/variables-input.tsx | 2 +- .../w/[workflowId]/components/variables/variables.tsx | 6 +++--- .../workflow-mcp-servers/workflow-mcp-servers.tsx | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx index 1dbda8f218..0aba5b90cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx @@ -268,7 +268,7 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
-
+
-
+
+
( -
+
{index > 0 && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx index ac78d29137..73563ba9bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx @@ -239,7 +239,7 @@ function InputMappingField({
{!collapsed && ( -
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 3ea3baa167..568d14536d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -358,7 +358,7 @@ export function KnowledgeTagFilters({ const isBetween = filter.operator === 'between' return ( -
+
( -
+
+
{renderNameInput(field)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index c3ab7a8cde..3ca20f572f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -377,7 +377,7 @@ export function VariablesInput({
{!collapsed && ( -
+
toggleCollapsed(variable.id)} onKeyDown={(e) => handleHeaderKeyDown(e, variable.id)} role='button' @@ -297,7 +297,7 @@ export function Variables() { {variable.name || `Variable ${index + 1}`} {variable.name && ( - + {variable.type} )} @@ -460,7 +460,7 @@ export function Variables() { {!(collapsedById[variable.id] ?? false) && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx index 7f49bfc618..8af747f7da 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -616,7 +616,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
-
+
Date: Thu, 26 Feb 2026 12:35:37 -0800 Subject: [PATCH 05/17] feat(tool-input): replace MCP server inline expand with drill-down navigation --- .../components/tool-input/tool-input.tsx | 232 +++++++++--------- 1 file changed, 118 insertions(+), 114 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index cfbc26f08d..28fc5bc74f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ChevronRight, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' +import { ArrowLeft, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -482,7 +482,7 @@ export const ToolInput = memo(function ToolInput({ const [draggedIndex, setDraggedIndex] = useState(null) const [dragOverIndex, setDragOverIndex] = useState(null) const [usageControlPopoverIndex, setUsageControlPopoverIndex] = useState(null) - const [expandedMcpServers, setExpandedMcpServers] = useState>(new Set()) + const [mcpServerDrilldown, setMcpServerDrilldown] = useState(null) const canonicalModeOverrides = useWorkflowStore( useCallback( @@ -719,19 +719,17 @@ export const ToolInput = memo(function ToolInput({ }, [availableMcpTools]) /** - * Toggles the expanded state of an MCP server in the dropdown. + * Resets the MCP server drilldown when the combobox closes. */ - const toggleMcpServerExpanded = useCallback((serverId: string) => { - setExpandedMcpServers((prev) => { - const next = new Set(prev) - if (next.has(serverId)) { - next.delete(serverId) - } else { - next.add(serverId) + const handleComboboxOpenChange = useCallback( + (isOpen: boolean) => { + setOpen(isOpen) + if (!isOpen) { + setMcpServerDrilldown(null) } - return next - }) - }, []) + }, + [] + ) const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { @@ -1256,6 +1254,107 @@ export const ToolInput = memo(function ToolInput({ const toolGroups = useMemo((): ComboboxOptionGroup[] => { const groups: ComboboxOptionGroup[] = [] + // MCP Server drill-down: when navigated into a server, show only its tools + if (mcpServerDrilldown && !permissionConfig.disableMcpTools && mcpToolsByServer.size > 0) { + const tools = mcpToolsByServer.get(mcpServerDrilldown) + if (tools && tools.length > 0) { + const server = mcpServers.find((s) => s.id === mcpServerDrilldown) + const serverName = tools[0]?.serverName || server?.name || 'Unknown Server' + const serverAlreadySelected = isMcpServerAlreadySelected( + selectedTools, + mcpServerDrilldown + ) + const toolCount = tools.length + const serverToolItems: ComboboxOption[] = [] + + // Back navigation + serverToolItems.push({ + label: 'Back', + value: `mcp-server-back`, + iconElement: ( + + ), + onSelect: () => { + setMcpServerDrilldown(null) + }, + keepOpen: true, + }) + + // "Use all tools" option + serverToolItems.push({ + label: `Use all ${toolCount} tools`, + value: `mcp-server-all-${mcpServerDrilldown}`, + iconElement: createToolIcon('#6366F1', ServerIcon), + onSelect: () => { + if (serverAlreadySelected) return + const filteredTools = selectedTools.filter( + (tool) => + !(tool.type === 'mcp' && tool.params?.serverId === mcpServerDrilldown) + ) + const newTool: StoredTool = { + type: 'mcp-server', + title: `${serverName} (all tools)`, + toolId: `mcp-server-${mcpServerDrilldown}`, + params: { + serverId: mcpServerDrilldown, + ...(server?.url && { serverUrl: server.url }), + serverName, + toolCount: String(toolCount), + }, + isExpanded: false, + usageControl: 'auto', + } + setStoreValue([ + ...filteredTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + setOpen(false) + }, + disabled: isPreview || disabled || serverAlreadySelected, + }) + + // Individual tools + for (const mcpTool of tools) { + const alreadySelected = + isMcpToolAlreadySelected(selectedTools, mcpTool.id) || serverAlreadySelected + serverToolItems.push({ + label: mcpTool.name, + value: `mcp-${mcpTool.id}`, + iconElement: createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon), + onSelect: () => { + if (alreadySelected) return + const newTool: StoredTool = { + type: 'mcp', + title: mcpTool.name, + toolId: mcpTool.id, + params: { + serverId: mcpTool.serverId, + ...(server?.url && { serverUrl: server.url }), + toolName: mcpTool.name, + serverName: mcpTool.serverName, + }, + isExpanded: true, + usageControl: 'auto', + schema: { + ...mcpTool.inputSchema, + description: mcpTool.description, + }, + } + handleMcpToolSelect(newTool, true) + }, + disabled: isPreview || disabled || alreadySelected, + }) + } + + groups.push({ + section: serverName, + items: serverToolItems, + }) + } + return groups + } + + // Root view: show all tool categories const actionItems: ComboboxOption[] = [] if (!permissionConfig.disableCustomTools) { actionItems.push({ @@ -1314,118 +1413,24 @@ export const ToolInput = memo(function ToolInput({ }) } - // MCP Servers section - grouped by server with expandable folders + // MCP Servers — root folder view if (!permissionConfig.disableMcpTools && mcpToolsByServer.size > 0) { const serverItems: ComboboxOption[] = [] for (const [serverId, tools] of mcpToolsByServer) { const server = mcpServers.find((s) => s.id === serverId) const serverName = tools[0]?.serverName || server?.name || 'Unknown Server' - const isExpanded = expandedMcpServers.has(serverId) - const serverAlreadySelected = isMcpServerAlreadySelected(selectedTools, serverId) const toolCount = tools.length - // Server folder header (clickable to expand/collapse) serverItems.push({ - label: serverName, + label: `${serverName} (${toolCount} tools)`, value: `mcp-server-folder-${serverId}`, - iconElement: ( -
- -
- -
-
- ), + iconElement: createToolIcon('#6366F1', ServerIcon), onSelect: () => { - toggleMcpServerExpanded(serverId) + setMcpServerDrilldown(serverId) }, - disabled: false, keepOpen: true, }) - - // If expanded, show "Use all tools" option and individual tools - if (isExpanded) { - serverItems.push({ - label: `Use all ${toolCount} tools`, - value: `mcp-server-all-${serverId}`, - iconElement: ( -
- -
- ), - onSelect: () => { - if (serverAlreadySelected) return - const filteredTools = selectedTools.filter( - (tool) => !(tool.type === 'mcp' && tool.params?.serverId === serverId) - ) - const newTool: StoredTool = { - type: 'mcp-server', - title: `${serverName} (all tools)`, - toolId: `mcp-server-${serverId}`, - params: { - serverId, - ...(server?.url && { serverUrl: server.url }), - serverName, - toolCount: String(toolCount), - }, - isExpanded: false, - usageControl: 'auto', - } - setStoreValue([ - ...filteredTools.map((tool) => ({ ...tool, isExpanded: false })), - newTool, - ]) - setOpen(false) - }, - disabled: isPreview || disabled || serverAlreadySelected, - }) - - // Individual tools from this server - for (const mcpTool of tools) { - const alreadySelected = - isMcpToolAlreadySelected(selectedTools, mcpTool.id) || serverAlreadySelected - serverItems.push({ - label: mcpTool.name, - value: `mcp-${mcpTool.id}`, - iconElement: ( -
- {createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon)} -
- ), - onSelect: () => { - if (alreadySelected) return - const newTool: StoredTool = { - type: 'mcp', - title: mcpTool.name, - toolId: mcpTool.id, - params: { - serverId: mcpTool.serverId, - ...(server?.url && { serverUrl: server.url }), - toolName: mcpTool.name, - serverName: mcpTool.serverName, - }, - isExpanded: true, - usageControl: 'auto', - schema: { - ...mcpTool.inputSchema, - description: mcpTool.description, - }, - } - handleMcpToolSelect(newTool, true) - }, - disabled: isPreview || disabled || alreadySelected, - }) - } - } } groups.push({ @@ -1507,11 +1512,11 @@ export const ToolInput = memo(function ToolInput({ return groups }, [ + mcpServerDrilldown, customTools, availableMcpTools, mcpServers, mcpToolsByServer, - expandedMcpServers, toolBlocks, isPreview, disabled, @@ -1523,7 +1528,6 @@ export const ToolInput = memo(function ToolInput({ permissionConfig.disableMcpTools, availableWorkflows, isToolAlreadySelected, - toggleMcpServerExpanded, ]) return ( @@ -1537,7 +1541,7 @@ export const ToolInput = memo(function ToolInput({ searchPlaceholder='Search tools...' maxHeight={240} emptyMessage='No tools found' - onOpenChange={setOpen} + onOpenChange={handleComboboxOpenChange} /> {selectedTools.length > 0 && From a83e6218e0a85865af9cba3bb0746428bf6a7de0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 12:46:04 -0800 Subject: [PATCH 06/17] feat(tool-input): add chevron affordance and keyboard nav for MCP server drill-down --- .../components/tool-input/tool-input.tsx | 6 +++- .../emcn/components/combobox/combobox.tsx | 28 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 28fc5bc74f..9f598c6c69 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ArrowLeft, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' +import { ArrowLeft, ChevronRight, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -1426,6 +1426,9 @@ export const ToolInput = memo(function ToolInput({ label: `${serverName} (${toolCount} tools)`, value: `mcp-server-folder-${serverId}`, iconElement: createToolIcon('#6366F1', ServerIcon), + suffixElement: ( + + ), onSelect: () => { setMcpServerDrilldown(serverId) }, @@ -1542,6 +1545,7 @@ export const ToolInput = memo(function ToolInput({ maxHeight={240} emptyMessage='No tools found' onOpenChange={handleComboboxOpenChange} + onArrowLeft={mcpServerDrilldown ? () => setMcpServerDrilldown(null) : undefined} /> {selectedTools.length > 0 && diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index 2c98353c46..c35af0b793 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -55,6 +55,8 @@ export type ComboboxOption = { disabled?: boolean /** When true, keep the dropdown open after selecting this option */ keepOpen?: boolean + /** Optional element rendered at the trailing end of the option (e.g. chevron for folders) */ + suffixElement?: ReactNode } /** @@ -109,6 +111,8 @@ export interface ComboboxProps error?: string | null /** Callback when popover open state changes */ onOpenChange?: (open: boolean) => void + /** Callback when ArrowLeft is pressed while dropdown is open (for folder back-navigation) */ + onArrowLeft?: () => void /** Enable search input in dropdown (useful for multiselect) */ searchable?: boolean /** Placeholder for search input */ @@ -160,6 +164,7 @@ const Combobox = memo( isLoading = false, error = null, onOpenChange, + onArrowLeft, searchable = false, searchPlaceholder = 'Search...', align = 'start', @@ -386,8 +391,25 @@ const Combobox = memo( setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1)) } } + + if (e.key === 'ArrowRight') { + if (open && highlightedIndex >= 0) { + const highlightedOption = filteredOptions[highlightedIndex] + if (highlightedOption?.keepOpen && highlightedOption?.onSelect) { + e.preventDefault() + highlightedOption.onSelect() + } + } + } + + if (e.key === 'ArrowLeft') { + if (open && onArrowLeft) { + e.preventDefault() + onArrowLeft() + } + } }, - [disabled, open, highlightedIndex, filteredOptions, handleSelect, editable, inputRef] + [disabled, open, highlightedIndex, filteredOptions, handleSelect, editable, inputRef, onArrowLeft] ) /** @@ -600,6 +622,8 @@ const Combobox = memo( if ( e.key === 'ArrowDown' || e.key === 'ArrowUp' || + e.key === 'ArrowRight' || + e.key === 'ArrowLeft' || e.key === 'Enter' || e.key === 'Escape' ) { @@ -699,6 +723,7 @@ const Combobox = memo( {option.label} + {option.suffixElement} {multiSelect && isSelected && ( )} @@ -772,6 +797,7 @@ const Combobox = memo( {option.label} + {option.suffixElement} {multiSelect && isSelected && ( )} From e0d7b4c4830afcde13aca803cab31c5b37709a32 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 12:51:51 -0800 Subject: [PATCH 07/17] fix(tool-input): handle mcp-server type in refresh, validation, badges, and usage control --- .../components/tool-input/tool-input.tsx | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 9f598c6c69..bf024c9b53 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -524,7 +524,7 @@ export const ToolInput = memo(function ToolInput({ ) const hasRefreshedRef = useRef(false) - const hasMcpTools = selectedTools.some((tool) => tool.type === 'mcp') + const hasMcpTools = selectedTools.some((tool) => tool.type === 'mcp' || tool.type === 'mcp-server') useEffect(() => { if (isPreview) return @@ -541,9 +541,36 @@ export const ToolInput = memo(function ToolInput({ */ const getMcpToolIssue = useCallback( (tool: StoredTool) => { - if (tool.type !== 'mcp') return null + if (tool.type !== 'mcp' && tool.type !== 'mcp-server') return null const serverId = tool.params?.serverId as string + const serverStates = mcpServers.map((s) => ({ + id: s.id, + url: s.url, + connectionStatus: s.connectionStatus, + lastError: s.lastError ?? undefined, + })) + const discoveredTools = mcpTools.map((t) => ({ + serverId: t.serverId, + name: t.name, + inputSchema: t.inputSchema, + })) + + if (tool.type === 'mcp-server') { + // Server-level validation: only check server connectivity + return validateMcpTool( + { + serverId, + serverUrl: tool.params?.serverUrl as string | undefined, + toolName: '__server_check__', + schema: undefined, + }, + serverStates, + // Pass a fake discovered tool so tool_not_found doesn't trigger + [...discoveredTools, { serverId, name: '__server_check__', inputSchema: undefined }] + ) + } + const toolName = tool.params?.toolName as string // Try to get fresh schema from DB (enables real-time updates after MCP refresh) @@ -563,17 +590,8 @@ export const ToolInput = memo(function ToolInput({ toolName, schema, }, - mcpServers.map((s) => ({ - id: s.id, - url: s.url, - connectionStatus: s.connectionStatus, - lastError: s.lastError ?? undefined, - })), - mcpTools.map((t) => ({ - serverId: t.serverId, - name: t.name, - inputSchema: t.inputSchema, - })) + serverStates, + discoveredTools ) }, [mcpTools, mcpServers, storedMcpTools, workflowId] @@ -1738,7 +1756,7 @@ export const ToolInput = memo(function ToolInput({ {tool.params?.toolCount || mcpServerTools.length} tools )} - {isMcpTool && + {(isMcpTool || isMcpServer) && !mcpDataLoading && (() => { const issue = getMcpToolIssue(tool) @@ -1773,7 +1791,7 @@ export const ToolInput = memo(function ToolInput({ )}
- {supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && ( + {supportsToolControl && !((isMcpTool || isMcpServer) && isMcpToolUnavailable(tool)) && ( setUsageControlPopoverIndex(open ? toolIndex : null)} From 70a3e16af84f78bb32f8c2706f4756341c30eb98 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 13:16:08 -0800 Subject: [PATCH 08/17] refactor(tool-validation): extract getMcpServerIssue, remove fake tool hack --- .../components/tool-input/tool-input.tsx | 28 ++++++++----------- apps/sim/lib/mcp/tool-validation.ts | 27 ++++++++++++++---- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index bf024c9b53..add469132f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -20,8 +20,9 @@ import { cn } from '@/lib/core/utils/cn' import { getIssueBadgeLabel, getIssueBadgeVariant, - isToolUnavailable, + getMcpServerIssue as validateMcpServer, getMcpToolIssue as validateMcpTool, + isToolUnavailable, } from '@/lib/mcp/tool-validation' import type { McpToolSchema } from '@/lib/mcp/types' import { getProviderIdFromServiceId, type OAuthProvider, type OAuthService } from '@/lib/oauth' @@ -550,28 +551,21 @@ export const ToolInput = memo(function ToolInput({ connectionStatus: s.connectionStatus, lastError: s.lastError ?? undefined, })) - const discoveredTools = mcpTools.map((t) => ({ - serverId: t.serverId, - name: t.name, - inputSchema: t.inputSchema, - })) if (tool.type === 'mcp-server') { - // Server-level validation: only check server connectivity - return validateMcpTool( - { - serverId, - serverUrl: tool.params?.serverUrl as string | undefined, - toolName: '__server_check__', - schema: undefined, - }, - serverStates, - // Pass a fake discovered tool so tool_not_found doesn't trigger - [...discoveredTools, { serverId, name: '__server_check__', inputSchema: undefined }] + return validateMcpServer( + serverId, + tool.params?.serverUrl as string | undefined, + serverStates ) } const toolName = tool.params?.toolName as string + const discoveredTools = mcpTools.map((t) => ({ + serverId: t.serverId, + name: t.name, + inputSchema: t.inputSchema, + })) // Try to get fresh schema from DB (enables real-time updates after MCP refresh) const storedTool = diff --git a/apps/sim/lib/mcp/tool-validation.ts b/apps/sim/lib/mcp/tool-validation.ts index 6de2438638..86b16c4868 100644 --- a/apps/sim/lib/mcp/tool-validation.ts +++ b/apps/sim/lib/mcp/tool-validation.ts @@ -39,13 +39,15 @@ export function hasSchemaChanged( return !isEqual(storedWithoutDesc, serverWithoutDesc) } -export function getMcpToolIssue( - storedTool: StoredMcpToolReference, - servers: ServerState[], - discoveredTools: DiscoveredTool[] +/** + * Validates server-level connectivity for an MCP server. + * Checks: server existence, connection status, URL changes. + */ +export function getMcpServerIssue( + serverId: string, + serverUrl: string | undefined, + servers: ServerState[] ): McpToolIssue | null { - const { serverId, serverUrl, toolName, schema } = storedTool - const server = servers.find((s) => s.id === serverId) if (!server) { return { type: 'server_not_found', message: 'Server not found' } @@ -62,6 +64,19 @@ export function getMcpToolIssue( return { type: 'url_changed', message: 'Server URL changed' } } + return null +} + +export function getMcpToolIssue( + storedTool: StoredMcpToolReference, + servers: ServerState[], + discoveredTools: DiscoveredTool[] +): McpToolIssue | null { + const { serverId, serverUrl, toolName, schema } = storedTool + + const serverIssue = getMcpServerIssue(serverId, serverUrl, servers) + if (serverIssue) return serverIssue + const serverTool = discoveredTools.find((t) => t.serverId === serverId && t.name === toolName) if (!serverTool) { return { type: 'tool_not_found', message: 'Tool not found on server' } From 29bc17248bae3aef41862dabef75e5aa6238b8a0 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 26 Feb 2026 13:18:42 -0800 Subject: [PATCH 09/17] lint --- .../components/tool-input/tool-input.tsx | 147 +++++++++--------- .../emcn/components/combobox/combobox.tsx | 11 +- 2 files changed, 81 insertions(+), 77 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index add469132f..75c5719342 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -20,9 +20,9 @@ import { cn } from '@/lib/core/utils/cn' import { getIssueBadgeLabel, getIssueBadgeVariant, + isToolUnavailable, getMcpServerIssue as validateMcpServer, getMcpToolIssue as validateMcpTool, - isToolUnavailable, } from '@/lib/mcp/tool-validation' import type { McpToolSchema } from '@/lib/mcp/types' import { getProviderIdFromServiceId, type OAuthProvider, type OAuthService } from '@/lib/oauth' @@ -525,7 +525,9 @@ export const ToolInput = memo(function ToolInput({ ) const hasRefreshedRef = useRef(false) - const hasMcpTools = selectedTools.some((tool) => tool.type === 'mcp' || tool.type === 'mcp-server') + const hasMcpTools = selectedTools.some( + (tool) => tool.type === 'mcp' || tool.type === 'mcp-server' + ) useEffect(() => { if (isPreview) return @@ -733,15 +735,12 @@ export const ToolInput = memo(function ToolInput({ /** * Resets the MCP server drilldown when the combobox closes. */ - const handleComboboxOpenChange = useCallback( - (isOpen: boolean) => { - setOpen(isOpen) - if (!isOpen) { - setMcpServerDrilldown(null) - } - }, - [] - ) + const handleComboboxOpenChange = useCallback((isOpen: boolean) => { + setOpen(isOpen) + if (!isOpen) { + setMcpServerDrilldown(null) + } + }, []) const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { @@ -1272,10 +1271,7 @@ export const ToolInput = memo(function ToolInput({ if (tools && tools.length > 0) { const server = mcpServers.find((s) => s.id === mcpServerDrilldown) const serverName = tools[0]?.serverName || server?.name || 'Unknown Server' - const serverAlreadySelected = isMcpServerAlreadySelected( - selectedTools, - mcpServerDrilldown - ) + const serverAlreadySelected = isMcpServerAlreadySelected(selectedTools, mcpServerDrilldown) const toolCount = tools.length const serverToolItems: ComboboxOption[] = [] @@ -1283,9 +1279,7 @@ export const ToolInput = memo(function ToolInput({ serverToolItems.push({ label: 'Back', value: `mcp-server-back`, - iconElement: ( - - ), + iconElement: , onSelect: () => { setMcpServerDrilldown(null) }, @@ -1300,8 +1294,7 @@ export const ToolInput = memo(function ToolInput({ onSelect: () => { if (serverAlreadySelected) return const filteredTools = selectedTools.filter( - (tool) => - !(tool.type === 'mcp' && tool.params?.serverId === mcpServerDrilldown) + (tool) => !(tool.type === 'mcp' && tool.params?.serverId === mcpServerDrilldown) ) const newTool: StoredTool = { type: 'mcp-server', @@ -1438,9 +1431,7 @@ export const ToolInput = memo(function ToolInput({ label: `${serverName} (${toolCount} tools)`, value: `mcp-server-folder-${serverId}`, iconElement: createToolIcon('#6366F1', ServerIcon), - suffixElement: ( - - ), + suffixElement: , onSelect: () => { setMcpServerDrilldown(serverId) }, @@ -1785,61 +1776,65 @@ export const ToolInput = memo(function ToolInput({ )}
- {supportsToolControl && !((isMcpTool || isMcpServer) && isMcpToolUnavailable(tool)) && ( - setUsageControlPopoverIndex(open ? toolIndex : null)} - > - - - - e.stopPropagation()} - className='gap-[2px]' - border + {supportsToolControl && + !((isMcpTool || isMcpServer) && isMcpToolUnavailable(tool)) && ( + + setUsageControlPopoverIndex(open ? toolIndex : null) + } > - { - handleUsageControlChange(toolIndex, 'auto') - setUsageControlPopoverIndex(null) - }} - > - Auto (model decides) - - { - handleUsageControlChange(toolIndex, 'force') - setUsageControlPopoverIndex(null) - }} - > - Force (always use) - - { - handleUsageControlChange(toolIndex, 'none') - setUsageControlPopoverIndex(null) - }} + + + + e.stopPropagation()} + className='gap-[2px]' + border > - None - - - - )} + { + handleUsageControlChange(toolIndex, 'auto') + setUsageControlPopoverIndex(null) + }} + > + Auto{' '} + (model decides) + + { + handleUsageControlChange(toolIndex, 'force') + setUsageControlPopoverIndex(null) + }} + > + Force (always use) + + { + handleUsageControlChange(toolIndex, 'none') + setUsageControlPopoverIndex(null) + }} + > + None + + + + )}