Compare commits

...

5 Commits

Author SHA1 Message Date
aadamgough
8099e824aa greptile comments resolved 2026-01-10 12:09:10 -08:00
aadamgough
210bf41ffe added validation and greptile comments 2026-01-09 19:42:08 -08:00
aadamgough
b7a3a4a37f Removed comment 2026-01-09 19:07:03 -08:00
aadamgough
4622b05674 fixed component and removed comments 2026-01-09 19:05:11 -08:00
aadamgough
64b382eb49 ui improvement 2026-01-09 18:57:07 -08:00
5 changed files with 450 additions and 126 deletions

View File

@@ -0,0 +1,44 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
const logger = createLogger('ValidateMcpWorkflowsAPI')
/**
* POST /api/mcp/workflow-servers/validate
* Validates if workflows have valid start blocks for MCP usage
*/
export async function POST(request: Request) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { workflowIds } = body
if (!Array.isArray(workflowIds) || workflowIds.length === 0) {
return NextResponse.json({ error: 'workflowIds must be a non-empty array' }, { status: 400 })
}
const results: Record<string, boolean> = {}
for (const workflowId of workflowIds) {
try {
const state = await loadWorkflowFromNormalizedTables(workflowId)
results[workflowId] = hasValidStartBlockInState(state)
} catch (error) {
logger.warn(`Failed to validate workflow ${workflowId}:`, error)
results[workflowId] = false
}
}
return NextResponse.json({ data: results })
} catch (error) {
logger.error('Failed to validate workflows for MCP:', error)
return NextResponse.json({ error: 'Failed to validate workflows' }, { status: 500 })
}
}

View File

