mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 21:15:56 +08:00
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:
+1
-1
@@ -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)` }}>
|
||||
|
||||
+44
-3
@@ -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]}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user