Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Skeleton,
Textarea,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
Expand All @@ -32,6 +33,14 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'

const logger = createLogger('McpToolDeploy')

/**
* Mirrors the server's `sanitizeToolName` output: lowercase alphanumerics with single
* underscores between segments. Disallows leading/trailing and consecutive underscores so
* the validated name matches exactly what the server persists (no silent rewrite).
*/
const TOOL_NAME_PATTERN = /^[a-z0-9]+(_[a-z0-9]+)*$/
const MAX_TOOL_NAME_LENGTH = 64

/** InputFormatField with guaranteed name (after normalization) */
type NormalizedField = InputFormatField & { name: string }

Expand Down Expand Up @@ -166,6 +175,18 @@ export function McpDeploy({
[inputFormat, parameterDescriptions]
)

const toolNameError = useMemo(() => {
const trimmed = toolName.trim()
if (!trimmed) return null
if (trimmed.length > MAX_TOOL_NAME_LENGTH) {
return `Tool name must be ${MAX_TOOL_NAME_LENGTH} characters or fewer`
}
if (!TOOL_NAME_PATTERN.test(trimmed)) {
return 'Use lowercase letters and numbers, separated by single underscores'
}
return null
Comment thread
waleedlatif1 marked this conversation as resolved.
}, [toolName])

const [serverToolsMap, setServerToolsMap] = useState<
Record<string, { tool: WorkflowMcpTool | null; isLoading: boolean }>
>({})
Expand Down Expand Up @@ -270,11 +291,11 @@ export function McpDeploy({
(hasToolConfigurationChanges && selectedServerIdsForForm.length > 0)

useEffect(() => {
onCanSaveChange?.(hasChanges && !!toolName.trim())
}, [hasChanges, toolName, onCanSaveChange])
onCanSaveChange?.(hasChanges && !!toolName.trim() && !toolNameError)
}, [hasChanges, toolName, toolNameError, onCanSaveChange])

const handleSave = async () => {
if (!toolName.trim()) return
if (!toolName.trim() || toolNameError) return

const currentIds = new Set(selectedServerIds)
const nextIds = new Set(selectedServerIdsForForm)
Expand Down Expand Up @@ -492,9 +513,16 @@ export function McpDeploy({
value={toolName}
onChange={(e) => setToolName(e.target.value)}
placeholder='e.g., book_flight'
aria-invalid={!!toolNameError}
className={cn(toolNameError && 'border-[var(--text-error)]')}
/>
<p className='mt-[6.5px] text-[var(--text-secondary)] text-xs'>
Use lowercase letters, numbers, and underscores only
<p
className={cn(
'mt-[6.5px] text-xs',
toolNameError ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]'
)}
>
{toolNameError ?? 'Use lowercase letters, numbers, and underscores only'}
</p>
</div>

Expand Down Expand Up @@ -564,16 +592,20 @@ export function McpDeploy({
placeholder='Select servers...'
searchable
searchPlaceholder='Search servers...'
disabled={!toolName.trim() || isPending}
disabled={!toolName.trim() || !!toolNameError || isPending}
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{selectedServersLabel}</span>
}
/>
{!toolName.trim() && (
{!toolName.trim() ? (
<p className='mt-[6.5px] text-[var(--text-secondary)] text-xs'>
Enter a tool name to select servers
</p>
)}
) : toolNameError ? (
<p className='mt-[6.5px] text-[var(--text-secondary)] text-xs'>
Fix the tool name to select servers
</p>
) : null}
</div>

{saveErrors.length > 0 && (
Expand Down
Loading