diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 047a55c..8629d57 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -58,6 +58,9 @@ export const buildWsUrl = (path: string): string => { return `${WS_URL}${cleanPath}` } +// MCP server URL (separate Railway service from the API) +export const MCP_URL = import.meta.env.VITE_MCP_URL || 'https://mcp.opencodeintel.com' + // free tier repo limit -- used in dashboard and GitHub import export const MAX_FREE_REPOS = 1 diff --git a/frontend/src/pages/APIKeysPage.tsx b/frontend/src/pages/APIKeysPage.tsx index 7fd880f..45de083 100644 --- a/frontend/src/pages/APIKeysPage.tsx +++ b/frontend/src/pages/APIKeysPage.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { Plus, Copy, Check, Loader2, Clock, Shield, Terminal, Zap } from 'lucide-react' +import { useState, useCallback } from 'react' +import { Plus, Copy, Check, Loader2, Clock, Shield, Terminal, Zap, Wifi, CircleCheck, CircleX, ArrowRight } from 'lucide-react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useAuth } from '@/contexts/AuthContext' import { Button } from '@/components/ui/button' @@ -14,7 +14,7 @@ import { DialogTitle, } from '@/components/ui/dialog' import { toast } from 'sonner' -import { API_URL } from '@/config/api' +import { API_URL, MCP_URL } from '@/config/api' import { cn } from '@/lib/utils' interface APIKey { @@ -166,19 +166,143 @@ function KeyCard({ ) } -function ConnectGuide() { +type TestStep = { label: string; status: 'idle' | 'running' | 'pass' | 'fail' } + +function ConnectionTest({ token }: { token: string | null }) { + const [steps, setSteps] = useState([ + { label: 'MCP server reachable', status: 'idle' }, + { label: 'API key authenticated', status: 'idle' }, + { label: 'Repositories accessible', status: 'idle' }, + ]) + const [running, setRunning] = useState(false) + const [tested, setTested] = useState(false) + + const runTest = useCallback(async () => { + if (!token || running) return + setRunning(true) + setTested(true) + const update = (idx: number, status: TestStep['status']) => + setSteps((prev) => prev.map((s, i) => (i === idx ? { ...s, status } : s))) + + // Reset + setSteps((prev) => prev.map((s) => ({ ...s, status: 'idle' }))) + + // Step 1: MCP health + update(0, 'running') + try { + const res = await fetch(`${MCP_URL}/health`) + update(0, res.ok ? 'pass' : 'fail') + if (!res.ok) { setRunning(false); return } + } catch { + update(0, 'fail'); setRunning(false); return + } + + // Step 2: Auth check (uses session JWT, not API key preview) + update(1, 'running') + try { + const res = await fetch(`${API_URL}/keys`, { + headers: { Authorization: `Bearer ${token}` }, + }) + update(1, res.ok ? 'pass' : 'fail') + if (!res.ok) { setRunning(false); return } + } catch { + update(1, 'fail'); setRunning(false); return + } + + // Step 3: Repos accessible + update(2, 'running') + try { + const res = await fetch(`${API_URL}/repos`, { + headers: { Authorization: `Bearer ${token}` }, + }) + update(2, res.ok ? 'pass' : 'fail') + } catch { + update(2, 'fail') + } + + setRunning(false) + }, [token, running]) + + const allPassed = steps.every((s) => s.status === 'pass') + const anyFailed = steps.some((s) => s.status === 'fail') + + return ( +
+
+ + Connection test + + +
+
+ {steps.map((step, i) => ( +
+ {i > 0 && ( + + )} +
+ {step.status === 'running' && } + {step.status === 'pass' && } + {step.status === 'fail' && } + {step.status === 'idle' &&
} + {step.label} +
+
+ ))} +
+ {anyFailed && tested && !running && ( +

+ Connection failed. Check that your API key is active and the MCP server is running. +

+ )} +
+ ) +} + +function ConnectGuide({ activeKeyPreview, sessionToken }: { activeKeyPreview: string | null; sessionToken: string | null }) { const [tab, setTab] = useState<'desktop' | 'code' | 'cursor'>('desktop') - const snippets: Record = { + const keyDisplay = activeKeyPreview || 'ci_your-key-here' + + const snippets: Record = { desktop: { label: 'Claude Desktop', + hint: 'Settings > Developer > Edit Config', config: `{ "mcpServers": { "codeintel": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.opencodeintel.com/mcp"], "env": { - "API_KEY": "ci_your-key-here" + "API_KEY": "${keyDisplay}" } } } @@ -186,16 +310,22 @@ function ConnectGuide() { }, code: { label: 'Claude Code', + hint: 'Run in terminal', config: `claude mcp add codeintel \\ --transport http \\ + --header "Authorization: Bearer ${keyDisplay}" \\ https://mcp.opencodeintel.com/mcp`, }, cursor: { label: 'Cursor', + hint: '.cursor/mcp.json', config: `{ "mcpServers": { "codeintel": { - "url": "https://mcp.opencodeintel.com/mcp" + "url": "https://mcp.opencodeintel.com/mcp", + "headers": { + "Authorization": "Bearer ${keyDisplay}" + } } } }`, @@ -207,7 +337,10 @@ function ConnectGuide() { return (
- Connect to your tools +
+ + Connect to your tools +
{Object.entries(snippets).map(([key, { label }]) => (
+ + {/* Hint line */} +
+

+ {current.hint} +

+
+
-
+        
           {current.config}
         
-
+
+ + {/* Connection test */} +
) } @@ -435,7 +579,12 @@ export function APIKeysPage() { )} {/* Connect guide */} - {activeKeys.length > 0 && } + {activeKeys.length > 0 && ( + + )} {/* Generate dialog */} { if (!open && !generatedKey) closeGenerateDialog() }}>