Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit c780b17

Browse files
committed
feat: enhance InspectorDashboard with connection state management and UI improvements
- Added connection state monitoring in InspectorDashboard to handle pending connections and display success/error messages. - Refactored connection handling logic to improve user experience during connection attempts. - Replaced the empty state message with a NotFound component for better clarity. - Updated button behavior to indicate connection status with a loading state. - Cleaned up imports and adjusted layout styles for consistency.
1 parent a27eb47 commit c780b17

14 files changed

Lines changed: 395 additions & 320 deletions

File tree

packages/inspector/src/cli/inspect.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { join, dirname } from 'node:path'
99
import { fileURLToPath } from 'node:url'
1010
import { exec } from 'node:child_process'
1111
import { promisify } from 'node:util'
12-
import faviconProxy from '../server/favicon-proxy.js'
1312
import { MCPInspector } from '../server/mcp-inspector.js'
1413

1514
const __filename = fileURLToPath(import.meta.url)
@@ -81,8 +80,6 @@ const app = new Hono()
8180
app.use('*', cors())
8281
app.use('*', logger())
8382

84-
// Mount favicon proxy
85-
app.route('/api/favicon', faviconProxy)
8683

8784
// Health check
8885
app.get('/health', (c) => {

packages/inspector/src/client/components/InspectorDashboard.tsx

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
import type { CustomHeader } from './CustomHeadersEditor'
2-
import { CircleMinus, Cog, Copy, FileText, RotateCcw, Server, Shield } from 'lucide-react'
3-
import { useState } from 'react'
2+
import { CircleMinus, Cog, Copy, FileText, RotateCcw, Shield } from 'lucide-react'
3+
import { useEffect, useRef, useState } from 'react'
44
import { useNavigate } from 'react-router-dom'
55
import { toast } from 'sonner'
66
import { Badge } from '@/components/ui/badge'
77
import { Button } from '@/components/ui/button'
8-
import { Card, CardContent } from '@/components/ui/card'
98
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
109
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
1110
import { Input } from '@/components/ui/input'
1211
import { Label } from '@/components/ui/label'
12+
import { NotFound } from '@/components/ui/not-found'
1313
import { RandomGradientBackground } from '@/components/ui/random-gradient-background'
1414
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
1515
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
1616
import { useMcpContext } from '../context/McpContext'
1717
import { CustomHeadersEditor } from './CustomHeadersEditor'
1818

1919
export function InspectorDashboard() {
20-
const { connections, addConnection, removeConnection } = useMcpContext()
20+
const mcpContext = useMcpContext()
21+
const { connections, addConnection, removeConnection } = mcpContext
2122
const navigate = useNavigate()
2223

24+
// Log connections on every render to debug
25+
console.warn('[InspectorDashboard] Render - connections:', connections.map(c => ({ id: c.id, state: c.state })))
26+
2327
// Form state
2428
const [transportType, setTransportType] = useState('SSE')
2529
const [url, setUrl] = useState('')
@@ -28,35 +32,100 @@ export function InspectorDashboard() {
2832
const [requestTimeout, setRequestTimeout] = useState('10000')
2933
const [resetTimeoutOnProgress, setResetTimeoutOnProgress] = useState('True')
3034
const [maxTotalTimeout, setMaxTotalTimeout] = useState('60000')
31-
const [proxyAddress, setProxyAddress] = useState('')
35+
const [proxyAddress, setProxyAddress] = useState(`${window.location.origin}/inspector/api/proxy`)
3236
const [proxyToken, setProxyToken] = useState('c96aeb0c195aa9c7d3846b90aec9bc5fcdd5df97b3049aaede8f5dd1a15d2d87')
3337

3438
// OAuth fields
3539
const [clientId, setClientId] = useState('')
3640
const [redirectUrl, setRedirectUrl] = useState(
3741
typeof window !== 'undefined'
3842
? new URL('/oauth/callback', window.location.origin).toString()
39-
: 'http://localhost:3000/oauth/callback',
43+
: '/oauth/callback',
4044
)
4145
const [scope, setScope] = useState('')
4246

4347
// UI state
4448
const [headersDialogOpen, setHeadersDialogOpen] = useState(false)
4549
const [authDialogOpen, setAuthDialogOpen] = useState(false)
4650
const [configDialogOpen, setConfigDialogOpen] = useState(false)
51+
const [isConnecting, setIsConnecting] = useState(false)
52+
const [pendingConnectionUrl, setPendingConnectionUrl] = useState<string | null>(null)
53+
const hasShownToastRef = useRef(false)
54+
55+
// Monitor the pending connection state
56+
useEffect(() => {
57+
if (!pendingConnectionUrl)
58+
return
59+
60+
const connection = connections.find(c => c.id === pendingConnectionUrl)
61+
if (!connection) {
62+
console.warn('[InspectorDashboard] Pending connection not found yet:', pendingConnectionUrl)
63+
return
64+
}
65+
66+
console.warn('[InspectorDashboard] Connection state:', connection.state, 'for', pendingConnectionUrl)
67+
68+
// Skip if we've already shown a toast for this connection
69+
if (hasShownToastRef.current)
70+
return
71+
72+
// Connection succeeded
73+
if (connection.state === 'ready') {
74+
console.warn('[InspectorDashboard] Connection ready!')
75+
hasShownToastRef.current = true
76+
setIsConnecting(false)
77+
setPendingConnectionUrl(null)
78+
toast.success('Connection established successfully')
79+
80+
// Reset form
81+
setUrl('')
82+
setCustomHeaders([])
83+
setClientId('')
84+
setScope('')
85+
}
86+
// Connection failed
87+
else if (connection.state === 'failed' || connection.error) {
88+
console.warn('[InspectorDashboard] Connection failed:', connection.error)
89+
hasShownToastRef.current = true
90+
setIsConnecting(false)
91+
setPendingConnectionUrl(null)
92+
const errorMessage = connection.error || 'Failed to connect to server'
93+
toast.error(errorMessage)
94+
// Don't remove the connection - let user see it in the list and manually remove if needed
95+
}
96+
}, [connections, pendingConnectionUrl])
4797

4898
const handleAddConnection = () => {
4999
if (!url.trim())
50100
return
51101

52-
// For now, use URL as both ID and name - this will need proper implementation
53-
addConnection(url, url)
102+
setIsConnecting(true)
103+
hasShownToastRef.current = false
104+
setPendingConnectionUrl(url)
105+
106+
// Prepare proxy configuration if "Via Proxy" is selected
107+
const proxyConfig = connectionType === 'Via Proxy' && proxyAddress.trim()
108+
? {
109+
proxyAddress: proxyAddress.trim(),
110+
proxyToken: proxyToken.trim(),
111+
customHeaders: customHeaders.reduce((acc, header) => {
112+
if (header.name && header.value) {
113+
acc[header.name] = header.value
114+
}
115+
return acc
116+
}, {} as Record<string, string>),
117+
}
118+
: {
119+
customHeaders: customHeaders.reduce((acc, header) => {
120+
if (header.name && header.value) {
121+
acc[header.name] = header.value
122+
}
123+
return acc
124+
}, {} as Record<string, string>),
125+
}
54126

55-
// Reset form
56-
setUrl('')
57-
setCustomHeaders([])
58-
setClientId('')
59-
setScope('')
127+
// For now, use URL as both ID and name - this will need proper implementation
128+
addConnection(url, url, proxyConfig)
60129
}
61130

62131
const handleClearAllConnections = () => {
@@ -184,15 +253,7 @@ export function InspectorDashboard() {
184253
</div>
185254
{connections.length === 0
186255
? (
187-
<Card>
188-
<CardContent className="flex flex-col items-center justify-center py-8">
189-
<Server className="h-12 w-12 text-muted-foreground mb-4" />
190-
<p className="text-muted-foreground">No servers connected yet</p>
191-
<p className="text-sm text-muted-foreground">
192-
Add a server above to get started
193-
</p>
194-
</CardContent>
195-
</Card>
256+
<NotFound message="No servers connected yet. Add a server above to get started." />
196257
)
197258
: (
198259
<div className="grid gap-3">
@@ -584,20 +645,28 @@ export function InspectorDashboard() {
584645
{/* Connect Button */}
585646
<Button
586647
onClick={handleAddConnection}
587-
disabled={!url.trim()}
648+
disabled={!url.trim() || isConnecting}
588649
className="w-full bg-white text-black hover:bg-white/90 font-semibold mt-4"
589650
>
590-
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
591-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
592-
</svg>
593-
Connect
651+
{isConnecting
652+
? (
653+
<>
654+
<svg className="w-4 h-4 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
655+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
656+
</svg>
657+
Connecting...
658+
</>
659+
)
660+
: (
661+
<>
662+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
663+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
664+
</svg>
665+
Connect
666+
</>
667+
)}
594668
</Button>
595669

596-
{/* Status */}
597-
<div className="flex items-center justify-center gap-2 text-white/60 text-sm">
598-
<div className="w-2 h-2 rounded-full bg-red-500 animate-status-pulse-red"></div>
599-
Disconnected
600-
</div>
601670
</div>
602671
<RandomGradientBackground className="absolute inset-0" />
603672
</div>

packages/inspector/src/client/components/Layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ export function Layout({ children }: LayoutProps) {
355355
<TooltipProvider>
356356
<div className="h-screen bg-[#f3f3f3] dark:bg-zinc-900 flex flex-col px-4 py-4 gap-4">
357357
{/* Header */}
358-
<header className="max-w-screen-2xl w-full mx-auto">
358+
<header className=" w-full mx-auto">
359359
<div className="flex items-center justify-between">
360360
{/* Left side: Server dropdown + Tabs */}
361361
<div className="flex items-center space-x-6">
@@ -588,7 +588,7 @@ export function Layout({ children }: LayoutProps) {
588588
</header>
589589

590590
{/* Main Content */}
591-
<main className="flex-1 max-w-screen-2xl w-full mx-auto bg-white dark:bg-zinc-800 rounded-2xl border border-zinc-200 dark:border-zinc-700 p-0 overflow-auto">
591+
<main className="flex-1 w-full mx-auto bg-white dark:bg-zinc-800 rounded-2xl border border-zinc-200 dark:border-zinc-700 p-0 overflow-auto">
592592
{selectedServer && activeTab === 'tools'
593593
? (
594594
<ToolsTab

packages/inspector/src/client/components/ServerIcon.tsx

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,73 @@ export function ServerIcon({
3636
setFaviconError(false)
3737

3838
try {
39-
const encodedUrl = encodeURIComponent(serverUrl)
40-
const proxyUrl = `/inspector/api/favicon/${encodedUrl}`
41-
42-
// Test if favicon exists
43-
const response = await fetch(proxyUrl)
44-
if (response.ok) {
45-
setFaviconUrl(proxyUrl)
39+
// Extract domain from serverUrl
40+
let domain = serverUrl
41+
if (serverUrl.startsWith('http://') || serverUrl.startsWith('https://')) {
42+
domain = new URL(serverUrl).hostname
43+
}
44+
else if (serverUrl.includes('://')) {
45+
domain = serverUrl.split('://')[1].split('/')[0]
4646
}
4747
else {
48+
domain = serverUrl.split('/')[0]
49+
}
50+
51+
// Check if this is a local server - skip remote favicon services
52+
const isLocalServer = domain === 'localhost'
53+
|| domain === '127.0.0.1'
54+
|| domain.startsWith('127.')
55+
|| domain.startsWith('192.168.')
56+
|| domain.startsWith('10.')
57+
|| domain.startsWith('172.')
58+
59+
if (isLocalServer) {
60+
// For local servers, skip favicon fetching and go straight to fallback
4861
setFaviconError(true)
62+
return
63+
}
64+
65+
// Try full domain first, then base domain with 1s timeout
66+
const baseDomain = domain.split('.').slice(-2).join('.')
67+
const domainsToTry = domain !== baseDomain ? [domain, baseDomain] : [domain]
68+
69+
const faviconServices = [
70+
// list of providers, google and duckduckgo are not working due to CORS
71+
// `https://www.google.com/s2/favicons?domain={domain}&sz=128`,
72+
`https://icon.horse/icon/{domain}`,
73+
// `https://icons.duckduckgo.com/ip3/{domain}.ico`,
74+
]
75+
76+
for (const currentDomain of domainsToTry) {
77+
for (const serviceTemplate of faviconServices) {
78+
try {
79+
const faviconUrl = serviceTemplate.replace('{domain}', currentDomain)
80+
81+
// Create a timeout promise
82+
const timeoutPromise = new Promise<never>((_, reject) => {
83+
setTimeout(() => reject(new Error('Timeout')), 1000)
84+
})
85+
86+
// Race between fetch and timeout
87+
const response = await Promise.race([
88+
fetch(faviconUrl),
89+
timeoutPromise,
90+
])
91+
92+
if (response.ok) {
93+
setFaviconUrl(faviconUrl)
94+
return
95+
}
96+
}
97+
catch {
98+
// Continue to next service
99+
continue
100+
}
101+
}
49102
}
103+
104+
// If all services fail
105+
setFaviconError(true)
50106
}
51107
catch {
52108
setFaviconError(true)

0 commit comments

Comments
 (0)