@@ -710,7 +807,7 @@ export function McpServerFormModal({
)}
-
{isRefreshing
? 'Refreshing...'
@@ -121,14 +125,29 @@ function ServerListItem({
)
}
+function buildEditInitialData(server: McpServer) {
+ const entries: { key: string; value: string }[] = server.headers
+ ? Object.entries(server.headers).map(([key, value]) => ({ key, value }))
+ : []
+ if (entries.length === 0) entries.push({ key: '', value: '' })
+ const last = entries[entries.length - 1]
+ if (last.key !== '' || last.value !== '') entries.push({ key: '', value: '' })
+
+ return {
+ name: server.name || '',
+ transport: (server.transport as McpTransport) || 'streamable-http',
+ url: server.url || '',
+ timeout: 30000,
+ headers: entries,
+ oauthClientId: server.oauthClientId || undefined,
+ hasOauthClientSecret: server.hasOauthClientSecret === true,
+ }
+}
+
interface MCPProps {
initialServerId?: string | null
}
-/**
- * MCP Settings component for managing Model Context Protocol servers.
- * Handles server CRUD operations, connection testing, and environment variable integration.
- */
export function MCP({ initialServerId }: MCPProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -145,7 +164,8 @@ export function MCP({ initialServerId }: MCPProps) {
isFetching: toolsFetching,
} = useMcpToolsQuery(workspaceId)
const { data: storedTools = [], refetch: refetchStoredTools } = useStoredMcpTools(workspaceId)
- const forceRefreshTools = useForceRefreshMcpTools()
+ const forceRefreshToolsMutation = useForceRefreshMcpTools()
+ const forceRefreshTools = forceRefreshToolsMutation.mutate
const createServerMutation = useCreateMcpServer()
const deleteServerMutation = useDeleteMcpServer()
const refreshServerMutation = useRefreshMcpServer()
@@ -154,23 +174,16 @@ export function MCP({ initialServerId }: MCPProps) {
const { data: allowedMcpDomains = null } = useAllowedMcpDomains()
const [showAddModal, setShowAddModal] = useState(false)
- const [showEditModal, setShowEditModal] = useState(false)
- const [editInitialData, setEditInitialData] = useState<
- | {
- name: string
- transport: McpTransport
- url?: string
- timeout?: number
- headers?: { key: string; value: string }[]
- }
- | undefined
- >(undefined)
+ const [editingServerId, setEditingServerId] = useState(null)
const [searchTerm, setSearchTerm] = useState('')
const [deletingServers, setDeletingServers] = useState>(() => new Set())
+ const { connectingServers: connectingOauthServers, startOauthForServer } = useMcpOauthPopup({
+ workspaceId,
+ })
- const [showDeleteDialog, setShowDeleteDialog] = useState(false)
- const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null)
+ const [serverToDeleteId, setServerToDeleteId] = useState(null)
+ const showDeleteDialog = serverToDeleteId !== null
const [selectedServerId, setSelectedServerId] = useState(initialServerId ?? null)
@@ -183,28 +196,23 @@ export function MCP({ initialServerId }: MCPProps) {
}
}, [])
- const [refreshingServers, setRefreshingServers] = useState<
- Record
- >({})
const [expandedTools, setExpandedTools] = useState>(() => new Set())
- const handleRemoveServer = useCallback((serverId: string, serverName: string) => {
- setServerToDelete({ id: serverId, name: serverName })
- setShowDeleteDialog(true)
- }, [])
+ const handleRemoveServer = (serverId: string) => {
+ setServerToDeleteId(serverId)
+ }
- const confirmDeleteServer = useCallback(async () => {
- if (!serverToDelete) return
+ const confirmDeleteServer = async () => {
+ if (!serverToDeleteId) return
- setShowDeleteDialog(false)
- const { id: serverId, name: serverName } = serverToDelete
- setServerToDelete(null)
+ const serverId = serverToDeleteId
+ setServerToDeleteId(null)
setDeletingServers((prev) => new Set(prev).add(serverId))
try {
await deleteServerMutation.mutateAsync({ workspaceId, serverId })
- logger.info(`Removed MCP server: ${serverName}`)
+ logger.info(`Removed MCP server: ${serverId}`)
} catch (error) {
logger.error('Failed to remove MCP server:', error)
} finally {
@@ -214,43 +222,36 @@ export function MCP({ initialServerId }: MCPProps) {
return newSet
})
}
- }, [serverToDelete, deleteServerMutation, workspaceId])
-
- const toolsByServer = useMemo(() => {
- return (mcpToolsData || []).reduce(
- (acc, tool) => {
- if (!tool?.serverId) return acc
- if (!acc[tool.serverId]) {
- acc[tool.serverId] = []
- }
- acc[tool.serverId].push(tool)
- return acc
- },
- {} as Record
- )
- }, [mcpToolsData])
-
- const filteredServers = useMemo(() => {
- return (servers || []).filter((server) =>
- server.name?.toLowerCase().includes(searchTerm.toLowerCase())
- )
- }, [servers, searchTerm])
+ }
- const handleViewDetails = useCallback(
- (serverId: string) => {
- setSelectedServerId(serverId)
- forceRefreshTools(workspaceId)
- refetchStoredTools()
+ const toolsByServer = (mcpToolsData || []).reduce(
+ (acc, tool) => {
+ if (!tool?.serverId) return acc
+ if (!acc[tool.serverId]) {
+ acc[tool.serverId] = []
+ }
+ acc[tool.serverId].push(tool)
+ return acc
},
- [workspaceId, forceRefreshTools, refetchStoredTools]
+ {} as Record
+ )
+
+ const filteredServers = (servers || []).filter((server) =>
+ server.name?.toLowerCase().includes(searchTerm.toLowerCase())
)
- const handleBackToList = useCallback(() => {
+ const handleViewDetails = (serverId: string) => {
+ setSelectedServerId(serverId)
+ forceRefreshTools(workspaceId)
+ refetchStoredTools()
+ }
+
+ const handleBackToList = () => {
setSelectedServerId(null)
setExpandedTools(new Set())
- }, [])
+ }
- const toggleToolExpanded = useCallback((toolName: string) => {
+ const toggleToolExpanded = (toolName: string) => {
setExpandedTools((prev) => {
const newSet = new Set(prev)
if (newSet.has(toolName)) {
@@ -260,131 +261,109 @@ export function MCP({ initialServerId }: MCPProps) {
}
return newSet
})
- }, [])
+ }
- const handleRefreshServer = useCallback(
- async (serverId: string) => {
- try {
- setRefreshingServers((prev) => ({ ...prev, [serverId]: { status: 'refreshing' } }))
- const result = await refreshServerMutation.mutateAsync({ workspaceId, serverId })
- logger.info(
- `Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}`
- )
-
- const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
- if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) {
- logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`)
- try {
- const { data: workflowData } = await requestJson(getWorkflowStateContract, {
- params: { id: activeWorkflowId },
- })
- if (workflowData?.state?.blocks) {
- useSubBlockStore
- .getState()
- .initializeFromWorkflow(
- activeWorkflowId,
- workflowData.state.blocks as Record
- )
- }
- } catch (reloadError) {
- logger.warn('Failed to reload workflow subblock values:', reloadError)
- }
- }
+ const handleRefreshServer = async (serverId: string) => {
+ try {
+ const result = await refreshServerMutation.mutateAsync({ workspaceId, serverId })
+ logger.info(
+ `Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}`
+ )
- setRefreshingServers((prev) => ({
- ...prev,
- [serverId]: { status: 'refreshed', workflowsUpdated: result.workflowsUpdated },
- }))
- setTimeout(() => {
- setRefreshingServers((prev) => {
- const newState = { ...prev }
- delete newState[serverId]
- return newState
+ const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
+ if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) {
+ logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`)
+ try {
+ const { data: workflowData } = await requestJson(getWorkflowStateContract, {
+ params: { id: activeWorkflowId },
})
- }, 3000)
- } catch (error) {
- logger.error('Failed to refresh MCP server:', error)
- setRefreshingServers((prev) => {
- const newState = { ...prev }
- delete newState[serverId]
- return newState
- })
+ if (workflowData?.state?.blocks) {
+ useSubBlockStore
+ .getState()
+ .initializeFromWorkflow(
+ activeWorkflowId,
+ workflowData.state.blocks as Record
+ )
+ }
+ } catch (reloadError) {
+ logger.warn('Failed to reload workflow subblock values:', reloadError)
+ }
}
- },
- [refreshServerMutation, workspaceId]
- )
-
- const handleOpenEditModal = useCallback((server: McpServer) => {
- const headers: { key: string; value: string }[] = server.headers
- ? Object.entries(server.headers).map(([key, value]) => ({ key, value }))
- : [{ key: '', value: '' }]
- if (headers.length === 0) headers.push({ key: '', value: '' })
-
- const lastHeader = headers[headers.length - 1]
- if (lastHeader.key !== '' || lastHeader.value !== '') {
- headers.push({ key: '', value: '' })
+ } catch (error) {
+ logger.error('Failed to refresh MCP server:', error)
}
+ }
- setEditInitialData({
- name: server.name || '',
- transport: (server.transport as McpTransport) || 'streamable-http',
- url: server.url || '',
- timeout: 30000,
- headers,
- })
- setShowEditModal(true)
- }, [])
-
- const selectedServer = useMemo(() => {
+ useEffect(() => {
+ if (!refreshServerMutation.isSuccess) return
+ const timeout = window.setTimeout(() => refreshServerMutation.reset(), 3000)
+ return () => window.clearTimeout(timeout)
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- mutation object is unstable; isSuccess flag is the trigger
+ }, [refreshServerMutation.isSuccess])
+
+ const refreshingServerId = refreshServerMutation.isPending
+ ? refreshServerMutation.variables?.serverId
+ : null
+ const refreshedServerId = refreshServerMutation.isSuccess
+ ? refreshServerMutation.variables?.serverId
+ : null
+ const refreshedWorkflowsUpdated = refreshServerMutation.data?.workflowsUpdated
+
+ const editingServer = editingServerId
+ ? (servers.find((s) => s.id === editingServerId) as McpServer | undefined)
+ : undefined
+ const editInitialData = editingServer ? buildEditInitialData(editingServer) : undefined
+
+ const selectedServer = (() => {
if (!selectedServerId) return null
const server = servers.find((s) => s.id === selectedServerId) as McpServer | undefined
if (!server) return null
const serverTools = (toolsByServer[selectedServerId] || []) as McpTool[]
return { server, tools: serverTools }
- }, [selectedServerId, servers, toolsByServer])
+ })()
+
+ const getStoredToolIssues = (
+ serverId: string,
+ toolName: string
+ ): { issue: McpToolIssue; workflowName: string }[] => {
+ const relevantStoredTools = storedTools.filter(
+ (st) => st.serverId === serverId && st.toolName === toolName
+ )
- const getStoredToolIssues = useCallback(
- (serverId: string, toolName: string): { issue: McpToolIssue; workflowName: string }[] => {
- const relevantStoredTools = storedTools.filter(
- (st) => st.serverId === serverId && st.toolName === toolName
+ const serverStates = servers.map((s) => ({
+ id: s.id,
+ url: s.url,
+ connectionStatus: s.connectionStatus,
+ lastError: s.lastError || undefined,
+ }))
+
+ const discoveredTools = mcpToolsData.map((t) => ({
+ serverId: t.serverId,
+ name: t.name,
+ inputSchema: t.inputSchema,
+ }))
+
+ const issues: { issue: McpToolIssue; workflowName: string }[] = []
+
+ for (const storedTool of relevantStoredTools) {
+ const issue = getMcpToolIssue(
+ {
+ serverId: storedTool.serverId,
+ serverUrl: storedTool.serverUrl,
+ toolName: storedTool.toolName,
+ schema: storedTool.schema,
+ },
+ serverStates,
+ discoveredTools
)
- const serverStates = servers.map((s) => ({
- id: s.id,
- url: s.url,
- connectionStatus: s.connectionStatus,
- lastError: s.lastError || undefined,
- }))
-
- const discoveredTools = mcpToolsData.map((t) => ({
- serverId: t.serverId,
- name: t.name,
- inputSchema: t.inputSchema,
- }))
-
- const issues: { issue: McpToolIssue; workflowName: string }[] = []
-
- for (const storedTool of relevantStoredTools) {
- const issue = getMcpToolIssue(
- {
- serverId: storedTool.serverId,
- serverUrl: storedTool.serverUrl,
- toolName: storedTool.toolName,
- schema: storedTool.schema,
- },
- serverStates,
- discoveredTools
- )
-
- if (issue) {
- issues.push({ issue, workflowName: storedTool.workflowName })
- }
+ if (issue) {
+ issues.push({ issue, workflowName: storedTool.workflowName })
}
+ }
- return issues
- },
- [storedTools, servers, mcpToolsData]
- )
+ return issues
+ }
const error = toolsError || serversError
const hasServers = servers && servers.length > 0
@@ -420,12 +399,32 @@ export function MCP({ initialServerId }: MCPProps) {
{server.connectionStatus === 'error' && (
Status
-
+
{server.lastError || 'Unable to connect'}
)}
+ {server.authType === 'oauth' && server.connectionStatus !== 'connected' && (
+
+
+ Authentication
+
+
+ {
+ await startOauthForServer(server.id)
+ }}
+ >
+ {connectingOauthServers.has(server.id) ? 'Connecting…' : 'Connect with OAuth'}
+
+
+
+ )}
+
Tools ({tools.length})
@@ -448,11 +447,12 @@ export function MCP({ initialServerId }: MCPProps) {
key={tool.name}
className='overflow-hidden rounded-md border bg-[var(--surface-3)]'
>
- hasParams && toggleToolExpanded(tool.name)}
className={cn(
- 'flex w-full items-start justify-between px-2.5 py-2 text-left',
+ 'flex h-auto w-full items-start justify-between rounded-none px-2.5 py-2 text-left text-sm',
hasParams && 'cursor-pointer hover-hover:bg-[var(--surface-4)]'
)}
disabled={!hasParams}
@@ -494,7 +494,7 @@ export function MCP({ initialServerId }: MCPProps) {
)}
/>
)}
-
+
{isExpanded && hasParams && (
@@ -561,25 +561,27 @@ export function MCP({ initialServerId }: MCPProps) {
handleRefreshServer(server.id)}
variant='default'
- disabled={!!refreshingServers[server.id]}
+ disabled={refreshingServerId === server.id || refreshedServerId === server.id}
>
- {refreshingServers[server.id]?.status === 'refreshing'
+ {refreshingServerId === server.id
? 'Refreshing...'
- : refreshingServers[server.id]?.status === 'refreshed'
- ? refreshingServers[server.id].workflowsUpdated
- ? `Synced (${refreshingServers[server.id].workflowsUpdated} workflow${refreshingServers[server.id].workflowsUpdated === 1 ? '' : 's'})`
+ : refreshedServerId === server.id
+ ? refreshedWorkflowsUpdated
+ ? `Synced (${refreshedWorkflowsUpdated} workflow${refreshedWorkflowsUpdated === 1 ? '' : 's'})`
: 'Refreshed'
: 'Refresh Tools'}
- handleOpenEditModal(server)} variant='default'>
+ setEditingServerId(server.id)} variant='default'>
Edit
{
+ if (!open) setEditingServerId(null)
+ }}
mode='edit'
initialData={editInitialData}
onSubmit={async (config) => {
@@ -618,7 +620,7 @@ export function MCP({ initialServerId }: MCPProps) {
/>