Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -354,6 +377,8 @@ const SubBlockRow = ({
variablesDisplayValue ||
knowledgeBaseDisplayName ||
workflowSelectionName ||
mcpServerDisplayName ||
mcpToolDisplayName ||
selectorDisplayName
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'

Expand All @@ -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<Set<string>>(new Set())
Expand Down Expand Up @@ -197,30 +194,20 @@ 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) => {
setDeletingServers((prev) => new Set(prev).add(serverId))

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) {
Expand All @@ -238,7 +225,7 @@ export function MCP() {
})
}
},
[deleteServerMutation, refreshTools, workspaceId]
[deleteServerMutation, workspaceId]
)

const toolsByServer = (mcpToolsData || []).reduce(
Expand Down Expand Up @@ -392,7 +379,7 @@ export function MCP() {
<div className='mt-1 ml-2 flex flex-wrap gap-1'>
{tools.map((tool) => (
<span
key={tool.id}
key={createMcpToolId(tool.serverId, tool.name)}
className='inline-flex h-5 items-center rounded bg-muted/50 px-2 text-muted-foreground text-xs'
>
{tool.name}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T extends ToolResponse ? T['output'] : never
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/hooks/queries/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ export interface McpServerConfig {
}

export interface McpTool {
id: string
serverId: string
serverName: string
name: string
description?: string
inputSchema?: any
}

/**
Expand Down
133 changes: 30 additions & 103 deletions apps/sim/hooks/use-mcp-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -37,81 +37,39 @@ export interface UseMcpToolsResult {
}

export function useMcpTools(workspaceId: string): UseMcpToolsResult {
const [mcpTools, setMcpTools] = useState<McpToolForUI[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const { data: servers = [] } = useMcpServers(workspaceId)

// Track the last fingerprint
const lastProcessedFingerprintRef = useRef<string>('')

// 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<McpToolForUI[]>(() => {
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(
Expand All @@ -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,
Expand Down