Skip to content

Commit 3162b3f

Browse files
committed
feat: MCP Connect guide upgrade -- live key injection + connection test (OPE-168)
ConnectGuide enhancements: - Active API key injected into config snippets (was hardcoded ci_your-key-here) - Per-tab setup hints (Settings > Developer > Edit Config, etc.) - Cursor config now includes Authorization header with user's key - Wifi icon in section header New ConnectionTest widget: - 3-step animated health check: MCP reachable -> Auth verified -> Repos accessible - Sequential test with per-step pass/fail/running states - Emerald badges on success, destructive on failure - Error message on failure with troubleshooting hint - Test button toggles between 'Test connection' -> 'Testing...' -> 'Connected' Props flow: activeKeys[0].key_preview passed to ConnectGuide -> ConnectionTest Build: clean. No new dependencies.
1 parent f0f339c commit 3162b3f

1 file changed

Lines changed: 155 additions & 10 deletions

File tree

frontend/src/pages/APIKeysPage.tsx

Lines changed: 155 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState } from 'react'
2-
import { Plus, Copy, Check, Loader2, Clock, Shield, Terminal, Zap } from 'lucide-react'
1+
import { useState, useCallback } from 'react'
2+
import { Plus, Copy, Check, Loader2, Clock, Shield, Terminal, Zap, Wifi, CircleCheck, CircleX, ArrowRight } from 'lucide-react'
33
import { useQuery, useQueryClient } from '@tanstack/react-query'
44
import { useAuth } from '@/contexts/AuthContext'
55
import { Button } from '@/components/ui/button'
@@ -166,36 +166,165 @@ function KeyCard({
166166
)
167167
}
168168

