From 38614fad7947487578a6cdf9c2b412723eb529e8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 27 Oct 2025 13:13:11 -0700 Subject: [PATCH] fix(mcp): resolve variables & block references in mcp subblocks (#1735) * fix(mcp): resolve variables & block references in mcp subblocks * cleanup * ack PR comment * added variables access to mcp tools when added in agent block * fix sequential migrations issues --- apps/sim/app/api/mcp/tools/execute/route.ts | 32 +- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 442 ++++++++++++++---- .../mcp-server-modal/mcp-tool-selector.tsx | 6 + .../tool-input/components/mcp-tools-list.tsx | 3 + .../components/tool-input/tool-input.tsx | 48 +- .../components/sub-block/sub-block.tsx | 1 + .../workflow-block/workflow-block.tsx | 93 ++-- .../executor/handlers/agent/agent-handler.ts | 13 +- apps/sim/providers/utils.ts | 4 +- ...ng.sql => 0101_missing_doc_processing.sql} | 0 10 files changed, 481 insertions(+), 161 deletions(-) rename packages/db/migrations/{0077_missing_doc_processing.sql => 0101_missing_doc_processing.sql} (100%) diff --git a/apps/sim/app/api/mcp/tools/execute/route.ts b/apps/sim/app/api/mcp/tools/execute/route.ts index cecac072c..63845865f 100644 --- a/apps/sim/app/api/mcp/tools/execute/route.ts +++ b/apps/sim/app/api/mcp/tools/execute/route.ts @@ -90,16 +90,38 @@ export const POST = withMcpAuth('read')( ) } - // Parse array arguments based on tool schema + // Cast arguments to their expected types based on tool schema if (tool.inputSchema?.properties) { for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) { const schema = paramSchema as any + const value = args[paramName] + + if (value === undefined || value === null) { + continue + } + + // Cast numbers if ( - schema.type === 'array' && - args[paramName] !== undefined && - typeof args[paramName] === 'string' + (schema.type === 'number' || schema.type === 'integer') && + typeof value === 'string' ) { - const stringValue = args[paramName].trim() + const numValue = + schema.type === 'integer' ? Number.parseInt(value) : Number.parseFloat(value) + if (!Number.isNaN(numValue)) { + args[paramName] = numValue + } + } + // Cast booleans + else if (schema.type === 'boolean' && typeof value === 'string') { + if (value.toLowerCase() === 'true') { + args[paramName] = true + } else if (value.toLowerCase() === 'false') { + args[paramName] = false + } + } + // Cast arrays + else if (schema.type === 'array' && typeof value === 'string') { + const stringValue = value.trim() if (stringValue) { try { // Try to parse as JSON first (handles ["item1", "item2"]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx index d40f8d030..484710f57 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useRef, useState } from 'react' import { useParams } from 'next/navigation' import { formatDisplayText } from '@/components/ui/formatted-text' import { Input } from '@/components/ui/input' @@ -12,19 +12,261 @@ import { } from '@/components/ui/select' import { Slider } from '@/components/ui/slider' import { Switch } from '@/components/ui/switch' +import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' import { Textarea } from '@/components/ui/textarea' +import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useMcpTools } from '@/hooks/use-mcp-tools' import { formatParameterLabel } from '@/tools/params' +const logger = createLogger('McpDynamicArgs') + +interface McpInputWithTagsProps { + value: string + onChange: (value: string) => void + placeholder?: string + disabled?: boolean + isPassword?: boolean + blockId: string + accessiblePrefixes?: Set + isConnecting?: boolean +} + +function McpInputWithTags({ + value, + onChange, + placeholder, + disabled, + isPassword, + blockId, + accessiblePrefixes, + isConnecting = false, +}: McpInputWithTagsProps) { + const [showTags, setShowTags] = useState(false) + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const inputRef = useRef(null) + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + const newCursorPosition = e.target.selectionStart ?? 0 + + onChange(newValue) + setCursorPosition(newCursorPosition) + + const tagTrigger = checkTagTrigger(newValue, newCursorPosition) + setShowTags(tagTrigger.show) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')) + if (data.type !== 'connectionBlock') return + + const dropPosition = inputRef.current?.selectionStart ?? value.length ?? 0 + const currentValue = value ?? '' + const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}` + + onChange(newValue) + setCursorPosition(dropPosition + 1) + setShowTags(true) + + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + + setTimeout(() => { + if (inputRef.current) { + inputRef.current.selectionStart = dropPosition + 1 + inputRef.current.selectionEnd = dropPosition + 1 + } + }, 0) + } catch (error) { + logger.error('Failed to parse drop data:', { error }) + } + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + } + + const handleTagSelect = (newValue: string) => { + onChange(newValue) + setShowTags(false) + setActiveSourceBlockId(null) + } + + return ( +
+
+ + {!isPassword && ( +
+
+ {formatDisplayText(value?.toString() || '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} +
+
+ )} +
+ { + setShowTags(false) + setActiveSourceBlockId(null) + }} + /> +
+ ) +} + +interface McpTextareaWithTagsProps { + value: string + onChange: (value: string) => void + placeholder?: string + disabled?: boolean + blockId: string + accessiblePrefixes?: Set + rows?: number + isConnecting?: boolean +} + +function McpTextareaWithTags({ + value, + onChange, + placeholder, + disabled, + blockId, + accessiblePrefixes, + rows = 4, + isConnecting = false, +}: McpTextareaWithTagsProps) { + const [showTags, setShowTags] = useState(false) + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const textareaRef = useRef(null) + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + const newCursorPosition = e.target.selectionStart ?? 0 + + onChange(newValue) + setCursorPosition(newCursorPosition) + + // Check for tag trigger + const tagTrigger = checkTagTrigger(newValue, newCursorPosition) + setShowTags(tagTrigger.show) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')) + if (data.type !== 'connectionBlock') return + + const dropPosition = textareaRef.current?.selectionStart ?? value.length ?? 0 + const currentValue = value ?? '' + const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}` + + onChange(newValue) + setCursorPosition(dropPosition + 1) + setShowTags(true) + + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.selectionStart = dropPosition + 1 + textareaRef.current.selectionEnd = dropPosition + 1 + } + }, 0) + } catch (error) { + logger.error('Failed to parse drop data:', { error }) + } + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + } + + const handleTagSelect = (newValue: string) => { + onChange(newValue) + setShowTags(false) + setActiveSourceBlockId(null) + } + + return ( +
+