Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import {
Button,
ButtonGroup,
ButtonGroupItem,
Combobox,
type ComboboxOption,
Input as EmcnInput,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea,
} from '@/components/emcn'
import { FormField } from '@/app/workspace/[workspaceId]/settings/components/mcp/components'
import { useCreateWorkflowMcpServer } from '@/hooks/queries/workflow-mcp-servers'

const logger = createLogger('CreateWorkflowMcpServerModal')

const INITIAL_FORM_DATA: { name: string; description: string; isPublic: boolean } = {
name: '',
description: '',
isPublic: false,
}

interface CreateWorkflowMcpServerModalProps {
open: boolean
onOpenChange: (open: boolean) => void
workspaceId: string
workflowOptions?: ComboboxOption[]
isLoadingWorkflows?: boolean
}

export function CreateWorkflowMcpServerModal({
open,
onOpenChange,
workspaceId,
workflowOptions,
isLoadingWorkflows = false,
}: CreateWorkflowMcpServerModalProps) {
const createServerMutation = useCreateWorkflowMcpServer()

const [formData, setFormData] = useState({ ...INITIAL_FORM_DATA })
const [selectedWorkflowIds, setSelectedWorkflowIds] = useState<string[]>([])

const isFormValid = formData.name.trim().length > 0

useEffect(() => {
if (open) {
setFormData({ ...INITIAL_FORM_DATA })
setSelectedWorkflowIds([])
}
}, [open])

const handleCreateServer = useCallback(async () => {
if (!formData.name.trim()) return

try {
await createServerMutation.mutateAsync({
workspaceId,
name: formData.name.trim(),
description: formData.description.trim() || undefined,
isPublic: formData.isPublic,
workflowIds: selectedWorkflowIds.length > 0 ? selectedWorkflowIds : undefined,
})
onOpenChange(false)
} catch (err) {
logger.error('Failed to create server:', err)
}
}, [formData, selectedWorkflowIds, workspaceId, onOpenChange])

const showWorkflows = workflowOptions !== undefined

return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>Add New MCP Server</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-3'>
<FormField label='Server Name'>
<EmcnInput
placeholder='e.g., My MCP Server'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className='h-9'
/>
</FormField>

<FormField label='Description'>
<Textarea
placeholder='Describe what this MCP server does (optional)'
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className='min-h-[60px] resize-none'
/>
</FormField>

{showWorkflows && (
<FormField label='Workflows'>
<Combobox
options={workflowOptions ?? []}
multiSelect
multiSelectValues={selectedWorkflowIds}
onMultiSelectChange={setSelectedWorkflowIds}
placeholder='Select workflows...'
searchable
searchPlaceholder='Search workflows...'
isLoading={isLoadingWorkflows}
disabled={createServerMutation.isPending}
emptyMessage='No deployed workflows available'
overlayContent={
selectedWorkflowIds.length > 0 ? (
<span className='text-[var(--text-primary)]'>
{selectedWorkflowIds.length} workflow
{selectedWorkflowIds.length !== 1 ? 's' : ''} selected
</span>
) : undefined
}
/>
</FormField>
)}

<FormField label='Access'>
<div className='flex items-center gap-3'>
<ButtonGroup
value={formData.isPublic ? 'public' : 'private'}
onValueChange={(value) =>
setFormData({ ...formData, isPublic: value === 'public' })
}
>
<ButtonGroupItem value='private'>API Key</ButtonGroupItem>
<ButtonGroupItem value='public'>Public</ButtonGroupItem>
</ButtonGroup>
{formData.isPublic && (
<span className='text-[var(--text-muted)] text-xs'>
No authentication required
</span>
)}
</div>
</FormField>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleCreateServer}
disabled={!isFormValid || createServerMutation.isPending}
variant='primary'
>
{createServerMutation.isPending ? 'Adding...' : 'Add Server'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import { useApiKeys } from '@/hooks/queries/api-keys'
import { useCreateMcpServer } from '@/hooks/queries/mcp'
import {
useAddWorkflowMcpTool,
useCreateWorkflowMcpServer,
useDeleteWorkflowMcpServer,
useDeleteWorkflowMcpTool,
useDeployedWorkflows,
Expand All @@ -49,6 +48,7 @@ import {
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
import { CreateApiKeyModal } from '../api-keys/components'
import { FormField, McpServerSkeleton } from '../mcp/components'
import { CreateWorkflowMcpServerModal } from './create-workflow-mcp-server-modal'

const logger = createLogger('WorkflowMcpServers')

Expand Down Expand Up @@ -955,13 +955,10 @@ export function WorkflowMcpServers() {
const { data: servers = [], isLoading, error } = useWorkflowMcpServers(workspaceId)
const { data: deployedWorkflows = [], isLoading: isLoadingWorkflows } =
useDeployedWorkflows(workspaceId)
const createServerMutation = useCreateWorkflowMcpServer()
const deleteServerMutation = useDeleteWorkflowMcpServer()

const [searchTerm, setSearchTerm] = useState('')
const [showAddModal, setShowAddModal] = useState(false)
const [formData, setFormData] = useState({ name: '', description: '', isPublic: false })
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())
Expand All @@ -979,29 +976,6 @@ export function WorkflowMcpServers() {
}))
}, [deployedWorkflows])

const resetForm = useCallback(() => {
setFormData({ name: '', description: '', isPublic: false })
setSelectedWorkflowIds([])
setShowAddModal(false)
}, [])

