fix(mcp): fix caret misalignment and tool schema contract validation (#5566)

* fix(mcp): fix caret misalignment in Add MCP Server modal fields

The Server URL and Header fields render a transparent input under a
formatted overlay div for env-var highlighting. The overlay used
font-medium/font-sans but the real input didn't, so glyph widths
diverged and the native caret drifted from the visible text as you
typed.

* fix(mcp): loosen tool schema contract to accept valid JSON Schema shapes

discoverMcpToolsContract's property schema rejected legal JSON Schema
that real MCP servers can return: array-form `items` (tuple
validation) and non-primitive `enum` values. Any server exercising
either shape failed contract validation client-side and blanked the
entire MCP tools list.

* fix(mcp): only render dropdown UI for primitive-valued enums

The MCP dynamic-args dropdown stringifies enum members for its
labels/values. Now that the tool schema contract accepts
non-primitive enum members (object/array), routing those through the
dropdown would collapse distinct values to "[object Object]" and
submit that string as the tool argument. Gate the dropdown on
primitive-only enums; non-primitive enums fall through to the
existing type-based branching (the JSON long-input editor for
object/array types), which round-trips arbitrary JSON correctly.

* fix(mcp): route non-primitive enums to the JSON editor regardless of type

isPrimitiveEnum() correctly excluded object/array enum members from
the dropdown, but the fallback only reached the long-input JSON
editor when paramSchema.type was 'array'. An object-typed (or
untyped) param with a non-primitive enum fell through to the default
short-input, which stringifies via toString() and drops the
enum-membership guarantee entirely. Any non-primitive enum now routes
straight to long-input, independent of the declared type.

* chore(mcp): fold inline comment into the existing TSDoc block

* fix(mcp): serialize non-string values before displaying in the long-input editor

The long-input JSON editor received value={value || ''} unconditionally,
so an argument already holding a parsed object/array (loaded from the
block's JSON arguments field) rendered as "[object Object]" or a
comma-joined list instead of valid JSON, and saving would overwrite the
real value with that mangled text. Serialize non-string values with
JSON.stringify before display; onChange still stores the raw text the
user edits, unchanged.

* fix(mcp): parse JSON-typed long-input edits back into real values

The long-input editor's onChange always stored the raw typed text, so
a param whose schema requires an object/array/non-primitive-enum value
(e.g. entering {"mode":"strict"}) was persisted as a string, not the
actual JSON value — the MCP tool call could receive the wrong type.
requiresJsonValue() identifies these schemas; onChange now parses the
edited text back into the real value once it's valid JSON, falling
back to the raw string mid-edit so the controlled textarea keeps
reflecting in-progress keystrokes.
This commit is contained in:
Waleed
2026-07-10 12:54:19 -07:00
committed by GitHub
parent 4952ddb73f
commit f0d85cb7ab
4 changed files with 51 additions and 8 deletions
@@ -157,7 +157,7 @@ function FormattedInput({
onChange={onChange}
onScroll={handleScroll}
onInput={handleScroll}
inputClassName='text-transparent caret-[var(--text-primary)]'
inputClassName='font-medium font-sans text-transparent caret-[var(--text-primary)]'
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-2 py-1.5 font-medium font-sans text-sm'>
<div className='whitespace-nowrap' style={{ transform: `translateX(-${scrollLeft}px)` }}>
@@ -15,6 +15,33 @@ import { formatParameterLabel } from '@/tools/params'
const logger = createLogger('McpDynamicArgs')
/**
* The dropdown UI renders each enum member as a string label/value, so it can only
* represent JSON Schema enums whose members are primitives — a non-primitive member
* (object/array) would collapse to "[object Object]" and lose its identity. Callers
* route a non-primitive enum to the JSON editor (`long-input`) instead.
*/
function isPrimitiveEnum(
enumValues: unknown
): enumValues is Array<string | number | boolean | null> {
return (
Array.isArray(enumValues) &&
enumValues.every((value) => value === null || typeof value !== 'object')
)
}
/**
* True when the schema's actual value must be a JSON object/array (a plain
* object/array type, or a non-primitive enum member) rather than a string.
*/
function requiresJsonValue(paramSchema: any): boolean {
return (
paramSchema.type === 'object' ||
paramSchema.type === 'array' ||
(Array.isArray(paramSchema.enum) && !isPrimitiveEnum(paramSchema.enum))
)
}
interface McpDynamicArgsProps {
blockId: string
subBlockId: string
@@ -116,7 +143,9 @@ export function McpDynamicArgs({
)
const getInputType = (paramSchema: any) => {
if (paramSchema.enum) return 'dropdown'
if (Array.isArray(paramSchema.enum)) {
return isPrimitiveEnum(paramSchema.enum) ? 'dropdown' : 'long-input'
}
if (paramSchema.type === 'boolean') return 'switch'
if (paramSchema.type === 'number' || paramSchema.type === 'integer') {
if (paramSchema.minimum !== undefined && paramSchema.maximum !== undefined) {
@@ -241,6 +270,8 @@ export function McpDynamicArgs({
case 'long-input': {
const config = createParamConfig(paramName, paramSchema, 'long-input')
const displayValue =
typeof value === 'string' || value == null ? value || '' : JSON.stringify(value)
return (
<LongInput
key={`${paramName}-long`}
@@ -249,8 +280,18 @@ export function McpDynamicArgs({
config={config}
placeholder={config.placeholder}
rows={4}
value={value || ''}
onChange={(newValue) => updateParameter(paramName, newValue)}
value={displayValue}
onChange={(newValue) => {
if (!requiresJsonValue(paramSchema)) {
updateParameter(paramName, newValue)
return
}
try {
updateParameter(paramName, JSON.parse(newValue))
} catch {
updateParameter(paramName, newValue)
}
}}
isPreview={isPreview}
disabled={disabled}
workflowSearchValuePath={[paramName]}
+4 -2
View File
@@ -50,10 +50,12 @@ export const mcpToolSchemaPropertySchema: z.ZodType<McpToolSchemaProperty> = z.l
.object({
type: z.union([z.string(), z.array(z.string())]).optional(),
description: z.string().optional(),
items: mcpToolSchemaPropertySchema.optional(),
items: z
.union([mcpToolSchemaPropertySchema, z.array(mcpToolSchemaPropertySchema)])
.optional(),
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
required: z.array(z.string()).optional(),
enum: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
enum: z.array(z.unknown()).optional(),
default: z.unknown().optional(),
})
.passthrough()
+2 -2
View File
@@ -68,10 +68,10 @@ export interface McpSecurityPolicy {
export interface McpToolSchemaProperty {
type?: string | string[]
description?: string
items?: McpToolSchemaProperty
items?: McpToolSchemaProperty | McpToolSchemaProperty[]
properties?: Record<string, McpToolSchemaProperty>
required?: string[]
enum?: Array<string | number | boolean | null>
enum?: unknown[]
default?: unknown
[key: string]: unknown
}