diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index abc49cd704..cdced8dd36 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/emcn/components/badge/badge' import { Tooltip } from '@/components/emcn/components/tooltip/tooltip' import { getEnv, isTruthy } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' +import { createMcpToolId } from '@/lib/mcp/utils' import { cn } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useBlockCore } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' @@ -13,6 +14,7 @@ import { useBlockDimensions, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' +import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp' import { useCredentialName } from '@/hooks/queries/oauth-credentials' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useKnowledgeBaseName } from '@/hooks/use-knowledge-base-name' @@ -313,10 +315,31 @@ const SubBlockRow = ({ ? (workflowMap[rawValue]?.name ?? null) : null - // Subscribe to variables store to reactively update when variables change + // Hydrate MCP server ID to name using TanStack Query + const { data: mcpServers = [] } = useMcpServers(workspaceId || '') + const mcpServerDisplayName = useMemo(() => { + if (subBlock?.type !== 'mcp-server-selector' || typeof rawValue !== 'string') { + return null + } + const server = mcpServers.find((s) => s.id === rawValue) + return server?.name ?? null + }, [subBlock?.type, rawValue, mcpServers]) + + const { data: mcpToolsData = [] } = useMcpToolsQuery(workspaceId || '') + const mcpToolDisplayName = useMemo(() => { + if (subBlock?.type !== 'mcp-tool-selector' || typeof rawValue !== 'string') { + return null + } + + const tool = mcpToolsData.find((t) => { + const toolId = createMcpToolId(t.serverId, t.name) + return toolId === rawValue + }) + return tool?.name ?? null + }, [subBlock?.type, rawValue, mcpToolsData]) + const allVariables = useVariablesStore((state) => state.variables) - // Special handling for variables-input to hydrate variable IDs to names from variables store const variablesDisplayValue = useMemo(() => { if (subBlock?.type !== 'variables-input' || !isVariableAssignmentsArray(rawValue)) { return null @@ -354,6 +377,8 @@ const SubBlockRow = ({ variablesDisplayValue || knowledgeBaseDisplayName || workflowSelectionName || + mcpServerDisplayName || + mcpToolDisplayName || selectorDisplayName const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx index 6af0e082fa..a2b9cd4efb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx @@ -6,6 +6,7 @@ import { useParams } from 'next/navigation' import { Button } from '@/components/emcn' import { Alert, AlertDescription, Input, Skeleton } from '@/components/ui' import { createLogger } from '@/lib/logs/console/logger' +import { createMcpToolId } from '@/lib/mcp/utils' import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' import { useCreateMcpServer, @@ -14,7 +15,6 @@ import { useMcpToolsQuery, } from '@/hooks/queries/mcp' import { useMcpServerTest } from '@/hooks/use-mcp-server-test' -import { useMcpTools } from '@/hooks/use-mcp-tools' import { AddServerForm } from './components/add-server-form' import type { McpServerFormData } from './types' @@ -34,9 +34,6 @@ export function MCP() { const createServerMutation = useCreateMcpServer() const deleteServerMutation = useDeleteMcpServer() - // Keep the old hook for backward compatibility with other features that use it - const { refreshTools } = useMcpTools(workspaceId) - const [showAddForm, setShowAddForm] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [deletingServers, setDeletingServers] = useState>(new Set()) @@ -197,22 +194,12 @@ export function MCP() { setActiveInputField(null) setActiveHeaderIndex(null) clearTestResult() - - refreshTools(true) // Force refresh after adding server } catch (error) { logger.error('Failed to add MCP server:', error) } finally { setIsAddingServer(false) } - }, [ - formData, - testResult, - testConnection, - createServerMutation, - refreshTools, - clearTestResult, - workspaceId, - ]) + }, [formData, testResult, testConnection, createServerMutation, clearTestResult, workspaceId]) const handleRemoveServer = useCallback( async (serverId: string) => { @@ -220,7 +207,7 @@ export function MCP() { try { await deleteServerMutation.mutateAsync({ workspaceId, serverId }) - await refreshTools(true) + // TanStack Query mutations automatically invalidate and refetch tools logger.info(`Removed MCP server: ${serverId}`) } catch (error) { @@ -238,7 +225,7 @@ export function MCP() { }) } }, - [deleteServerMutation, refreshTools, workspaceId] + [deleteServerMutation, workspaceId] ) const toolsByServer = (mcpToolsData || []).reduce( @@ -392,7 +379,7 @@ export function MCP() {
{tools.map((tool) => ( {tool.name} diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 9822d749da..4799199481 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -87,6 +87,8 @@ export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [ 'knowledge-base-selector', 'document-selector', 'variables-input', + 'mcp-server-selector', + 'mcp-tool-selector', ] as const export type ExtractToolOutput = T extends ToolResponse ? T['output'] : never diff --git a/apps/sim/hooks/queries/mcp.ts b/apps/sim/hooks/queries/mcp.ts index 08bc09b0ae..421287a64f 100644 --- a/apps/sim/hooks/queries/mcp.ts +++ b/apps/sim/hooks/queries/mcp.ts @@ -43,10 +43,11 @@ export interface McpServerConfig { } export interface McpTool { - id: string serverId: string + serverName: string name: string description?: string + inputSchema?: any } /** diff --git a/apps/sim/hooks/use-mcp-tools.ts b/apps/sim/hooks/use-mcp-tools.ts index 7d5e0d7858..4960bedcfa 100644 --- a/apps/sim/hooks/use-mcp-tools.ts +++ b/apps/sim/hooks/use-mcp-tools.ts @@ -2,16 +2,16 @@ * Hook for discovering and managing MCP tools * * This hook provides a unified interface for accessing MCP tools - * alongside regular platform tools in the tool-input component + * using TanStack Query for optimal caching and performance */ import type React from 'react' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useMemo } from 'react' +import { useQueryClient } from '@tanstack/react-query' import { WrenchIcon } from 'lucide-react' import { createLogger } from '@/lib/logs/console/logger' -import type { McpTool } from '@/lib/mcp/types' import { createMcpToolId } from '@/lib/mcp/utils' -import { useMcpServers } from '@/hooks/queries/mcp' +import { mcpKeys, useMcpToolsQuery } from '@/hooks/queries/mcp' const logger = createLogger('useMcpTools') @@ -37,81 +37,39 @@ export interface UseMcpToolsResult { } export function useMcpTools(workspaceId: string): UseMcpToolsResult { - const [mcpTools, setMcpTools] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - - const { data: servers = [] } = useMcpServers(workspaceId) - - // Track the last fingerprint - const lastProcessedFingerprintRef = useRef('') - - // Create a stable server fingerprint - const serversFingerprint = useMemo(() => { - return servers - .filter((s) => s.enabled && !s.deletedAt) - .map((s) => `${s.id}-${s.enabled}-${s.updatedAt}`) - .sort() - .join('|') - }, [servers]) + const queryClient = useQueryClient() + + const { data: mcpToolsData = [], isLoading, error: queryError } = useMcpToolsQuery(workspaceId) + + const mcpTools = useMemo(() => { + return mcpToolsData.map((tool) => ({ + id: createMcpToolId(tool.serverId, tool.name), + name: tool.name, + description: tool.description, + serverId: tool.serverId, + serverName: tool.serverName, + type: 'mcp' as const, + inputSchema: tool.inputSchema, + bgColor: '#6366F1', + icon: WrenchIcon, + })) + }, [mcpToolsData]) const refreshTools = useCallback( async (forceRefresh = false) => { - // Skip if no workspaceId (e.g., on template preview pages) if (!workspaceId) { - setMcpTools([]) - setIsLoading(false) + logger.warn('Cannot refresh tools: no workspaceId provided') return } - setIsLoading(true) - setError(null) - - try { - logger.info('Discovering MCP tools', { forceRefresh, workspaceId }) - - const response = await fetch( - `/api/mcp/tools/discover?workspaceId=${workspaceId}&refresh=${forceRefresh}` - ) - - if (!response.ok) { - throw new Error(`Failed to discover MCP tools: ${response.status} ${response.statusText}`) - } - - const data = await response.json() - - if (!data.success) { - throw new Error(data.error || 'Failed to discover MCP tools') - } - - const tools = data.data.tools || [] - const transformedTools = tools.map((tool: McpTool) => ({ - id: createMcpToolId(tool.serverId, tool.name), - name: tool.name, - description: tool.description, - serverId: tool.serverId, - serverName: tool.serverName, - type: 'mcp' as const, - inputSchema: tool.inputSchema, - bgColor: '#6366F1', - icon: WrenchIcon, - })) - - setMcpTools(transformedTools) - - logger.info( - `Discovered ${transformedTools.length} MCP tools from ${data.data.byServer ? Object.keys(data.data.byServer).length : 0} servers` - ) - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to discover MCP tools' - logger.error('Error discovering MCP tools:', err) - setError(errorMessage) - setMcpTools([]) - } finally { - setIsLoading(false) - } + logger.info('Refreshing MCP tools', { forceRefresh, workspaceId }) + + await queryClient.invalidateQueries({ + queryKey: mcpKeys.tools(workspaceId), + refetchType: forceRefresh ? 'active' : 'all', + }) }, - [workspaceId] + [workspaceId, queryClient] ) const getToolById = useCallback( @@ -128,41 +86,10 @@ export function useMcpTools(workspaceId: string): UseMcpToolsResult { [mcpTools] ) - useEffect(() => { - refreshTools() - }, [refreshTools]) - - // Refresh tools when servers change - useEffect(() => { - if (!serversFingerprint || serversFingerprint === lastProcessedFingerprintRef.current) return - - logger.info('Active servers changed, refreshing MCP tools', { - serverCount: servers.filter((s) => s.enabled && !s.deletedAt).length, - fingerprint: serversFingerprint, - }) - - lastProcessedFingerprintRef.current = serversFingerprint - refreshTools() - }, [serversFingerprint, refreshTools]) - - // Auto-refresh every 5 minutes - useEffect(() => { - const interval = setInterval( - () => { - if (!isLoading) { - refreshTools() - } - }, - 5 * 60 * 1000 - ) - - return () => clearInterval(interval) - }, [refreshTools]) - return { mcpTools, isLoading, - error, + error: queryError instanceof Error ? queryError.message : null, refreshTools, getToolById, getToolsByServer,