Skip to content

Commit 073e14e

Browse files
committed
fix(inspector): update connection type and enhance retry logic in MCP client
- Change default connection type from 'Via Proxy' to 'Direct' in InspectorDashboard - Modify connection retry logic in useMcp to prevent infinite authentication loops - Improve logging for connection attempts and authentication success scenarios
1 parent d8d6949 commit 073e14e

File tree

3 files changed

+282
-9
lines changed

3 files changed

+282
-9
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function InspectorDashboard() {
2323
// Form state
2424
const [transportType, setTransportType] = useState('SSE')
2525
const [url, setUrl] = useState('')
26-
const [connectionType, setConnectionType] = useState('Via Proxy')
26+
const [connectionType, setConnectionType] = useState('Direct')
2727
const [customHeaders, setCustomHeaders] = useState<CustomHeader[]>([])
2828
const [requestTimeout, setRequestTimeout] = useState('10000')
2929
const [resetTimeoutOnProgress, setResetTimeoutOnProgress] = useState('True')
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { exec } from 'node:child_process'
2+
import { existsSync } from 'node:fs'
3+
import { dirname, join } from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
import { promisify } from 'node:util'
6+
import { serve } from '@hono/node-server'
7+
import { Hono } from 'hono'
8+
import { cors } from 'hono/cors'
9+
import { logger } from 'hono/logger'
10+
import faviconProxy from './favicon-proxy.js'
11+
import { MCPInspector } from './mcp-inspector.js'
12+
13+
const __filename = fileURLToPath(import.meta.url)
14+
const __dirname = dirname(__filename)
15+
const execAsync = promisify(exec)
16+
17+
// Find available port starting from 8080
18+
async function findAvailablePort(startPort = 8080): Promise<number> {
19+
const net = await import('node:net')
20+
21+
for (let port = startPort; port < startPort + 100; port++) {
22+
try {
23+
await new Promise<void>((resolve, reject) => {
24+
const server = net.createServer()
25+
server.listen(port, () => {
26+
server.close(() => resolve())
27+
})
28+
server.on('error', () => reject(new Error(`Port ${port} is in use`)))
29+
})
30+
return port
31+
}
32+
catch {
33+
continue
34+
}
35+
}
36+
throw new Error(`No available port found starting from ${startPort}`)
37+
}
38+
39+
const app = new Hono()
40+
41+
// Middleware
42+
app.use('*', cors())
43+
app.use('*', logger())
44+
45+
// Mount favicon proxy
46+
app.route('/api/favicon', faviconProxy)
47+
48+
// Health check
49+
app.get('/health', (c) => {
50+
return c.json({ status: 'ok', timestamp: new Date().toISOString() })
51+
})
52+
53+
// MCP Inspector routes
54+
const mcpInspector = new MCPInspector()
55+
56+
// List available MCP servers
57+
app.get('/api/servers', async (c) => {
58+
try {
59+
const servers = await mcpInspector.listServers()
60+
return c.json({ servers })
61+
}
62+
catch {
63+
return c.json({ error: 'Failed to list servers' }, 500)
64+
}
65+
})
66+
67+
// Connect to an MCP server
68+
app.post('/api/servers/connect', async (c) => {
69+
try {
70+
const { url, command } = await c.req.json()
71+
const server = await mcpInspector.connectToServer(url, command)
72+
return c.json({ server })
73+
}
74+
catch {
75+
return c.json({ error: 'Failed to connect to server' }, 500)
76+
}
77+
})
78+
79+
// Get server details
80+
app.get('/api/servers/:id', async (c) => {
81+
try {
82+
const id = c.req.param('id')
83+
const server = await mcpInspector.getServer(id)
84+
if (!server) {
85+
return c.json({ error: 'Server not found' }, 404)
86+
}
87+
return c.json({ server })
88+
}
89+
catch {
90+
return c.json({ error: 'Failed to get server details' }, 500)
91+
}
92+
})
93+
94+
// Execute a tool on a server
95+
app.post('/api/servers/:id/tools/:toolName/execute', async (c) => {
96+
try {
97+
const id = c.req.param('id')
98+
const toolName = c.req.param('toolName')
99+
const input = await c.req.json()
100+
101+
const result = await mcpInspector.executeTool(id, toolName, input)
102+
return c.json({ result })
103+
}
104+
catch {
105+
return c.json({ error: 'Failed to execute tool' }, 500)
106+
}
107+
})
108+
109+
// Get server tools
110+
app.get('/api/servers/:id/tools', async (c) => {
111+
try {
112+
const id = c.req.param('id')
113+
const tools = await mcpInspector.getServerTools(id)
114+
return c.json({ tools })
115+
}
116+
catch {
117+
return c.json({ error: 'Failed to get server tools' }, 500)
118+
}
119+
})
120+
121+
// Get server resources
122+
app.get('/api/servers/:id/resources', async (c) => {
123+
try {
124+
const id = c.req.param('id')
125+
const resources = await mcpInspector.getServerResources(id)
126+
return c.json({ resources })
127+
}
128+
catch {
129+
return c.json({ error: 'Failed to get server resources' }, 500)
130+
}
131+
})
132+
133+
// Disconnect from a server
134+
app.delete('/api/servers/:id', async (c) => {
135+
try {
136+
const id = c.req.param('id')
137+
await mcpInspector.disconnectServer(id)
138+
return c.json({ success: true })
139+
}
140+
catch {
141+
return c.json({ error: 'Failed to disconnect server' }, 500)
142+
}
143+
})
144+
145+
// Serve static assets from the built client
146+
const clientDistPath = join(__dirname, '../../dist/client')
147+
148+
if (existsSync(clientDistPath)) {
149+
// Serve static assets from /inspector/assets/* (matching Vite's base path)
150+
app.get('/inspector/assets/*', async (c) => {
151+
const path = c.req.path.replace('/inspector/assets/', 'assets/')
152+
const fullPath = join(clientDistPath, path)
153+
154+
if (existsSync(fullPath)) {
155+
const content = await import('node:fs').then(fs => fs.readFileSync(fullPath))
156+
157+
// Set appropriate content type based on file extension
158+
if (path.endsWith('.js')) {
159+
c.header('Content-Type', 'application/javascript')
160+
}
161+
else if (path.endsWith('.css')) {
162+
c.header('Content-Type', 'text/css')
163+
}
164+
else if (path.endsWith('.svg')) {
165+
c.header('Content-Type', 'image/svg+xml')
166+
}
167+
168+
return c.body(content)
169+
}
170+
171+
return c.notFound()
172+
})
173+
174+
// Redirect root path to /inspector
175+
app.get('/', (c) => {
176+
return c.redirect('/inspector')
177+
})
178+
179+
// Serve the main HTML file for /inspector and all other routes (SPA routing)
180+
app.get('*', (c) => {
181+
const indexPath = join(clientDistPath, 'index.html')
182+
if (existsSync(indexPath)) {
183+
const content = import('node:fs').then(fs => fs.readFileSync(indexPath, 'utf-8'))
184+
return c.html(content)
185+
}
186+
return c.html(`
187+
<!DOCTYPE html>
188+
<html>
189+
<head>
190+
<title>MCP Inspector</title>
191+
</head>
192+
<body>
193+
<h1>MCP Inspector</h1>
194+
<p>Client files not found. Please run 'yarn build' to build the UI.</p>
195+
<p>API is available at <a href="/api/servers">/api/servers</a></p>
196+
</body>
197+
</html>
198+
`)
199+
})
200+
}
201+
else {
202+
console.warn(`⚠️ MCP Inspector client files not found at ${clientDistPath}`)
203+
console.warn(` Run 'yarn build' in the inspector package to build the UI`)
204+
205+
// Fallback for when client is not built
206+
app.get('*', (c) => {
207+
return c.html(`
208+
<!DOCTYPE html>
209+
<html>
210+
<head>
211+
<title>MCP Inspector</title>
212+
</head>
213+
<body>
214+
<h1>MCP Inspector</h1>
215+
<p>Client files not found. Please run 'yarn build' to build the UI.</p>
216+
<p>API is available at <a href="/api/servers">/api/servers</a></p>
217+
</body>
218+
</html>
219+
`)
220+
})
221+
}
222+
223+
// Start the server with automatic port selection
224+
async function startServer() {
225+
try {
226+
const port = await findAvailablePort()
227+
228+
serve({
229+
fetch: app.fetch,
230+
port,
231+
})
232+
233+
console.log(`🚀 MCP Inspector running on http://localhost:${port}`)
234+
235+
// Auto-open browser in development
236+
if (process.env.NODE_ENV !== 'production') {
237+
try {
238+
const command = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'
239+
await execAsync(`${command} http://localhost:${port}`)
240+
console.log(`🌐 Browser opened automatically`)
241+
}
242+
catch (error) {
243+
console.log(`🌐 Please open http://localhost:${port} in your browser`)
244+
}
245+
}
246+
247+
return { port, fetch: app.fetch }
248+
}
249+
catch (error) {
250+
console.error('Failed to start server:', error)
251+
process.exit(1)
252+
}
253+
}
254+
255+
// Start the server if this file is run directly
256+
if (import.meta.url === `file://${process.argv[1]}`) {
257+
startServer()
258+
}
259+
260+
export default { startServer }

packages/mcp-use/src/react/useMcp.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
186186
addLog('debug', 'MCP Client initialized in connect.')
187187
}
188188

