@@ -14,6 +14,7 @@ import {
1414 Textarea ,
1515} from '@/components/emcn'
1616import { generateToolInputSchema , sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
17+ import { cn } from '@/lib/utils'
1718import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
1819import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
1920import type { InputFormatField } from '@/lib/workflows/types'
@@ -32,6 +33,10 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3233
3334const logger = createLogger ( 'McpToolDeploy' )
3435
36+ /** MCP tool names allow lowercase letters, numbers, and underscores only */
37+ const TOOL_NAME_PATTERN = / ^ [ a - z 0 - 9 _ ] + $ /
38+ const MAX_TOOL_NAME_LENGTH = 64
39+
3540/** InputFormatField with guaranteed name (after normalization) */
3641type NormalizedField = InputFormatField & { name : string }
3742
@@ -166,6 +171,18 @@ export function McpDeploy({
166171 [ inputFormat , parameterDescriptions ]
167172 )
168173
174+ const toolNameError = useMemo ( ( ) => {
175+ const trimmed = toolName . trim ( )
176+ if ( ! trimmed ) return null
177+ if ( trimmed . length > MAX_TOOL_NAME_LENGTH ) {
178+ return `Tool name must be ${ MAX_TOOL_NAME_LENGTH } characters or fewer`
179+ }
180+ if ( ! TOOL_NAME_PATTERN . test ( trimmed ) ) {
181+ return 'Tool name can only contain lowercase letters, numbers, and underscores'
182+ }
183+ return null
184+ } , [ toolName ] )
185+
169186 const [ serverToolsMap , setServerToolsMap ] = useState <
170187 Record < string , { tool : WorkflowMcpTool | null ; isLoading : boolean } >
171188 > ( { } )
@@ -270,11 +287,11 @@ export function McpDeploy({
270287 ( hasToolConfigurationChanges && selectedServerIdsForForm . length > 0 )
271288
272289 useEffect ( ( ) => {
273- onCanSaveChange ?.( hasChanges && ! ! toolName . trim ( ) )
274- } , [ hasChanges , toolName , onCanSaveChange ] )
290+ onCanSaveChange ?.( hasChanges && ! ! toolName . trim ( ) && ! toolNameError )
291+ } , [ hasChanges , toolName , toolNameError , onCanSaveChange ] )
275292
276293 const handleSave = async ( ) => {
277- if ( ! toolName . trim ( ) ) return
294+ if ( ! toolName . trim ( ) || toolNameError ) return
278295
279296 const currentIds = new Set ( selectedServerIds )
280297 const nextIds = new Set ( selectedServerIdsForForm )
@@ -492,9 +509,16 @@ export function McpDeploy({
492509 value = { toolName }
493510 onChange = { ( e ) => setToolName ( e . target . value ) }
494511 placeholder = 'e.g., book_flight'
512+ aria-invalid = { ! ! toolNameError }
513+ className = { cn ( toolNameError && 'border-[var(--text-error)]' ) }
495514 />
496- < p className = 'mt-[6.5px] text-[var(--text-secondary)] text-xs' >
497- Use lowercase letters, numbers, and underscores only
515+ < p
516+ className = { cn (
517+ 'mt-[6.5px] text-xs' ,
518+ toolNameError ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]'
519+ ) }
520+ >
521+ { toolNameError ?? 'Use lowercase letters, numbers, and underscores only' }
498522 </ p >
499523 </ div >
500524
@@ -564,7 +588,7 @@ export function McpDeploy({
564588 placeholder = 'Select servers...'
565589 searchable
566590 searchPlaceholder = 'Search servers...'
567- disabled = { ! toolName . trim ( ) || isPending }
591+ disabled = { ! toolName . trim ( ) || ! ! toolNameError || isPending }
568592 overlayContent = {
569593 < span className = 'truncate text-[var(--text-primary)]' > { selectedServersLabel } </ span >
570594 }
0 commit comments