Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<CodeLanguage, string> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'

Expand All @@ -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 {
Expand All @@ -60,14 +64,36 @@ function haveSameServerSelection(a: string[], b: string[]): boolean {
return a.every((id) => bSet.has(id))
}

function haveSameParameterDescriptions(
a: Record<string, string>,
b: Record<string, string>
): 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<string, string> {
const properties = (parameterSchema as { properties?: Record<string, { description?: string }> })
?.properties
if (!properties) return {}
const descriptions: Record<string, string> = {}
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
}

/**
Expand Down Expand Up @@ -102,6 +128,7 @@ export function McpDeploy({
onAddedToServer,
onSubmittingChange,
onCanSaveChange,
onExposedChange,
}: McpDeployProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
Expand All @@ -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)

Expand All @@ -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<Record<string, string>>({})
const [toolDescription, setToolDescription] = useState(() =>
isDefaultDescription(workflowDescription, workflowName) ? '' : (workflowDescription ?? '')
)
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(() => new Set())
const [saveErrors, setSaveErrors] = useState<string[]>([])

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)
Comment thread
waleedlatif1 marked this conversation as resolved.
},
[starterBlockId, collaborativeSetSubblockValue]
)
Comment thread
waleedlatif1 marked this conversation as resolved.

const toolNameError = useMemo(() => {
Expand Down Expand Up @@ -207,8 +243,15 @@ export function McpDeploy({
const [savedValues, setSavedValues] = useState<{
toolName: string
toolDescription: string
parameterDescriptions: Record<string, string>
} | 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<string, string>
>({})

useEffect(() => {
if (savedValues) return
Expand All @@ -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<string, unknown> | undefined
const properties = schema?.properties as
| Record<string, { description?: string }>
| undefined
const initialParameterDescriptions: Record<string, string> = {}
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)
Comment thread
waleedlatif1 marked this conversation as resolved.
setLegacyParameterDescriptions(extractToolSchemaDescriptions(toolInfo.tool.parameterSchema))
setSavedValues({
toolName: initialToolName,
toolDescription: initialToolDescription,
parameterDescriptions: initialParameterDescriptions,
})
break
}
Expand All @@ -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]
Expand All @@ -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

Expand Down Expand Up @@ -307,7 +329,6 @@ export function McpDeploy({
workflowId,
toolName: toolName.trim(),
toolDescription: toolDescription.trim() || undefined,
parameterSchema,
})
addedEntries[serverId] = { tool: addedTool, isLoading: false }
onAddedToServer?.()
Expand Down Expand Up @@ -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
Expand All @@ -387,7 +407,6 @@ export function McpDeploy({
setSavedValues({
toolName,
toolDescription,
parameterDescriptions: { ...parameterDescriptions },
})
onCanSaveChange?.(false)
}
Expand Down Expand Up @@ -516,7 +535,7 @@ export function McpDeploy({
<Label className='mb-[6.5px] block pl-0.5 font-medium text-[var(--text-primary)] text-small'>
Description
</Label>
<Textarea
<ChipTextarea
placeholder='Describe what this tool does...'
className='min-h-[100px] resize-none'
value={toolDescription}
Expand All @@ -529,6 +548,9 @@ export function McpDeploy({
<Label className='mb-[6.5px] block pl-0.5 font-medium text-[var(--text-primary)] text-small'>
Parameters ({inputFormat.length})
</Label>
<p className='mb-[6.5px] pl-0.5 text-[var(--text-secondary)] text-xs'>
Descriptions are part of the workflow's inputs. Redeploy to apply changes to the tool.
</p>
<div className='flex flex-col gap-2'>
{inputFormat.map((field) => (
<div
Expand All @@ -549,13 +571,8 @@ export function McpDeploy({
<div className='flex flex-col gap-1.5'>
<Label className='text-small'>Description</Label>
<ChipInput
value={parameterDescriptions[field.name] || ''}
onChange={(e) =>
setParameterDescriptions((prev) => ({
...prev,
[field.name]: e.target.value,
}))
}
value={field.description ?? legacyParameterDescriptions[field.name] ?? ''}
onChange={(e) => updateFieldDescription(field.name, e.target.value)}
placeholder={`Enter description for ${field.name}`}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function DeployModal({
const [undeployTargetWorkflowId, setUndeployTargetWorkflowId] = useState<string | null>(null)
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
const [mcpToolExposed, setMcpToolExposed] = useState(false)
const [a2aSubmitting, setA2aSubmitting] = useState(false)
const [a2aCanSave, setA2aCanSave] = useState(false)
const [a2aNeedsRepublish, setA2aNeedsRepublish] = useState(false)
Expand Down Expand Up @@ -642,6 +643,7 @@ export function DeployModal({
isDeployed={isDeployed}
onSubmittingChange={setMcpToolSubmitting}
onCanSaveChange={setMcpToolCanSave}
onExposedChange={setMcpToolExposed}
/>
)}
</ModalTabsContent>
Expand Down Expand Up @@ -733,7 +735,13 @@ export function DeployModal({
)}
{activeTab === 'mcp' && isDeployed && hasMcpServers && (
<ModalFooter className='items-center justify-between'>
<div />
{mcpToolExposed ? (
<Badge variant={needsRedeployment ? 'amber' : 'green'} size='lg' dot>
{needsRedeployment ? 'Update deployment' : 'Live'}
</Badge>
) : (
<div />
)}
<div className='flex items-center gap-2'>
<Button
type='button'
Expand Down
Loading
Loading