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..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,11 +105,19 @@ function McpInputWithTags({ onDragOver={handleDragOver} placeholder={placeholder} disabled={disabled} + name={inputNameRef.current} 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 && ( -
+
{formatDisplayText(value?.toString() || '', { accessiblePrefixes, @@ -157,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 @@ -220,9 +230,16 @@ function McpTextareaWithTags({ placeholder={placeholder} disabled={disabled} rows={rows} + name={textareaNameRef.current} + 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')} /> -
+
{formatDisplayText(value || '', { accessiblePrefixes, highlightAll: !accessiblePrefixes, @@ -298,6 +315,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) }, @@ -509,7 +537,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) 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({ { - // 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..812c3c96e5d 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,10 @@ export class McpClient { return typeof serverVersion === 'string' ? serverVersion : undefined } + getSessionId(): string | undefined { + return this.transport.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() } }