From 8ce8f0a3d801561ecf48d71cc6dcc8fe92a513b9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 11:12:33 -0700 Subject: [PATCH 1/7] improvement(workflow-mcp): single-source MCP tool params, deploy status, chip styling - Make the start block input format the single source of truth for MCP tool parameter descriptions; the deploy modal writes them back collaboratively so they persist with the workflow and survive redeploys (fixes descriptions getting wiped on workflow edits) - Derive the tool parameter schema from the deployed workflow instead of the draft, so a saved tool can never advertise params the running workflow lacks - Add a Live / Update deployment status badge to the MCP tab, mirroring A2A - Swap the legacy Textarea for ChipTextarea on the tool Description - Extract the duplicated isDefaultDescription helper into one shared util --- .../deploy-modal/components/a2a/a2a.tsx | 15 +-- .../general/components/api-info-modal.tsx | 15 ++- .../deploy-modal/components/mcp/mcp.tsx | 112 +++++++----------- .../components/deploy-modal/deploy-modal.tsx | 10 +- .../deploy-modal/lib/default-description.ts | 28 +++++ 5 files changed, 89 insertions(+), 91 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description.ts 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..07c558cba93 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,16 +64,6 @@ 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]) -} - /** * Component to query tools for a single server and report back via callback. */ @@ -102,6 +96,7 @@ export function McpDeploy({ onAddedToServer, onSubmittingChange, onCanSaveChange, + onExposedChange, }: McpDeployProps) { const params = useParams() const workspaceId = params.workspaceId as string @@ -111,6 +106,7 @@ export function McpDeploy({ const addToolMutation = useAddWorkflowMcpTool() const deleteToolMutation = useDeleteWorkflowMcpTool() const updateToolMutation = useUpdateWorkflowMcpTool() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const blocks = useWorkflowStore((state) => state.blocks) @@ -142,23 +138,31 @@ export function McpDeploy({ }, [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] + /** + * Per-parameter descriptions live on the start block's input format — the single + * source of truth shared by every deployment surface. The tool's parameter schema + * is derived server-side from the deployed workflow on each deploy, so the tool can + * never drift from what actually runs and descriptions are never wiped by a redeploy. + * Editing a description here mutates the workflow and surfaces the redeploy prompt. + */ + const updateFieldDescription = useCallback( + (fieldName: string, description: string) => { + if (!starterBlockId) return + const currentFields = normalizeInputFormatValue( + useSubBlockStore.getState().getValue(starterBlockId, 'inputFormat') + ) as NormalizedField[] + const nextFields = currentFields.map((field) => + field.name === fieldName ? { ...field, description } : field + ) + collaborativeSetSubblockValue(starterBlockId, 'inputFormat', nextFields) + }, + [starterBlockId, collaborativeSetSubblockValue] ) const toolNameError = useMemo(() => { @@ -207,7 +211,6 @@ export function McpDeploy({ const [savedValues, setSavedValues] = useState<{ toolName: string toolDescription: string - parameterDescriptions: Record } | null>(null) useEffect(() => { @@ -219,38 +222,15 @@ 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) setSavedValues({ toolName: initialToolName, toolDescription: initialToolDescription, - parameterDescriptions: initialParameterDescriptions, }) break } @@ -263,11 +243,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 +257,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 +288,6 @@ export function McpDeploy({ workflowId, toolName: toolName.trim(), toolDescription: toolDescription.trim() || undefined, - parameterSchema, }) addedEntries[serverId] = { tool: addedTool, isLoading: false } onAddedToServer?.() @@ -363,7 +343,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 +366,6 @@ export function McpDeploy({ setSavedValues({ toolName, toolDescription, - parameterDescriptions: { ...parameterDescriptions }, }) onCanSaveChange?.(false) } @@ -516,7 +494,7 @@ export function McpDeploy({ -