mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
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:
@@ -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"])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -522,6 +522,7 @@ export const SubBlock = memo(
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
isConnecting={isConnecting}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user