@@ -147,6 +147,8 @@ export function McpDeploy({
})
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(new Set())
const [saveError, setSaveError] = useState<string | null>(null)
const isSavingRef = useRef(false)
const parameterSchema = useMemo(
() => generateParameterSchema(inputFormat, parameterDescriptions),
@@ -173,7 +175,7 @@ export function McpDeploy({
[]
)
const selectedServerIds = useMemo(() => {
const actualServerIds = useMemo(() => {
const ids: string[] = []
for (const server of servers) {
const toolInfo = serverToolsMap[server.id]
@@ -184,6 +186,21 @@ export function McpDeploy({
return ids
}, [servers, serverToolsMap])
const [pendingSelectedServerIds, setPendingSelectedServerIds] = useState<string[] | null>(null)
const selectedServerIds = pendingSelectedServerIds ?? actualServerIds
useEffect(() => {
if (isSavingRef.current) return
if (pendingSelectedServerIds !== null) {
const pendingSet = new Set(pendingSelectedServerIds)
const actualSet = new Set(actualServerIds)
if (pendingSet.size === actualSet.size && [...pendingSet].every((id) => actualSet.has(id))) {
setPendingSelectedServerIds(null)
}
}
}, [actualServerIds, pendingSelectedServerIds])
const hasLoadedInitialData = useRef(false)
useEffect(() => {
@@ -241,7 +258,17 @@ export function McpDeploy({
}, [toolName, toolDescription, parameterDescriptions, savedValues])
const hasDeployedTools = selectedServerIds.length > 0
const hasServerSelectionChanges = useMemo(() => {
if (pendingSelectedServerIds === null) return false
const pendingSet = new Set(pendingSelectedServerIds)
const actualSet = new Set(actualServerIds)
if (pendingSet.size !== actualSet.size) return true
return ![...pendingSet].every((id) => actualSet.has(id))
}, [pendingSelectedServerIds, actualServerIds])
const hasChanges = useMemo(() => {
if (hasServerSelectionChanges && selectedServerIds.length > 0) return true
if (!savedValues || !hasDeployedTools) return false
if (toolName !== savedValues.toolName) return true
if (toolDescription !== savedValues.toolDescription) return true
@@ -251,7 +278,15 @@ export function McpDeploy({
return true
}
return false
}, [toolName, toolDescription, parameterDescriptions, hasDeployedTools, savedValues])
}, [
toolName,
toolDescription,
parameterDescriptions,
hasDeployedTools,
savedValues,
hasServerSelectionChanges,
selectedServerIds.length,
])
useEffect(() => {
onCanSaveChange?.(hasChanges && hasDeployedTools && !!toolName.trim())
@@ -262,45 +297,121 @@ export function McpDeploy({
}, [servers.length, onHasServersChange])
/**
* Save tool configuration to all deployed servers
* Save tool configuration to all selected servers.
* This handles both adding to new servers and updating existing tools.
*/
const handleSave = useCallback(async () => {
if (!toolName.trim()) return
if (selectedServerIds.length === 0) return
const toolsToUpdate: Array<{ serverId: string; toolId: string }> = []
for (const server of servers) {
const toolInfo = serverToolsMap[server.id]
if (toolInfo?.tool) {
toolsToUpdate.push({ serverId: server.id, toolId: toolInfo.tool.id })
}
}
if (toolsToUpdate.length === 0) return
isSavingRef.current = true
onSubmittingChange?.(true)
try {
for (const { serverId, toolId } of toolsToUpdate) {
await updateToolMutation.mutateAsync({
setSaveError(null)
const actualSet = new Set(actualServerIds)
const toAdd = selectedServerIds.filter((id) => !actualSet.has(id))
const toRemove = actualServerIds.filter((id) => !selectedServerIds.includes(id))
const toUpdate = selectedServerIds.filter((id) => actualSet.has(id))
const errors: string[] = []
for (const serverId of toAdd) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
try {
await addToolMutation.mutateAsync({
workspaceId,
serverId,
toolId,
workflowId,
toolName: toolName.trim(),
toolDescription: toolDescription.trim() || undefined,
parameterSchema,
})
onAddedToServer?.()
logger.info(`Added workflow ${workflowId} as tool to server ${serverId}`)
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to add to "${serverName}"`)
logger.error(`Failed to add tool to server ${serverId}:`, error)
} finally {
setPendingServerChanges((prev) => {
const next = new Set(prev)
next.delete(serverId)
return next
})
}
// Update saved values after successful save (triggers re-render → hasChanges becomes false)
}
for (const serverId of toRemove) {
const toolInfo = serverToolsMap[serverId]
if (toolInfo?.tool) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
try {
await deleteToolMutation.mutateAsync({
workspaceId,
serverId,
toolId: toolInfo.tool.id,
})
setServerToolsMap((prev) => {
const next = { ...prev }
delete next[serverId]
return next
})
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to remove from "${serverName}"`)
logger.error(`Failed to remove tool from server ${serverId}:`, error)
} finally {
setPendingServerChanges((prev) => {
const next = new Set(prev)
next.delete(serverId)
return next
})
}
}
}
for (const serverId of toUpdate) {
const toolInfo = serverToolsMap[serverId]
if (toolInfo?.tool) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
try {
await updateToolMutation.mutateAsync({
workspaceId,
serverId,
toolId: toolInfo.tool.id,
toolName: toolName.trim(),
toolDescription: toolDescription.trim() || undefined,
parameterSchema,
})
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to update "${serverName}"`)
logger.error(`Failed to update tool on server ${serverId}:`, error)
} finally {
setPendingServerChanges((prev) => {
const next = new Set(prev)
next.delete(serverId)
return next
})
}
}
}
if (errors.length > 0) {
setSaveError(errors.join('. '))
} else {
refetchServers()
setPendingSelectedServerIds(null)
setSavedValues({
toolName,
toolDescription,
parameterDescriptions: { ...parameterDescriptions },
})
onCanSaveChange?.(false)
onSubmittingChange?.(false)
} catch (error) {
logger.error('Failed to save tool configuration:', error)
onSubmittingChange?.(false)
}
isSavingRef.current = false
onSubmittingChange?.(false)
}, [
toolName,
toolDescription,
@@ -309,9 +420,16 @@ export function McpDeploy({
servers,
serverToolsMap,
workspaceId,
workflowId,
selectedServerIds,
actualServerIds,
addToolMutation,
deleteToolMutation,
updateToolMutation,
refetchServers,
onSubmittingChange,
onCanSaveChange,
onAddedToServer,
])
const serverOptions: ComboboxOption[] = useMemo(() => {
@@ -321,83 +439,13 @@ export function McpDeploy({
}))
}, [servers])
const handleServerSelectionChange = useCallback(
async (newSelectedIds: string[]) => {
if (!toolName.trim()) return
const currentIds = new Set(selectedServerIds)
const newIds = new Set(newSelectedIds)
const toAdd = newSelectedIds.filter((id) => !currentIds.has(id))
const toRemove = selectedServerIds.filter((id) => !newIds.has(id))
for (const serverId of toAdd) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
try {
await addToolMutation.mutateAsync({
workspaceId,
serverId,
workflowId,
toolName: toolName.trim(),
toolDescription: toolDescription.trim() || undefined,
parameterSchema,
})
refetchServers()
onAddedToServer?.()
logger.info(`Added workflow ${workflowId} as tool to server ${serverId}`)
} catch (error) {
logger.error('Failed to add tool:', error)
} finally {
setPendingServerChanges((prev) => {
const next = new Set(prev)
next.delete(serverId)
return next
})
}
}
for (const serverId of toRemove) {
const toolInfo = serverToolsMap[serverId]
if (toolInfo?.tool) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
try {
await deleteToolMutation.mutateAsync({
workspaceId,
serverId,
toolId: toolInfo.tool.id,
})
setServerToolsMap((prev) => {
const next = { ...prev }
delete next[serverId]
return next
})
refetchServers()
} catch (error) {
logger.error('Failed to remove tool:', error)
} finally {
setPendingServerChanges((prev) => {
const next = new Set(prev)
next.delete(serverId)
return next
})
}
}
}
},
[
selectedServerIds,
serverToolsMap,
toolName,
toolDescription,
workspaceId,
workflowId,
parameterSchema,
addToolMutation,
deleteToolMutation,
refetchServers,
onAddedToServer,
]
)
/**
* Handle server selection change - only updates local state.
* Actual add/remove operations happen when user clicks Save.
*/
const handleServerSelectionChange = useCallback((newSelectedIds: string[]) => {
setPendingSelectedServerIds(newSelectedIds)
}, [])
const selectedServersLabel = useMemo(() => {
const count = selectedServerIds.length
@@ -563,11 +611,7 @@ export function McpDeploy({
)}
</div>
{addToolMutation.isError && (
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>
{addToolMutation.error?.message || 'Failed to add tool'}
</p>
)}
{saveError && <p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{saveError}</p>}
</form>
)
}

View File

@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Check, Clipboard, Plus, Search } from 'lucide-react'
import { Check, Clipboard, Plus, Search, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
@@ -36,13 +36,108 @@ import { FormField, McpServerSkeleton } from '../mcp/components'
const logger = createLogger('WorkflowMcpServers')
interface WorkflowTagSelectProps {
workflows: { id: string; name: string }[]
selectedIds: string[]
onSelectionChange: (ids: string[]) => void
isLoading?: boolean
disabled?: boolean
}
/**
* Multi-select workflow selector using Combobox.
* Shows selected workflows as removable badges inside the trigger.
*/
function WorkflowTagSelect({
workflows,
selectedIds,
onSelectionChange,
isLoading = false,
disabled = false,
}: WorkflowTagSelectProps) {
const options: ComboboxOption[] = useMemo(() => {
return workflows.map((w) => ({
label: w.name,
value: w.id,
}))
}, [workflows])
const selectedWorkflows = useMemo(() => {
return workflows.filter((w) => selectedIds.includes(w.id))
}, [workflows, selectedIds])
const validSelectedIds = useMemo(() => {
const workflowIds = new Set(workflows.map((w) => w.id))
return selectedIds.filter((id) => workflowIds.has(id))
}, [workflows, selectedIds])
const handleRemove = (e: React.MouseEvent, id: string) => {
e.preventDefault()
e.stopPropagation()
onSelectionChange(selectedIds.filter((i) => i !== id))
}
const overlayContent = useMemo(() => {
if (selectedWorkflows.length === 0) {
return null
}
return (
<div className='flex items-center gap-[4px] overflow-hidden'>
{selectedWorkflows.slice(0, 2).map((w) => (
<Badge
key={w.id}
variant='outline'
className='pointer-events-auto cursor-pointer gap-[4px] rounded-[6px] px-[8px] py-[2px] text-[11px]'
onMouseDown={(e) => handleRemove(e, w.id)}
>
{w.name}
<X className='h-3 w-3' />
</Badge>
))}
{selectedWorkflows.length > 2 && (
<Badge variant='outline' className='rounded-[6px] px-[8px] py-[2px] text-[11px]'>
+{selectedWorkflows.length - 2}
</Badge>
)}
</div>
)
}, [selectedWorkflows, selectedIds])
const isEmpty = workflows.length === 0
if (isLoading) {
return <Skeleton className='h-[34px] w-full rounded-[6px]' />
}
return (
<Combobox
options={options}
multiSelect
multiSelectValues={validSelectedIds}
onMultiSelectChange={onSelectionChange}
placeholder={isEmpty ? 'No deployed workflows available' : 'Select deployed workflows...'}
overlayContent={overlayContent}
searchable
searchPlaceholder='Search workflows...'
disabled={disabled || isEmpty}
/>
)
}
interface ServerDetailViewProps {
workspaceId: string
serverId: string
onBack: () => void
onToolsChanged?: () => void
}
function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewProps) {
function ServerDetailView({
workspaceId,
serverId,
onBack,
onToolsChanged,
}: ServerDetailViewProps) {
const { data, isLoading, error, refetch } = useWorkflowMcpServer(workspaceId, serverId)
const { data: deployedWorkflows = [], isLoading: isLoadingWorkflows } =
useDeployedWorkflows(workspaceId)
@@ -81,6 +176,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
toolId: toolToDelete.id,
})
setToolToDelete(null)
onToolsChanged?.()
} catch (err) {
logger.error('Failed to delete tool:', err)
}
@@ -97,6 +193,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
setShowAddWorkflow(false)
setSelectedWorkflowId(null)
refetch()
onToolsChanged?.()
} catch (err) {
logger.error('Failed to add workflow:', err)
}
@@ -120,6 +217,8 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
return availableWorkflows.find((w) => w.id === selectedWorkflowId)
}, [availableWorkflows, selectedWorkflowId])
const selectedWorkflowInvalid = selectedWorkflow && selectedWorkflow.hasStartBlock !== true
if (isLoading) {
return (
<div className='flex h-full flex-col gap-[16px]'>
@@ -178,6 +277,17 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
</div>
</div>
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
Authentication Header
</span>
<div className='rounded-[6px] border bg-[var(--surface-3)] px-[10px] py-[8px]'>
<code className='font-mono text-[12px] text-[var(--text-primary)]'>
X-API-Key: {'<your-api-key>'}
</code>
</div>
</div>
<div className='flex flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
@@ -407,7 +517,12 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
) : undefined
}
/>
{addToolMutation.isError && (
{selectedWorkflowInvalid && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
Workflow must have a Start block to be used as an MCP tool
</p>
)}
{addToolMutation.isError && !selectedWorkflowInvalid && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
{addToolMutation.error?.message || 'Failed to add workflow'}
</p>
@@ -428,7 +543,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
<Button
variant='tertiary'
onClick={handleAddWorkflow}
disabled={!selectedWorkflowId || addToolMutation.isPending}
disabled={!selectedWorkflowId || selectedWorkflowInvalid || addToolMutation.isPending}
>
{addToolMutation.isPending ? 'Adding...' : 'Add Workflow'}
</Button>
@@ -439,24 +554,44 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
)
}
interface WorkflowMcpServersProps {
resetKey?: number
}
/**
* MCP Servers settings component.
* Allows users to create and manage MCP servers that expose workflows as tools.
*/
export function WorkflowMcpServers() {
export function WorkflowMcpServers({ resetKey }: WorkflowMcpServersProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const { data: servers = [], isLoading, error } = useWorkflowMcpServers(workspaceId)
const {
data: servers = [],
isLoading,
error,
refetch: refetchServers,
} = useWorkflowMcpServers(workspaceId)
const { data: deployedWorkflows = [], isLoading: isLoadingWorkflows } =
useDeployedWorkflows(workspaceId)
const createServerMutation = useCreateWorkflowMcpServer()
const addToolMutation = useAddWorkflowMcpTool()
const deleteServerMutation = useDeleteWorkflowMcpServer()
const [searchTerm, setSearchTerm] = useState('')
const [showAddForm, setShowAddForm] = useState(false)
const [formData, setFormData] = useState({ name: '' })
const [selectedWorkflowIds, setSelectedWorkflowIds] = useState<string[]>([])
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
const [serverToDelete, setServerToDelete] = useState<WorkflowMcpServer | null>(null)
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
const [createError, setCreateError] = useState<string | null>(null)
useEffect(() => {
if (resetKey !== undefined) {
setSelectedServerId(null)
}
}, [resetKey])
const filteredServers = useMemo(() => {
if (!searchTerm.trim()) return servers
@@ -464,23 +599,64 @@ export function WorkflowMcpServers() {
return servers.filter((server) => server.name.toLowerCase().includes(search))
}, [servers, searchTerm])
const invalidWorkflows = useMemo(() => {
return selectedWorkflowIds
.map((id) => deployedWorkflows.find((w) => w.id === id))
.filter((w) => w && w.hasStartBlock !== true)
.map((w) => w!.name)
}, [selectedWorkflowIds, deployedWorkflows])
const hasInvalidWorkflows = invalidWorkflows.length > 0
const resetForm = useCallback(() => {
setFormData({ name: '' })
setSelectedWorkflowIds([])
setShowAddForm(false)
setCreateError(null)
}, [])
const handleCreateServer = async () => {
if (!formData.name.trim()) return
setCreateError(null)
let server: WorkflowMcpServer | undefined
try {
await createServerMutation.mutateAsync({
server = await createServerMutation.mutateAsync({
workspaceId,
name: formData.name.trim(),
})
resetForm()
} catch (err) {
logger.error('Failed to create server:', err)
setCreateError(err instanceof Error ? err.message : 'Failed to create server')
return
}
if (selectedWorkflowIds.length > 0 && server?.id) {
const workflowErrors: string[] = []
for (const workflowId of selectedWorkflowIds) {
try {
await addToolMutation.mutateAsync({
workspaceId,
serverId: server.id,
workflowId,
})
} catch (err) {
const workflowName =
deployedWorkflows.find((w) => w.id === workflowId)?.name || workflowId
workflowErrors.push(workflowName)
logger.error(`Failed to add workflow ${workflowId} to server:`, err)
}
}
if (workflowErrors.length > 0) {
setCreateError(`Server created but failed to add workflows: ${workflowErrors.join(', ')}`)
return
}
}
resetForm()
}
const handleDeleteServer = async () => {
@@ -516,6 +692,7 @@ export function WorkflowMcpServers() {
workspaceId={workspaceId}
serverId={selectedServerId}
onBack={() => setSelectedServerId(null)}
onToolsChanged={refetchServers}
/>
)
}
@@ -544,7 +721,11 @@ export function WorkflowMcpServers() {
{shouldShowForm && !isLoading && (
<div className='rounded-[8px] border p-[10px]'>
<div className='flex flex-col gap-[8px]'>
<div className='flex flex-col gap-[12px]'>
<p className='text-[12px] text-[var(--text-secondary)] leading-relaxed'>
Create an MCP server to expose your deployed workflows as tools.
</p>
<FormField label='Server Name'>
<EmcnInput
placeholder='e.g., My MCP Server'
@@ -554,16 +735,44 @@ export function WorkflowMcpServers() {
/>
</FormField>
<div className='flex items-center justify-end gap-[8px] pt-[12px]'>
<p className='ml-[112px] text-[12px] text-[var(--text-secondary)]'>
Select deployed workflows to add to this MCP server. Each workflow will be available
as a tool.
</p>
<FormField label='Workflows'>
<WorkflowTagSelect
workflows={deployedWorkflows}
selectedIds={selectedWorkflowIds}
onSelectionChange={setSelectedWorkflowIds}
isLoading={isLoadingWorkflows}
disabled={deployedWorkflows.length === 0}
/>
</FormField>
{hasInvalidWorkflows && (
<p className='ml-[112px] text-[11px] text-[var(--text-error)] leading-tight'>
Workflow must have a Start block to be used as an MCP tool
</p>
)}
{createError && <p className='text-[12px] text-[var(--text-error)]'>{createError}</p>}
<div className='flex items-center justify-end gap-[8px] pt-[4px]'>
<Button variant='ghost' onClick={resetForm}>
Cancel
</Button>
<Button
onClick={handleCreateServer}
disabled={!isFormValid || createServerMutation.isPending}
disabled={
!isFormValid ||
hasInvalidWorkflows ||
createServerMutation.isPending ||
addToolMutation.isPending
}
variant='tertiary'
>
{createServerMutation.isPending ? 'Adding...' : 'Add Server'}
{createServerMutation.isPending || addToolMutation.isPending
? 'Adding...'
: 'Add Server'}
</Button>
</div>
</div>

View File

@@ -162,6 +162,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
const { initialSection, mcpServerId, clearInitialState } = useSettingsModalStore()
const [pendingMcpServerId, setPendingMcpServerId] = useState<string | null>(null)
const [workflowMcpResetKey, setWorkflowMcpResetKey] = useState(0)
const { data: session } = useSession()
const queryClient = useQueryClient()
const { data: organizationsData } = useOrganizations()
@@ -245,7 +246,12 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const handleSectionChange = useCallback(
(sectionId: SettingsSection) => {
if (sectionId === activeSection) return
if (sectionId === activeSection) {
if (sectionId === 'workflow-mcp-servers') {
setWorkflowMcpResetKey((prev) => prev + 1)
}
return
}
if (activeSection === 'environment' && environmentBeforeLeaveHandler.current) {
environmentBeforeLeaveHandler.current(() => setActiveSection(sectionId))
@@ -470,7 +476,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
{activeSection === 'copilot' && <Copilot />}
{activeSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
{activeSection === 'custom-tools' && <CustomTools />}
{activeSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{activeSection === 'workflow-mcp-servers' && (
<WorkflowMcpServers resetKey={workflowMcpResetKey} />
)}
</SModalMainBody>
</SModalMain>
</SModalContent>

View File

@@ -11,6 +11,7 @@ export interface DeployedWorkflow {
name: string
description: string | null
isDeployed: boolean
hasStartBlock?: boolean
}
/**
@@ -442,14 +443,32 @@ async function fetchDeployedWorkflows(workspaceId: string): Promise<DeployedWork
const { data }: { data: any[] } = await response.json()
return data
.filter((w) => w.isDeployed)
.map((w) => ({
id: w.id,
name: w.name,
description: w.description,
isDeployed: w.isDeployed,
}))
const deployedWorkflows = data.filter((w) => w.isDeployed)
let startBlockMap: Record<string, boolean> = {}
if (deployedWorkflows.length > 0) {
try {
const validateResponse = await fetch('/api/mcp/workflow-servers/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workflowIds: deployedWorkflows.map((w) => w.id) }),
})
if (validateResponse.ok) {
const validateData = await validateResponse.json()
startBlockMap = validateData.data || {}
}
} catch (error) {
logger.warn('Failed to validate workflows for MCP:', error)
}
}
return deployedWorkflows.map((w) => ({
id: w.id,
name: w.name,
description: w.description,
isDeployed: w.isDeployed,
hasStartBlock: startBlockMap[w.id],
}))
}
/**