From 6dbaf6e9c11fa5307eff4c3db8ce5ea58c1a000e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:14:26 -0700 Subject: [PATCH 1/7] 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. --- .../components/mcp-server-form-modal/mcp-server-form-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c93dd8fa01..fe6d59e0967 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)]' />
From 4749825f7b7f750c6f86d2ef51b11a4303e42ade Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:14:31 -0700 Subject: [PATCH 2/7] 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. --- apps/sim/lib/api/contracts/mcp.ts | 6 ++++-- apps/sim/lib/mcp/types.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/api/contracts/mcp.ts b/apps/sim/lib/api/contracts/mcp.ts index dfcddac85ad..1c0159e9ec2 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 be506fd5b07..575e86ea2f8 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 } From 54f7326e7e6174db89463667e1bd48e5851d993e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:21:22 -0700 Subject: [PATCH 3/7] 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. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 5fa807b3f73..d5ae57cd3ca 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,20 @@ 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. + */ +function isPrimitiveEnum( + enumValues: unknown +): enumValues is Array { + return ( + Array.isArray(enumValues) && + enumValues.every((value) => value === null || typeof value !== 'object') + ) +} + interface McpDynamicArgsProps { blockId: string subBlockId: string @@ -116,7 +130,7 @@ export function McpDynamicArgs({ ) const getInputType = (paramSchema: any) => { - if (paramSchema.enum) return 'dropdown' + if (isPrimitiveEnum(paramSchema.enum)) return 'dropdown' if (paramSchema.type === 'boolean') return 'switch' if (paramSchema.type === 'number' || paramSchema.type === 'integer') { if (paramSchema.minimum !== undefined && paramSchema.maximum !== undefined) { From 5154960d7f5e05ab3e1c795fc7da0646f38e71b4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:24:58 -0700 Subject: [PATCH 4/7] 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. --- .../components/mcp-dynamic-args/mcp-dynamic-args.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d5ae57cd3ca..e5d4702e413 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 @@ -130,7 +130,12 @@ export function McpDynamicArgs({ ) const getInputType = (paramSchema: any) => { - if (isPrimitiveEnum(paramSchema.enum)) return 'dropdown' + if (Array.isArray(paramSchema.enum)) { + // A non-primitive enum member (object/array) can't be represented by the + // dropdown's string label/value model — fall back to the JSON editor so the + // user can enter (and round-trip) one of the allowed JSON values verbatim. + 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) { From 5fa3f68cf1c886ae9c5d11705934a5c11b7a2ec9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:28:13 -0700 Subject: [PATCH 5/7] chore(mcp): fold inline comment into the existing TSDoc block --- .../components/mcp-dynamic-args/mcp-dynamic-args.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 e5d4702e413..e2d6d8009d4 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 @@ -18,7 +18,8 @@ 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. + * (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 @@ -131,9 +132,6 @@ export function McpDynamicArgs({ const getInputType = (paramSchema: any) => { if (Array.isArray(paramSchema.enum)) { - // A non-primitive enum member (object/array) can't be represented by the - // dropdown's string label/value model — fall back to the JSON editor so the - // user can enter (and round-trip) one of the allowed JSON values verbatim. return isPrimitiveEnum(paramSchema.enum) ? 'dropdown' : 'long-input' } if (paramSchema.type === 'boolean') return 'switch' From 8822d45f7200c32c3c7220ba73bb9c83c314fc14 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:34:32 -0700 Subject: [PATCH 6/7] 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. --- .../components/mcp-dynamic-args/mcp-dynamic-args.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e2d6d8009d4..c59257dfa84 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 @@ -258,6 +258,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)} isPreview={isPreview} disabled={disabled} From a6d48f87d1dee66546a5f8fe3a5bc86a6c632d48 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 12:42:19 -0700 Subject: [PATCH 7/7] fix(mcp): parse JSON-typed long-input edits back into real values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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-dynamic-args/mcp-dynamic-args.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 c59257dfa84..3994248ef6d 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 @@ -30,6 +30,18 @@ function isPrimitiveEnum( ) } +/** + * 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 @@ -269,7 +281,17 @@ export function McpDynamicArgs({ placeholder={config.placeholder} rows={4} value={displayValue} - onChange={(newValue) => updateParameter(paramName, newValue)} + 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]}