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
This commit is contained in:
Waleed
2025-10-27 13:13:11 -07:00
committed by GitHub
parent 6f32aea96b
commit 38614fad79
10 changed files with 481 additions and 161 deletions

View File

@@ -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"])

View File

@@ -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<string>
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<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
e.preventDefault()
}
const handleTagSelect = (newValue: string) => {
onChange(newValue)
setShowTags(false)
setActiveSourceBlockId(null)
}
return (
<div className='relative'>
<div className='relative'>
<Input
ref={inputRef}
type={isPassword ? 'password' : 'text'}
value={value || ''}
onChange={handleChange}
onDrop={handleDrop}
onDragOver={handleDragOver}
placeholder={placeholder}
disabled={disabled}
className={cn(
!isPassword && 'text-transparent caret-foreground',
isConnecting && 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
{!isPassword && (
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>
{formatDisplayText(value?.toString() || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
)}
</div>
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
</div>
)
}
interface McpTextareaWithTagsProps {
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
blockId: string
accessiblePrefixes?: Set<string>
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<string | null>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
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<HTMLTextAreaElement>) => {
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<HTMLTextAreaElement>) => {
e.preventDefault()
}
const handleTagSelect = (newValue: string) => {
onChange(newValue)
setShowTags(false)
setActiveSourceBlockId(null)
}
return (
<div className='relative'>
<Textarea
ref={textareaRef}
value={value || ''}
onChange={handleChange}
onDrop={handleDrop}
onDragOver={handleDragOver}
placeholder={placeholder}
disabled={disabled}
rows={rows}
className={cn(
'min-h-[80px] resize-none text-transparent caret-foreground',
isConnecting && 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words p-3 text-sm'>
{formatDisplayText(value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
</div>
)
}
interface McpDynamicArgsProps {
blockId: string
subBlockId: string
disabled?: boolean
isPreview?: boolean
previewValue?: any
isConnecting?: boolean
}
export function McpDynamicArgs({
@@ -33,16 +275,18 @@ export function McpDynamicArgs({
disabled = false,
isPreview = false,
previewValue,
isConnecting = false,
}: McpDynamicArgsProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const { mcpTools } = useMcpTools(workspaceId)
const { mcpTools, isLoading } = useMcpTools(workspaceId)
const [selectedTool] = useSubBlockValue(blockId, 'tool')
const [cachedSchema] = useSubBlockValue(blockId, '_toolSchema')
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
const toolSchema = selectedToolConfig?.inputSchema
const toolSchema = cachedSchema || selectedToolConfig?.inputSchema
const currentArgs = useCallback(() => {
if (isPreview && previewValue) {
@@ -68,14 +312,13 @@ export function McpDynamicArgs({
}, [toolArgs, previewValue, isPreview])
const updateParameter = useCallback(
(paramName: string, value: any, paramSchema?: any) => {
(paramName: string, value: any) => {
if (disabled) return
const current = currentArgs()
// Store the value as-is, without processing
// Store the value as-is, preserving types (number, boolean, etc.)
const updated = { ...current, [paramName]: value }
const jsonString = JSON.stringify(updated, null, 2)
setToolArgs(jsonString)
setToolArgs(updated)
},
[currentArgs, setToolArgs, disabled]
)
@@ -110,7 +353,7 @@ export function McpDynamicArgs({
<Switch
id={`${paramName}-switch`}
checked={!!value}
onCheckedChange={(checked) => updateParameter(paramName, checked, paramSchema)}
onCheckedChange={(checked) => updateParameter(paramName, checked)}
disabled={disabled}
/>
<Label
@@ -127,9 +370,7 @@ export function McpDynamicArgs({
<div key={`${paramName}-dropdown`}>
<Select
value={value || ''}
onValueChange={(selectedValue) =>
updateParameter(paramName, selectedValue, paramSchema)
}
onValueChange={(selectedValue) => updateParameter(paramName, selectedValue)}
disabled={disabled}
>
<SelectTrigger className='w-full'>
@@ -148,19 +389,23 @@ export function McpDynamicArgs({
</div>
)
case 'slider':
case 'slider': {
const minValue = paramSchema.minimum ?? 0
const maxValue = paramSchema.maximum ?? 100
const currentValue = value ?? minValue
const normalizedPosition = ((currentValue - minValue) / (maxValue - minValue)) * 100
return (
<div key={`${paramName}-slider`} className='relative pt-2 pb-6'>
<Slider
value={[value || paramSchema.minimum || 0]}
min={paramSchema.minimum || 0}
max={paramSchema.maximum || 100}
value={[currentValue]}
min={minValue}
max={maxValue}
step={paramSchema.type === 'integer' ? 1 : 0.1}
onValueChange={(newValue) =>
updateParameter(
paramName,
paramSchema.type === 'integer' ? Math.round(newValue[0]) : newValue[0],
paramSchema
paramSchema.type === 'integer' ? Math.round(newValue[0]) : newValue[0]
)
}
disabled={disabled}
@@ -169,41 +414,37 @@ export function McpDynamicArgs({
<div
className='absolute text-muted-foreground text-sm'
style={{
left: `clamp(0%, ${(((value || paramSchema.minimum || 0) - (paramSchema.minimum || 0)) / ((paramSchema.maximum || 100) - (paramSchema.minimum || 0))) * 100}%, 100%)`,
left: `clamp(0%, ${normalizedPosition}%, 100%)`,
transform: 'translateX(-50%)',
top: '24px',
}}
>
{paramSchema.type === 'integer'
? Math.round(value || paramSchema.minimum || 0).toString()
: Number(value || paramSchema.minimum || 0).toFixed(1)}
? Math.round(currentValue).toString()
: Number(currentValue).toFixed(1)}
</div>
</div>
)
}
case 'long-input':
return (
<div key={`${paramName}-long`} className='relative'>
<Textarea
value={value || ''}
onChange={(e) => updateParameter(paramName, e.target.value, paramSchema)}
placeholder={
paramSchema.type === 'array'
? `Enter JSON array, e.g. ["item1", "item2"] or comma-separated values`
: paramSchema.description ||
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
}
disabled={disabled}
rows={4}
className='min-h-[80px] resize-none text-transparent caret-foreground'
/>
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words p-3 text-sm'>
{formatDisplayText(value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
<McpTextareaWithTags
key={`${paramName}-long`}
value={value || ''}
onChange={(newValue) => updateParameter(paramName, newValue)}
placeholder={
paramSchema.type === 'array'
? `Enter JSON array, e.g. ["item1", "item2"] or comma-separated values`
: paramSchema.description ||
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
}
disabled={disabled}
blockId={blockId}
accessiblePrefixes={accessiblePrefixes}
rows={4}
isConnecting={isConnecting}
/>
)
default: {
@@ -212,48 +453,39 @@ export function McpDynamicArgs({
paramName.toLowerCase().includes('password') ||
paramName.toLowerCase().includes('token')
const isNumeric = paramSchema.type === 'number' || paramSchema.type === 'integer'
const isTextInput = !isPassword && !isNumeric
return (
<div key={`${paramName}-short`} className={isTextInput ? 'relative' : ''}>
<Input
type={isPassword ? 'password' : isNumeric ? 'number' : 'text'}
value={value || ''}
onChange={(e) => {
let processedValue: any = e.target.value
if (isNumeric && processedValue !== '') {
processedValue =
paramSchema.type === 'integer'
? Number.parseInt(processedValue)
: Number.parseFloat(processedValue)
<McpInputWithTags
key={`${paramName}-short`}
value={value?.toString() || ''}
onChange={(newValue) => {
let processedValue: any = newValue
const hasTag = newValue.includes('<') || newValue.includes('>')
if (Number.isNaN(processedValue)) {
processedValue = ''
return
}
if (isNumeric && processedValue !== '' && !hasTag) {
processedValue =
paramSchema.type === 'integer'
? Number.parseInt(processedValue)
: Number.parseFloat(processedValue)
if (Number.isNaN(processedValue)) {
processedValue = ''
}
updateParameter(paramName, processedValue, paramSchema)
}}
placeholder={
paramSchema.type === 'array'
? `Enter JSON array, e.g. ["item1", "item2"] or comma-separated values`
: paramSchema.description ||
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
}
disabled={disabled}
className={isTextInput ? 'text-transparent caret-foreground' : ''}
/>
{isTextInput && (
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>
{formatDisplayText(value?.toString() || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
)}
</div>
updateParameter(paramName, processedValue)
}}
placeholder={
paramSchema.type === 'array'
? `Enter JSON array, e.g. ["item1", "item2"] or comma-separated values`
: paramSchema.description ||
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
}
disabled={disabled}
isPassword={isPassword}
blockId={blockId}
accessiblePrefixes={accessiblePrefixes}
isConnecting={isConnecting}
/>
)
}
}
@@ -267,6 +499,19 @@ export function McpDynamicArgs({
)
}
if (
selectedTool &&
!cachedSchema &&
!selectedToolConfig &&
(isLoading || mcpTools.length === 0)
) {
return (
<div className='rounded-lg border border-dashed p-8 text-center'>
<p className='text-muted-foreground text-sm'>Loading tool schema...</p>
</div>
)
}
if (!toolSchema?.properties || Object.keys(toolSchema.properties).length === 0) {
return (
<div className='rounded-lg border border-dashed p-8 text-center'>
@@ -277,27 +522,28 @@ export function McpDynamicArgs({
return (
<div className='space-y-4'>
{Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any)
const showLabel = inputType !== 'switch' // Switch component includes its own label
{toolSchema.properties &&
Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any)
const showLabel = inputType !== 'switch'
return (
<div key={paramName} className='space-y-2'>
{showLabel && (
<Label
className={cn(
'font-medium text-sm',
toolSchema.required?.includes(paramName) &&
'after:ml-1 after:text-red-500 after:content-["*"]'
)}
>
{formatParameterLabel(paramName)}
</Label>
)}
{renderParameterInput(paramName, paramSchema as any)}
</div>
)
})}
return (
<div key={paramName} className='space-y-2'>
{showLabel && (
<Label
className={cn(
'font-medium text-sm',
toolSchema.required?.includes(paramName) &&
'after:ml-1 after:text-red-500 after:content-["*"]'
)}
>
{formatParameterLabel(paramName)}
</Label>
)}
{renderParameterInput(paramName, paramSchema as any)}
</div>
)
})}
</div>
)
}

View File

@@ -39,6 +39,7 @@ export function McpToolSelector({
const { mcpTools, isLoading, error, refreshTools, getToolsByServer } = useMcpTools(workspaceId)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
const [serverValue] = useSubBlockValue(blockId, 'server')
@@ -82,6 +83,11 @@ export function McpToolSelector({
const handleSelect = (toolId: string) => {
if (!isPreview) {
setStoreValue(toolId)
const tool = availableTools.find((t) => t.id === toolId)
if (tool?.inputSchema) {
setSchemaCache(tool.inputSchema)
}
}
setOpen(false)
}

View File

@@ -13,6 +13,7 @@ interface McpTool {
serverName: string
icon: React.ComponentType<any>
bgColor: string
inputSchema?: any
}
interface StoredTool {
@@ -26,6 +27,7 @@ interface StoredTool {
}
isExpanded: boolean
usageControl: 'auto'
schema?: any
}
interface McpToolsListProps {
@@ -71,6 +73,7 @@ export function McpToolsList({
},
isExpanded: true,
usageControl: 'auto',
schema: mcpTool.inputSchema,
}
onToolSelect(newTool)

View File

@@ -968,21 +968,25 @@ export function ToolInput({
toolIndex?: number,
currentToolParams?: Record<string, string>
) => {
// Create unique blockId for tool parameters to avoid conflicts with main block
const uniqueBlockId = toolIndex !== undefined ? `${blockId}-tool-${toolIndex}` : blockId
// Create unique subBlockId for tool parameters to avoid conflicts
// Use real blockId so tag dropdown and drag-drop work correctly
const uniqueSubBlockId =
toolIndex !== undefined
? `${subBlockId}-tool-${toolIndex}-${param.id}`
: `${subBlockId}-${param.id}`
const uiComponent = param.uiComponent
// If no UI component info, fall back to basic input
if (!uiComponent) {
return (
<ShortInput
blockId={uniqueBlockId}
subBlockId={`${subBlockId}-param`}
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={param.description}
password={isPasswordParameter(param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
}}
@@ -1024,12 +1028,12 @@ export function ToolInput({
case 'long-input':
return (
<LongInput
blockId={uniqueBlockId}
subBlockId={`${subBlockId}-param`}
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
id: uniqueSubBlockId,
type: 'long-input',
title: param.id,
}}
@@ -1041,13 +1045,13 @@ export function ToolInput({
case 'short-input':
return (
<ShortInput
blockId={uniqueBlockId}
subBlockId={`${subBlockId}-param`}
blockId={blockId}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
}}
@@ -1140,8 +1144,8 @@ export function ToolInput({
case 'slider':
return (
<SliderInputSyncWrapper
blockId={uniqueBlockId}
paramId={param.id}
blockId={blockId}
paramId={uniqueSubBlockId}
value={value}
onChange={onChange}
uiComponent={uiComponent}
@@ -1201,12 +1205,12 @@ export function ToolInput({
return (
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-param`}
subBlockId={uniqueSubBlockId}
placeholder={uiComponent.placeholder || param.description}
password={uiComponent.password || isPasswordParameter(param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
id: uniqueSubBlockId,
type: 'short-input',
title: param.id,
}}
@@ -1410,15 +1414,17 @@ export function ToolInput({
: []
// For MCP tools, extract parameters from input schema
// Use cached schema from tool object if available, otherwise fetch from mcpTools
const mcpTool = isMcpTool ? mcpTools.find((t) => t.id === tool.toolId) : null
const mcpToolSchema = isMcpTool ? tool.schema || mcpTool?.inputSchema : null
const mcpToolParams =
isMcpTool && mcpTool?.inputSchema?.properties
? Object.entries(mcpTool.inputSchema.properties || {}).map(
isMcpTool && mcpToolSchema?.properties
? Object.entries(mcpToolSchema.properties || {}).map(
([paramId, param]: [string, any]) => ({
id: paramId,
type: param.type || 'string',
description: param.description || '',
visibility: (mcpTool.inputSchema.required?.includes(paramId)
visibility: (mcpToolSchema.required?.includes(paramId)
? 'user-or-llm'
: 'user-only') as 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden',
})
@@ -1740,13 +1746,13 @@ export function ToolInput({
)
) : (
<ShortInput
blockId={`${blockId}-tool-${toolIndex}`}
subBlockId={`${subBlockId}-param`}
blockId={blockId}
subBlockId={`${subBlockId}-tool-${toolIndex}-${param.id}`}
placeholder={param.description}
password={isPasswordParameter(param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
id: `${subBlockId}-tool-${toolIndex}-${param.id}`,
type: 'short-input',
title: param.id,
}}

View File

@@ -522,6 +522,7 @@ export const SubBlock = memo(
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue}
isConnecting={isConnecting}
/>
)
default:

View File

@@ -433,8 +433,25 @@ export const WorkflowBlock = memo(
)
)
// Memoized SubBlock layout management - only recalculate when dependencies change
const subBlockRows = useMemo(() => {
const getSubBlockStableKey = useCallback(
(subBlock: SubBlockConfig, stateToUse: Record<string, any>): string => {
if (subBlock.type === 'mcp-dynamic-args') {
const serverValue = stateToUse.server?.value || 'no-server'
const toolValue = stateToUse.tool?.value || 'no-tool'
return `${id}-${subBlock.id}-${serverValue}-${toolValue}`
}
if (subBlock.type === 'mcp-tool-selector') {
const serverValue = stateToUse.server?.value || 'no-server'
return `${id}-${subBlock.id}-${serverValue}`
}
return `${id}-${subBlock.id}`
},
[id]
)
const subBlockRowsData = useMemo(() => {
const rows: SubBlockConfig[][] = []
let currentRow: SubBlockConfig[] = []
let currentRowWidth = 0
@@ -542,7 +559,8 @@ export const WorkflowBlock = memo(
rows.push(currentRow)
}
return rows
// Return both rows and state for stable key generation
return { rows, stateToUse }
}, [
config.subBlocks,
id,
@@ -556,6 +574,10 @@ export const WorkflowBlock = memo(
activeWorkflowId,
])
// Extract rows and state from the memoized value
const subBlockRows = subBlockRowsData.rows
const subBlockState = subBlockRowsData.stateToUse
// Name editing handlers
const handleNameClick = (e: React.MouseEvent) => {
e.stopPropagation() // Prevent drag handler from interfering
@@ -1110,35 +1132,42 @@ export const WorkflowBlock = memo(
>
{subBlockRows.map((row, rowIndex) => (
<div key={`row-${rowIndex}`} className='flex gap-4'>
{row.map((subBlock, blockIndex) => (
<div
key={`${id}-${rowIndex}-${blockIndex}`}
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
>
<SubBlock
blockId={id}
config={subBlock}
isConnecting={isConnecting}
isPreview={data.isPreview || currentWorkflow.isDiffMode}
subBlockValues={
data.subBlockValues ||
(currentWorkflow.isDiffMode && currentBlock
? (currentBlock as any).subBlocks
: undefined)
}
disabled={!userPermissions.canEdit}
fieldDiffStatus={
fieldDiff?.changed_fields?.includes(subBlock.id)
? 'changed'
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
? 'unchanged'
: undefined
}
allowExpandInPreview={currentWorkflow.isDiffMode}
isWide={displayIsWide}
/>
</div>
))}
{row.map((subBlock) => {
const stableKey = getSubBlockStableKey(subBlock, subBlockState)
return (
<div
key={stableKey}
className={cn(
'space-y-1',
subBlock.layout === 'half' ? 'flex-1' : 'w-full'
)}
>
<SubBlock
blockId={id}
config={subBlock}
isConnecting={isConnecting}
isPreview={data.isPreview || currentWorkflow.isDiffMode}
subBlockValues={
data.subBlockValues ||
(currentWorkflow.isDiffMode && currentBlock
? (currentBlock as any).subBlocks
: undefined)
}
disabled={!userPermissions.canEdit}
fieldDiffStatus={
fieldDiff?.changed_fields?.includes(subBlock.id)
? 'changed'
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
? 'unchanged'
: undefined
}
allowExpandInPreview={currentWorkflow.isDiffMode}
isWide={displayIsWide}
/>
</div>
)
})}
</div>
))}
</div>

View File

@@ -241,7 +241,7 @@ export class AgentBlockHandler implements BlockHandler {
}
private async createMcpTool(tool: ToolInput, context: ExecutionContext): Promise<any> {
const { serverId, toolName, params } = tool.params || {}
const { serverId, toolName, ...userProvidedParams } = tool.params || {}
if (!serverId || !toolName) {
logger.error('MCP tool missing required parameters:', { serverId, toolName })
@@ -294,11 +294,18 @@ export class AgentBlockHandler implements BlockHandler {
const toolId = createMcpToolId(serverId, toolName)
const { filterSchemaForLLM } = await import('@/tools/params')
const filteredSchema = filterSchemaForLLM(
mcpTool.inputSchema || { type: 'object', properties: {} },
userProvidedParams
)
return {
id: toolId,
name: toolName,
description: mcpTool.description || `MCP tool ${toolName} from ${mcpTool.serverName}`,
parameters: mcpTool.inputSchema || { type: 'object', properties: {} },
parameters: filteredSchema,
params: userProvidedParams,
usageControl: tool.usageControl || 'auto',
executeFunction: async (callParams: Record<string, any>) => {
logger.info(`Executing MCP tool ${toolName} on server ${serverId}`)
@@ -321,7 +328,7 @@ export class AgentBlockHandler implements BlockHandler {
body: JSON.stringify({
serverId,
toolName,
arguments: { ...params, ...callParams },
arguments: callParams,
workspaceId: context.workspaceId,
workflowId: context.workflowId,
}),

View File

@@ -991,10 +991,10 @@ export function prepareToolExecution(
toolParams: Record<string, any>
executionParams: Record<string, any>
} {
// Only merge actual tool parameters for logging
// User-provided params take precedence over LLM-generated params
const toolParams = {
...tool.params,
...llmArgs,
...tool.params,
}
// Add system parameters for execution