169-
function ConnectGuide() {
169+
type TestStep = { label: string; status: 'idle' | 'running' | 'pass' | 'fail' }
170+
171+
function ConnectionTest({ apiKey }: { apiKey: string | null }) {
172+
const [steps, setSteps] = useState<TestStep[]>([
173+
{ label: 'MCP server reachable', status: 'idle' },
174+
{ label: 'API key authenticated', status: 'idle' },
175+
{ label: 'Repositories accessible', status: 'idle' },
176+
])
177+
const [running, setRunning] = useState(false)
178+
const [tested, setTested] = useState(false)
179+
180+
const runTest = useCallback(async () => {
181+
if (!apiKey || running) return
182+
setRunning(true)
183+
setTested(true)
184+
const update = (idx: number, status: TestStep['status']) =>
185+
setSteps((prev) => prev.map((s, i) => (i === idx ? { ...s, status } : s)))
186+
187+
// Reset
188+
setSteps((prev) => prev.map((s) => ({ ...s, status: 'idle' })))
189+
190+
// Step 1: MCP health
191+
update(0, 'running')
192+
try {
193+
const res = await fetch('https://mcp.opencodeintel.com/health')
194+
update(0, res.ok ? 'pass' : 'fail')
195+
if (!res.ok) { setRunning(false); return }
196+
} catch {
197+
update(0, 'fail'); setRunning(false); return
198+
}
199+
200+
// Step 2: Auth check
201+
update(1, 'running')
202+
try {
203+
const res = await fetch(`${API_URL}/keys`, {
204+
headers: { Authorization: `Bearer ${apiKey}` },
205+
})
206+
update(1, res.ok ? 'pass' : 'fail')
207+
if (!res.ok) { setRunning(false); return }
208+
} catch {
209+
update(1, 'fail'); setRunning(false); return
210+
}
211+
212+
// Step 3: Repos accessible
213+
update(2, 'running')
214+
try {
215+
const res = await fetch(`${API_URL}/repos`, {
216+
headers: { Authorization: `Bearer ${apiKey}` },
217+
})
218+
update(2, res.ok ? 'pass' : 'fail')
219+
} catch {
220+
update(2, 'fail')
221+
}
222+
223+
setRunning(false)
224+
}, [apiKey, running])
225+
226+
const allPassed = steps.every((s) => s.status === 'pass')
227+
const anyFailed = steps.some((s) => s.status === 'fail')
228+
229+
return (
230+
<div className="px-5 py-4 border-t border-border/40">
231+
<div className="flex items-center justify-between mb-3">
232+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
233+
Connection test
234+
</span>
235+
<button
236+
onClick={runTest}
237+
disabled={!apiKey || running}
238+
className={cn(
239+
'text-xs px-3 py-1.5 rounded-md transition-all flex items-center gap-1.5',
240+
running
241+
? 'text-muted-foreground bg-muted/30'
242+
: allPassed && tested
243+
? 'text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/15'
244+
: 'text-primary bg-primary/10 hover:bg-primary/15',
245+
)}
246+
>
247+
{running ? (
248+
<><Loader2 className="w-3 h-3 animate-spin" /> Testing...</>
249+
) : allPassed && tested ? (
250+
<><CircleCheck className="w-3 h-3" /> Connected</>
251+
) : (
252+
<><Wifi className="w-3 h-3" /> Test connection</>
253+
)}
254+
</button>
255+
</div>
256+
<div className="flex items-center gap-3">
257+
{steps.map((step, i) => (
258+
<div key={step.label} className="flex items-center gap-1.5">
259+
{i > 0 && (
260+
<ArrowRight className={cn(
261+
'w-3 h-3',
262+
step.status === 'pass' ? 'text-emerald-400/50' : 'text-border/60',
263+
)} />
264+
)}
265+
<div className={cn(
266+
'flex items-center gap-1.5 text-xs px-2 py-1 rounded-md',
267+
step.status === 'pass' && 'text-emerald-400 bg-emerald-500/8',
268+
step.status === 'fail' && 'text-destructive bg-destructive/8',
269+
step.status === 'running' && 'text-primary bg-primary/8',
270+
step.status === 'idle' && 'text-muted-foreground/60',
271+
)}>
272+
{step.status === 'running' && <Loader2 className="w-3 h-3 animate-spin" />}
273+
{step.status === 'pass' && <CircleCheck className="w-3 h-3" />}
274+
{step.status === 'fail' && <CircleX className="w-3 h-3" />}
275+
{step.status === 'idle' && <div className="w-3 h-3 rounded-full border border-current opacity-40" />}
276+
{step.label}
277+
</div>
278+
</div>
279+
))}
280+
</div>
281+
{anyFailed && tested && !running && (
282+
<p className="text-[11px] text-destructive/70 mt-2">
283+
Connection failed. Check that your API key is active and the MCP server is running.
284+
</p>
285+
)}
286+
</div>
287+
)
288+
}
289+
290+
function ConnectGuide({ activeKeyPreview }: { activeKeyPreview: string | null }) {
170291
const [tab, setTab] = useState<'desktop' | 'code' | 'cursor'>('desktop')
171292

172-
const snippets: Record<string, { label: string; config: string }> = {
293+
const keyDisplay = activeKeyPreview || 'ci_your-key-here'
294+
295+
const snippets: Record<string, { label: string; config: string; hint: string }> = {
173296
desktop: {
174297
label: 'Claude Desktop',
298+
hint: 'Settings > Developer > Edit Config',
175299
config: `{
176300
"mcpServers": {
177301
"codeintel": {
178302
"command": "npx",
179303
"args": ["-y", "mcp-remote", "https://mcp.opencodeintel.com/mcp"],
180304
"env": {
181-
"API_KEY": "ci_your-key-here"
305+
"API_KEY": "${keyDisplay}"
182306
}
183307
}
184308
}
185309
}`,
186310
},
187311
code: {
188312
label: 'Claude Code',
313+
hint: 'Run in terminal',
189314
config: `claude mcp add codeintel \\
190315
--transport http \\
191316
https://mcp.opencodeintel.com/mcp`,
192317
},
193318
cursor: {
194319
label: 'Cursor',
320+
hint: '.cursor/mcp.json',
195321
config: `{
196322
"mcpServers": {
197323
"codeintel": {
198-
"url": "https://mcp.opencodeintel.com/mcp"
324+
"url": "https://mcp.opencodeintel.com/mcp",
325+
"headers": {
326+
"Authorization": "Bearer ${keyDisplay}"
327+
}
199328
}
200329
}
201330
}`,
@@ -207,7 +336,10 @@ function ConnectGuide() {
207336
return (
208337
<div className="rounded-lg border border-border/60 bg-card/40 overflow-hidden">
209338
<div className="px-5 py-3 border-b border-border/40 flex items-center justify-between">
210-
<span className="text-sm font-medium text-foreground">Connect to your tools</span>
339+
<div className="flex items-center gap-2">
340+
<Wifi className="w-3.5 h-3.5 text-primary/70" />
341+
<span className="text-sm font-medium text-foreground">Connect to your tools</span>
342+
</div>
211343
<div className="flex gap-1">
212344
{Object.entries(snippets).map(([key, { label }]) => (
213345
<button
@@ -225,14 +357,25 @@ function ConnectGuide() {
225357
))}
226358
</div>
227359
</div>
360+
361+
{/* Hint line */}
362+
<div className="px-5 pt-3 pb-0">
363+
<p className="text-[11px] text-muted-foreground/60">
364+
{current.hint}
365+
</p>
366+
</div>
367+
228368
<div className="relative">
229-
<pre className="px-5 py-4 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto">
369+
<pre className="px-5 py-3 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto">
230370
{current.config}
231371
</pre>
232-
<div className="absolute top-3 right-3">
372+
<div className="absolute top-2 right-3">
233373
<CopyInline text={current.config} label="Config" />
234374
</div>
235375
</div>
376+
377+
{/* Connection test */}
378+
<ConnectionTest apiKey={activeKeyPreview} />
236379
</div>
237380
)
238381
}
@@ -435,7 +578,9 @@ export function APIKeysPage() {
435578
)}
436579

437580
{/* Connect guide */}
438-
{activeKeys.length > 0 && <ConnectGuide />}
581+
{activeKeys.length > 0 && (
582+
<ConnectGuide activeKeyPreview={activeKeys[0]?.key_preview || null} />
583+
)}
439584

440585
{/* Generate dialog */}
441586
<Dialog open={generateOpen} onOpenChange={(open) => { if (!open && !generatedKey) closeGenerateDialog() }}>

0 commit comments

Comments
 (0)