fix(mcp-preview): server and tool name fetch to use tanstack (#2058)

* fix(mcp-preview): server and tool name fetch to use tanstack

* remove comments

* fix mcp tool interface

* change incorrect reference

* fix

* remove comments
This commit is contained in:
Vikhyath Mondreti
2025-11-19 16:22:17 -08:00
committed by GitHub
parent 570b8d61f0
commit 2be3007d69
5 changed files with 63 additions and 121 deletions

View File

@@ -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)

View File

@@ -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<Set<string>>(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() {
<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}

View File

@@ -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

View File

@@ -43,10 +43,11 @@ export interface McpServerConfig {
}
export interface McpTool {
id: string
serverId: string
serverName: string
name: string
description?: string
inputSchema?: any
}
/**

View File

@@ -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<McpToolForUI[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const queryClient = useQueryClient()
const { data: servers = [] } = useMcpServers(workspaceId)
const { data: mcpToolsData = [], isLoading, error: queryError } = useMcpToolsQuery(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 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)
logger.info('Refreshing MCP tools', { forceRefresh, workspaceId })
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)
}
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,