Name
{renderNameInput(field)}
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..5ca7f92a03 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 { ArrowLeft, ChevronRight, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
@@ -21,6 +21,7 @@ import {
getIssueBadgeLabel,
getIssueBadgeVariant,
isToolUnavailable,
+ getMcpServerIssue as validateMcpServer,
getMcpToolIssue as validateMcpTool,
} from '@/lib/mcp/tool-validation'
import type { McpToolSchema } from '@/lib/mcp/types'
@@ -41,6 +42,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 +483,7 @@ export const ToolInput = memo(function ToolInput({
const [draggedIndex, setDraggedIndex] = useState
(null)
const [dragOverIndex, setDragOverIndex] = useState(null)
const [usageControlPopoverIndex, setUsageControlPopoverIndex] = useState(null)
+ const [mcpServerDrilldown, setMcpServerDrilldown] = useState(null)
const canonicalModeOverrides = useWorkflowStore(
useCallback(
@@ -522,7 +525,9 @@ 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
@@ -539,10 +544,30 @@ 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,
+ }))
+
+ if (tool.type === 'mcp-server') {
+ 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 =
@@ -561,17 +586,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]
@@ -702,6 +718,30 @@ 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])
+
+ /**
+ * Resets the MCP server drilldown when the combobox closes.
+ */
+ const handleComboboxOpenChange = useCallback((isOpen: boolean) => {
+ setOpen(isOpen)
+ if (!isOpen) {
+ setMcpServerDrilldown(null)
+ }
+ }, [])
+
const handleSelectTool = useCallback(
(toolBlock: (typeof toolBlocks)[0]) => {
if (isPreview || disabled) return
@@ -1012,6 +1052,7 @@ export const ToolInput = memo(function ToolInput({
])
if (closePopover) {
+ setMcpServerDrilldown(null)
setOpen(false)
}
},
@@ -1225,6 +1266,102 @@ 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,
+ ])
+ setMcpServerDrilldown(null)
+ 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({
@@ -1283,40 +1420,30 @@ export const ToolInput = memo(function ToolInput({
})
}
- if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) {
+ // 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 toolCount = tools.length
+
+ serverItems.push({
+ label: `${serverName} (${toolCount} tools)`,
+ value: `mcp-server-folder-${serverId}`,
+ iconElement: createToolIcon('#6366F1', ServerIcon),
+ suffixElement: ,
+ onSelect: () => {
+ setMcpServerDrilldown(serverId)
+ },
+ keepOpen: true,
+ })
+ }
+
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),
- 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,
- }
- }),
+ section: 'MCP Servers',
+ items: serverItems,
})
}
@@ -1393,9 +1520,11 @@ export const ToolInput = memo(function ToolInput({
return groups
}, [
+ mcpServerDrilldown,
customTools,
availableMcpTools,
mcpServers,
+ mcpToolsByServer,
toolBlocks,
isPreview,
disabled,
@@ -1420,26 +1549,28 @@ export const ToolInput = memo(function ToolInput({
searchPlaceholder='Search tools...'
maxHeight={240}
emptyMessage='No tools found'
- onOpenChange={setOpen}
+ onOpenChange={handleComboboxOpenChange}
+ onArrowLeft={mcpServerDrilldown ? () => setMcpServerDrilldown(null) : undefined}
/>
{selectedTools.length > 0 &&
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 +1580,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 +1643,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 +1670,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,7 +1738,12 @@ export const ToolInput = memo(function ToolInput({
{isCustomTool ? customToolTitle : tool.title}
- {isMcpTool &&
+ {isMcpServer && (
+
+ {tool.params?.toolCount || mcpServerTools.length} tools
+
+ )}
+ {(isMcpTool || isMcpServer) &&
!mcpDataLoading &&
(() => {
const issue = getMcpToolIssue(tool)
@@ -1628,61 +1778,65 @@ export const ToolInput = memo(function ToolInput({
)}
- {supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && (
-
setUsageControlPopoverIndex(open ? toolIndex : null)}
- >
-
- e.stopPropagation()}
- aria-label='Tool usage control'
- >
- {tool.usageControl === 'auto' && 'Auto'}
- {tool.usageControl === 'force' && 'Force'}
- {tool.usageControl === 'none' && 'None'}
- {!tool.usageControl && 'Auto'}
-
-
- 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()}
+ aria-label='Tool usage control'
+ >
+ {tool.usageControl === 'auto' && 'Auto'}
+ {tool.usageControl === 'force' && 'Force'}
+ {tool.usageControl === 'none' && 'None'}
+ {!tool.usageControl && 'Auto'}
+
+
+ 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
+
+
+
+ )}
{
e.stopPropagation()
@@ -1698,30 +1852,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/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 && (
-
+
Variable
Auto layout
+ {
+
setVariablesOpen(!isVariablesOpen)}>
+
+ Variables
+
+ }
{userPermissions.canAdmin && !isSnapshotView && (
{allBlocksLocked ? (
@@ -433,12 +439,6 @@ export const Panel = memo(function Panel() {
{allBlocksLocked ? 'Unlock workflow' : 'Lock workflow'}
)}
- {
-
setVariablesOpen(!isVariablesOpen)}>
-
- Variables
-
- }
{/*
Debug
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
index 8e5ebc1b9a..08b0968960 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx
@@ -284,7 +284,7 @@ export function Variables() {
const isCollapsed = collapsedById[variable.id] ?? false
return (
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) && (
{STRINGS.labels.name}
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
-
+
Description
void
/** Whether this option is disabled */
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
}
/**
@@ -107,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 */
@@ -158,6 +164,7 @@ const Combobox = memo(
isLoading = false,
error = null,
onOpenChange,
+ onArrowLeft,
searchable = false,
searchPlaceholder = 'Search...',
align = 'start',
@@ -254,13 +261,16 @@ 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)
+ // Always reset search/highlight so stale queries don't filter new options
setSearchQuery('')
+ setHighlightedIndex(-1)
+ if (!keepOpen) {
+ setOpen(false)
+ }
return
}
@@ -272,11 +282,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 +357,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()
@@ -380,8 +392,36 @@ 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()
+ handleSelect(highlightedOption.value, highlightedOption.onSelect, true)
+ }
+ }
+ }
+
+ if (e.key === 'ArrowLeft') {
+ if (open && onArrowLeft) {
+ e.preventDefault()
+ onArrowLeft()
+ setSearchQuery('')
+ setHighlightedIndex(-1)
+ }
+ }
},
- [disabled, open, highlightedIndex, filteredOptions, handleSelect, editable, inputRef]
+ [
+ disabled,
+ open,
+ highlightedIndex,
+ filteredOptions,
+ handleSelect,
+ editable,
+ inputRef,
+ onArrowLeft,
+ ]
)
/**
@@ -591,9 +631,17 @@ const Combobox = memo(
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
// Forward navigation keys to main handler
+ // Only forward ArrowLeft/ArrowRight when cursor is at the boundary
+ // so normal text cursor movement still works in the search input
+ const input = e.currentTarget
+ const forwardArrowLeft = e.key === 'ArrowLeft' && input.selectionStart === 0
+ const forwardArrowRight =
+ e.key === 'ArrowRight' && input.selectionStart === input.value.length
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
+ forwardArrowRight ||
+ forwardArrowLeft ||
e.key === 'Enter' ||
e.key === 'Escape'
) {
@@ -671,7 +719,7 @@ const Combobox = memo(
e.preventDefault()
e.stopPropagation()
if (!option.disabled) {
- handleSelect(option.value, option.onSelect)
+ handleSelect(option.value, option.onSelect, option.keepOpen)
}
}}
onMouseEnter={() =>
@@ -693,6 +741,7 @@ const Combobox = memo(
{option.label}
+ {option.suffixElement}
{multiSelect && isSelected && (
)}
@@ -746,7 +795,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)}
@@ -766,6 +815,7 @@ const Combobox = memo(
{option.label}
+ {option.suffixElement}
{multiSelect && isSelected && (
)}
diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts
index c76ab48c0d..ee07f276b5 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 mcpServerSelections: ToolInput[] = []
const otherTools: ToolInput[] = []
for (const tool of filtered) {
if (tool.type === 'mcp') {
mcpTools.push(tool)
+ } else if (tool.type === 'mcp-server') {
+ mcpServerSelections.push(tool)
} else {
otherTools.push(tool)
}
@@ -249,7 +252,7 @@ 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') {
await validateBlockType(ctx.userId, tool.type, ctx)
}
if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) {
@@ -265,12 +268,86 @@ export class AgentBlockHandler implements BlockHandler {
const mcpResults = await this.processMcpToolsBatched(ctx, mcpTools)
- const allTools = [...otherResults, ...mcpResults]
+ // Process MCP server selections (all tools from server mode)
+ const mcpServerResults = await this.processMcpServerSelections(ctx, mcpServerSelections)
+
+ 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 = await Promise.all(
+ mcpServerSelections.map(async (serverSelection) => {
+ 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)
+ return []
+ }
+
+ try {
+ const discoveredTools = await this.discoverMcpToolsForServer(ctx, serverId)
+ const createdTools = await Promise.all(
+ discoveredTools.map((mcpTool) =>
+ this.createMcpToolFromDiscoveredServerTool(
+ mcpTool,
+ serverId,
+ serverName || serverId,
+ usageControl
+ )
+ )
+ )
+ logger.info(
+ `[AgentHandler] Expanded MCP server ${serverName} into ${discoveredTools.length} tools`
+ )
+ return createdTools.filter(Boolean)
+ } catch (error) {
+ logger.error(`[AgentHandler] Failed to process MCP server selection:`, {
+ serverId,
+ error,
+ })
+ return []
+ }
+ })
+ )
+
+ return results.flat()
+ }
+
+ /**
+ * 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'
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' }