mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(mcp): surface better errors for MCP connection failures (#1796)
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
'use client'
|
||||
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { Button, Input, Label } from '@/components/ui'
|
||||
import { EnvVarDropdown } from '@/components/ui/env-var-dropdown'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import type { McpServerFormData, McpServerTestResult } from '../types'
|
||||
|
||||
interface AddServerFormProps {
|
||||
formData: McpServerFormData
|
||||
testResult: McpServerTestResult | null
|
||||
isTestingConnection: boolean
|
||||
isAddingServer: boolean
|
||||
serversLoading: boolean
|
||||
showEnvVars: boolean
|
||||
activeInputField: 'url' | 'header-key' | 'header-value' | null
|
||||
activeHeaderIndex: number | null
|
||||
envSearchTerm: string
|
||||
cursorPosition: number
|
||||
urlScrollLeft: number
|
||||
headerScrollLeft: Record<string, number>
|
||||
workspaceId: string
|
||||
urlInputRef: React.RefObject<HTMLInputElement | null>
|
||||
onNameChange: (value: string) => void
|
||||
onInputChange: (
|
||||
field: 'url' | 'header-key' | 'header-value',
|
||||
value: string,
|
||||
index?: number
|
||||
) => void
|
||||
onUrlScroll: (scrollLeft: number) => void
|
||||
onHeaderScroll: (key: string, scrollLeft: number) => void
|
||||
onEnvVarSelect: (value: string) => void
|
||||
onEnvVarClose: () => void
|
||||
onAddHeader: () => void
|
||||
onRemoveHeader: (key: string) => void
|
||||
onTestConnection: () => void
|
||||
onCancel: () => void
|
||||
onAddServer: () => void
|
||||
onClearTestResult: () => void
|
||||
}
|
||||
|
||||
export function AddServerForm({
|
||||
formData,
|
||||
testResult,
|
||||
isTestingConnection,
|
||||
isAddingServer,
|
||||
serversLoading,
|
||||
showEnvVars,
|
||||
activeInputField,
|
||||
activeHeaderIndex,
|
||||
envSearchTerm,
|
||||
cursorPosition,
|
||||
urlScrollLeft,
|
||||
headerScrollLeft,
|
||||
workspaceId,
|
||||
urlInputRef,
|
||||
onNameChange,
|
||||
onInputChange,
|
||||
onUrlScroll,
|
||||
onHeaderScroll,
|
||||
onEnvVarSelect,
|
||||
onEnvVarClose,
|
||||
onAddHeader,
|
||||
onRemoveHeader,
|
||||
onTestConnection,
|
||||
onCancel,
|
||||
onAddServer,
|
||||
onClearTestResult,
|
||||
}: AddServerFormProps) {
|
||||
return (
|
||||
<div className='rounded-[8px] border bg-background p-3 shadow-xs'>
|
||||
<div className='space-y-1.5'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Label className='w-[100px] shrink-0 font-normal text-sm'>Server Name</Label>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
placeholder='e.g., My MCP Server'
|
||||
value={formData.name}
|
||||
onChange={(e) => {
|
||||
if (testResult) onClearTestResult()
|
||||
onNameChange(e.target.value)
|
||||
}}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Label className='w-[100px] shrink-0 font-normal text-sm'>Server URL</Label>
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
ref={urlInputRef}
|
||||
placeholder='https://mcp.server.dev/{{YOUR_API_KEY}}/sse'
|
||||
value={formData.url}
|
||||
onChange={(e) => onInputChange('url', e.target.value)}
|
||||
onScroll={(e) => onUrlScroll(e.currentTarget.scrollLeft)}
|
||||
onInput={(e) => onUrlScroll(e.currentTarget.scrollLeft)}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
{/* Overlay for styled text display */}
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{ transform: `translateX(-${urlScrollLeft}px)` }}
|
||||
>
|
||||
{formatDisplayText(formData.url || '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables Dropdown */}
|
||||
{showEnvVars && activeInputField === 'url' && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={onEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={formData.url || ''}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={onEnvVarClose}
|
||||
className='w-full'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(formData.headers || {}).map(([key, value], index) => (
|
||||
<div key={index} className='relative flex items-center justify-between gap-3'>
|
||||
<Label className='w-[100px] shrink-0 font-normal text-sm'>Header</Label>
|
||||
<div className='relative flex flex-1 gap-2'>
|
||||
{/* Header Key Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Name'
|
||||
value={key}
|
||||
onChange={(e) => onInputChange('header-key', e.target.value, index)}
|
||||
onScroll={(e) => onHeaderScroll(`key-${index}`, e.currentTarget.scrollLeft)}
|
||||
onInput={(e) => onHeaderScroll(`key-${index}`, e.currentTarget.scrollLeft)}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(key || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Value Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Value'
|
||||
value={value}
|
||||
onChange={(e) => onInputChange('header-value', e.target.value, index)}
|
||||
onScroll={(e) => onHeaderScroll(`value-${index}`, e.currentTarget.scrollLeft)}
|
||||
onInput={(e) => onHeaderScroll(`value-${index}`, e.currentTarget.scrollLeft)}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(value || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => onRemoveHeader(key)}
|
||||
className='h-9 w-9 p-0 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
|
||||
{/* Environment Variables Dropdown for Header Key */}
|
||||
{showEnvVars && activeInputField === 'header-key' && activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={onEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={key}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={onEnvVarClose}
|
||||
className='w-full'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Environment Variables Dropdown for Header Value */}
|
||||
{showEnvVars &&
|
||||
activeInputField === 'header-value' &&
|
||||
activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={onEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={value}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={onEnvVarClose}
|
||||
className='w-full'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='w-[100px] shrink-0' />
|
||||
<div className='flex-1'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onAddHeader}
|
||||
className='h-9 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<Plus className='mr-2 h-3 w-3' />
|
||||
Add Header
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-border border-t pt-2'>
|
||||
<div className='space-y-1.5'>
|
||||
{/* Error message above buttons */}
|
||||
{testResult && !testResult.success && (
|
||||
<p className='text-red-600 text-sm'>{testResult.error || testResult.message}</p>
|
||||
)}
|
||||
|
||||
{/* Buttons row */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onTestConnection}
|
||||
disabled={isTestingConnection || !formData.name.trim() || !formData.url?.trim()}
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
{isTestingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
{testResult?.success && <span className='text-green-600 text-xs'>✓ Connected</span>}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onCancel}
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={onAddServer}
|
||||
disabled={
|
||||
serversLoading ||
|
||||
isAddingServer ||
|
||||
!formData.name.trim() ||
|
||||
!formData.url?.trim()
|
||||
}
|
||||
className='h-9 rounded-[8px]'
|
||||
>
|
||||
{serversLoading || isAddingServer ? 'Adding...' : 'Add Server'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AlertCircle, Plus, Search, X } from 'lucide-react'
|
||||
import { AlertCircle, Plus, Search } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Alert, AlertDescription, Button, Input, Label, Skeleton } from '@/components/ui'
|
||||
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Alert, AlertDescription, Button, Input, Skeleton } from '@/components/ui'
|
||||
import { checkEnvVarTrigger } from '@/components/ui/env-var-dropdown'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { McpTransport } from '@/lib/mcp/types'
|
||||
import { useMcpServerTest } from '@/hooks/use-mcp-server-test'
|
||||
import { useMcpTools } from '@/hooks/use-mcp-tools'
|
||||
import { useMcpServersStore } from '@/stores/mcp-servers/store'
|
||||
import { AddServerForm } from './components/add-server-form'
|
||||
import type { McpServerFormData } from './types'
|
||||
|
||||
const logger = createLogger('McpSettings')
|
||||
|
||||
interface McpServerFormData {
|
||||
name: string
|
||||
transport: McpTransport
|
||||
url?: string
|
||||
timeout?: number
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export function MCP() {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
@@ -46,7 +38,6 @@ export function MCP() {
|
||||
headers: {}, // Start with no headers
|
||||
})
|
||||
|
||||
// Environment variable dropdown state
|
||||
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||
const [envSearchTerm, setEnvSearchTerm] = useState('')
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
@@ -56,17 +47,13 @@ export function MCP() {
|
||||
const [activeHeaderIndex, setActiveHeaderIndex] = useState<number | null>(null)
|
||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// MCP server testing
|
||||
const { testResult, isTestingConnection, testConnection, clearTestResult } = useMcpServerTest()
|
||||
|
||||
// Loading state for adding server
|
||||
const [isAddingServer, setIsAddingServer] = useState(false)
|
||||
|
||||
// State for tracking input scroll position
|
||||
const [urlScrollLeft, setUrlScrollLeft] = useState(0)
|
||||
const [headerScrollLeft, setHeaderScrollLeft] = useState<Record<string, number>>({})
|
||||
|
||||
// Handle environment variable selection
|
||||
const handleEnvVarSelect = useCallback(
|
||||
(newValue: string) => {
|
||||
if (activeInputField === 'url') {
|
||||
@@ -93,7 +80,6 @@ export function MCP() {
|
||||
[activeInputField, activeHeaderIndex, formData.headers]
|
||||
)
|
||||
|
||||
// Handle input change with env var detection
|
||||
const handleInputChange = useCallback(
|
||||
(field: 'url' | 'header-key' | 'header-value', value: string, headerIndex?: number) => {
|
||||
const input = document.activeElement as HTMLInputElement
|
||||
@@ -101,12 +87,10 @@ export function MCP() {
|
||||
|
||||
setCursorPosition(pos)
|
||||
|
||||
// Clear test result when any field changes
|
||||
if (testResult) {
|
||||
clearTestResult()
|
||||
}
|
||||
|
||||
// Check if we should show the environment variables dropdown
|
||||
const envVarTrigger = checkEnvVarTrigger(value, pos)
|
||||
setShowEnvVars(envVarTrigger.show)
|
||||
setEnvSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '')
|
||||
@@ -119,7 +103,6 @@ export function MCP() {
|
||||
setActiveHeaderIndex(null)
|
||||
}
|
||||
|
||||
// Update form data
|
||||
if (field === 'url') {
|
||||
setFormData((prev) => ({ ...prev, url: value }))
|
||||
} else if (field === 'header-key' && headerIndex !== undefined) {
|
||||
@@ -159,7 +142,6 @@ export function MCP() {
|
||||
|
||||
setIsAddingServer(true)
|
||||
try {
|
||||
// If no test has been done, test first
|
||||
if (!testResult) {
|
||||
const result = await testConnection({
|
||||
name: formData.name,
|
||||
@@ -170,13 +152,11 @@ export function MCP() {
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
// If test fails, don't proceed
|
||||
if (!result.success) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a failed test result, don't proceed
|
||||
if (testResult && !testResult.success) {
|
||||
return
|
||||
}
|
||||
@@ -192,13 +172,12 @@ export function MCP() {
|
||||
|
||||
logger.info(`Added MCP server: ${formData.name}`)
|
||||
|
||||
// Reset form and hide form immediately after server creation
|
||||
setFormData({
|
||||
name: '',
|
||||
transport: 'streamable-http',
|
||||
url: '',
|
||||
timeout: 30000,
|
||||
headers: {}, // Reset with no headers
|
||||
headers: {},
|
||||
})
|
||||
setShowAddForm(false)
|
||||
setShowEnvVars(false)
|
||||
@@ -206,7 +185,6 @@ export function MCP() {
|
||||
setActiveHeaderIndex(null)
|
||||
clearTestResult()
|
||||
|
||||
// Refresh tools in the background without waiting
|
||||
refreshTools(true) // Force refresh after adding server
|
||||
} catch (error) {
|
||||
logger.error('Failed to add MCP server:', error)
|
||||
@@ -225,24 +203,21 @@ export function MCP() {
|
||||
|
||||
const handleRemoveServer = useCallback(
|
||||
async (serverId: string) => {
|
||||
// Add server to deleting set
|
||||
setDeletingServers((prev) => new Set(prev).add(serverId))
|
||||
|
||||
try {
|
||||
await deleteServer(workspaceId, serverId)
|
||||
await refreshTools(true) // Force refresh after removing server
|
||||
await refreshTools(true)
|
||||
|
||||
logger.info(`Removed MCP server: ${serverId}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove MCP server:', error)
|
||||
// Remove from deleting set on error so user can try again
|
||||
setDeletingServers((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
newSet.delete(serverId)
|
||||
return newSet
|
||||
})
|
||||
} finally {
|
||||
// Remove from deleting set after successful deletion
|
||||
setDeletingServers((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
newSet.delete(serverId)
|
||||
@@ -253,16 +228,15 @@ export function MCP() {
|
||||
[deleteServer, refreshTools, workspaceId]
|
||||
)
|
||||
|
||||
// Load data on mount only
|
||||
useEffect(() => {
|
||||
fetchServers(workspaceId)
|
||||
refreshTools() // Don't force refresh on mount
|
||||
refreshTools()
|
||||
}, [fetchServers, refreshTools, workspaceId])
|
||||
|
||||
const toolsByServer = (mcpTools || []).reduce(
|
||||
(acc, tool) => {
|
||||
if (!tool || !tool.serverId) {
|
||||
return acc // Skip invalid tools
|
||||
return acc
|
||||
}
|
||||
if (!acc[tool.serverId]) {
|
||||
acc[tool.serverId] = []
|
||||
@@ -273,7 +247,6 @@ export function MCP() {
|
||||
{} as Record<string, typeof mcpTools>
|
||||
)
|
||||
|
||||
// Filter servers based on search term
|
||||
const filteredServers = (servers || []).filter((server) =>
|
||||
server.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
@@ -281,12 +254,12 @@ export function MCP() {
|
||||
return (
|
||||
<div className='relative flex h-full flex-col'>
|
||||
{/* Fixed Header with Search */}
|
||||
<div className='px-6 pt-4 pb-2'>
|
||||
<div className='px-6 pt-2 pb-2'>
|
||||
{/* Search Input */}
|
||||
{serversLoading ? (
|
||||
<Skeleton className='h-9 w-56 rounded-lg' />
|
||||
<Skeleton className='h-9 w-56 rounded-[8px]' />
|
||||
) : (
|
||||
<div className='flex h-9 w-56 items-center gap-2 rounded-lg border bg-transparent pr-2 pl-3'>
|
||||
<div className='flex h-9 w-56 items-center gap-2 rounded-[8px] border bg-transparent pr-2 pl-3'>
|
||||
<Search className='h-4 w-4 flex-shrink-0 text-muted-foreground' strokeWidth={2} />
|
||||
<Input
|
||||
placeholder='Search servers...'
|
||||
@@ -308,303 +281,56 @@ export function MCP() {
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className='scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent min-h-0 flex-1 overflow-y-auto px-6'>
|
||||
<div className='h-full space-y-4 py-2'>
|
||||
<div className='h-full space-y-2 py-2'>
|
||||
{/* Server List */}
|
||||
{serversLoading ? (
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<McpServerSkeleton />
|
||||
<McpServerSkeleton />
|
||||
<McpServerSkeleton />
|
||||
</div>
|
||||
) : !servers || servers.length === 0 ? (
|
||||
showAddForm ? (
|
||||
<div className='rounded-[8px] border bg-background p-4 shadow-xs'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Server Name</Label>
|
||||
</div>
|
||||
<div className='w-[380px]'>
|
||||
<Input
|
||||
placeholder='e.g., My MCP Server'
|
||||
value={formData.name}
|
||||
onChange={(e) => {
|
||||
if (testResult) clearTestResult()
|
||||
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Server URL</Label>
|
||||
</div>
|
||||
<div className='relative w-[380px]'>
|
||||
<Input
|
||||
ref={urlInputRef}
|
||||
placeholder='https://mcp.server.dev/{{YOUR_API_KEY}}/sse'
|
||||
value={formData.url}
|
||||
onChange={(e) => handleInputChange('url', e.target.value)}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setUrlScrollLeft(scrollLeft)
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setUrlScrollLeft(scrollLeft)
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
{/* Overlay for styled text display */}
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{ transform: `translateX(-${urlScrollLeft}px)` }}
|
||||
>
|
||||
{formatDisplayText(formData.url || '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables Dropdown */}
|
||||
{showEnvVars && activeInputField === 'url' && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={formData.url || ''}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(formData.headers || {}).map(([key, value], index) => (
|
||||
<div key={index} className='relative flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Header</Label>
|
||||
</div>
|
||||
<div className='relative flex w-[380px] gap-2'>
|
||||
{/* Header Key Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Name'
|
||||
value={key}
|
||||
onChange={(e) => handleInputChange('header-key', e.target.value, index)}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`key-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`key-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(key || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Value Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Value'
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleInputChange('header-value', e.target.value, index)
|
||||
}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`value-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`value-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(value || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const newHeaders = { ...formData.headers }
|
||||
delete newHeaders[key]
|
||||
setFormData((prev) => ({ ...prev, headers: newHeaders }))
|
||||
}}
|
||||
className='h-9 w-9 p-0 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
|
||||
{/* Environment Variables Dropdown for Header Key */}
|
||||
{showEnvVars &&
|
||||
activeInputField === 'header-key' &&
|
||||
activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={key}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Environment Variables Dropdown for Header Value */}
|
||||
{showEnvVars &&
|
||||
activeInputField === 'header-value' &&
|
||||
activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={value}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div />
|
||||
<div className='w-[380px]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
headers: { ...prev.headers, '': '' },
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<Plus className='mr-2 h-3 w-3' />
|
||||
Add Header
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-t pt-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={handleTestConnection}
|
||||
disabled={
|
||||
isTestingConnection || !formData.name.trim() || !formData.url?.trim()
|
||||
}
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
{isTestingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
{testResult?.success && (
|
||||
<span className='text-green-600 text-xs'>✓ Connected</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{testResult && !testResult.success && (
|
||||
<span className='ml-4 text-red-600 text-xs'>
|
||||
{testResult.error || testResult.message}
|
||||
</span>
|
||||
)}
|
||||
<Button variant='ghost' size='sm' onClick={() => setShowAddForm(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={handleAddServer}
|
||||
disabled={
|
||||
serversLoading ||
|
||||
isAddingServer ||
|
||||
!formData.name.trim() ||
|
||||
!formData.url?.trim()
|
||||
}
|
||||
>
|
||||
{serversLoading || isAddingServer ? 'Adding...' : 'Add Server'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AddServerForm
|
||||
formData={formData}
|
||||
testResult={testResult}
|
||||
isTestingConnection={isTestingConnection}
|
||||
isAddingServer={isAddingServer}
|
||||
serversLoading={serversLoading}
|
||||
showEnvVars={showEnvVars}
|
||||
activeInputField={activeInputField}
|
||||
activeHeaderIndex={activeHeaderIndex}
|
||||
envSearchTerm={envSearchTerm}
|
||||
cursorPosition={cursorPosition}
|
||||
urlScrollLeft={urlScrollLeft}
|
||||
headerScrollLeft={headerScrollLeft}
|
||||
workspaceId={workspaceId}
|
||||
urlInputRef={urlInputRef}
|
||||
onNameChange={(value) => setFormData((prev) => ({ ...prev, name: value }))}
|
||||
onInputChange={handleInputChange}
|
||||
onUrlScroll={(scrollLeft) => setUrlScrollLeft(scrollLeft)}
|
||||
onHeaderScroll={(key, scrollLeft) =>
|
||||
setHeaderScrollLeft((prev) => ({ ...prev, [key]: scrollLeft }))
|
||||
}
|
||||
onEnvVarSelect={handleEnvVarSelect}
|
||||
onEnvVarClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
onAddHeader={() =>
|
||||
setFormData((prev) => ({ ...prev, headers: { ...prev.headers, '': '' } }))
|
||||
}
|
||||
onRemoveHeader={(key) => {
|
||||
const newHeaders = { ...formData.headers }
|
||||
delete newHeaders[key]
|
||||
setFormData((prev) => ({ ...prev, headers: newHeaders }))
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onCancel={() => setShowAddForm(false)}
|
||||
onAddServer={handleAddServer}
|
||||
onClearTestResult={clearTestResult}
|
||||
/>
|
||||
) : (
|
||||
!showAddForm && (
|
||||
<div className='flex h-full items-center justify-center text-muted-foreground text-sm'>
|
||||
@@ -613,7 +339,7 @@ export function MCP() {
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
{filteredServers.map((server: any) => {
|
||||
// Add defensive checks for server properties
|
||||
if (!server || !server.id) {
|
||||
@@ -673,294 +399,47 @@ export function MCP() {
|
||||
|
||||
{/* Add Server Form for when servers exist */}
|
||||
{showAddForm && (
|
||||
<div className='mt-4 rounded-[8px] border bg-background p-4 shadow-xs'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Server Name</Label>
|
||||
</div>
|
||||
<div className='w-[380px]'>
|
||||
<Input
|
||||
placeholder='e.g., My MCP Server'
|
||||
value={formData.name}
|
||||
onChange={(e) => {
|
||||
if (testResult) clearTestResult()
|
||||
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Server URL</Label>
|
||||
</div>
|
||||
<div className='relative w-[380px]'>
|
||||
<Input
|
||||
ref={urlInputRef}
|
||||
placeholder='https://mcp.server.dev/{{YOUR_API_KEY}}/sse'
|
||||
value={formData.url}
|
||||
onChange={(e) => handleInputChange('url', e.target.value)}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setUrlScrollLeft(scrollLeft)
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setUrlScrollLeft(scrollLeft)
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
{/* Overlay for styled text display */}
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{ transform: `translateX(-${urlScrollLeft}px)` }}
|
||||
>
|
||||
{formatDisplayText(formData.url || '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables Dropdown */}
|
||||
{showEnvVars && activeInputField === 'url' && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={formData.url || ''}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='180px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(formData.headers || {}).map(([key, value], index) => (
|
||||
<div key={index} className='relative flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-normal'>Header</Label>
|
||||
</div>
|
||||
<div className='relative flex w-[380px] gap-2'>
|
||||
{/* Header Key Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Name'
|
||||
value={key}
|
||||
onChange={(e) =>
|
||||
handleInputChange('header-key', e.target.value, index)
|
||||
}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`key-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`key-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(key || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Value Input */}
|
||||
<div className='relative flex-1'>
|
||||
<Input
|
||||
placeholder='Value'
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
handleInputChange('header-value', e.target.value, index)
|
||||
}
|
||||
onScroll={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`value-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const scrollLeft = e.currentTarget.scrollLeft
|
||||
setHeaderScrollLeft((prev) => ({
|
||||
...prev,
|
||||
[`value-${index}`]: scrollLeft,
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
|
||||
<div
|
||||
className='whitespace-nowrap'
|
||||
style={{
|
||||
transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`,
|
||||
}}
|
||||
>
|
||||
{formatDisplayText(value || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const newHeaders = { ...formData.headers }
|
||||
delete newHeaders[key]
|
||||
setFormData((prev) => ({ ...prev, headers: newHeaders }))
|
||||
}}
|
||||
className='h-9 w-9 p-0 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
|
||||
{/* Environment Variables Dropdown for Header Key */}
|
||||
{showEnvVars &&
|
||||
activeInputField === 'header-key' &&
|
||||
activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={key}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Environment Variables Dropdown for Header Value */}
|
||||
{showEnvVars &&
|
||||
activeInputField === 'header-value' &&
|
||||
activeHeaderIndex === index && (
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={envSearchTerm}
|
||||
inputValue={value}
|
||||
cursorPosition={cursorPosition}
|
||||
workspaceId={workspaceId}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
className='w-[380px]'
|
||||
maxHeight='200px'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
zIndex: 99999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div />
|
||||
<div className='w-[380px]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
headers: { ...prev.headers, '': '' },
|
||||
}))
|
||||
}}
|
||||
className='h-9 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<Plus className='mr-2 h-3 w-3' />
|
||||
Add Header
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-t pt-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={handleTestConnection}
|
||||
disabled={
|
||||
isTestingConnection || !formData.name.trim() || !formData.url?.trim()
|
||||
}
|
||||
className='text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
{isTestingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
{testResult?.success && (
|
||||
<span className='text-green-600 text-xs'>✓ Connected</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{testResult && !testResult.success && (
|
||||
<span className='ml-4 text-red-600 text-xs'>
|
||||
{testResult.error || testResult.message}
|
||||
</span>
|
||||
)}
|
||||
<Button variant='ghost' size='sm' onClick={() => setShowAddForm(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={handleAddServer}
|
||||
disabled={
|
||||
serversLoading ||
|
||||
isAddingServer ||
|
||||
!formData.name.trim() ||
|
||||
!formData.url?.trim()
|
||||
}
|
||||
>
|
||||
{serversLoading || isAddingServer ? 'Adding...' : 'Add Server'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<AddServerForm
|
||||
formData={formData}
|
||||
testResult={testResult}
|
||||
isTestingConnection={isTestingConnection}
|
||||
isAddingServer={isAddingServer}
|
||||
serversLoading={serversLoading}
|
||||
showEnvVars={showEnvVars}
|
||||
activeInputField={activeInputField}
|
||||
activeHeaderIndex={activeHeaderIndex}
|
||||
envSearchTerm={envSearchTerm}
|
||||
cursorPosition={cursorPosition}
|
||||
urlScrollLeft={urlScrollLeft}
|
||||
headerScrollLeft={headerScrollLeft}
|
||||
workspaceId={workspaceId}
|
||||
urlInputRef={urlInputRef}
|
||||
onNameChange={(value) => setFormData((prev) => ({ ...prev, name: value }))}
|
||||
onInputChange={handleInputChange}
|
||||
onUrlScroll={(scrollLeft) => setUrlScrollLeft(scrollLeft)}
|
||||
onHeaderScroll={(key, scrollLeft) =>
|
||||
setHeaderScrollLeft((prev) => ({ ...prev, [key]: scrollLeft }))
|
||||
}
|
||||
onEnvVarSelect={handleEnvVarSelect}
|
||||
onEnvVarClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setActiveInputField(null)
|
||||
setActiveHeaderIndex(null)
|
||||
}}
|
||||
onAddHeader={() =>
|
||||
setFormData((prev) => ({ ...prev, headers: { ...prev.headers, '': '' } }))
|
||||
}
|
||||
onRemoveHeader={(key) => {
|
||||
const newHeaders = { ...formData.headers }
|
||||
delete newHeaders[key]
|
||||
setFormData((prev) => ({ ...prev, headers: newHeaders }))
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onCancel={() => setShowAddForm(false)}
|
||||
onAddServer={handleAddServer}
|
||||
onClearTestResult={clearTestResult}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { McpTransport } from '@/lib/mcp/types'
|
||||
|
||||
export interface McpServerFormData {
|
||||
name: string
|
||||
transport: McpTransport
|
||||
url?: string
|
||||
timeout?: number
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface McpServerTestResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
error?: string
|
||||
warnings?: string[]
|
||||
}
|
||||
@@ -6,9 +6,6 @@ import type { McpTransport } from '@/lib/mcp/types'
|
||||
|
||||
const logger = createLogger('useMcpServerTest')
|
||||
|
||||
/**
|
||||
* Check if transport type requires a URL
|
||||
*/
|
||||
function isUrlBasedTransport(transport: McpTransport): boolean {
|
||||
return transport === 'streamable-http'
|
||||
}
|
||||
@@ -84,12 +81,23 @@ export function useMcpServerTest() {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (result.data?.error || result.data?.success === false) {
|
||||
const testResult: McpServerTestResult = {
|
||||
success: false,
|
||||
message: result.data.error || 'Connection failed',
|
||||
error: result.data.error,
|
||||
warnings: result.data.warnings,
|
||||
}
|
||||
setTestResult(testResult)
|
||||
logger.error('MCP server test failed:', result.data.error)
|
||||
return testResult
|
||||
}
|
||||
throw new Error(result.error || 'Connection test failed')
|
||||
}
|
||||
|
||||
setTestResult(result)
|
||||
logger.info(`MCP server test ${result.success ? 'passed' : 'failed'}:`, config.name)
|
||||
return result
|
||||
setTestResult(result.data || result)
|
||||
logger.info(`MCP server test ${result.data?.success ? 'passed' : 'failed'}:`, config.name)
|
||||
return result.data || result
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
const result: McpServerTestResult = {
|
||||
|
||||
Reference in New Issue
Block a user