diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx index 0047c8e354d..455aa3f50fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx @@ -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 >({}) @@ -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)]')} /> -

- Use lowercase letters, numbers, and underscores only +

+ {toolNameError ?? 'Use lowercase letters, numbers, and underscores only'}

@@ -564,16 +592,20 @@ export function McpDeploy({ placeholder='Select servers...' searchable searchPlaceholder='Search servers...' - disabled={!toolName.trim() || isPending} + disabled={!toolName.trim() || !!toolNameError || isPending} overlayContent={ {selectedServersLabel} } /> - {!toolName.trim() && ( + {!toolName.trim() ? (

Enter a tool name to select servers

- )} + ) : toolNameError ? ( +

+ Fix the tool name to select servers +

+ ) : null} {saveErrors.length > 0 && (