Skip to content

Commit d80f0c6

Browse files
authored
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
1 parent 402472e commit d80f0c6

1 file changed

Lines changed: 40 additions & 8 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Skeleton,
1414
Textarea,
1515
} from '@/components/emcn'
16+
import { cn } from '@/lib/core/utils/cn'
1617
import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
1718
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
1819
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
@@ -32,6 +33,14 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3233

3334
const 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-z0-9]+(_[a-z0-9]+)*$/
42+
const MAX_TOOL_NAME_LENGTH = 64
43+
3544
/** InputFormatField with guaranteed name (after normalization) */
3645
type 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

Comments
 (0)