Skip to content

Commit 3af7d13

Browse files
authored
feat(mcp): surface better errors for MCP connection failures (#1796)
1 parent 2eea3ca commit 3af7d13

File tree

4 files changed

+430
-623
lines changed

4 files changed

+430
-623
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
'use client'
2+
3+
import { Plus, X } from 'lucide-react'
4+
import { Button, Input, Label } from '@/components/ui'
5+
import { EnvVarDropdown } from '@/components/ui/env-var-dropdown'
6+
import { formatDisplayText } from '@/components/ui/formatted-text'
7+
import type { McpServerFormData, McpServerTestResult } from '../types'
8+
9+
interface AddServerFormProps {
10+
formData: McpServerFormData
11+
testResult: McpServerTestResult | null
12+
isTestingConnection: boolean
13+
isAddingServer: boolean
14+
serversLoading: boolean
15+
showEnvVars: boolean
16+
activeInputField: 'url' | 'header-key' | 'header-value' | null
17+
activeHeaderIndex: number | null
18+
envSearchTerm: string
19+
cursorPosition: number
20+
urlScrollLeft: number
21+
headerScrollLeft: Record<string, number>
22+
workspaceId: string
23+
urlInputRef: React.RefObject<HTMLInputElement | null>
24+
onNameChange: (value: string) => void
25+
onInputChange: (
26+
field: 'url' | 'header-key' | 'header-value',
27+
value: string,
28+
index?: number
29+
) => void
30+
onUrlScroll: (scrollLeft: number) => void
31+
onHeaderScroll: (key: string, scrollLeft: number) => void
32+
onEnvVarSelect: (value: string) => void
33+
onEnvVarClose: () => void
34+
onAddHeader: () => void
35+
onRemoveHeader: (key: string) => void
36+
onTestConnection: () => void
37+
onCancel: () => void
38+
onAddServer: () => void
39+
onClearTestResult: () => void
40+
}
41+
42+
export function AddServerForm({
43+
formData,
44+
testResult,
45+
isTestingConnection,
46+
isAddingServer,
47+
serversLoading,
48+
showEnvVars,
49+
activeInputField,
50+
activeHeaderIndex,
51+
envSearchTerm,
52+
cursorPosition,
53+
urlScrollLeft,
54+
headerScrollLeft,
55+
workspaceId,
56+
urlInputRef,
57+
onNameChange,
58+
onInputChange,
59+
onUrlScroll,
60+
onHeaderScroll,
61+
onEnvVarSelect,
62+
onEnvVarClose,
63+
onAddHeader,
64+
onRemoveHeader,
65+
onTestConnection,
66+
onCancel,
67+
onAddServer,
68+
onClearTestResult,
69+
}: AddServerFormProps) {
70+
return (
71+
<div className='rounded-[8px] border bg-background p-3 shadow-xs'>
72+
<div className='space-y-1.5'>
73+
<div className='flex items-center justify-between gap-3'>
74+
<Label className='w-[100px] shrink-0 font-normal text-sm'>Server Name</Label>
75+
<div className='flex-1'>
76+
<Input
77+
placeholder='e.g., My MCP Server'
78+
value={formData.name}
79+
onChange={(e) => {
80+
if (testResult) onClearTestResult()
81+
onNameChange(e.target.value)
82+
}}
83+
className='h-9'
84+
/>
85+
</div>
86+
</div>
87+
88+
<div className='flex items-center justify-between gap-3'>
89+
<Label className='w-[100px] shrink-0 font-normal text-sm'>Server URL</Label>
90+
<div className='relative flex-1'>
91+
<Input
92+
ref={urlInputRef}
93+
placeholder='https://mcp.server.dev/{{YOUR_API_KEY}}/sse'
94+
value={formData.url}
95+
onChange={(e) => onInputChange('url', e.target.value)}
96+
onScroll={(e) => onUrlScroll(e.currentTarget.scrollLeft)}
97+
onInput={(e) => onUrlScroll(e.currentTarget.scrollLeft)}
98+
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
99+
/>
100+
{/* Overlay for styled text display */}
101+
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
102+
<div
103+
className='whitespace-nowrap'
104+
style={{ transform: `translateX(-${urlScrollLeft}px)` }}
105+
>
106+
{formatDisplayText(formData.url || '')}
107+
</div>
108+
</div>
109+
110+
{/* Environment Variables Dropdown */}
111+
{showEnvVars && activeInputField === 'url' && (
112+
<EnvVarDropdown
113+
visible={showEnvVars}
114+
onSelect={onEnvVarSelect}
115+
searchTerm={envSearchTerm}
116+
inputValue={formData.url || ''}
117+
cursorPosition={cursorPosition}
118+
workspaceId={workspaceId}
119+
onClose={onEnvVarClose}
120+
className='w-full'
121+
maxHeight='200px'
122+
style={{
123+
position: 'absolute',
124+
top: '100%',
125+
left: 0,
126+
zIndex: 99999,
127+
}}
128+
/>
129+
)}
130+
</div>
131+
</div>
132+
133+
{Object.entries(formData.headers || {}).map(([key, value], index) => (
134+
<div key={index} className='relative flex items-center justify-between gap-3'>
135+
<Label className='w-[100px] shrink-0 font-normal text-sm'>Header</Label>
136+
<div className='relative flex flex-1 gap-2'>
137+
{/* Header Key Input */}
138+
<div className='relative flex-1'>
139+
<Input
140+
placeholder='Name'
141+
value={key}
142+
onChange={(e) => onInputChange('header-key', e.target.value, index)}
143+
onScroll={(e) => onHeaderScroll(`key-${index}`, e.currentTarget.scrollLeft)}
144+
onInput={(e) => onHeaderScroll(`key-${index}`, e.currentTarget.scrollLeft)}
145+
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
146+
/>
147+
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
148+
<div
149+
className='whitespace-nowrap'
150+
style={{
151+
transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`,
152+
}}
153+
>
154+
{formatDisplayText(key || '')}
155+
</div>
156+
</div>
157+
</div>
158+
159+
{/* Header Value Input */}
160+
<div className='relative flex-1'>
161+
<Input
162+
placeholder='Value'
163+
value={value}
164+
onChange={(e) => onInputChange('header-value', e.target.value, index)}
165+
onScroll={(e) => onHeaderScroll(`value-${index}`, e.currentTarget.scrollLeft)}
166+
onInput={(e) => onHeaderScroll(`value-${index}`, e.currentTarget.scrollLeft)}
167+
className='h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50'
168+
/>
169+
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-3 text-sm'>
170+
<div
171+
className='whitespace-nowrap'
172+
style={{
173+
transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`,
174+
}}
175+
>
176+
{formatDisplayText(value || '')}
177+
</div>
178+
</div>
179+
</div>
180+
181+
<Button
182+
type='button'
183+
variant='ghost'
184+
size='sm'
185+
onClick={() => onRemoveHeader(key)}
186+
className='h-9 w-9 p-0 text-muted-foreground hover:text-foreground'
187+
>
188+
<X className='h-3 w-3' />
189+
</Button>
190+
191+
{/* Environment Variables Dropdown for Header Key */}
192+
{showEnvVars && activeInputField === 'header-key' && activeHeaderIndex === index && (
193+
<EnvVarDropdown
194+
visible={showEnvVars}
195+
onSelect={onEnvVarSelect}
196+
searchTerm={envSearchTerm}
197+
inputValue={key}
198+
cursorPosition={cursorPosition}
199+
workspaceId={workspaceId}
200+
onClose={onEnvVarClose}
201+
className='w-full'
202+
maxHeight='200px'
203+
style={{
204+
position: 'absolute',
205+
top: '100%',
206+
left: 0,
207+
zIndex: 99999,
208+
}}
209+
/>
210+
)}
211+
212+
{/* Environment Variables Dropdown for Header Value */}
213+
{showEnvVars &&
214+
activeInputField === 'header-value' &&
215+
activeHeaderIndex === index && (
216+
<EnvVarDropdown
217+
visible={showEnvVars}
218+
onSelect={onEnvVarSelect}
219+
searchTerm={envSearchTerm}
220+
inputValue={value}
221+
cursorPosition={cursorPosition}
222+
workspaceId={workspaceId}
223+
onClose={onEnvVarClose}
224+
className='w-full'
225+
maxHeight='200px'
226+
style={{
227+
position: 'absolute',
228+
top: '100%',
229+
left: 0,
230+
zIndex: 99999,
231+
}}
232+
/>
233+
)}
234+
</div>
235+
</div>
236+
))}
237+
238+
<div className='flex items-center justify-between gap-3'>
239+
<div className='w-[100px] shrink-0' />
240+
<div className='flex-1'>
241+
<Button
242+
type='button'
243+
variant='outline'
244+
size='sm'
245+
onClick={onAddHeader}
246+
className='h-9 text-muted-foreground hover:text-foreground'
247+
>
248+
<Plus className='mr-2 h-3 w-3' />
249+
Add Header
250+
</Button>
251+
</div>
252+
</div>
253+
254+
<div className='border-border border-t pt-2'>
255+
<div className='space-y-1.5'>
256+
{/* Error message above buttons */}
257+
{testResult && !testResult.success && (
258+
<p className='text-red-600 text-sm'>{testResult.error || testResult.message}</p>
259+
)}
260+
261+
{/* Buttons row */}
262+
<div className='flex items-center justify-between'>
263+
<div className='flex items-center gap-2'>
264+
<Button
265+
variant='ghost'
266+
size='sm'
267+
onClick={onTestConnection}
268+
disabled={isTestingConnection || !formData.name.trim() || !formData.url?.trim()}
269+
className='text-muted-foreground hover:text-foreground'
270+
>
271+
{isTestingConnection ? 'Testing...' : 'Test Connection'}
272+
</Button>
273+
{testResult?.success && <span className='text-green-600 text-xs'>✓ Connected</span>}
274+
</div>
275+
<div className='flex items-center gap-2'>
276+
<Button
277+
variant='ghost'
278+
size='sm'
279+
onClick={onCancel}
280+
className='text-muted-foreground hover:text-foreground'
281+
>
282+
Cancel
283+
</Button>
284+
<Button
285+
size='sm'
286+
onClick={onAddServer}
287+
disabled={
288+
serversLoading ||
289+
isAddingServer ||
290+
!formData.name.trim() ||
291+
!formData.url?.trim()
292+
}
293+
className='h-9 rounded-[8px]'
294+
>
295+
{serversLoading || isAddingServer ? 'Adding...' : 'Add Server'}
296+
</Button>
297+
</div>
298+
</div>
299+
</div>
300+
</div>
301+
</div>
302+
</div>
303+
)
304+
}

0 commit comments

Comments
 (0)