From 09ab34ba31e877f3c1508ed88c084d87452fabbf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 13:30:19 -0700 Subject: [PATCH 1/8] fix(mcp): route object-typed params to JSON editor, keep invalid drafts out of tool args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up gaps from the earlier MCP schema fix: - getInputType only routed array-typed and non-primitive-enum params to the long-input JSON editor; a plain object-typed param (no enum) fell through to the default short-input, which stores raw text via toString() and never round-trips a real object. - The long-input onChange fell back to storing the raw typed text whenever JSON.parse failed (needed to keep the controlled textarea responsive mid-edit), but that meant an incomplete/invalid edit (e.g. `{"a":1` before the closing brace) could persist into the actual tool arguments. If executed in that state, the MCP execute route's array-coercion step would silently wrap the malformed string into a corrupted array instead of failing validation. Invalid-edit text now lives in local `invalidJsonDrafts` state, keyed by param name, instead of the real argument store — the textarea still reflects every keystroke, but the persisted tool argument is always either the last successfully parsed value or untouched. Drafts reset when the selected tool changes. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 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 3994248ef6d..156511103bb 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 @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { Combobox, FieldDivider, Label, Slider, Switch } from '@sim/emcn' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' @@ -94,6 +94,19 @@ export function McpDynamicArgs({ : schemaFromStore const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId) + /** + * Draft text for JSON-value params (object/array/non-primitive-enum) whose current + * edit isn't valid JSON yet. Keeping this out of toolArgs means the stored argument + * is always either the last valid parsed value or untouched — never malformed text + * that could reach tool execution. + */ + const [invalidJsonDrafts, setInvalidJsonDrafts] = useState>({}) + const [prevSelectedTool, setPrevSelectedTool] = useState(selectedTool) + if (prevSelectedTool !== selectedTool) { + setPrevSelectedTool(selectedTool) + setInvalidJsonDrafts({}) + } + const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool) const toolSchema = cachedSchema || selectedToolConfig?.inputSchema @@ -158,7 +171,7 @@ export function McpDynamicArgs({ if (paramSchema.maxLength && paramSchema.maxLength > 100) return 'long-input' return 'short-input' } - if (paramSchema.type === 'array') return 'long-input' + if (paramSchema.type === 'array' || paramSchema.type === 'object') return 'long-input' return 'short-input' } @@ -270,8 +283,14 @@ export function McpDynamicArgs({ case 'long-input': { const config = createParamConfig(paramName, paramSchema, 'long-input') + const needsJsonValue = requiresJsonValue(paramSchema) + const draft = invalidJsonDrafts[paramName] const displayValue = - typeof value === 'string' || value == null ? value || '' : JSON.stringify(value) + needsJsonValue && draft !== undefined + ? draft + : typeof value === 'string' || value == null + ? value || '' + : JSON.stringify(value) return ( { - if (!requiresJsonValue(paramSchema)) { + if (!needsJsonValue) { updateParameter(paramName, newValue) return } + const clearDraft = () => + setInvalidJsonDrafts((prev) => { + if (!(paramName in prev)) return prev + const { [paramName]: _removed, ...rest } = prev + return rest + }) + if (newValue === '') { + updateParameter(paramName, '') + clearDraft() + return + } try { updateParameter(paramName, JSON.parse(newValue)) + clearDraft() } catch { - updateParameter(paramName, newValue) + setInvalidJsonDrafts((prev) => ({ ...prev, [paramName]: newValue })) } }} isPreview={isPreview} From 0f7b3a530cb94fff0a72696d5b5d3ccf624331d5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 13:38:09 -0700 Subject: [PATCH 2/8] fix(mcp): reset invalid JSON drafts on schema change, not just tool change The draft reset only fired when the selected tool id changed, so a same-tool schema refresh (e.g. re-discovering tools from the live MCP server) could leave a stale invalid draft displayed under a param name whose shape had since changed. Key the reset off both the tool id and a signature of the effective schema's properties, so any change to what's actually being edited clears stale drafts. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 156511103bb..27aa371fd51 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 @@ -94,22 +94,25 @@ export function McpDynamicArgs({ : schemaFromStore const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId) + const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool) + const toolSchema = cachedSchema || selectedToolConfig?.inputSchema + /** * Draft text for JSON-value params (object/array/non-primitive-enum) whose current * edit isn't valid JSON yet. Keeping this out of toolArgs means the stored argument * is always either the last valid parsed value or untouched — never malformed text - * that could reach tool execution. + * that could reach tool execution. Drafts reset whenever the selected tool or its + * effective schema changes, so a stale draft can't outlive the param shape it was + * typed against. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState>({}) - const [prevSelectedTool, setPrevSelectedTool] = useState(selectedTool) - if (prevSelectedTool !== selectedTool) { - setPrevSelectedTool(selectedTool) + const draftResetKey = `${selectedTool ?? ''}|${toolSchema?.properties ? JSON.stringify(toolSchema.properties) : ''}` + const [prevDraftResetKey, setPrevDraftResetKey] = useState(draftResetKey) + if (prevDraftResetKey !== draftResetKey) { + setPrevDraftResetKey(draftResetKey) setInvalidJsonDrafts({}) } - const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool) - const toolSchema = cachedSchema || selectedToolConfig?.inputSchema - const currentArgs = useCallback(() => { if (isPreview && previewValue) { if (typeof previewValue === 'string') { From fda94bc76bc865d2452f84e0abba4b533130059e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 13:45:55 -0700 Subject: [PATCH 3/8] fix(mcp): key draft reset off both schema sources, not the resolved toolSchema toolSchema resolves to cachedSchema || selectedToolConfig?.inputSchema, so a live-only schema refresh (the discovered tool's inputSchema changes but the cached _toolSchema snapshot doesn't) left the reset key unchanged and could keep a stale invalid draft on screen. Track a signature of each schema source independently so a change in either one clears drafts, regardless of which source toolSchema resolves to. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 18 ++++++++++++++---- 1 file changed, 14 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 27aa371fd51..174cc012c7b 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 @@ -42,6 +42,15 @@ function requiresJsonValue(paramSchema: any): boolean { ) } +/** + * Stable signature of a tool schema's properties, for detecting whether the + * effective param shape has changed (independent of object identity). + */ +function schemaSignature(schema: unknown): string { + const properties = (schema as { properties?: unknown } | undefined)?.properties + return properties ? JSON.stringify(properties) : '' +} + interface McpDynamicArgsProps { blockId: string subBlockId: string @@ -101,12 +110,13 @@ export function McpDynamicArgs({ * Draft text for JSON-value params (object/array/non-primitive-enum) whose current * edit isn't valid JSON yet. Keeping this out of toolArgs means the stored argument * is always either the last valid parsed value or untouched — never malformed text - * that could reach tool execution. Drafts reset whenever the selected tool or its - * effective schema changes, so a stale draft can't outlive the param shape it was - * typed against. + * that could reach tool execution. Drafts reset whenever the selected tool or either + * schema source changes — `toolSchema` prefers the cached `_toolSchema` snapshot over + * the live discovered schema, so the reset key tracks both independently rather than + * the resolved `toolSchema`, which wouldn't change on a live-only refresh. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState>({}) - const draftResetKey = `${selectedTool ?? ''}|${toolSchema?.properties ? JSON.stringify(toolSchema.properties) : ''}` + const draftResetKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}|${schemaSignature(selectedToolConfig?.inputSchema)}` const [prevDraftResetKey, setPrevDraftResetKey] = useState(draftResetKey) if (prevDraftResetKey !== draftResetKey) { setPrevDraftResetKey(draftResetKey) From 5a0466d42596000761e707b81ae7e296b8ad2da3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 13:54:30 -0700 Subject: [PATCH 4/8] fix(mcp): sign whole schema for draft reset; invalidate drafts on external value changes Two more follow-up gaps: - schemaSignature only serialized schema.properties, so a same-tool refresh that changed only top-level fields like `required` (properties byte-identical) left the reset key unchanged. Sign the entire schema instead of cherry-picking fields, so nothing schema-level can be missed. - A draft only reset on tool/schema change, so if the persisted argument changed for any other reason (undo/redo, a diff baseline switch, a collaborator's concurrent edit), the draft could keep shadowing the now-current value in the editor while execution used the real one. Drafts now carry a baseline signature of the value they were typed against; a draft only displays while that baseline still matches the live persisted value, so any external change makes it fall back to showing the real value instead of stale text. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 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 174cc012c7b..b1388cb19d0 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 @@ -43,12 +43,13 @@ function requiresJsonValue(paramSchema: any): boolean { } /** - * Stable signature of a tool schema's properties, for detecting whether the - * effective param shape has changed (independent of object identity). + * Stable signature of an entire tool schema, for detecting whether the effective + * param shape has changed (independent of object identity). Signs the whole schema + * rather than cherry-picking fields (e.g. just `properties`) so a refresh that only + * changes `required`, or any other schema-level field, isn't silently missed. */ function schemaSignature(schema: unknown): string { - const properties = (schema as { properties?: unknown } | undefined)?.properties - return properties ? JSON.stringify(properties) : '' + return schema ? JSON.stringify(schema) : '' } interface McpDynamicArgsProps { @@ -108,14 +109,20 @@ export function McpDynamicArgs({ /** * Draft text for JSON-value params (object/array/non-primitive-enum) whose current - * edit isn't valid JSON yet. Keeping this out of toolArgs means the stored argument - * is always either the last valid parsed value or untouched — never malformed text - * that could reach tool execution. Drafts reset whenever the selected tool or either - * schema source changes — `toolSchema` prefers the cached `_toolSchema` snapshot over - * the live discovered schema, so the reset key tracks both independently rather than - * the resolved `toolSchema`, which wouldn't change on a live-only refresh. + * edit isn't valid JSON yet, paired with a signature of the persisted value it was + * typed against. Keeping this out of toolArgs means the stored argument is always + * either the last valid parsed value or untouched — never malformed text that could + * reach tool execution. A draft is only displayed while its baseline still matches + * the live persisted value, so an external change to that value (undo/redo, a diff + * baseline switch, a collaborator's edit) can't be shadowed by stale draft text. + * Drafts also reset wholesale whenever the selected tool or either schema source + * changes — `toolSchema` prefers the cached `_toolSchema` snapshot over the live + * discovered schema, so the reset key tracks both independently rather than the + * resolved `toolSchema`, which wouldn't change on a live-only refresh. */ - const [invalidJsonDrafts, setInvalidJsonDrafts] = useState>({}) + const [invalidJsonDrafts, setInvalidJsonDrafts] = useState< + Record + >({}) const draftResetKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}|${schemaSignature(selectedToolConfig?.inputSchema)}` const [prevDraftResetKey, setPrevDraftResetKey] = useState(draftResetKey) if (prevDraftResetKey !== draftResetKey) { @@ -297,10 +304,13 @@ export function McpDynamicArgs({ case 'long-input': { const config = createParamConfig(paramName, paramSchema, 'long-input') const needsJsonValue = requiresJsonValue(paramSchema) + const valueSignature = JSON.stringify(value ?? null) const draft = invalidJsonDrafts[paramName] + const activeDraft = + needsJsonValue && draft && draft.baseline === valueSignature ? draft.text : undefined const displayValue = - needsJsonValue && draft !== undefined - ? draft + activeDraft !== undefined + ? activeDraft : typeof value === 'string' || value == null ? value || '' : JSON.stringify(value) @@ -333,7 +343,10 @@ export function McpDynamicArgs({ updateParameter(paramName, JSON.parse(newValue)) clearDraft() } catch { - setInvalidJsonDrafts((prev) => ({ ...prev, [paramName]: newValue })) + setInvalidJsonDrafts((prev) => ({ + ...prev, + [paramName]: { text: newValue, baseline: valueSignature }, + })) } }} isPreview={isPreview} From 261f296d8d7b45c59cd8b8cff4169aefa0e58c11 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 14:01:38 -0700 Subject: [PATCH 5/8] fix(mcp): restore comma-separated array input; stop spurious draft reset on tool load Two more follow-up gaps: - Holding every JSON.parse failure in a local draft blocked the documented comma-separated array shorthand (see the placeholder text) from ever reaching toolArgs, since plain comma-separated text is never valid JSON. Only an in-progress JSON array/object literal (starting with `[` or `{`) needs to stay in the draft until valid; plain array-typed text that isn't attempting JSON persists immediately as before, letting the execute route's existing comma-split/wrap coercion handle it as designed. - draftResetKey always included the live selectedToolConfig schema signature, even when cachedSchema wins the `toolSchema` resolution. That segment flips from empty to populated the moment mcpTools finishes an unrelated async load, wiping in-progress drafts though neither the rendered schema nor the stored args changed. The live signature now only factors into the key when there's no cached snapshot for toolSchema to prefer. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 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 b1388cb19d0..5880f29690d 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 @@ -52,6 +52,17 @@ function schemaSignature(schema: unknown): string { return schema ? JSON.stringify(schema) : '' } +/** + * True when text looks like an attempted JSON array/object literal (starts with `[` + * or `{`), as opposed to plain freeform text. Used to tell an in-progress, incomplete + * JSON literal (which must not persist until valid — see `requiresJsonValue`) apart + * from the comma-separated/plain-text shorthand array params also accept. + */ +function looksLikeJsonLiteral(text: string): boolean { + const trimmed = text.trim() + return trimmed.startsWith('[') || trimmed.startsWith('{') +} + interface McpDynamicArgsProps { blockId: string subBlockId: string @@ -115,15 +126,16 @@ export function McpDynamicArgs({ * reach tool execution. A draft is only displayed while its baseline still matches * the live persisted value, so an external change to that value (undo/redo, a diff * baseline switch, a collaborator's edit) can't be shadowed by stale draft text. - * Drafts also reset wholesale whenever the selected tool or either schema source - * changes — `toolSchema` prefers the cached `_toolSchema` snapshot over the live - * discovered schema, so the reset key tracks both independently rather than the - * resolved `toolSchema`, which wouldn't change on a live-only refresh. + * Drafts also reset wholesale whenever the selected tool or the schema actually + * backing `toolSchema` changes. The live discovered schema only factors in when + * there's no cached snapshot to prefer — otherwise it's not what's rendered, so + * live schema arriving/changing while a cached snapshot is already in effect + * (e.g. `mcpTools` finishing an async load) must not spuriously reset drafts. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState< Record >({}) - const draftResetKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}|${schemaSignature(selectedToolConfig?.inputSchema)}` + const draftResetKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}|${cachedSchema ? '' : schemaSignature(selectedToolConfig?.inputSchema)}` const [prevDraftResetKey, setPrevDraftResetKey] = useState(draftResetKey) if (prevDraftResetKey !== draftResetKey) { setPrevDraftResetKey(draftResetKey) @@ -343,6 +355,11 @@ export function McpDynamicArgs({ updateParameter(paramName, JSON.parse(newValue)) clearDraft() } catch { + if (paramSchema.type === 'array' && !looksLikeJsonLiteral(newValue)) { + updateParameter(paramName, newValue) + clearDraft() + return + } setInvalidJsonDrafts((prev) => ({ ...prev, [paramName]: { text: newValue, baseline: valueSignature }, From 72bdb1f4bf4ac6fb8a033c9e0c86b04c8b75a71a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 14:09:49 -0700 Subject: [PATCH 6/8] fix(mcp): reset drafts on genuine live schema refresh, not just its first load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Excluding the live schema signature whenever a cached snapshot exists (the prior fix for a Cursor finding about mcpTools' initial load spuriously wiping drafts) went too far the other way: a genuine same-tool live schema refresh while a cached snapshot is still present would no longer reset drafts either. Track the live schema signature unconditionally, but only treat a change as a real reset trigger when it goes from one non-empty signature to a *different* non-empty one. The bare empty → non-empty transition (mcpTools completing its initial fetch) is excluded, since that's not a schema change; a populated → differently-populated transition (an actual re-discovery) still resets drafts regardless of whether a cached snapshot is present. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 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 5880f29690d..65c959096e5 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 @@ -126,20 +126,37 @@ export function McpDynamicArgs({ * reach tool execution. A draft is only displayed while its baseline still matches * the live persisted value, so an external change to that value (undo/redo, a diff * baseline switch, a collaborator's edit) can't be shadowed by stale draft text. - * Drafts also reset wholesale whenever the selected tool or the schema actually - * backing `toolSchema` changes. The live discovered schema only factors in when - * there's no cached snapshot to prefer — otherwise it's not what's rendered, so - * live schema arriving/changing while a cached snapshot is already in effect - * (e.g. `mcpTools` finishing an async load) must not spuriously reset drafts. + * Drafts also reset wholesale on either of two independent triggers: + * - the selected tool or the cached `_toolSchema` snapshot changes (this pair + * always drives `toolSchema` whenever a cached snapshot exists), or + * - the live discovered schema's signature changes from one non-empty value to + * a *different* non-empty value — a genuine re-discovery/refresh. A bare empty + * → non-empty transition is excluded because that's just `mcpTools` finishing + * its initial async load, not a schema change, and would otherwise spuriously + * wipe drafts even while a cached snapshot (unaffected by the load) still + * governs what's actually rendered. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState< Record >({}) - const draftResetKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}|${cachedSchema ? '' : schemaSignature(selectedToolConfig?.inputSchema)}` - const [prevDraftResetKey, setPrevDraftResetKey] = useState(draftResetKey) - if (prevDraftResetKey !== draftResetKey) { - setPrevDraftResetKey(draftResetKey) - setInvalidJsonDrafts({}) + const toolAndCachedSchemaKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}` + const liveSchemaSignature = schemaSignature(selectedToolConfig?.inputSchema) + const [prevToolAndCachedSchemaKey, setPrevToolAndCachedSchemaKey] = + useState(toolAndCachedSchemaKey) + const [prevLiveSchemaSignature, setPrevLiveSchemaSignature] = useState(liveSchemaSignature) + if ( + prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey || + prevLiveSchemaSignature !== liveSchemaSignature + ) { + const isGenuineLiveRefresh = + prevLiveSchemaSignature !== '' && + liveSchemaSignature !== '' && + prevLiveSchemaSignature !== liveSchemaSignature + if (prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey || isGenuineLiveRefresh) { + setInvalidJsonDrafts({}) + } + setPrevToolAndCachedSchemaKey(toolAndCachedSchemaKey) + setPrevLiveSchemaSignature(liveSchemaSignature) } const currentArgs = useCallback(() => { From 16e8021eab68e554843b681b3f85d6495dc87575 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 14:16:43 -0700 Subject: [PATCH 7/8] fix(mcp): compare live schema against last-known-non-empty, not prior render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparing the live schema signature only against the immediately preceding render's value meant a schema that dropped to empty and then reappeared with different content was invisible to the reset check — both the drop (X → '') and the reappearance ('' → Y) look like a bare empty/non-empty transition, which was deliberately excluded to avoid resetting on mcpTools' initial load. Track the last non-empty value actually observed instead, so a transient empty gap no longer erases the baseline: the schema reset now fires correctly when the tool reappears with a genuinely different schema, while a real first-ever load (no prior non-empty value at all) still doesn't spuriously reset. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 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 65c959096e5..c8e5ead7485 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 @@ -129,12 +129,14 @@ export function McpDynamicArgs({ * Drafts also reset wholesale on either of two independent triggers: * - the selected tool or the cached `_toolSchema` snapshot changes (this pair * always drives `toolSchema` whenever a cached snapshot exists), or - * - the live discovered schema's signature changes from one non-empty value to - * a *different* non-empty value — a genuine re-discovery/refresh. A bare empty - * → non-empty transition is excluded because that's just `mcpTools` finishing - * its initial async load, not a schema change, and would otherwise spuriously - * wipe drafts even while a cached snapshot (unaffected by the load) still - * governs what's actually rendered. + * - the live discovered schema's signature changes to a *different* non-empty + * value than the last non-empty value actually observed — a genuine + * re-discovery/refresh. Comparing against the last known non-empty value + * (rather than merely the previous render's value) means a schema that + * transiently disappears and reappears — e.g. `mcpTools` refetching, or the + * tool briefly missing from a fresh list — still resets drafts if it comes + * back different, while a plain empty → non-empty transition (the initial + * async load) does not, since there is no prior non-empty value to compare. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState< Record @@ -143,20 +145,24 @@ export function McpDynamicArgs({ const liveSchemaSignature = schemaSignature(selectedToolConfig?.inputSchema) const [prevToolAndCachedSchemaKey, setPrevToolAndCachedSchemaKey] = useState(toolAndCachedSchemaKey) - const [prevLiveSchemaSignature, setPrevLiveSchemaSignature] = useState(liveSchemaSignature) + const [lastNonEmptyLiveSchemaSignature, setLastNonEmptyLiveSchemaSignature] = + useState(liveSchemaSignature) + const toolOrCachedSchemaChanged = prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey + const nextLastNonEmptyLiveSchemaSignature = + liveSchemaSignature !== '' ? liveSchemaSignature : lastNonEmptyLiveSchemaSignature if ( - prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey || - prevLiveSchemaSignature !== liveSchemaSignature + toolOrCachedSchemaChanged || + nextLastNonEmptyLiveSchemaSignature !== lastNonEmptyLiveSchemaSignature ) { const isGenuineLiveRefresh = - prevLiveSchemaSignature !== '' && liveSchemaSignature !== '' && - prevLiveSchemaSignature !== liveSchemaSignature - if (prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey || isGenuineLiveRefresh) { + lastNonEmptyLiveSchemaSignature !== '' && + liveSchemaSignature !== lastNonEmptyLiveSchemaSignature + if (toolOrCachedSchemaChanged || isGenuineLiveRefresh) { setInvalidJsonDrafts({}) } setPrevToolAndCachedSchemaKey(toolAndCachedSchemaKey) - setPrevLiveSchemaSignature(liveSchemaSignature) + setLastNonEmptyLiveSchemaSignature(nextLastNonEmptyLiveSchemaSignature) } const currentArgs = useCallback(() => { From 9cd912041de7d46757d3b5da6571b6f1b5d88fc1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 10 Jul 2026 14:23:55 -0700 Subject: [PATCH 8/8] fix(mcp): re-baseline live schema tracker fresh on every tool switch lastNonEmptyLiveSchemaSignature carried over across a tool switch whenever the newly-selected tool's live schema hadn't loaded yet in that same render (still empty). When it loaded a moment later, the comparison was against the *previous* tool's signature, so the new tool's first schema load could be misread as a "genuine refresh" and wipe drafts the user had already started typing against the new tool. A tool/cached-schema change now always re-baselines the live-schema tracker to the new tool's current signature (even if still empty), so the "same tool" refresh comparison never bleeds across tool switches. --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 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 c8e5ead7485..08addbf7ab4 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 @@ -128,15 +128,19 @@ export function McpDynamicArgs({ * baseline switch, a collaborator's edit) can't be shadowed by stale draft text. * Drafts also reset wholesale on either of two independent triggers: * - the selected tool or the cached `_toolSchema` snapshot changes (this pair - * always drives `toolSchema` whenever a cached snapshot exists), or - * - the live discovered schema's signature changes to a *different* non-empty - * value than the last non-empty value actually observed — a genuine - * re-discovery/refresh. Comparing against the last known non-empty value - * (rather than merely the previous render's value) means a schema that - * transiently disappears and reappears — e.g. `mcpTools` refetching, or the - * tool briefly missing from a fresh list — still resets drafts if it comes - * back different, while a plain empty → non-empty transition (the initial - * async load) does not, since there is no prior non-empty value to compare. + * always drives `toolSchema` whenever a cached snapshot exists) — the live + * schema tracker is also re-baselined to the new tool's current signature + * here (even if still empty), so a tool switch never leaves the *previous* + * tool's signature behind to be misread as a "refresh" once the new tool's + * schema loads a moment later, or + * - for the *same* tool, the live discovered schema's signature changes to a + * different non-empty value than the last non-empty value actually observed + * — a genuine re-discovery/refresh. Comparing against the last known + * non-empty value (rather than merely the previous render's value) means a + * schema that transiently disappears and reappears — e.g. `mcpTools` + * refetching — still resets drafts if it comes back different, while a + * plain empty → non-empty transition (the initial async load) does not, + * since there is no prior non-empty value for this tool to compare against. */ const [invalidJsonDrafts, setInvalidJsonDrafts] = useState< Record @@ -147,22 +151,23 @@ export function McpDynamicArgs({ useState(toolAndCachedSchemaKey) const [lastNonEmptyLiveSchemaSignature, setLastNonEmptyLiveSchemaSignature] = useState(liveSchemaSignature) - const toolOrCachedSchemaChanged = prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey - const nextLastNonEmptyLiveSchemaSignature = - liveSchemaSignature !== '' ? liveSchemaSignature : lastNonEmptyLiveSchemaSignature - if ( - toolOrCachedSchemaChanged || - nextLastNonEmptyLiveSchemaSignature !== lastNonEmptyLiveSchemaSignature - ) { - const isGenuineLiveRefresh = - liveSchemaSignature !== '' && - lastNonEmptyLiveSchemaSignature !== '' && - liveSchemaSignature !== lastNonEmptyLiveSchemaSignature - if (toolOrCachedSchemaChanged || isGenuineLiveRefresh) { - setInvalidJsonDrafts({}) - } + if (prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey) { + setInvalidJsonDrafts({}) setPrevToolAndCachedSchemaKey(toolAndCachedSchemaKey) - setLastNonEmptyLiveSchemaSignature(nextLastNonEmptyLiveSchemaSignature) + setLastNonEmptyLiveSchemaSignature(liveSchemaSignature) + } else { + const nextLastNonEmptyLiveSchemaSignature = + liveSchemaSignature !== '' ? liveSchemaSignature : lastNonEmptyLiveSchemaSignature + if (nextLastNonEmptyLiveSchemaSignature !== lastNonEmptyLiveSchemaSignature) { + const isGenuineLiveRefresh = + liveSchemaSignature !== '' && + lastNonEmptyLiveSchemaSignature !== '' && + liveSchemaSignature !== lastNonEmptyLiveSchemaSignature + if (isGenuineLiveRefresh) { + setInvalidJsonDrafts({}) + } + setLastNonEmptyLiveSchemaSignature(nextLastNonEmptyLiveSchemaSignature) + } } const currentArgs = useCallback(() => {