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

Commit 49a87f6

Browse files
committed
refactor(inspector): streamline development setup and enhance server functionality
- Simplify development script by removing concurrent execution of client and server - Update Vite configuration to remove unnecessary proxy settings - Refactor inspector CLI to use Hono framework, adding automatic port selection and improved health checks - Enhance resource handling in UI components, including automatic resource reading and improved loading states - Update ToolsTab to conditionally display MCP UI resources and JSON content based on user selection
1 parent 79e5bfa commit 49a87f6

9 files changed

Lines changed: 626 additions & 195 deletions

File tree

packages/inspector/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
"dist"
2727
],
2828
"scripts": {
29-
"dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
30-
"dev:client": "vite",
31-
"dev:server": "tsx watch src/server/standalone.ts",
29+
"dev": "tsx watch src/server/standalone.ts",
3230
"build": "npm run build:client && npm run build:server",
3331
"build:client": "vite build",
3432
"build:server": "tsc -p tsconfig.server.json",
Lines changed: 252 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,53 @@
11
#!/usr/bin/env node
22

3-
import express from 'express'
4-
import { mountInspector } from '../server/middleware.js'
3+
import { serve } from '@hono/node-server'
4+
import { Hono } from 'hono'
5+
import { cors } from 'hono/cors'
6+
import { logger } from 'hono/logger'
7+
import { existsSync } from 'node:fs'
8+
import { join, dirname } from 'node:path'
9+
import { fileURLToPath } from 'node:url'
10+
import { exec } from 'node:child_process'
11+
import { promisify } from 'node:util'
12+
import faviconProxy from '../server/favicon-proxy.js'
13+
import { MCPInspector } from '../server/mcp-inspector.js'
14+
15+
const __filename = fileURLToPath(import.meta.url)
16+
const __dirname = dirname(__filename)
17+
const execAsync = promisify(exec)
18+
19+
// Find available port starting from 8080
20+
async function findAvailablePort(startPort = 8080): Promise<number> {
21+
const net = await import('node:net')
22+
23+
for (let port = startPort; port < startPort + 100; port++) {
24+
try {
25+
await new Promise<void>((resolve, reject) => {
26+
const server = net.createServer()
27+
server.listen(port, () => {
28+
server.close(() => resolve())
29+
})
30+
server.on('error', () => reject(new Error(`Port ${port} is in use`)))
31+
})
32+
return port
33+
} catch {
34+
continue
35+
}
36+
}
37+
throw new Error(`No available port found starting from ${startPort}`)
38+
}
539

640
// Parse command line arguments
741
const args = process.argv.slice(2)
842
let mcpUrl: string | undefined
9-
let port = 3100
43+
let startPort = 8080
1044

1145
for (let i = 0; i < args.length; i++) {
1246
if (args[i] === '--url' && i + 1 < args.length) {
1347
mcpUrl = args[i + 1]
1448
i++
1549
} else if (args[i] === '--port' && i + 1 < args.length) {
16-
port = parseInt(args[i + 1], 10)
50+
startPort = parseInt(args[i + 1], 10)
1751
i++
1852
} else if (args[i] === '--help' || args[i] === '-h') {
1953
console.log(`
@@ -24,15 +58,15 @@ Usage:
2458
2559
Options:
2660
--url <url> MCP server URL to auto-connect to (e.g., http://localhost:3000/mcp)
27-
--port <port> Port to run the inspector on (default: 3100)
61+
--port <port> Starting port to try (default: 8080, will find next available)
2862
--help, -h Show this help message
2963
3064
Examples:
3165
# Run inspector with auto-connect
3266
npx @mcp-use/inspect --url http://localhost:3000/mcp
3367
34-
# Run on custom port
35-
npx @mcp-use/inspect --url http://localhost:3000/mcp --port 8080
68+
# Run starting from custom port
69+
npx @mcp-use/inspect --url http://localhost:3000/mcp --port 9000
3670
3771
# Run without auto-connect
3872
npx @mcp-use/inspect
@@ -41,17 +75,219 @@ Examples:
4175
}
4276
}
4377

44-
const app = express()
78+
const app = new Hono()
4579

46-
// Mount the inspector
47-
mountInspector(app, '/', mcpUrl)
80+
// Middleware
81+
app.use('*', cors())
82+
app.use('*', logger())
4883

49-
// Start the server
50-
app.listen(port, () => {
51-
console.log(`MCP Inspector running at http://localhost:${port}`)
52-
if (mcpUrl) {
53-
console.log(`📡 Auto-connecting to: ${mcpUrl}`)
84+
// Mount favicon proxy
85+
app.route('/api/favicon', faviconProxy)
86+
87+
// Health check
88+
app.get('/health', (c) => {
89+
return c.json({ status: 'ok', timestamp: new Date().toISOString() })
90+
})
91+
92+
// MCP Inspector routes
93+
const mcpInspector = new MCPInspector()
94+
95+
// List available MCP servers
96+
app.get('/api/servers', async (c) => {
97+
try {
98+
const servers = await mcpInspector.listServers()
99+
return c.json({ servers })
100+
}
101+
catch {
102+
return c.json({ error: 'Failed to list servers' }, 500)
103+
}
104+
})
105+
106+
// Connect to an MCP server
107+
app.post('/api/servers/connect', async (c) => {
108+
try {
109+
const { url, command } = await c.req.json()
110+
const server = await mcpInspector.connectToServer(url, command)
111+
return c.json({ server })
112+
}
113+
catch {
114+
return c.json({ error: 'Failed to connect to server' }, 500)
115+
}
116+
})
117+
118+
// Get server details
119+
app.get('/api/servers/:id', async (c) => {
120+
try {
121+
const id = c.req.param('id')
122+
const server = await mcpInspector.getServer(id)
123+
if (!server) {
124+
return c.json({ error: 'Server not found' }, 404)
125+
}
126+
return c.json({ server })
127+
}
128+
catch {
129+
return c.json({ error: 'Failed to get server details' }, 500)
130+
}
131+
})
132+
133+
// Execute a tool on a server
134+
app.post('/api/servers/:id/tools/:toolName/execute', async (c) => {
135+
try {
136+
const id = c.req.param('id')
137+
const toolName = c.req.param('toolName')
138+
const input = await c.req.json()
139+
140+
const result = await mcpInspector.executeTool(id, toolName, input)
141+
return c.json({ result })
142+
}
143+
catch {
144+
return c.json({ error: 'Failed to execute tool' }, 500)
145+
}
146+
})
147+
148+
// Get server tools
149+
app.get('/api/servers/:id/tools', async (c) => {
150+
try {
151+
const id = c.req.param('id')
152+
const tools = await mcpInspector.getServerTools(id)
153+
return c.json({ tools })
154+
}
155+
catch {
156+
return c.json({ error: 'Failed to get server tools' }, 500)
157+
}
158+
})
159+
160+
// Get server resources
161+
app.get('/api/servers/:id/resources', async (c) => {
162+
try {
163+
const id = c.req.param('id')
164+
const resources = await mcpInspector.getServerResources(id)
165+
return c.json({ resources })
166+
}
167+
catch {
168+
return c.json({ error: 'Failed to get server resources' }, 500)
169+
}
170+
})
171+
172+
// Disconnect from a server
173+
app.delete('/api/servers/:id', async (c) => {
174+
try {
175+
const id = c.req.param('id')
176+
await mcpInspector.disconnectServer(id)
177+
return c.json({ success: true })
178+
}
179+
catch {
180+
return c.json({ error: 'Failed to disconnect server' }, 500)
54181
}
55-
console.log(`\nOpen http://localhost:${port} in your browser to inspect MCP servers`)
56182
})
57183

184+
// Serve static assets from the built client
185+
const clientDistPath = join(__dirname, '../../dist/client')
186+
187+
if (existsSync(clientDistPath)) {
188+
// Serve static assets from /inspector/assets/* (matching Vite's base path)
189+
app.get('/inspector/assets/*', async (c) => {
190+
const path = c.req.path.replace('/inspector/assets/', 'assets/')
191+
const fullPath = join(clientDistPath, path)
192+
193+
if (existsSync(fullPath)) {
194+
const content = await import('node:fs').then(fs => fs.readFileSync(fullPath))
195+
196+
// Set appropriate content type based on file extension
197+
if (path.endsWith('.js')) {
198+
c.header('Content-Type', 'application/javascript')
199+
} else if (path.endsWith('.css')) {
200+
c.header('Content-Type', 'text/css')
201+
} else if (path.endsWith('.svg')) {
202+
c.header('Content-Type', 'image/svg+xml')
203+
}
204+
205+
return c.body(content)
206+
}
207+
208+
return c.notFound()
209+
})
210+
211+
// Redirect root path to /inspector
212+
app.get('/', (c) => {
213+
return c.redirect('/inspector')
214+
})
215+
216+
// Serve the main HTML file for /inspector and all other routes (SPA routing)
217+
app.get('*', (c) => {
218+
const indexPath = join(clientDistPath, 'index.html')
219+
if (existsSync(indexPath)) {
220+
const content = import('node:fs').then(fs => fs.readFileSync(indexPath, 'utf-8'))
221+
return c.html(content)
222+
}
223+
return c.html(`
224+
<!DOCTYPE html>
225+
<html>
226+
<head>
227+
<title>MCP Inspector</title>
228+
</head>
229+
<body>
230+
<h1>MCP Inspector</h1>
231+
<p>Client files not found. Please run 'yarn build' to build the UI.</p>
232+
<p>API is available at <a href="/api/servers">/api/servers</a></p>
233+
</body>
234+
</html>
235+
`)
236+
})
237+
} else {
238+
console.warn(`⚠️ MCP Inspector client files not found at ${clientDistPath}`)
239+
console.warn(` Run 'yarn build' in the inspector package to build the UI`)
240+
241+
// Fallback for when client is not built
242+
app.get('*', (c) => {
243+
return c.html(`
244+
<!DOCTYPE html>
245+
<html>
246+
<head>
247+
<title>MCP Inspector</title>
248+
</head>
249+
<body>
250+
<h1>MCP Inspector</h1>
251+
<p>Client files not found. Please run 'yarn build' to build the UI.</p>
252+
<p>API is available at <a href="/api/servers">/api/servers</a></p>
253+
</body>
254+
</html>
255+
`)
256+
})
257+
}
258+
259+
// Start the server with automatic port selection
260+
async function startServer() {
261+
try {
262+
const port = await findAvailablePort(startPort)
263+
264+
serve({
265+
fetch: app.fetch,
266+
port,
267+
})
268+
269+
console.log(`🚀 MCP Inspector running on http://localhost:${port}`)
270+
271+
if (mcpUrl) {
272+
console.log(`📡 Auto-connecting to: ${mcpUrl}`)
273+
}
274+
275+
// Auto-open browser
276+
try {
277+
const command = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'
278+
await execAsync(`${command} http://localhost:${port}`)
279+
console.log(`🌐 Browser opened automatically`)
280+
} catch (error) {
281+
console.log(`🌐 Please open http://localhost:${port} in your browser`)
282+
}
283+
284+
return { port, fetch: app.fetch }
285+
} catch (error) {
286+
console.error('Failed to start server:', error)
287+
process.exit(1)
288+
}
289+
}
290+
291+
// Start the server
292+
startServer()
293+

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

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,46 @@ export function Layout({ children }: LayoutProps) {
188188
}, [location.pathname])
189189

190190
// If no server is selected and we're on a server route, navigate to root
191+
// But only after we've given connections time to load and establish
191192
useEffect(() => {
192193
const serverIdFromRoute = location.pathname.split('/servers/')[1]
193194
const decodedServerId = serverIdFromRoute ? decodeURIComponent(serverIdFromRoute) : null
194195

195-
// Only navigate away if we've already set selectedServerId from the route
196-
// and the server still wasn't found in connections
197-
if (!selectedServer && location.pathname.startsWith('/servers/') && selectedServerId === decodedServerId) {
198-
navigate('/')
196+
const isServerRoute = location.pathname.startsWith('/servers/')
197+
const hasServerId = selectedServerId === decodedServerId
198+
199+
if (!isServerRoute || !hasServerId || !configLoaded) {
200+
return
199201
}
200-
}, [selectedServer, location.pathname, navigate, selectedServerId])
202+
203+
// Check if any connection exists for this server
204+
const serverConnection = connections.find(conn => conn.id === decodedServerId)
205+
206+
if (serverConnection) {
207+
// Server connection exists - check its state
208+
if (serverConnection.state === 'failed') {
209+
// Server failed to connect, redirect after a short delay
210+
const timeoutId = setTimeout(() => {
211+
navigate('/')
212+
}, 2000)
213+
return () => clearTimeout(timeoutId)
214+
}
215+
// If server is connecting/loading/discovering, don't redirect yet
216+
if (serverConnection.state === 'connecting' || serverConnection.state === 'loading' || serverConnection.state === 'discovering') {
217+
return
218+
}
219+
// If server is ready, we're good - no redirect needed
220+
return
221+
}
222+
223+
// No connection found for this server
224+
// Wait a bit for auto-connection to potentially kick in, then redirect
225+
const timeoutId = setTimeout(() => {
226+
navigate('/')
227+
}, 3000)
228+
229+
return () => clearTimeout(timeoutId)
230+
}, [selectedServer, location.pathname, navigate, selectedServerId, connections, configLoaded])
201231

202232
// Handle keyboard shortcuts
203233
useEffect(() => {
@@ -393,7 +423,7 @@ export function Layout({ children }: LayoutProps) {
393423
? (
394424
<ResourcesTab
395425
resources={selectedServer.resources}
396-
readResource={(uri: string) => selectedServer.callTool('read_resource', { uri })} // Using callTool for now, should be readResource when available
426+
readResource={selectedServer.readResource}
397427
isConnected={selectedServer.state === 'ready'}
398428
/>
399429
)

0 commit comments

Comments
 (0)