Skip to content

Commit 09513de

Browse files
authored
Merge pull request #292 from DevanshuNEU/feat/mcp-connect-ope-168
feat: MCP Connect guide -- live key injection + connection test (OPE-168)
2 parents f0f339c + df958de commit 09513de

2 files changed

Lines changed: 163 additions & 11 deletions

File tree

frontend/src/config/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const buildWsUrl = (path: string): string => {
5858
return `${WS_URL}${cleanPath}`
5959
}
6060

61+
// MCP server URL (separate Railway service from the API)
62+
export const MCP_URL = import.meta.env.VITE_MCP_URL || 'https://mcp.opencodeintel.com'
63+
6164
// free tier repo limit -- used in dashboard and GitHub import
6265
export const MAX_FREE_REPOS = 1
6366

frontend/src/pages/APIKeysPage.tsx

Lines changed: 160 additions & 11 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'
@@ -14,7 +14,7 @@ import {
1414
DialogTitle,
1515
} from '@/components/ui/dialog'
1616
import { toast } from 'sonner'
17-
import { API_URL } from '@/config/api'
17+
import { API_URL, MCP_URL } from '@/config/api'
1818
import { cn } from '@/lib/utils'
1919

2020
interface APIKey {
@@ -166,36 +166,166 @@ function KeyCard({
166166
)
167167
}
168168

169-
function ConnectGuide() {
169+
type TestStep = { label: string; status: 'idle' | 'running' | 'pass' | 'fail' }
170+
171+
function ConnectionTest({ token }: { token: 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 (!token || 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(`${MCP_URL}/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 (uses session JWT, not API key preview)
201+
update(1, 'running')
202+
try {
203+
const res = await fetch(`${API_URL}/keys`, {
204+
headers: { Authorization: `Bearer ${token}` },
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 ${token}` },
217+
})
218+
update(2, res.ok ? 'pass' : 'fail')
219+
} catch {
220+
update(2, 'fail')
221+
}
222+
223+
setRunning(false)
224+
}, [token, 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={!token || 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, sessionToken }: { activeKeyPreview: string | null; sessionToken: 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 \\
316+
--header "Authorization: Bearer ${keyDisplay}" \\
191317
https://mcp.opencodeintel.com/mcp`,
192318
},
193319
cursor: {
194320
label: 'Cursor',
321+
hint: '.cursor/mcp.json',
195322
config: `{
196323
"mcpServers": {
197324
"codeintel": {
198-
"url": "https://mcp.opencodeintel.com/mcp"
325+
"url": "https://mcp.opencodeintel.com/mcp",
326+
"headers": {
327+
"Authorization": "Bearer ${keyDisplay}"
328+
}
199329
}
200330
}
201331
}`,
@@ -207,7 +337,10 @@ function ConnectGuide() {
207337
return (
208338
<div className="rounded-lg border border-border/60 bg-card/40 overflow-hidden">
209339
<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>
340+
<div className="flex items-center gap-2">
341+
<Wifi className="w-3.5 h-3.5 text-primary/70" />
342+
<span className="text-sm font-medium text-foreground">Connect to your tools</span>
343+
</div>
211344
<div className="flex gap-1">
212345
{Object.entries(snippets).map(([key, { label }]) => (
213346
<button
@@ -225,14 +358,25 @@ function ConnectGuide() {
225358
))}
226359
</div>
227360
</div>
361+
362+
{/* Hint line */}
363+
<div className="px-5 pt-3 pb-0">
364+
<p className="text-[11px] text-muted-foreground/60">
365+
{current.hint}
366+
</p>
367+
</div>
368+
228369
<div className="relative">
229-
<pre className="px-5 py-4 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto">
370+
<pre className="px-5 py-3 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto">
230371
{current.config}
231372
</pre>
232-
<div className="absolute top-3 right-3">
373+
<div className="absolute top-2 right-3">
233374
<CopyInline text={current.config} label="Config" />
234375
</div>
235376
</div>
377+
378+
{/* Connection test */}
379+
<ConnectionTest token={sessionToken} />
236380
</div>
237381
)
238382
}
@@ -435,7 +579,12 @@ export function APIKeysPage() {
435579
)}
436580

437581
{/* Connect guide */}
438-
{activeKeys.length > 0 && <ConnectGuide />}
582+
{activeKeys.length > 0 && (
583+
<ConnectGuide
584+
activeKeyPreview={activeKeys[0]?.key_preview || null}
585+
sessionToken={token}
586+
/>
587+
)}
439588

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

0 commit comments

Comments
 (0)