From f0d85cb7abc354e1e574bcfc7912bcbffb76ff75 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 10 Jul 2026 12:54:19 -0700 Subject: [PATCH] fix(mcp): fix caret misalignment and tool schema contract validation (#5566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../mcp-server-form-modal.tsx | 2 +- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 47 +++++++++++++++++-- apps/sim/lib/api/contracts/mcp.ts | 6 ++- apps/sim/lib/mcp/types.ts | 4 +- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx index 9c93dd8fa0..fe6d59e096 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx @@ -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)]' />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx index 5fa807b3f7..3994248ef6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx @@ -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 { + 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 ( 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]} diff --git a/apps/sim/lib/api/contracts/mcp.ts b/apps/sim/lib/api/contracts/mcp.ts index dfcddac85a..1c0159e9ec 100644 --- a/apps/sim/lib/api/contracts/mcp.ts +++ b/apps/sim/lib/api/contracts/mcp.ts @@ -50,10 +50,12 @@ export const mcpToolSchemaPropertySchema: z.ZodType = 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() diff --git a/apps/sim/lib/mcp/types.ts b/apps/sim/lib/mcp/types.ts index be506fd5b0..575e86ea2f 100644 --- a/apps/sim/lib/mcp/types.ts +++ b/apps/sim/lib/mcp/types.ts @@ -68,10 +68,10 @@ export interface McpSecurityPolicy { export interface McpToolSchemaProperty { type?: string | string[] description?: string - items?: McpToolSchemaProperty + items?: McpToolSchemaProperty | McpToolSchemaProperty[] properties?: Record required?: string[] - enum?: Array + enum?: unknown[] default?: unknown [key: string]: unknown }