diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index f56f4444..a74cebe8 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @mcp-use/cli +## 2.1.8 + +### Patch Changes + +- Updated dependencies + - @mcp-use/inspector@0.3.8 + - mcp-use@1.0.4 + ## 2.1.7 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 57ee2a5e..96831b15 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mcp-use/cli", - "version": "2.1.7", + "version": "2.1.8", "description": "Build tool for MCP UI widgets - bundles React components into standalone HTML pages for Model Context Protocol servers", "author": "mcp-use, Inc.", "license": "MIT", diff --git a/packages/inspector/CHANGELOG.md b/packages/inspector/CHANGELOG.md index c93d6a57..a08ec166 100644 --- a/packages/inspector/CHANGELOG.md +++ b/packages/inspector/CHANGELOG.md @@ -1,5 +1,13 @@ # @mcp-use/inspector +## 0.3.8 + +### Patch Changes + +- fix: support multiple clients per server +- Updated dependencies + - mcp-use@1.0.4 + ## 0.3.7 ### Patch Changes diff --git a/packages/inspector/package.json b/packages/inspector/package.json index c2c25957..bdc8b75a 100644 --- a/packages/inspector/package.json +++ b/packages/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@mcp-use/inspector", "type": "module", - "version": "0.3.7", + "version": "0.3.8", "description": "MCP Inspector - A tool for inspecting and debugging MCP servers", "author": "", "license": "MIT", @@ -62,6 +62,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/cli": "^4.1.14", "@tailwindcss/vite": "^4.1.14", diff --git a/packages/inspector/src/client/components/InspectorDashboard.tsx b/packages/inspector/src/client/components/InspectorDashboard.tsx index 40046c70..3713fa97 100644 --- a/packages/inspector/src/client/components/InspectorDashboard.tsx +++ b/packages/inspector/src/client/components/InspectorDashboard.tsx @@ -1,6 +1,7 @@ import type { CustomHeader } from './CustomHeadersEditor' -import { CircleMinus, Cog, Copy, FileText, RotateCcw, Shield } from 'lucide-react' -import React, { useEffect, useRef, useState } from 'react' +import { CircleMinus, Cog, Copy, FileText, Loader2, RotateCcw, Shield } from 'lucide-react' +import { useMcp } from 'mcp-use/react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' @@ -12,14 +13,82 @@ import { Label } from '@/components/ui/label' import { NotFound } from '@/components/ui/not-found' import { RandomGradientBackground } from '@/components/ui/random-gradient-background' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useMcpContext } from '../context/McpContext' import { CustomHeadersEditor } from './CustomHeadersEditor' +// Temporary connection tester component +function ConnectionTester({ config, onSuccess, onFailure }: { + config: { + url: string + name: string + proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' + } + onSuccess: () => void + onFailure: (error: string) => void +}) { + const callbackUrl = typeof window !== 'undefined' + ? new URL('/oauth/callback', window.location.origin).toString() + : '/oauth/callback' + + // Apply proxy configuration + let finalUrl = config.url + let customHeaders: Record = {} + + if (config.proxyConfig?.proxyAddress) { + const proxyUrl = new URL(config.proxyConfig.proxyAddress) + const originalUrl = new URL(config.url) + finalUrl = `${proxyUrl.origin}${proxyUrl.pathname}${originalUrl.pathname}${originalUrl.search}` + + if (config.proxyConfig.proxyToken) { + customHeaders['X-Proxy-Token'] = config.proxyConfig.proxyToken + } + customHeaders['X-Target-URL'] = config.url + } + + if (config.proxyConfig?.customHeaders) { + customHeaders = { ...customHeaders, ...config.proxyConfig.customHeaders } + } + + const mcpHook = useMcp({ + url: finalUrl, + callbackUrl, + customHeaders: Object.keys(customHeaders).length > 0 ? customHeaders : undefined, + transportType: config.transportType || 'http', + }) + + const hasCalledRef = useRef(false) + + useEffect(() => { + if (hasCalledRef.current) + return + + if (mcpHook.state === 'ready') { + hasCalledRef.current = true + // Don't clear storage on success - we want to keep the connection alive + // The real McpConnectionWrapper will take over + onSuccess() + } + else if (mcpHook.state === 'failed' || mcpHook.error) { + hasCalledRef.current = true + const errorMessage = mcpHook.error || 'Failed to connect to server' + // Clear storage on failure to clean up the failed connection attempt + mcpHook.clearStorage() + onFailure(errorMessage) + } + }, [mcpHook.state, mcpHook.error, onSuccess, onFailure, mcpHook]) + + return null +} + export function InspectorDashboard() { const mcpContext = useMcpContext() - const { connections, addConnection, removeConnection } = mcpContext + const { connections, addConnection, removeConnection, autoConnect, setAutoConnect, connectServer, disconnectServer: _disconnectServer } = mcpContext const navigate = useNavigate() + const [connectingServers, setConnectingServers] = useState>(new Set()) + const [pendingNavigation, setPendingNavigation] = useState(null) // Log connections on every render to debug console.warn('[InspectorDashboard] Render - connections:', connections.map(c => ({ id: c.id, state: c.state }))) @@ -49,59 +118,31 @@ export function InspectorDashboard() { const [authDialogOpen, setAuthDialogOpen] = useState(false) const [configDialogOpen, setConfigDialogOpen] = useState(false) const [isConnecting, setIsConnecting] = useState(false) - const [pendingConnectionUrl, setPendingConnectionUrl] = useState(null) + const [autoSwitch, setAutoSwitch] = useState(true) const hasShownToastRef = useRef(false) - - // Monitor the pending connection state + const [hasTriedBothConnectionTypes, setHasTriedBothConnectionTypes] = useState(false) + const [pendingConnectionConfig, setPendingConnectionConfig] = useState<{ + url: string + name: string + proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' + } | null>(null) + + // Load auto-switch setting from localStorage on mount useEffect(() => { - if (!pendingConnectionUrl) - return - - const connection = connections.find(c => c.id === pendingConnectionUrl) - if (!connection) { - console.warn('[InspectorDashboard] Pending connection not found yet:', pendingConnectionUrl) - return + const autoSwitchSetting = localStorage.getItem('mcp-inspector-auto-switch') + if (autoSwitchSetting !== null) { + setAutoSwitch(autoSwitchSetting === 'true') } + }, []) - console.warn('[InspectorDashboard] Connection state:', connection.state, 'for', pendingConnectionUrl) - - // Skip if we've already shown a toast for this connection - if (hasShownToastRef.current) - return - - // Connection succeeded - if (connection.state === 'ready') { - console.warn('[InspectorDashboard] Connection ready!') - hasShownToastRef.current = true - setIsConnecting(false) - setPendingConnectionUrl(null) - toast.success('Connection established successfully') - - // Reset form - setUrl('') - setCustomHeaders([]) - setClientId('') - setScope('') - } - // Connection failed - else if (connection.state === 'failed' || connection.error) { - console.warn('[InspectorDashboard] Connection failed:', connection.error) - hasShownToastRef.current = true - setIsConnecting(false) - setPendingConnectionUrl(null) - const errorMessage = connection.error || 'Failed to connect to server' - toast.error(errorMessage) - // Don't remove the connection - let user see it in the list and manually remove if needed - } - }, [connections, pendingConnectionUrl]) - - const handleAddConnection = () => { + const handleAddConnection = useCallback(() => { if (!url.trim()) return setIsConnecting(true) hasShownToastRef.current = false - setPendingConnectionUrl(url) + setHasTriedBothConnectionTypes(false) // Prepare proxy configuration if "Via Proxy" is selected const proxyConfig = connectionType === 'Via Proxy' && proxyAddress.trim() @@ -124,9 +165,87 @@ export function InspectorDashboard() { }, {} as Record), } - // For now, use URL as both ID and name - this will need proper implementation - addConnection(url, url, proxyConfig) - } + // Map UI transport type to actual transport type + // "SSE" in UI means "Streamable HTTP" which uses 'http' transport + // "WebSocket" in UI means "WebSocket" which uses 'sse' transport + const actualTransportType = transportType === 'SSE' ? 'http' : 'sse' + + // Store pending connection config - don't add to saved connections yet + setPendingConnectionConfig({ + url, + name: url, + proxyConfig, + transportType: actualTransportType, + }) + }, [url, connectionType, proxyAddress, proxyToken, customHeaders, transportType]) + + // Handle successful connection + const handleConnectionSuccess = useCallback(() => { + if (!pendingConnectionConfig) + return + + console.warn('[InspectorDashboard] Connection ready! Saving to list...') + setIsConnecting(false) + + // Add to saved connections now that it's successful + addConnection( + pendingConnectionConfig.url, + pendingConnectionConfig.name, + pendingConnectionConfig.proxyConfig, + pendingConnectionConfig.transportType, + ) + + setPendingConnectionConfig(null) + toast.success('Connection established successfully') + + // Reset form + setUrl('') + setCustomHeaders([]) + setClientId('') + setScope('') + }, [pendingConnectionConfig, addConnection]) + + // Handle failed connection + const handleConnectionFailure = useCallback((errorMessage: string) => { + console.warn('[InspectorDashboard] Connection failed:', errorMessage) + + // Try auto-switch if enabled and we haven't tried both connection types yet + if (autoSwitch && !hasTriedBothConnectionTypes) { + const shouldTryProxy = connectionType === 'Direct' + const shouldTryDirect = connectionType === 'Via Proxy' + + if (shouldTryProxy) { + toast.error('Direct connection failed, trying with proxy...') + setHasTriedBothConnectionTypes(true) + // Clear pending config first to unmount the old ConnectionTester + setPendingConnectionConfig(null) + // Switch to proxy and retry after a brief delay + setConnectionType('Via Proxy') + setTimeout(() => { + setIsConnecting(true) + handleAddConnection() + }, 1000) // Small delay to show the toast + } + else if (shouldTryDirect) { + toast.error('Proxy connection failed, trying direct...') + setHasTriedBothConnectionTypes(true) + // Clear pending config first to unmount the old ConnectionTester + setPendingConnectionConfig(null) + // Switch to direct and retry after a brief delay + setConnectionType('Direct') + setTimeout(() => { + setIsConnecting(true) + handleAddConnection() + }, 1000) // Small delay to show the toast + } + } + else { + toast.error(errorMessage) + // Clear pending config on final failure + setPendingConnectionConfig(null) + setIsConnecting(false) + } + }, [autoSwitch, hasTriedBothConnectionTypes, connectionType, handleAddConnection]) const handleClearAllConnections = () => { // Remove all connections @@ -151,6 +270,15 @@ export function InspectorDashboard() { } const handleServerClick = (connection: any) => { + // If disconnected, connect the server + if (connection.state === 'disconnected') { + console.warn('[InspectorDashboard] Connecting server and setting pending navigation:', connection.id) + setConnectingServers(prev => new Set(prev).add(connection.id)) + setPendingNavigation(connection.id) + connectServer(connection.id) + return + } + if (connection.state !== 'ready') { toast.error('Server is not connected and cannot be inspected') return @@ -158,6 +286,50 @@ export function InspectorDashboard() { navigate(`/servers/${encodeURIComponent(connection.id)}`) } + // Monitor connecting servers and remove them from the set when they connect or fail + useEffect(() => { + connectingServers.forEach((serverId) => { + const connection = connections.find(c => c.id === serverId) + if (connection && (connection.state === 'ready' || connection.state === 'failed')) { + setConnectingServers((prev) => { + const next = new Set(prev) + next.delete(serverId) + return next + }) + } + }) + }, [connections, connectingServers]) + + // Monitor pending navigation and navigate when server becomes ready + useEffect(() => { + if (!pendingNavigation) + return + + const connection = connections.find(c => c.id === pendingNavigation) + const hasData = (connection?.tools?.length || 0) > 0 + || (connection?.resources?.length || 0) > 0 + || (connection?.prompts?.length || 0) > 0 + + console.warn('[InspectorDashboard] Pending navigation check:', { + pendingNavigation, + connectionState: connection?.state, + hasData, + toolsCount: connection?.tools?.length || 0, + }) + + // Navigate if connection is ready OR if it has loaded some data (partial success) + if (connection && (connection.state === 'ready' || (hasData && connection.state !== 'connecting'))) { + console.warn('[InspectorDashboard] Navigating to server:', connection.id) + setPendingNavigation(null) + navigate(`/servers/${encodeURIComponent(connection.id)}`) + } + // Only cancel navigation if connection truly failed with no data loaded + else if (connection && connection.state === 'failed' && !hasData && connection.error) { + console.warn('[InspectorDashboard] Connection failed with no data, canceling navigation') + setPendingNavigation(null) + } + }, [connections, pendingNavigation, navigate]) + const handleExportServerEntry = async () => { if (!url.trim()) { toast.error('Please enter a URL first') @@ -222,7 +394,7 @@ export function InspectorDashboard() { > v - {typeof window !== 'undefined' && (window as any).__INSPECTOR_VERSION__ || '1.0.0'} + {(typeof window !== 'undefined' && (window as any).__INSPECTOR_VERSION__) || '1.0.0'} @@ -238,7 +410,17 @@ export function InspectorDashboard() {

Connected Servers

-
+
+
+ + +
{connections.length > 0 && (
{connections.length === 0 @@ -268,36 +449,43 @@ export function InspectorDashboard() {

{connection.name}

- {connection.error + {connectingServers.has(connection.id) ? ( - - -
@@ -307,6 +495,7 @@ export function InspectorDashboard() { - - -

Resync connection

-
-
+ {connection.state !== 'disconnected' && ( + + + + + +

Resync connection

+
+
+ )}
{connection.state === 'pending_auth' && connection.authUrl && ( @@ -398,8 +589,8 @@ export function InspectorDashboard() { - Streamable HTTP - WebSocket + Streamable HTTP (Recommended) + Server-Sent Events (SSE)
@@ -417,7 +608,23 @@ export function InspectorDashboard() { {/* Connection Type */}
- +
+ +
+ + { + setAutoSwitch(value) + localStorage.setItem('mcp-inspector-auto-switch', String(value)) + }} + className="scale-75" + /> +
+