const handleCreateServer = async () => {
if (!formData.name.trim()) return

try {
await createServerMutation.mutateAsync({
workspaceId,
name: formData.name.trim(),
description: formData.description.trim() || undefined,
isPublic: formData.isPublic,
workflowIds: selectedWorkflowIds.length > 0 ? selectedWorkflowIds : undefined,
})
resetForm()
} catch (err) {
logger.error('Failed to create server:', err)
}
}

const handleDeleteServer = async () => {
if (!serverToDelete) return

Expand All @@ -1026,7 +1000,6 @@ export function WorkflowMcpServers() {

const hasServers = servers.length > 0
const showNoResults = searchTerm.trim() && filteredServers.length === 0 && hasServers
const isFormValid = formData.name.trim().length > 0

if (selectedServerId) {
return (
Expand Down Expand Up @@ -1123,86 +1096,13 @@ export function WorkflowMcpServers() {
</div>
</div>

<Modal open={showAddModal} onOpenChange={(open) => !open && resetForm()}>
<ModalContent>
<ModalHeader>Add New MCP Server</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-3'>
<FormField label='Server Name'>
<EmcnInput
placeholder='e.g., My MCP Server'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className='h-9'
/>
</FormField>

<FormField label='Description'>
<Textarea
placeholder='Describe what this MCP server does (optional)'
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className='min-h-[60px] resize-none'
/>
</FormField>

<FormField label='Workflows'>
<Combobox
options={workflowOptions}
multiSelect
multiSelectValues={selectedWorkflowIds}
onMultiSelectChange={setSelectedWorkflowIds}
placeholder='Select workflows...'
searchable
searchPlaceholder='Search workflows...'
isLoading={isLoadingWorkflows}
disabled={createServerMutation.isPending}
emptyMessage='No deployed workflows available'
overlayContent={
selectedWorkflowIds.length > 0 ? (
<span className='text-[var(--text-primary)]'>
{selectedWorkflowIds.length} workflow
{selectedWorkflowIds.length !== 1 ? 's' : ''} selected
</span>
) : undefined
}
/>
</FormField>

<FormField label='Access'>
<div className='flex items-center gap-3'>
<ButtonGroup
value={formData.isPublic ? 'public' : 'private'}
onValueChange={(value) =>
setFormData({ ...formData, isPublic: value === 'public' })
}
>
<ButtonGroupItem value='private'>API Key</ButtonGroupItem>
<ButtonGroupItem value='public'>Public</ButtonGroupItem>
</ButtonGroup>
{formData.isPublic && (
<span className='text-[var(--text-muted)] text-xs'>
No authentication required
</span>
)}
</div>
</FormField>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={resetForm}>
Cancel
</Button>
<Button
onClick={handleCreateServer}
disabled={!isFormValid || createServerMutation.isPending}
variant='primary'
>
{createServerMutation.isPending ? 'Adding...' : 'Add Server'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<CreateWorkflowMcpServerModal
open={showAddModal}
onOpenChange={setShowAddModal}
workspaceId={workspaceId}
workflowOptions={workflowOptions}
isLoadingWorkflows={isLoadingWorkflows}
/>

<Modal open={!!serverToDelete} onOpenChange={(open) => !open && setServerToDelete(null)}>
<ModalContent size='sm'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-to
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import type { InputFormatField } from '@/lib/workflows/types'
import { McpServerFormModal } from '@/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal'
import { useAllowedMcpDomains, useCreateMcpServer } from '@/hooks/queries/mcp'
import { CreateWorkflowMcpServerModal } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/create-workflow-mcp-server-modal'
import {
useAddWorkflowMcpTool,
useDeleteWorkflowMcpTool,
Expand All @@ -28,7 +27,6 @@ import {
type WorkflowMcpServer,
type WorkflowMcpTool,
} from '@/hooks/queries/workflow-mcp-servers'
import { useAvailableEnvVarKeys } from '@/hooks/use-available-env-vars'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

Expand Down Expand Up @@ -102,11 +100,7 @@ export function McpDeploy({
}: McpDeployProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const [showMcpModal, setShowMcpModal] = useState(false)

const createMcpServer = useCreateMcpServer()
const { data: allowedMcpDomains = null } = useAllowedMcpDomains()
const availableEnvVars = useAvailableEnvVarKeys(workspaceId)
const [showCreateModal, setShowCreateModal] = useState(false)

const { data: servers = [], isLoading: isLoadingServers } = useWorkflowMcpServers(workspaceId)
const addToolMutation = useAddWorkflowMcpTool()
Expand Down Expand Up @@ -473,22 +467,16 @@ export function McpDeploy({
<>
<div className='flex h-full flex-col items-center justify-center gap-3'>
<p className='text-[13px] text-[var(--text-muted)]'>
Create an MCP Server in Settings → MCP Servers first.
Create an MCP Server to expose your workflows as tools.
</p>
<Button variant='tertiary' onClick={() => setShowMcpModal(true)}>
<Button variant='tertiary' onClick={() => setShowCreateModal(true)}>
Create MCP Server
</Button>
</div>
<McpServerFormModal
open={showMcpModal}
onOpenChange={setShowMcpModal}
mode='add'
onSubmit={async (config) => {
await createMcpServer.mutateAsync({ workspaceId, config: { ...config, enabled: true } })
}}
<CreateWorkflowMcpServerModal
open={showCreateModal}
onOpenChange={setShowCreateModal}
workspaceId={workspaceId}
availableEnvVars={availableEnvVars}
allowedMcpDomains={allowedMcpDomains}
/>
</>
)
Expand Down
Loading