diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx index 90c3222cf4c..a82bff3ccd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx @@ -23,6 +23,7 @@ import type { AgentAuthentication, AgentCapabilities } from '@/lib/a2a/types' import { getBaseUrl } from '@/lib/core/utils/urls' import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers' +import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description' import { useA2AAgentByWorkflow, useCreateA2AAgent, @@ -44,20 +45,6 @@ interface InputFormatField { collapsed?: boolean } -/** - * Check if a description is a default/placeholder value that should be filtered out - */ -function isDefaultDescription(desc: string | null | undefined, workflowName: string): boolean { - if (!desc) return true - const normalized = desc.toLowerCase().trim() - return ( - normalized === '' || - normalized === 'new workflow' || - normalized === 'your first workflow - start building here!' || - normalized === workflowName.toLowerCase() - ) -} - type CodeLanguage = 'curl' | 'python' | 'javascript' | 'typescript' const LANGUAGE_LABELS: Record = { 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 1e8b3f3126f..b4a6de32582 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 @@ -20,6 +20,7 @@ import { import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import type { InputFormatField } from '@/lib/workflows/types' +import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description' import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deployments' import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -89,14 +90,12 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro useEffect(() => { if (open) { - const normalizedDesc = workflowMetadata?.description?.toLowerCase().trim() - const isDefaultDescription = - !workflowMetadata?.description || - workflowMetadata.description === workflowMetadata.name || - normalizedDesc === 'new workflow' || - normalizedDesc === 'your first workflow - start building here!' - - const initialDescription = isDefaultDescription ? '' : workflowMetadata?.description || '' + const initialDescription = isDefaultDescription( + workflowMetadata?.description, + workflowMetadata?.name || '' + ) + ? '' + : workflowMetadata?.description || '' setDescription(initialDescription) initialDescriptionRef.current = initialDescription diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx index 149de3ee089..e40ea909504 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx @@ -8,17 +8,18 @@ import { Button, ChipCombobox, ChipInput, + ChipTextarea, type ComboboxOption, Label, Skeleton, - Textarea, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' -import { generateParameterSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' +import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import type { InputFormatField } from '@/lib/workflows/types' import { CreateWorkflowMcpServerModal } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/components/create-workflow-mcp-server-modal' +import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description' import { useAddWorkflowMcpTool, useDeleteWorkflowMcpTool, @@ -28,6 +29,7 @@ import { type WorkflowMcpServer, type WorkflowMcpTool, } from '@/hooks/queries/workflow-mcp-servers' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -52,6 +54,8 @@ interface McpDeployProps { onAddedToServer?: () => void onSubmittingChange?: (submitting: boolean) => void onCanSaveChange?: (canSave: boolean) => void + /** Reports whether this workflow is currently exposed as a tool on any server. */ + onExposedChange?: (exposed: boolean) => void } function haveSameServerSelection(a: string[], b: string[]): boolean { @@ -60,14 +64,36 @@ function haveSameServerSelection(a: string[], b: string[]): boolean { return a.every((id) => bSet.has(id)) } -function haveSameParameterDescriptions( - a: Record, - b: Record -): boolean { - const aKeys = Object.keys(a) - const bKeys = Object.keys(b) - if (aKeys.length !== bKeys.length) return false - return aKeys.every((key) => a[key] === b[key]) +/** + * Picks the active raw input-format array: the subblock store value, or the block's + * persisted value when the store holds no named fields. Returns the array untouched + * (no filtering) so the writer can preserve in-progress fields; the display memo + * normalizes the result. Shared so both read the exact same source. + */ +function pickRawInputFormat(storeValue: unknown, blockFallbackValue: unknown): unknown[] { + const storeArray = Array.isArray(storeValue) ? storeValue : [] + if (normalizeInputFormatValue(storeArray).length > 0) return storeArray + return Array.isArray(blockFallbackValue) ? blockFallbackValue : [] +} + +/** + * Extracts real per-parameter descriptions from a previously-saved tool schema, + * skipping the synthetic fallbacks the generator emits (the field name itself, or + * the file-array placeholder). Used only as a display fallback so descriptions on + * legacy tools — saved before descriptions moved to the start block — stay visible. + */ +function extractToolSchemaDescriptions(parameterSchema: unknown): Record { + const properties = (parameterSchema as { properties?: Record }) + ?.properties + if (!properties) return {} + const descriptions: Record = {} + for (const [name, prop] of Object.entries(properties)) { + const description = prop?.description + if (description && description !== name && description !== 'Array of file objects') { + descriptions[name] = description + } + } + return descriptions } /** @@ -102,6 +128,7 @@ export function McpDeploy({ onAddedToServer, onSubmittingChange, onCanSaveChange, + onExposedChange, }: McpDeployProps) { const params = useParams() const workspaceId = params.workspaceId as string @@ -111,6 +138,7 @@ export function McpDeploy({ const addToolMutation = useAddWorkflowMcpTool() const deleteToolMutation = useDeleteWorkflowMcpTool() const updateToolMutation = useUpdateWorkflowMcpTool() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const blocks = useWorkflowStore((state) => state.blocks) @@ -131,34 +159,42 @@ export function McpDeploy({ const inputFormat = useMemo((): NormalizedField[] => { if (!starterBlockId) return [] - - const storeValue = subBlockValues[starterBlockId]?.inputFormat - const normalized = normalizeInputFormatValue(storeValue) as NormalizedField[] - if (normalized.length > 0) return normalized - - const startBlock = blocks[starterBlockId] - const blockValue = startBlock?.subBlocks?.inputFormat?.value - return normalizeInputFormatValue(blockValue) as NormalizedField[] + return normalizeInputFormatValue( + pickRawInputFormat( + subBlockValues[starterBlockId]?.inputFormat, + blocks[starterBlockId]?.subBlocks?.inputFormat?.value + ) + ) as NormalizedField[] }, [starterBlockId, subBlockValues, blocks]) const [toolName, setToolName] = useState(() => sanitizeToolName(workflowName)) - const [toolDescription, setToolDescription] = useState(() => { - const normalizedDesc = workflowDescription?.toLowerCase().trim() - const isDefaultDescription = - !workflowDescription || - workflowDescription === workflowName || - normalizedDesc === 'new workflow' || - normalizedDesc === 'your first workflow - start building here!' - - return isDefaultDescription ? '' : workflowDescription - }) - const [parameterDescriptions, setParameterDescriptions] = useState>({}) + const [toolDescription, setToolDescription] = useState(() => + isDefaultDescription(workflowDescription, workflowName) ? '' : (workflowDescription ?? '') + ) const [pendingServerChanges, setPendingServerChanges] = useState>(() => new Set()) const [saveErrors, setSaveErrors] = useState([]) - const parameterSchema = useMemo( - () => generateParameterSchema(inputFormat, parameterDescriptions), - [inputFormat, parameterDescriptions] + /** + * Persists a parameter description to the start block's input format, the single + * source the deployed tool schema is derived from. Maps over the raw array so every + * other field — including unnamed, in-progress rows — is preserved untouched. + */ + const updateFieldDescription = useCallback( + (fieldName: string, description: string) => { + if (!starterBlockId) return + const rawFields = pickRawInputFormat( + useSubBlockStore.getState().getValue(starterBlockId, 'inputFormat'), + useWorkflowStore.getState().blocks[starterBlockId]?.subBlocks?.inputFormat?.value + ) + if (rawFields.length === 0) return + const nextFields = rawFields.map((field) => + field && typeof field === 'object' && (field as { name?: string }).name === fieldName + ? { ...field, description } + : field + ) + collaborativeSetSubblockValue(starterBlockId, 'inputFormat', nextFields) + }, + [starterBlockId, collaborativeSetSubblockValue] ) const toolNameError = useMemo(() => { @@ -207,8 +243,15 @@ export function McpDeploy({ const [savedValues, setSavedValues] = useState<{ toolName: string toolDescription: string - parameterDescriptions: Record } | null>(null) + /** + * Descriptions read from an existing tool's saved schema, shown as a fallback when + * the start block has none yet — keeps descriptions from tools saved before the + * start-block migration visible. Editing one writes through to the start block. + */ + const [legacyParameterDescriptions, setLegacyParameterDescriptions] = useState< + Record + >({}) useEffect(() => { if (savedValues) return @@ -219,38 +262,16 @@ export function McpDeploy({ const initialToolName = toolInfo.tool.toolName const loadedDescription = toolInfo.tool.toolDescription || '' - const normalizedLoadedDesc = loadedDescription.toLowerCase().trim() - const isDefaultDescription = - !loadedDescription || - loadedDescription === workflowName || - normalizedLoadedDesc === 'new workflow' || - normalizedLoadedDesc === 'your first workflow - start building here!' - const initialToolDescription = isDefaultDescription ? '' : loadedDescription - - const schema = toolInfo.tool.parameterSchema as Record | undefined - const properties = schema?.properties as - | Record - | undefined - const initialParameterDescriptions: Record = {} - if (properties) { - for (const [name, prop] of Object.entries(properties)) { - if ( - prop.description && - prop.description !== name && - prop.description !== 'Array of file objects' - ) { - initialParameterDescriptions[name] = prop.description - } - } - } + const initialToolDescription = isDefaultDescription(loadedDescription, workflowName) + ? '' + : loadedDescription setToolName(initialToolName) setToolDescription(initialToolDescription) - setParameterDescriptions(initialParameterDescriptions) + setLegacyParameterDescriptions(extractToolSchemaDescriptions(toolInfo.tool.parameterSchema)) setSavedValues({ toolName: initialToolName, toolDescription: initialToolDescription, - parameterDescriptions: initialParameterDescriptions, }) break } @@ -263,11 +284,8 @@ export function McpDeploy({ if (!savedValues) return false if (toolName !== savedValues.toolName) return true if (toolDescription !== savedValues.toolDescription) return true - if (!haveSameParameterDescriptions(parameterDescriptions, savedValues.parameterDescriptions)) { - return true - } return false - }, [toolName, toolDescription, parameterDescriptions, savedValues]) + }, [toolName, toolDescription, savedValues]) const hasServerSelectionChanges = useMemo( () => !haveSameServerSelection(selectedServerIdsForForm, selectedServerIds), [selectedServerIdsForForm, selectedServerIds] @@ -280,6 +298,10 @@ export function McpDeploy({ onCanSaveChange?.(hasChanges && !!toolName.trim() && !toolNameError) }, [hasChanges, toolName, toolNameError, onCanSaveChange]) + useEffect(() => { + onExposedChange?.(selectedServerIds.length > 0) + }, [selectedServerIds, onExposedChange]) + const handleSave = async () => { if (!toolName.trim() || toolNameError) return @@ -307,7 +329,6 @@ export function McpDeploy({ workflowId, toolName: toolName.trim(), toolDescription: toolDescription.trim() || undefined, - parameterSchema, }) addedEntries[serverId] = { tool: addedTool, isLoading: false } onAddedToServer?.() @@ -363,7 +384,6 @@ export function McpDeploy({ toolId: toolInfo.tool.id, toolName: toolName.trim(), toolDescription: toolDescription.trim() || undefined, - parameterSchema, }) } catch (error) { const serverName = servers.find((s) => s.id === serverId)?.name || serverId @@ -387,7 +407,6 @@ export function McpDeploy({ setSavedValues({ toolName, toolDescription, - parameterDescriptions: { ...parameterDescriptions }, }) onCanSaveChange?.(false) } @@ -516,7 +535,7 @@ export function McpDeploy({ -