Skip to content

Commit 2be3007

Browse files
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
1 parent 570b8d6 commit 2be3007

File tree

5 files changed

+66
-124
lines changed

5 files changed

+66
-124
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Badge } from '@/components/emcn/components/badge/badge'
55
import { Tooltip } from '@/components/emcn/components/tooltip/tooltip'
66
import { getEnv, isTruthy } from '@/lib/env'
77
import { createLogger } from '@/lib/logs/console/logger'
8+
import { createMcpToolId } from '@/lib/mcp/utils'
89
import { cn } from '@/lib/utils'
910
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
1011
import { useBlockCore } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
@@ -13,6 +14,7 @@ import {
1314
useBlockDimensions,
1415
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
1516
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
17+
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
1618
import { useCredentialName } from '@/hooks/queries/oauth-credentials'
1719
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
1820
import { useKnowledgeBaseName } from '@/hooks/use-knowledge-base-name'
@@ -313,10 +315,31 @@ const SubBlockRow = ({
313315
? (workflowMap[rawValue]?.name ?? null)
314316
: null
315317

316-
// Subscribe to variables store to reactively update when variables change
318+
// Hydrate MCP server ID to name using TanStack Query
319+
const { data: mcpServers = [] } = useMcpServers(workspaceId || '')
320+
const mcpServerDisplayName = useMemo(() => {
321+
if (subBlock?.type !== 'mcp-server-selector' || typeof rawValue !== 'string') {
322+
return null
323+
}
324+
const server = mcpServers.find((s) => s.id === rawValue)
325+
return server?.name ?? null
326+
}, [subBlock?.type, rawValue, mcpServers])
327+
328+
const { data: mcpToolsData = [] } = useMcpToolsQuery(workspaceId || '')
329+
const mcpToolDisplayName = useMemo(() => {
330+
if (subBlock?.type !== 'mcp-tool-selector' || typeof rawValue !== 'string') {
331+
return null
332+
}
333+
334+
const tool = mcpToolsData.find((t) => {
335+
const toolId = createMcpToolId(t.serverId, t.name)
336+
return toolId === rawValue
337+
})
338+
return tool?.name ?? null
339+
}, [subBlock?.type, rawValue, mcpToolsData])
340+
317341
const allVariables = useVariablesStore((state) => state.variables)
318342

319-
// Special handling for variables-input to hydrate variable IDs to names from variables store
320343
const variablesDisplayValue = useMemo(() => {
321344
if (subBlock?.type !== 'variables-input' || !isVariableAssignmentsArray(rawValue)) {
322345
return null
@@ -354,6 +377,8 @@ const SubBlockRow = ({
354377
variablesDisplayValue ||
355378
knowledgeBaseDisplayName ||
356379
workflowSelectionName ||
380+
mcpServerDisplayName ||
381+
mcpToolDisplayName ||
357382
selectorDisplayName
358383
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)
359384

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useParams } from 'next/navigation'
66
import { Button } from '@/components/emcn'
77
import { Alert, AlertDescription, Input, Skeleton } from '@/components/ui'
88
import { createLogger } from '@/lib/logs/console/logger'
9+
import { createMcpToolId } from '@/lib/mcp/utils'
910
import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown'
1011
import {
1112
useCreateMcpServer,
@@ -14,7 +15,6 @@ import {
1415
useMcpToolsQuery,
1516
} from '@/hooks/queries/mcp'
1617
import { useMcpServerTest } from '@/hooks/use-mcp-server-test'
17-
import { useMcpTools } from '@/hooks/use-mcp-tools'
1818
import { AddServerForm } from './components/add-server-form'
1919
import type { McpServerFormData } from './types'
2020

@@ -34,9 +34,6 @@ export function MCP() {
3434
const createServerMutation = useCreateMcpServer()
3535
const deleteServerMutation = useDeleteMcpServer()
3636

37-
// Keep the old hook for backward compatibility with other features that use it
38-
const { refreshTools } = useMcpTools(workspaceId)
39-
4037
const [showAddForm, setShowAddForm] = useState(false)
4138
const [searchTerm, setSearchTerm] = useState('')
4239
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
@@ -197,30 +194,20 @@ export function MCP() {
197194
setActiveInputField(null)
198195
setActiveHeaderIndex(null)
199196
clearTestResult()
200-
201-
refreshTools(true) // Force refresh after adding server
202197
} catch (error) {
203198
logger.error('Failed to add MCP server:', error)
204199
} finally {
205200
setIsAddingServer(false)
206201
}
207-
}, [
208-
formData,
209-
testResult,
210-
testConnection,
211-
createServerMutation,
212-
refreshTools,
213-
clearTestResult,
214-
workspaceId,
215-
])
202+
}, [formData, testResult, testConnection, createServerMutation, clearTestResult, workspaceId])
216203

217204
const handleRemoveServer = useCallback(
218205
async (serverId: string) => {
219206
setDeletingServers((prev) => new Set(prev).add(serverId))
220207

221208
try {
222209
await deleteServerMutation.mutateAsync({ workspaceId, serverId })
223-
await refreshTools(true)
210+
// TanStack Query mutations automatically invalidate and refetch tools
224211

225212
logger.info(`Removed MCP server: ${serverId}`)
226213
} catch (error) {
@@ -238,7 +225,7 @@ export function MCP() {
238225
})
239226
}
240227
},
241-
[deleteServerMutation, refreshTools, workspaceId]
228+
[deleteServerMutation, workspaceId]
242229
)
243230

244231
const toolsByServer = (mcpToolsData || []).reduce(
@@ -392,7 +379,7 @@ export function MCP() {
392379
<div className='mt-1 ml-2 flex flex-wrap gap-1'>
393380
{tools.map((tool) => (
394381
<span
395-
key={tool.id}
382+
key={createMcpToolId(tool.serverId, tool.name)}
396383
className='inline-flex h-5 items-center rounded bg-muted/50 px-2 text-muted-foreground text-xs'
397384
>
398385
{tool.name}

apps/sim/blocks/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [
8787
'knowledge-base-selector',
8888
'document-selector',
8989
'variables-input',
90+
'mcp-server-selector',
91+
'mcp-tool-selector',
9092
] as const
9193

9294
export type ExtractToolOutput<T> = T extends ToolResponse ? T['output'] : never

apps/sim/hooks/queries/mcp.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ export interface McpServerConfig {
4343
}
4444

4545
export interface McpTool {
46-
id: string
4746
serverId: string
47+
serverName: string
4848
name: string
4949
description?: string
50+
inputSchema?: any
5051
}
5152

5253
/**

apps/sim/hooks/use-mcp-tools.ts

Lines changed: 30 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
* Hook for discovering and managing MCP tools
33
*
44
* This hook provides a unified interface for accessing MCP tools
5-
* alongside regular platform tools in the tool-input component
5+
* using TanStack Query for optimal caching and performance
66
*/
77

88
import type React from 'react'
9-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
9+
import { useCallback, useMemo } from 'react'
10+
import { useQueryClient } from '@tanstack/react-query'
1011
import { WrenchIcon } from 'lucide-react'
1112
import { createLogger } from '@/lib/logs/console/logger'
12-
import type { McpTool } from '@/lib/mcp/types'
1313
import { createMcpToolId } from '@/lib/mcp/utils'
14-
import { useMcpServers } from '@/hooks/queries/mcp'
14+
import { mcpKeys, useMcpToolsQuery } from '@/hooks/queries/mcp'
1515

1616
const logger = createLogger('useMcpTools')
1717

@@ -37,81 +37,39 @@ export interface UseMcpToolsResult {
3737
}
3838

3939
export function useMcpTools(workspaceId: string): UseMcpToolsResult {
40-
const [mcpTools, setMcpTools] = useState<McpToolForUI[]>([])
41-
const [isLoading, setIsLoading] = useState(false)
42-
const [error, setError] = useState<string | null>(null)
43-
44-
const { data: servers = [] } = useMcpServers(workspaceId)
45-
46-
// Track the last fingerprint
47-
const lastProcessedFingerprintRef = useRef<string>('')
48-
49-
// Create a stable server fingerprint
50-
const serversFingerprint = useMemo(() => {
51-
return servers
52-
.filter((s) => s.enabled && !s.deletedAt)
53-
.map((s) => `${s.id}-${s.enabled}-${s.updatedAt}`)
54-
.sort()
55-
.join('|')
56-
}, [servers])
40+
const queryClient = useQueryClient()
41+
42+
const { data: mcpToolsData = [], isLoading, error: queryError } = useMcpToolsQuery(workspaceId)
43+
44+
const mcpTools = useMemo<McpToolForUI[]>(() => {
45+
return mcpToolsData.map((tool) => ({
46+
id: createMcpToolId(tool.serverId, tool.name),
47+
name: tool.name,
48+
description: tool.description,
49+
serverId: tool.serverId,
50+
serverName: tool.serverName,
51+
type: 'mcp' as const,
52+
inputSchema: tool.inputSchema,
53+
bgColor: '#6366F1',
54+
icon: WrenchIcon,
55+
}))
56+
}, [mcpToolsData])
5757

5858
const refreshTools = useCallback(
5959
async (forceRefresh = false) => {
60-
// Skip if no workspaceId (e.g., on template preview pages)
6160
if (!workspaceId) {
62-
setMcpTools([])
63-
setIsLoading(false)
61+
logger.warn('Cannot refresh tools: no workspaceId provided')
6462
return
6563
}
6664

67-
setIsLoading(true)
68-
setError(null)
69-
70-
try {
71-
logger.info('Discovering MCP tools', { forceRefresh, workspaceId })
72-
73-
const response = await fetch(
74-
`/api/mcp/tools/discover?workspaceId=${workspaceId}&refresh=${forceRefresh}`
75-
)
76-
77-
if (!response.ok) {
78-
throw new Error(`Failed to discover MCP tools: ${response.status} ${response.statusText}`)
79-
}
80-
81-
const data = await response.json()
82-
83-
if (!data.success) {
84-
throw new Error(data.error || 'Failed to discover MCP tools')
85-
}
86-
87-
const tools = data.data.tools || []
88-
const transformedTools = tools.map((tool: McpTool) => ({
89-
id: createMcpToolId(tool.serverId, tool.name),
90-
name: tool.name,
91-
description: tool.description,
92-
serverId: tool.serverId,
93-
serverName: tool.serverName,
94-
type: 'mcp' as const,
95-
inputSchema: tool.inputSchema,
96-
bgColor: '#6366F1',
97-
icon: WrenchIcon,
98-
}))
99-
100-
setMcpTools(transformedTools)
101-
102-
logger.info(
103-
`Discovered ${transformedTools.length} MCP tools from ${data.data.byServer ? Object.keys(data.data.byServer).length : 0} servers`
104-
)
105-
} catch (err) {
106-
const errorMessage = err instanceof Error ? err.message : 'Failed to discover MCP tools'
107-
logger.error('Error discovering MCP tools:', err)
108-
setError(errorMessage)
109-
setMcpTools([])
110-
} finally {
111-
setIsLoading(false)
112-
}
65+
logger.info('Refreshing MCP tools', { forceRefresh, workspaceId })
66+
67+
await queryClient.invalidateQueries({
68+
queryKey: mcpKeys.tools(workspaceId),
69+
refetchType: forceRefresh ? 'active' : 'all',
70+
})
11371
},
114-
[workspaceId]
72+
[workspaceId, queryClient]
11573
)
11674

11775
const getToolById = useCallback(
@@ -128,41 +86,10 @@ export function useMcpTools(workspaceId: string): UseMcpToolsResult {
12886
[mcpTools]
12987
)
13088

131-
useEffect(() => {
132-
refreshTools()
133-
}, [refreshTools])
134-
135-
// Refresh tools when servers change
136-
useEffect(() => {
137-
if (!serversFingerprint || serversFingerprint === lastProcessedFingerprintRef.current) return
138-
139-
logger.info('Active servers changed, refreshing MCP tools', {
140-
serverCount: servers.filter((s) => s.enabled && !s.deletedAt).length,
141-
fingerprint: serversFingerprint,
142-
})
143-
144-
lastProcessedFingerprintRef.current = serversFingerprint
145-
refreshTools()
146-
}, [serversFingerprint, refreshTools])
147-
148-
// Auto-refresh every 5 minutes
149-
useEffect(() => {
150-
const interval = setInterval(
151-
() => {
152-
if (!isLoading) {
153-
refreshTools()
154-
}
155-
},
156-
5 * 60 * 1000
157-
)
158-
159-
return () => clearInterval(interval)
160-
}, [refreshTools])
161-
16289
return {
16390
mcpTools,
16491
isLoading,
165-
error,
92+
error: queryError instanceof Error ? queryError.message : null,
16693
refreshTools,
16794
getToolById,
16895
getToolsByServer,

0 commit comments

Comments
 (0)