@@ -13,6 +13,7 @@ import {
1313 Skeleton ,
1414 Textarea ,
1515} from '@/components/emcn'
16+ import { cn } from '@/lib/core/utils/cn'
1617import { generateToolInputSchema , sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
1718import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
1819import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
@@ -32,6 +33,14 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3233
3334const logger = createLogger ( 'McpToolDeploy' )
3435
36+ /**
37+ * Mirrors the server's `sanitizeToolName` output: lowercase alphanumerics with single
38+ * underscores between segments. Disallows leading/trailing and consecutive underscores so
39+ * the validated name matches exactly what the server persists (no silent rewrite).
40+ */
41+ const TOOL_NAME_PATTERN = / ^ [ a - z 0 - 9 ] + ( _ [ a - z 0 - 9 ] + ) * $ /
42+ const MAX_TOOL_NAME_LENGTH = 64
43+
3544/** InputFormatField with guaranteed name (after normalization) */
3645type NormalizedField = InputFormatField & { name : string }
3746
@@ -166,6 +175,18 @@ export function McpDeploy({
166175 [ inputFormat , parameterDescriptions ]
167176 )
168177
178+ const toolNameError = useMemo ( ( ) => {
179+ const trimmed = toolName . trim ( )
180+ if ( ! trimmed ) return null
181+ if ( trimmed . length > MAX_TOOL_NAME_LENGTH ) {
182+ return `Tool name must be ${ MAX_TOOL_NAME_LENGTH } characters or fewer`
183+ }
184+ if ( ! TOOL_NAME_PATTERN . test ( trimmed ) ) {
185+ return 'Use lowercase letters and numbers, separated by single underscores'
186+ }
187+ return null
188+ } , [ toolName ] )
189+
169190 const [ serverToolsMap , setServerToolsMap ] = useState <
170191 Record < string , { tool : WorkflowMcpTool | null ; isLoading : boolean } >
171192 > ( { } )
@@ -270,11 +291,11 @@ export function McpDeploy({
270291 ( hasToolConfigurationChanges && selectedServerIdsForForm . length > 0 )
271292
272293 useEffect ( ( ) => {
273- onCanSaveChange ?.( hasChanges && ! ! toolName . trim ( ) )
274- } , [ hasChanges , toolName , onCanSaveChange ] )
294+ onCanSaveChange ?.( hasChanges && ! ! toolName . trim ( ) && ! toolNameError )
295+ } , [ hasChanges , toolName , toolNameError , onCanSaveChange ] )
275296
276297 const handleSave = async ( ) => {
277- if ( ! toolName . trim ( ) ) return
298+ if ( ! toolName . trim ( ) || toolNameError ) return
278299
279300 const currentIds = new Set ( selectedServerIds )
280301 const nextIds = new Set ( selectedServerIdsForForm )
@@ -492,9 +513,16 @@ export function McpDeploy({
492513 value = { toolName }
493514 onChange = { ( e ) => setToolName ( e . target . value ) }
494515 placeholder = 'e.g., book_flight'
516+ aria-invalid = { ! ! toolNameError }
517+ className = { cn ( toolNameError && 'border-[var(--text-error)]' ) }
495518 />
496- < p className = 'mt-[6.5px] text-[var(--text-secondary)] text-xs' >
497- Use lowercase letters, numbers, and underscores only
519+ < p
520+ className = { cn (
521+ 'mt-[6.5px] text-xs' ,
522+ toolNameError ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]'
523+ ) }
524+ >
525+ { toolNameError ?? 'Use lowercase letters, numbers, and underscores only' }
498526 </ p >
499527 </ div >
500528
@@ -564,16 +592,20 @@ export function McpDeploy({
564592 placeholder = 'Select servers...'
565593 searchable
566594 searchPlaceholder = 'Search servers...'
567- disabled = { ! toolName . trim ( ) || isPending }
595+ disabled = { ! toolName . trim ( ) || ! ! toolNameError || isPending }
568596 overlayContent = {
569597 < span className = 'truncate text-[var(--text-primary)]' > { selectedServersLabel } </ span >
570598 }
571599 />
572- { ! toolName . trim ( ) && (
600+ { ! toolName . trim ( ) ? (
573601 < p className = 'mt-[6.5px] text-[var(--text-secondary)] text-xs' >
574602 Enter a tool name to select servers
575603 </ p >
576- ) }
604+ ) : toolNameError ? (
605+ < p className = 'mt-[6.5px] text-[var(--text-secondary)] text-xs' >
606+ Fix the tool name to select servers
607+ </ p >
608+ ) : null }
577609 </ div >
578610
579611 { saveErrors . length > 0 && (
0 commit comments