Skip to content

Commit 8ce8f0a

Browse files
committed
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
1 parent eb1009d commit 8ce8f0a

5 files changed

Lines changed: 89 additions & 91 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { AgentAuthentication, AgentCapabilities } from '@/lib/a2a/types'
2323
import { getBaseUrl } from '@/lib/core/utils/urls'
2424
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
2525
import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers'
26+
import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description'
2627
import {
2728
useA2AAgentByWorkflow,
2829
useCreateA2AAgent,
@@ -44,20 +45,6 @@ interface InputFormatField {
4445
collapsed?: boolean
4546
}
4647

47-
/**
48-
* Check if a description is a default/placeholder value that should be filtered out
49-
*/
50-
function isDefaultDescription(desc: string | null | undefined, workflowName: string): boolean {
51-
if (!desc) return true
52-
const normalized = desc.toLowerCase().trim()
53-
return (
54-
normalized === '' ||
55-
normalized === 'new workflow' ||
56-
normalized === 'your first workflow - start building here!' ||
57-
normalized === workflowName.toLowerCase()
58-
)
59-
}
60-
6148
type CodeLanguage = 'curl' | 'python' | 'javascript' | 'typescript'
6249

6350
const LANGUAGE_LABELS: Record<CodeLanguage, string> = {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
2121
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
2222
import type { InputFormatField } from '@/lib/workflows/types'
23+
import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description'
2324
import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deployments'
2425
import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
2526
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -89,14 +90,12 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
8990

9091
useEffect(() => {
9192
if (open) {
92-
const normalizedDesc = workflowMetadata?.description?.toLowerCase().trim()
93-
const isDefaultDescription =
94-
!workflowMetadata?.description ||
95-
workflowMetadata.description === workflowMetadata.name ||
96-
normalizedDesc === 'new workflow' ||
97-
normalizedDesc === 'your first workflow - start building here!'
98-
99-
const initialDescription = isDefaultDescription ? '' : workflowMetadata?.description || ''
93+
const initialDescription = isDefaultDescription(
94+
workflowMetadata?.description,
95+
workflowMetadata?.name || ''
96+
)
97+
? ''
98+
: workflowMetadata?.description || ''
10099
setDescription(initialDescription)
101100
initialDescriptionRef.current = initialDescription
102101

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ import {
88
Button,
99
ChipCombobox,
1010
ChipInput,
11+
ChipTextarea,
1112
type ComboboxOption,
1213
Label,
1314
Skeleton,
14-
Textarea,
1515
} from '@/components/emcn'
1616
import { cn } from '@/lib/core/utils/cn'
17-
import { generateParameterSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
17+
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
1818
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
1919
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
2020
import type { InputFormatField } from '@/lib/workflows/types'
2121
import { CreateWorkflowMcpServerModal } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/components/create-workflow-mcp-server-modal'
22+
import { isDefaultDescription } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/lib/default-description'
2223
import {
2324
useAddWorkflowMcpTool,
2425
useDeleteWorkflowMcpTool,
@@ -28,6 +29,7 @@ import {
2829
type WorkflowMcpServer,
2930
type WorkflowMcpTool,
3031
} from '@/hooks/queries/workflow-mcp-servers'
32+
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
3133
import { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
3234
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3335

@@ -52,6 +54,8 @@ interface McpDeployProps {
5254
onAddedToServer?: () => void
5355
onSubmittingChange?: (submitting: boolean) => void
5456
onCanSaveChange?: (canSave: boolean) => void
57+
/** Reports whether this workflow is currently exposed as a tool on any server. */
58+
onExposedChange?: (exposed: boolean) => void
5559
}
5660

5761
function haveSameServerSelection(a: string[], b: string[]): boolean {
@@ -60,16 +64,6 @@ function haveSameServerSelection(a: string[], b: string[]): boolean {
6064
return a.every((id) => bSet.has(id))
6165
}
6266

63-
function haveSameParameterDescriptions(
64-
a: Record<string, string>,
65-
b: Record<string, string>
66-
): boolean {
67-
const aKeys = Object.keys(a)
68-
const bKeys = Object.keys(b)
69-
if (aKeys.length !== bKeys.length) return false
70-
return aKeys.every((key) => a[key] === b[key])
71-
}
72-
7367
/**
7468
* Component to query tools for a single server and report back via callback.
7569
*/
@@ -102,6 +96,7 @@ export function McpDeploy({
10296
onAddedToServer,
10397
onSubmittingChange,
10498
onCanSaveChange,
99+
onExposedChange,
105100
}: McpDeployProps) {
106101
const params = useParams()
107102
const workspaceId = params.workspaceId as string
@@ -111,6 +106,7 @@ export function McpDeploy({
111106
const addToolMutation = useAddWorkflowMcpTool()
112107
const deleteToolMutation = useDeleteWorkflowMcpTool()
113108
const updateToolMutation = useUpdateWorkflowMcpTool()
109+
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
114110

115111
const blocks = useWorkflowStore((state) => state.blocks)
116112

@@ -142,23 +138,31 @@ export function McpDeploy({
142138
}, [starterBlockId, subBlockValues, blocks])
143139

144140
const [toolName, setToolName] = useState(() => sanitizeToolName(workflowName))
145-
const [toolDescription, setToolDescription] = useState(() => {
146-
const normalizedDesc = workflowDescription?.toLowerCase().trim()
147-
const isDefaultDescription =
148-
!workflowDescription ||
149-
workflowDescription === workflowName ||
150-
normalizedDesc === 'new workflow' ||
151-
normalizedDesc === 'your first workflow - start building here!'
152-
153-
return isDefaultDescription ? '' : workflowDescription
154-
})
155-
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
141+
const [toolDescription, setToolDescription] = useState(() =>
142+
isDefaultDescription(workflowDescription, workflowName) ? '' : (workflowDescription ?? '')
143+
)
156144
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(() => new Set())
157145
const [saveErrors, setSaveErrors] = useState<string[]>([])
158146

159-
const parameterSchema = useMemo(
160-
() => generateParameterSchema(inputFormat, parameterDescriptions),
161-
[inputFormat, parameterDescriptions]
147+
/**
148+
* Per-parameter descriptions live on the start block's input format — the single
149+
* source of truth shared by every deployment surface. The tool's parameter schema
150+
* is derived server-side from the deployed workflow on each deploy, so the tool can
151+
* never drift from what actually runs and descriptions are never wiped by a redeploy.
152+
* Editing a description here mutates the workflow and surfaces the redeploy prompt.
153+
*/
154+
const updateFieldDescription = useCallback(
155+
(fieldName: string, description: string) => {
156+
if (!starterBlockId) return
157+
const currentFields = normalizeInputFormatValue(
158+
useSubBlockStore.getState().getValue(starterBlockId, 'inputFormat')
159+
) as NormalizedField[]
160+
const nextFields = currentFields.map((field) =>
161+
field.name === fieldName ? { ...field, description } : field
162+
)
163+
collaborativeSetSubblockValue(starterBlockId, 'inputFormat', nextFields)
164+
},
165+
[starterBlockId, collaborativeSetSubblockValue]
162166
)
163167

164168
const toolNameError = useMemo(() => {
@@ -207,7 +211,6 @@ export function McpDeploy({
207211
const [savedValues, setSavedValues] = useState<{
208212
toolName: string
209213
toolDescription: string
210-
parameterDescriptions: Record<string, string>
211214
} | null>(null)
212215

213216
useEffect(() => {
@@ -219,38 +222,15 @@ export function McpDeploy({
219222
const initialToolName = toolInfo.tool.toolName
220223

221224
const loadedDescription = toolInfo.tool.toolDescription || ''
222-
const normalizedLoadedDesc = loadedDescription.toLowerCase().trim()
223-
const isDefaultDescription =
224-
!loadedDescription ||
225-
loadedDescription === workflowName ||
226-
normalizedLoadedDesc === 'new workflow' ||
227-
normalizedLoadedDesc === 'your first workflow - start building here!'
228-
const initialToolDescription = isDefaultDescription ? '' : loadedDescription
229-
230-
const schema = toolInfo.tool.parameterSchema as Record<string, unknown> | undefined
231-
const properties = schema?.properties as
232-
| Record<string, { description?: string }>
233-
| undefined
234-
const initialParameterDescriptions: Record<string, string> = {}
235-
if (properties) {
236-
for (const [name, prop] of Object.entries(properties)) {
237-
if (
238-
prop.description &&
239-
prop.description !== name &&
240-
prop.description !== 'Array of file objects'
241-
) {
242-
initialParameterDescriptions[name] = prop.description
243-
}
244-
}
245-
}
225+
const initialToolDescription = isDefaultDescription(loadedDescription, workflowName)
226+
? ''
227+
: loadedDescription
246228

247229
setToolName(initialToolName)
248230
setToolDescription(initialToolDescription)
249-
setParameterDescriptions(initialParameterDescriptions)
250231
setSavedValues({
251232
toolName: initialToolName,
252233
toolDescription: initialToolDescription,
253-
parameterDescriptions: initialParameterDescriptions,
254234
})
255235
break
256236
}
@@ -263,11 +243,8 @@ export function McpDeploy({
263243
if (!savedValues) return false
264244
if (toolName !== savedValues.toolName) return true
265245
if (toolDescription !== savedValues.toolDescription) return true
266-
if (!haveSameParameterDescriptions(parameterDescriptions, savedValues.parameterDescriptions)) {
267-
return true
268-
}
269246
return false
270-
}, [toolName, toolDescription, parameterDescriptions, savedValues])
247+
}, [toolName, toolDescription, savedValues])
271248
const hasServerSelectionChanges = useMemo(
272249
() => !haveSameServerSelection(selectedServerIdsForForm, selectedServerIds),
273250
[selectedServerIdsForForm, selectedServerIds]
@@ -280,6 +257,10 @@ export function McpDeploy({
280257
onCanSaveChange?.(hasChanges && !!toolName.trim() && !toolNameError)
281258
}, [hasChanges, toolName, toolNameError, onCanSaveChange])
282259

260+
useEffect(() => {
261+
onExposedChange?.(selectedServerIds.length > 0)
262+
}, [selectedServerIds, onExposedChange])
263+
283264
const handleSave = async () => {
284265
if (!toolName.trim() || toolNameError) return
285266

@@ -307,7 +288,6 @@ export function McpDeploy({
307288
workflowId,
308289
toolName: toolName.trim(),
309290
toolDescription: toolDescription.trim() || undefined,
310-
parameterSchema,
311291
})
312292
addedEntries[serverId] = { tool: addedTool, isLoading: false }
313293
onAddedToServer?.()
@@ -363,7 +343,6 @@ export function McpDeploy({
363343
toolId: toolInfo.tool.id,
364344
toolName: toolName.trim(),
365345
toolDescription: toolDescription.trim() || undefined,
366-
parameterSchema,
367346
})
368347
} catch (error) {
369348
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
@@ -387,7 +366,6 @@ export function McpDeploy({
387366
setSavedValues({
388367
toolName,
389368
toolDescription,
390-
parameterDescriptions: { ...parameterDescriptions },
391369
})
392370
onCanSaveChange?.(false)
393371
}
@@ -516,7 +494,7 @@ export function McpDeploy({
516494
<Label className='mb-[6.5px] block pl-0.5 font-medium text-[var(--text-primary)] text-small'>
517495
Description
518496
</Label>
519-
<Textarea
497+
<ChipTextarea
520498
placeholder='Describe what this tool does...'
521499
className='min-h-[100px] resize-none'
522500
value={toolDescription}
@@ -529,6 +507,9 @@ export function McpDeploy({
529507
<Label className='mb-[6.5px] block pl-0.5 font-medium text-[var(--text-primary)] text-small'>
530508
Parameters ({inputFormat.length})
531509
</Label>
510+
<p className='mb-[6.5px] pl-0.5 text-[var(--text-secondary)] text-xs'>
511+
Descriptions are part of the workflow's inputs. Redeploy to apply changes to the tool.
512+
</p>
532513
<div className='flex flex-col gap-2'>
533514
{inputFormat.map((field) => (
534515
<div
@@ -549,13 +530,8 @@ export function McpDeploy({
549530
<div className='flex flex-col gap-1.5'>
550531
<Label className='text-small'>Description</Label>
551532
<ChipInput
552-
value={parameterDescriptions[field.name] || ''}
553-
onChange={(e) =>
554-
setParameterDescriptions((prev) => ({
555-
...prev,
556-
[field.name]: e.target.value,
557-
}))
558-
}
533+
value={field.description ?? ''}
534+
onChange={(e) => updateFieldDescription(field.name, e.target.value)}
559535
placeholder={`Enter description for ${field.name}`}
560536
/>
561537
</div>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export function DeployModal({
126126
const [undeployTargetWorkflowId, setUndeployTargetWorkflowId] = useState<string | null>(null)
127127
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
128128
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
129+
const [mcpToolExposed, setMcpToolExposed] = useState(false)
129130
const [a2aSubmitting, setA2aSubmitting] = useState(false)
130131
const [a2aCanSave, setA2aCanSave] = useState(false)
131132
const [a2aNeedsRepublish, setA2aNeedsRepublish] = useState(false)
@@ -642,6 +643,7 @@ export function DeployModal({
642643
isDeployed={isDeployed}
643644
onSubmittingChange={setMcpToolSubmitting}
644645
onCanSaveChange={setMcpToolCanSave}
646+
onExposedChange={setMcpToolExposed}
645647
/>
646648
)}
647649
</ModalTabsContent>
@@ -733,7 +735,13 @@ export function DeployModal({
733735
)}
734736
{activeTab === 'mcp' && isDeployed && hasMcpServers && (
735737
<ModalFooter className='items-center justify-between'>
736-
<div />
738+
{mcpToolExposed ? (
739+
<Badge variant={needsRedeployment ? 'amber' : 'green'} size='lg' dot>
740+
{needsRedeployment ? 'Update deployment' : 'Live'}
741+
</Badge>
742+
) : (
743+
<div />
744+
)}
737745
<div className='flex items-center gap-2'>
738746
<Button
739747
type='button'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Placeholder descriptions auto-assigned to new workflows. These should be
3+
* treated as "no description" so deployment surfaces (API info, MCP tool, A2A
4+
* agent) don't echo boilerplate back to the user as if it were intentional copy.
5+
*/
6+
const DEFAULT_WORKFLOW_DESCRIPTIONS = [
7+
'new workflow',
8+
'your first workflow - start building here!',
9+
] as const
10+
11+
/**
12+
* Returns true when a workflow description is empty or a known auto-generated
13+
* placeholder (including the workflow name used as a fallback description).
14+
* Shared by every deployment tab so the "is this a real description?" rule has
15+
* a single definition.
16+
*/
17+
export function isDefaultDescription(
18+
description: string | null | undefined,
19+
workflowName: string
20+
): boolean {
21+
if (!description) return true
22+
const normalized = description.toLowerCase().trim()
23+
return (
24+
normalized === '' ||
25+
normalized === workflowName.toLowerCase().trim() ||
26+
(DEFAULT_WORKFLOW_DESCRIPTIONS as readonly string[]).includes(normalized)
27+
)
28+
}

0 commit comments

Comments
 (0)