From f364c2eaee28d2646ff0cafb7ac48eec81bb9436 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 2 Dec 2025 10:36:31 -0800 Subject: [PATCH 1/5] fix(mcp): reuse sessionID for consecutive MCP tool calls, fix dynamic args clearing, fix refreshing tools on save --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 11 ++ apps/sim/hooks/queries/mcp.ts | 8 +- apps/sim/lib/mcp/client.ts | 17 ++- apps/sim/lib/mcp/service.ts | 142 ++++++++++++------ 4 files changed, 129 insertions(+), 49 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 33707f2d0c7..fa973f55467 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 @@ -298,6 +298,17 @@ export function McpDynamicArgs({ if (disabled) return const current = currentArgs() + + if (value === '' && (current[paramName] === undefined || current[paramName] === null)) { + return + } + + if (value === '') { + const { [paramName]: _, ...rest } = current + setToolArgs(Object.keys(rest).length > 0 ? rest : {}) + return + } + const updated = { ...current, [paramName]: value } setToolArgs(updated) }, diff --git a/apps/sim/hooks/queries/mcp.ts b/apps/sim/hooks/queries/mcp.ts index 421287a64f5..09684d23584 100644 --- a/apps/sim/hooks/queries/mcp.ts +++ b/apps/sim/hooks/queries/mcp.ts @@ -152,12 +152,14 @@ export function useCreateMcpServer() { } logger.info(`Created MCP server: ${config.name} in workspace: ${workspaceId}`) - return { ...serverData, connectionStatus: 'disconnected' as const } + return { + ...serverData, + connectionStatus: 'disconnected' as const, + serverId: data.data?.serverId, + } }, onSuccess: (_data, variables) => { - // Invalidate servers list to refetch queryClient.invalidateQueries({ queryKey: mcpKeys.servers(variables.workspaceId) }) - // Invalidate tools as new server may provide new tools queryClient.invalidateQueries({ queryKey: mcpKeys.tools(variables.workspaceId) }) }, }) diff --git a/apps/sim/lib/mcp/client.ts b/apps/sim/lib/mcp/client.ts index ca3fc604b2a..361964ac6e6 100644 --- a/apps/sim/lib/mcp/client.ts +++ b/apps/sim/lib/mcp/client.ts @@ -42,7 +42,13 @@ export class McpClient { '2024-11-05', // Initial stable release ] - constructor(config: McpServerConfig, securityPolicy?: McpSecurityPolicy) { + /** + * Creates a new MCP client + * @param config - Server configuration + * @param securityPolicy - Optional security policy + * @param sessionId - Optional session ID for session restoration (from previous connection) + */ + constructor(config: McpServerConfig, securityPolicy?: McpSecurityPolicy, sessionId?: string) { this.config = config this.connectionStatus = { connected: false } this.securityPolicy = securityPolicy ?? { @@ -59,6 +65,7 @@ export class McpClient { requestInit: { headers: this.config.headers, }, + sessionId, }) this.client = new Client( @@ -255,6 +262,14 @@ export class McpClient { return typeof serverVersion === 'string' ? serverVersion : undefined } + /** + * Get the session ID from the transport (available after successful connection) + * This can be used to restore the session on subsequent connections + */ + getSessionId(): string | undefined { + return (this.transport as unknown as { sessionId?: string }).sessionId + } + /** * Request user consent for tool execution */ diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index a8c3ba088df..ac741e11bc5 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -49,10 +49,35 @@ class McpService { private cacheMisses = 0 private entriesEvicted = 0 + private sessionCache = new Map() + constructor() { this.startPeriodicCleanup() } + /** + * Get cached session ID for a server + */ + private getCachedSessionId(serverId: string): string | undefined { + return this.sessionCache.get(serverId) + } + + /** + * Cache session ID for a server + */ + private cacheSessionId(serverId: string, sessionId: string): void { + this.sessionCache.set(serverId, sessionId) + logger.debug(`Cached session ID for server ${serverId}`) + } + + /** + * Clear cached session ID for a server + */ + private clearCachedSessionId(serverId: string): void { + this.sessionCache.delete(serverId) + logger.debug(`Cleared cached session ID for server ${serverId}`) + } + /** * Start periodic cleanup of expired cache entries */ @@ -306,7 +331,7 @@ class McpService { } /** - * Create and connect to an MCP client with security policy + * Create and connect to an MCP client */ private async createClient(config: McpServerConfig): Promise { const securityPolicy = { @@ -316,9 +341,49 @@ class McpService { allowedOrigins: config.url ? [new URL(config.url).origin] : undefined, } - const client = new McpClient(config, securityPolicy) - await client.connect() - return client + const cachedSessionId = this.getCachedSessionId(config.id) + + const client = new McpClient(config, securityPolicy, cachedSessionId) + + try { + await client.connect() + + const newSessionId = client.getSessionId() + if (newSessionId) { + this.cacheSessionId(config.id, newSessionId) + } + + return client + } catch (error) { + if (cachedSessionId && this.isSessionError(error)) { + logger.debug(`Session restoration failed for server ${config.id}, retrying fresh`) + this.clearCachedSessionId(config.id) + + const freshClient = new McpClient(config, securityPolicy) + await freshClient.connect() + + const freshSessionId = freshClient.getSessionId() + if (freshSessionId) { + this.cacheSessionId(config.id, freshSessionId) + } + + return freshClient + } + + throw error + } + } + + private isSessionError(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase() + return ( + message.includes('no valid session') || + message.includes('invalid session') || + message.includes('session expired') + ) + } + return false } /** @@ -332,33 +397,25 @@ class McpService { ): Promise { const requestId = generateRequestId() - try { - logger.info( - `[${requestId}] Executing MCP tool ${toolCall.name} on server ${serverId} for user ${userId}` - ) + logger.info( + `[${requestId}] Executing MCP tool ${toolCall.name} on server ${serverId} for user ${userId}` + ) - const config = await this.getServerConfig(serverId, workspaceId) - if (!config) { - throw new Error(`Server ${serverId} not found or not accessible`) - } + const config = await this.getServerConfig(serverId, workspaceId) + if (!config) { + throw new Error(`Server ${serverId} not found or not accessible`) + } - const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId) + const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId) - const client = await this.createClient(resolvedConfig) + const client = await this.createClient(resolvedConfig) - try { - const result = await client.callTool(toolCall) - logger.info(`[${requestId}] Successfully executed tool ${toolCall.name}`) - return result - } finally { - await client.disconnect() - } - } catch (error) { - logger.error( - `[${requestId}] Failed to execute tool ${toolCall.name} on server ${serverId}:`, - error - ) - throw error + try { + const result = await client.callTool(toolCall) + logger.info(`[${requestId}] Successfully executed tool ${toolCall.name}`) + return result + } finally { + await client.disconnect() } } @@ -442,28 +499,23 @@ class McpService { ): Promise { const requestId = generateRequestId() - try { - logger.info(`[${requestId}] Discovering tools from server ${serverId} for user ${userId}`) + logger.info(`[${requestId}] Discovering tools from server ${serverId} for user ${userId}`) - const config = await this.getServerConfig(serverId, workspaceId) - if (!config) { - throw new Error(`Server ${serverId} not found or not accessible`) - } + const config = await this.getServerConfig(serverId, workspaceId) + if (!config) { + throw new Error(`Server ${serverId} not found or not accessible`) + } - const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId) + const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId) - const client = await this.createClient(resolvedConfig) + const client = await this.createClient(resolvedConfig) - try { - const tools = await client.listTools() - logger.info(`[${requestId}] Discovered ${tools.length} tools from server ${config.name}`) - return tools - } finally { - await client.disconnect() - } - } catch (error) { - logger.error(`[${requestId}] Failed to discover tools from server ${serverId}:`, error) - throw error + try { + const tools = await client.listTools() + logger.info(`[${requestId}] Discovered ${tools.length} tools from server ${config.name}`) + return tools + } finally { + await client.disconnect() } } From 4545f29cb59ae5825cd7f5af376a6e6d1fc3f495 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 2 Dec 2025 10:43:36 -0800 Subject: [PATCH 2/5] prevent defaults --- .../mcp-dynamic-args/mcp-dynamic-args.tsx | 42 ++++++++++++++++++- 1 file changed, 41 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 fa973f55467..08e2545d55a 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 @@ -104,7 +104,15 @@ function McpInputWithTags({ onDragOver={handleDragOver} placeholder={placeholder} disabled={disabled} + name={`mcp_input_${Math.random()}`} autoComplete='off' + autoCapitalize='off' + spellCheck='false' + data-form-type='other' + data-lpignore='true' + data-1p-ignore + readOnly + onFocus={(e) => e.currentTarget.removeAttribute('readOnly')} className={cn(!isPassword && 'text-transparent caret-foreground')} /> {!isPassword && ( @@ -220,6 +228,13 @@ function McpTextareaWithTags({ placeholder={placeholder} disabled={disabled} rows={rows} + name={`mcp_textarea_${Math.random()}`} + autoComplete='off' + autoCapitalize='off' + spellCheck='false' + data-form-type='other' + data-lpignore='true' + data-1p-ignore className={cn('min-h-[80px] resize-none text-transparent caret-foreground')} />
@@ -520,7 +535,32 @@ export function McpDynamicArgs({ } return ( -
+
+ {/* Hidden dummy inputs to prevent browser password manager autofill */} + + + {toolSchema.properties && Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => { const inputType = getInputType(paramSchema as any) From e267f027975b1b085105b969d0888e52f078670c Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 2 Dec 2025 11:16:44 -0800 Subject: [PATCH 3/5] fix subblock text area --- .../components/mcp-dynamic-args/mcp-dynamic-args.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 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 08e2545d55a..acf885c0562 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 @@ -116,7 +116,7 @@ function McpInputWithTags({ className={cn(!isPassword && 'text-transparent caret-foreground')} /> {!isPassword && ( -
+
{formatDisplayText(value?.toString() || '', { accessiblePrefixes, @@ -237,7 +237,7 @@ function McpTextareaWithTags({ data-1p-ignore className={cn('min-h-[80px] resize-none text-transparent caret-foreground')} /> -
+
{formatDisplayText(value || '', { accessiblePrefixes, highlightAll: !accessiblePrefixes, From f74c06f5d34b7075240c495e51fabef8a188288f Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 2 Dec 2025 12:33:25 -0800 Subject: [PATCH 4/5] added placeholders in tool-inp for mcp dynamic args --- .../sub-block/components/tool-input/tool-input.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index e8335071919..5be254f93e1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -2107,7 +2107,10 @@ export function ToolInput({ Date: Tue, 2 Dec 2025 12:37:18 -0800 Subject: [PATCH 5/5] ack PR comments --- .../components/mcp-dynamic-args/mcp-dynamic-args.tsx | 6 ++++-- apps/sim/lib/mcp/client.ts | 6 +----- 2 files changed, 5 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 acf885c0562..fd5e8209a07 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 @@ -40,6 +40,7 @@ function McpInputWithTags({ const [cursorPosition, setCursorPosition] = useState(0) const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) const inputRef = useRef(null) + const inputNameRef = useRef(`mcp_input_${Math.random()}`) const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value @@ -104,7 +105,7 @@ function McpInputWithTags({ onDragOver={handleDragOver} placeholder={placeholder} disabled={disabled} - name={`mcp_input_${Math.random()}`} + name={inputNameRef.current} autoComplete='off' autoCapitalize='off' spellCheck='false' @@ -165,6 +166,7 @@ function McpTextareaWithTags({ const [cursorPosition, setCursorPosition] = useState(0) const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) const textareaRef = useRef(null) + const textareaNameRef = useRef(`mcp_textarea_${Math.random()}`) const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value @@ -228,7 +230,7 @@ function McpTextareaWithTags({ placeholder={placeholder} disabled={disabled} rows={rows} - name={`mcp_textarea_${Math.random()}`} + name={textareaNameRef.current} autoComplete='off' autoCapitalize='off' spellCheck='false' diff --git a/apps/sim/lib/mcp/client.ts b/apps/sim/lib/mcp/client.ts index 361964ac6e6..812c3c96e5d 100644 --- a/apps/sim/lib/mcp/client.ts +++ b/apps/sim/lib/mcp/client.ts @@ -262,12 +262,8 @@ export class McpClient { return typeof serverVersion === 'string' ? serverVersion : undefined } - /** - * Get the session ID from the transport (available after successful connection) - * This can be used to restore the session on subsequent connections - */ getSessionId(): string | undefined { - return (this.transport as unknown as { sessionId?: string }).sessionId + return this.transport.sessionId } /**