fix(mcp): enforce tool name validation in deploy modal (#4879)

* fix(mcp): enforce tool name validation in deploy modal

* fix(mcp): correct cn import path to fix build

* fix(mcp): align tool-name regex with server sanitization, add disabled-combobox hint
This commit is contained in:
Waleed
2026-06-04 11:19:29 -07:00
committed by GitHub
parent 402472e4cc
commit d80f0c63bc
@@ -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'
@@ -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 }
@@ -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
}, [toolName])
const [serverToolsMap, setServerToolsMap] = useState<
Record<string, { tool: WorkflowMcpTool | null; isLoading: boolean }>
>({})
@@ -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)
@@ -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>
@@ -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 && (