mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
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:
committed by
GitHub
parent
570b8d61f0
commit
2be3007d69
@@ -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)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,10 +43,11 @@ export interface McpServerConfig {
|
||||
}
|
||||
|
||||
export interface McpTool {
|
||||
id: string
|
||||
serverId: string
|
||||
serverName: string
|
||||
name: string
|
||||
description?: string
|
||||
inputSchema?: any
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user