diff --git a/apps/docs/content/docs/en/tools/google_translate.mdx b/apps/docs/content/docs/en/tools/google_translate.mdx index 2e890b1149..858ad1d95d 100644 --- a/apps/docs/content/docs/en/tools/google_translate.mdx +++ b/apps/docs/content/docs/en/tools/google_translate.mdx @@ -10,6 +10,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#E0E0E0" /> +{/* MANUAL-CONTENT-START:intro */} +[Google Translate](https://translate.google.com/) is Google's powerful translation service, supporting over 100 languages for text, documents, and websites. Backed by advanced neural machine translation, Google Translate delivers fast and accurate translations for a wide range of use cases, from casual communication to professional workflows. + +In Sim, the Google Translate integration allows your agents to translate text and detect languages as part of automated workflows. Agents can translate content between languages, auto-detect source languages, and process multilingual data—all without manual intervention. By connecting Sim with Google Cloud Translation, you can build intelligent workflows that handle localization, multilingual support, content translation, and language detection at scale. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Translate and detect languages using the Google Cloud Translation API. Supports auto-detection of the source language. 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 ( -
+
@@ -351,15 +350,14 @@ export function McpDynamicArgs({
{showLabel && ( - +
+ +
)} {renderParameterInput(paramName, paramSchema as any)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx index 72716ac946..77d238e88e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx @@ -70,7 +70,7 @@ export function SortRuleRow({ ) const renderContent = () => ( -
+
+
{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()} - 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 + + + + )}