189-
const tryConnectWithTransport = async (transportTypeParam: TransportType): Promise<'success' | 'fallback' | 'auth_redirect' | 'failed'> => {
190-
addLog('info', `Attempting connection with ${transportTypeParam.toUpperCase()} transport...`)
189+
const tryConnectWithTransport = async (transportTypeParam: TransportType, isAuthRetry = false): Promise<'success' | 'fallback' | 'auth_redirect' | 'failed'> => {
190+
addLog('info', `Attempting connection with ${transportTypeParam.toUpperCase()} transport${isAuthRetry ? ' (after auth)' : ''}...`)
191191
if (stateRef.current !== 'authenticating') {
192192
setState('connecting')
193193
}
@@ -335,6 +335,13 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
335335
}
336336

337337
if (errorInstance instanceof UnauthorizedError || errorMessage.includes('Unauthorized') || errorMessage.includes('401')) {
338+
// Prevent infinite auth loops - only retry once after auth
339+
if (isAuthRetry) {
340+
addLog('error', 'Authentication failed even after successful token refresh. This may indicate a server issue.')
341+
failConnection('Authentication loop detected - auth succeeded but connection still unauthorized.')
342+
return 'failed'
343+
}
344+
338345
addLog('info', 'Authentication required.')
339346

340347
assert(authProviderRef.current, 'Auth Provider not available for auth flow')
@@ -366,11 +373,11 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
366373
if (!isMountedRef.current) return 'failed'
367374

368375
if (authResult === 'AUTHORIZED') {
369-
addLog('info', 'Authentication successful via existing token or refresh. Re-attempting connection...')
376+
addLog('info', 'Authentication successful via existing token or refresh. Retrying transport connection...')
370377
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
371-
connectingRef.current = false
372-
connect()
373-
return 'failed'
378+
authTimeoutRef.current = null
379+
// Retry the same transport type with isAuthRetry=true to prevent loops
380+
return await tryConnectWithTransport(transportTypeParam, true)
374381
} else if (authResult === 'REDIRECT') {
375382
addLog('info', 'Redirecting for authentication. Waiting for callback...')
376383
return 'auth_redirect'
@@ -673,11 +680,17 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
673680
if (event.data?.type === 'mcp_auth_callback') {
674681
addLog('info', 'Received auth callback message.', event.data)
675682
if (authTimeoutRef.current) clearTimeout(authTimeoutRef.current)
683+
authTimeoutRef.current = null
676684

677685
if (event.data.success) {
678686
addLog('info', 'Authentication successful via popup. Reconnecting client...')
679-
connectingRef.current = false
680-
connectRef.current()
687+
// Ensure we're not already in a connection attempt to prevent loops
688+
if (!connectingRef.current) {
689+
connectingRef.current = false // Reset flag before connecting
690+
connectRef.current()
691+
} else {
692+
addLog('warn', 'Connection already in progress, skipping reconnection from auth callback')
693+
}
681694
} else {
682695
failConnectionRef.current(`Authentication failed in callback: ${event.data.error || 'Unknown reason.'}`)
683696
}

0 commit comments

Comments
 (0)