From c1dc6663ef9911784dd87a59b5667b19f131e9cf Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:12:06 +0200 Subject: [PATCH 1/6] fix status dot --- .../client/components/InspectorDashboard.tsx | 15 +++++++---- .../src/client/components/Layout.tsx | 6 ++--- .../src/client/context/McpContext.tsx | 26 ++++++++++++++----- packages/mcp-use/src/server/mcp-server.ts | 8 ++++-- pnpm-lock.yaml | 6 +++++ 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/inspector/src/client/components/InspectorDashboard.tsx b/packages/inspector/src/client/components/InspectorDashboard.tsx index 40046c70..f44c80d0 100644 --- a/packages/inspector/src/client/components/InspectorDashboard.tsx +++ b/packages/inspector/src/client/components/InspectorDashboard.tsx @@ -124,8 +124,13 @@ export function InspectorDashboard() { }, {} as Record), } + // 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' + // For now, use URL as both ID and name - this will need proper implementation - addConnection(url, url, proxyConfig) + addConnection(url, url, proxyConfig, actualTransportType) } const handleClearAllConnections = () => { @@ -222,7 +227,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'} @@ -268,7 +273,7 @@ export function InspectorDashboard() {

{connection.name}

- {connection.error + {connection.error && connection.state !== 'ready' ? ( @@ -398,8 +403,8 @@ export function InspectorDashboard() { - Streamable HTTP - WebSocket + Streamable HTTP (Recommended) + Server-Sent Events (SSE)
diff --git a/packages/inspector/src/client/components/Layout.tsx b/packages/inspector/src/client/components/Layout.tsx index 001e3fa6..fea366a1 100644 --- a/packages/inspector/src/client/components/Layout.tsx +++ b/packages/inspector/src/client/components/Layout.tsx @@ -233,7 +233,7 @@ export function Layout({ children }: LayoutProps) { const existing = connections.find(c => c.url === autoConnectUrl) if (!existing) { setIsAutoConnecting(true) - addConnection(autoConnectUrl, 'Local MCP Server') + addConnection(autoConnectUrl, 'Local MCP Server', undefined, 'http') // Navigate immediately but keep loading screen visible a bit longer to avoid flash navigate(`/servers/${encodeURIComponent(autoConnectUrl)}`) setTimeout(() => { @@ -254,7 +254,7 @@ export function Layout({ children }: LayoutProps) { const existing = connections.find(c => c.url === config.autoConnectUrl) if (!existing) { // Auto-connect to the local server - addConnection(config.autoConnectUrl, 'Local MCP Server') + addConnection(config.autoConnectUrl, 'Local MCP Server', undefined, 'http') } } }) @@ -383,7 +383,7 @@ export function Layout({ children }: LayoutProps) { {selectedServer && (
- {selectedServer.error + {selectedServer.error && selectedServer.state !== 'ready' ? ( diff --git a/packages/inspector/src/client/context/McpContext.tsx b/packages/inspector/src/client/context/McpContext.tsx index b3945974..284800ce 100644 --- a/packages/inspector/src/client/context/McpContext.tsx +++ b/packages/inspector/src/client/context/McpContext.tsx @@ -21,7 +21,7 @@ interface MCPConnection { interface McpContextType { connections: MCPConnection[] - addConnection: (url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }) => void + addConnection: (url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }, transportType?: 'http' | 'sse') => void removeConnection: (id: string) => void getConnection: (id: string) => MCPConnection | undefined } @@ -33,12 +33,14 @@ interface SavedConnection { url: string name: string proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' } -function McpConnectionWrapper({ url, name, proxyConfig, onUpdate, onRemove: _onRemove }: { +function McpConnectionWrapper({ url, name, proxyConfig, transportType, onUpdate, onRemove: _onRemove }: { url: string name: string proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' onUpdate: (connection: MCPConnection) => void onRemove: () => void }) { @@ -79,6 +81,7 @@ function McpConnectionWrapper({ url, name, proxyConfig, onUpdate, onRemove: _onR url: finalUrl, callbackUrl, customHeaders: Object.keys(customHeaders).length > 0 ? customHeaders : undefined, + transportType: transportType || 'http', // Default to 'http' for Streamable HTTP }) const onUpdateRef = useRef(onUpdate) const prevConnectionRef = useRef(null) @@ -194,12 +197,21 @@ export function McpProvider({ children }: { children: ReactNode }) { && typeof conn.id === 'string' && typeof conn.url === 'string' && typeof conn.name === 'string' + }).map((conn: any) => { + // Migrate existing connections to include transportType + if (!conn.transportType) { + conn.transportType = 'http' // Default to 'http' for Streamable HTTP + } + return conn }) : [] - // If we filtered out any invalid connections, update localStorage - if (validConnections.length !== parsed.length) { - console.warn('Cleaned up invalid connections from localStorage') + // If we filtered out any invalid connections or migrated transport types, update localStorage + const hasChanges = validConnections.length !== parsed.length + || validConnections.some((conn: any) => conn.transportType === 'http' && !parsed.find((p: any) => p.id === conn.id && p.transportType)) + + if (hasChanges) { + console.warn('Updated connections in localStorage with transport type migration') localStorage.setItem('mcp-inspector-connections', JSON.stringify(validConnections)) } @@ -218,13 +230,14 @@ export function McpProvider({ children }: { children: ReactNode }) { setConnectionVersion(v => v + 1) }, []) - const addConnection = useCallback((url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }) => { + const addConnection = useCallback((url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }, transportType?: 'http' | 'sse') => { const connectionName = name || url const newConnection: SavedConnection = { id: url, url, name: connectionName, proxyConfig, + transportType: transportType || 'http', // Default to 'http' for Streamable HTTP } setSavedConnections((prev) => { @@ -289,6 +302,7 @@ export function McpProvider({ children }: { children: ReactNode }) { url={saved.url} name={saved.name} proxyConfig={saved.proxyConfig} + transportType={saved.transportType} onUpdate={updateConnection} onRemove={() => removeConnection(saved.id)} /> diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 20b6bb50..9ca59382 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -46,7 +46,7 @@ export class McpServer { this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', 'Content-Type') + res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, mcp-protocol-version, mcp-session-id, X-Proxy-Token, X-Target-URL') next() }) @@ -316,10 +316,14 @@ export class McpServer { const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js') // Create StreamableHTTPServerTransport in stateless mode + // Note: Stateless mode means one active connection at a time + // For multiple concurrent inspectors, use the built-in proxy or same-origin connections const httpTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined // Stateless mode + sessionIdGenerator: undefined, // Stateless mode + enableJsonResponse: true }) + // Connect the MCP server to the transport await this.server.connect(httpTransport) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c81d68..2c82159d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -521,6 +521,9 @@ importers: '@mcp-ui/server': specifier: ^5.11.0 version: 5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11) + '@modelcontextprotocol/sdk': + specifier: ^1.0.0 + version: 1.20.0 cors: specifier: ^2.8.5 version: 2.8.5 @@ -530,6 +533,9 @@ importers: mcp-use: specifier: workspace:* version: link:../packages/mcp-use + zod: + specifier: ^3.22.0 + version: 3.25.76 devDependencies: '@mcp-use/cli': specifier: workspace:* From 169967e2c75989097a90796b434f266df8ea2be5 Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:18:28 +0200 Subject: [PATCH 2/6] fix: 1 transport per request --- packages/mcp-use/src/server/mcp-server.ts | 56 +++++++++++++++-------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 9ca59382..7ef56dd9 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -296,7 +296,8 @@ export class McpServer { * * Sets up the HTTP transport layer for the MCP server, creating endpoints for * Server-Sent Events (SSE) streaming, POST message handling, and DELETE session cleanup. - * Uses stateless mode for session management, making it suitable for stateless deployments. + * Each request gets its own transport instance to prevent state conflicts between + * concurrent client connections. * * This method is called automatically when the server starts listening and ensures * that MCP clients can communicate with the server over HTTP. @@ -314,34 +315,53 @@ export class McpServer { if (this.mcpMounted) return const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js') - - // Create StreamableHTTPServerTransport in stateless mode - // Note: Stateless mode means one active connection at a time - // For multiple concurrent inspectors, use the built-in proxy or same-origin connections - const httpTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // Stateless mode - enableJsonResponse: true - }) + const endpoint = '/mcp' + + // POST endpoint for messages + // Create a new transport for each request to support multiple concurrent clients + this.app.post(endpoint, express.json(), async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) - // Connect the MCP server to the transport - await this.server.connect(httpTransport) + res.on('close', () => { + transport.close() + }) - const endpoint = '/mcp' + await this.server.connect(transport) + await transport.handleRequest(req, res, req.body) + }) // GET endpoint for SSE streaming this.app.get(endpoint, async (req, res) => { - await httpTransport.handleRequest(req, res) - }) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) - // POST endpoint for messages - this.app.post(endpoint, express.json(), async (req, res) => { - await httpTransport.handleRequest(req, res, req.body) + res.on('close', () => { + transport.close() + }) + + await this.server.connect(transport) + await transport.handleRequest(req, res) }) // DELETE endpoint for session cleanup this.app.delete(endpoint, async (req, res) => { - await httpTransport.handleRequest(req, res) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) + + res.on('close', () => { + transport.close() + }) + + await this.server.connect(transport) + await transport.handleRequest(req, res) }) this.mcpMounted = true From cc9c03ace66724fea52651a68acc4007722630dd Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:32:56 +0200 Subject: [PATCH 3/6] feat: add @radix-ui/react-switch and implement auto-connect feature - Added @radix-ui/react-switch dependency to manage auto-connect setting. - Updated InspectorDashboard to include a switch for enabling/disabling auto-connect. - Enhanced McpContext to handle auto-connect state and connect/disconnect servers accordingly. - Implemented auto-connection logic in ServerDetail for seamless user experience. --- packages/inspector/package.json | 1 + .../client/components/InspectorDashboard.tsx | 159 ++++++++++++------ .../src/client/components/ServerDetail.tsx | 13 +- .../src/client/context/McpContext.tsx | 144 ++++++++++++++-- .../inspector/src/components/ui/switch.tsx | 28 +++ pnpm-lock.yaml | 31 ++++ 6 files changed, 310 insertions(+), 66 deletions(-) create mode 100644 packages/inspector/src/components/ui/switch.tsx diff --git a/packages/inspector/package.json b/packages/inspector/package.json index c2c25957..817376cf 100644 --- a/packages/inspector/package.json +++ b/packages/inspector/package.json @@ -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 f44c80d0..a4f13bc0 100644 --- a/packages/inspector/src/client/components/InspectorDashboard.tsx +++ b/packages/inspector/src/client/components/InspectorDashboard.tsx @@ -1,5 +1,5 @@ import type { CustomHeader } from './CustomHeadersEditor' -import { CircleMinus, Cog, Copy, FileText, RotateCcw, Shield } from 'lucide-react' +import { CircleMinus, Cog, Copy, FileText, Loader2, RotateCcw, Shield } from 'lucide-react' import React, { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' @@ -12,14 +12,17 @@ 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' 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 }))) @@ -156,6 +159,15 @@ export function InspectorDashboard() { } const handleServerClick = (connection: any) => { + // If disconnected, connect the server + if (connection.state === 'disconnected') { + console.log('[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 @@ -163,6 +175,42 @@ 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) + console.log('[InspectorDashboard] Pending navigation check:', { + pendingNavigation, + connectionState: connection?.state, + }) + + if (connection?.state === 'ready') { + console.log('[InspectorDashboard] Navigating to server:', connection.id) + setPendingNavigation(null) + navigate(`/servers/${encodeURIComponent(connection.id)}`) + } + else if (connection?.state === 'failed') { + console.log('[InspectorDashboard] Connection failed, canceling navigation') + setPendingNavigation(null) + } + }, [connections, pendingNavigation, navigate]) + const handleExportServerEntry = async () => { if (!url.trim()) { toast.error('Please enter a URL first') @@ -243,7 +291,17 @@ export function InspectorDashboard() {

Connected Servers

-
+
+
+ + +
{connections.length > 0 && (
{connections.length === 0 @@ -273,36 +330,42 @@ export function InspectorDashboard() {

{connection.name}

- {connection.error && connection.state !== 'ready' + {connectingServers.has(connection.id) ? ( - - -
@@ -345,21 +408,23 @@ export function InspectorDashboard() {

Remove connection

- - - - - -

Resync connection

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

Resync connection

+
+
+ )}
{connection.state === 'pending_auth' && connection.authUrl && ( diff --git a/packages/inspector/src/client/components/ServerDetail.tsx b/packages/inspector/src/client/components/ServerDetail.tsx index 27b2289d..8e11f7fb 100644 --- a/packages/inspector/src/client/components/ServerDetail.tsx +++ b/packages/inspector/src/client/components/ServerDetail.tsx @@ -1,5 +1,5 @@ import { CheckCircle2, Code, Copy, Database, Loader2, Play, Zap } from 'lucide-react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -8,7 +8,7 @@ import { useMcpContext } from '../context/McpContext' export function ServerDetail() { const { serverId } = useParams() - const { getConnection } = useMcpContext() + const { getConnection, connectServer } = useMcpContext() const decodedServerId = serverId ? decodeURIComponent(serverId) : '' const connection = getConnection(decodedServerId) @@ -17,6 +17,15 @@ export function ServerDetail() { const [toolResult, setToolResult] = useState(null) const [isExecuting, setIsExecuting] = useState(false) + // Auto-connect the server when viewing its details page + // This ensures the server connects even if auto-connect is disabled globally + useEffect(() => { + if (decodedServerId && connection?.state === 'disconnected') { + console.log('[ServerDetail] Auto-connecting server for details page:', decodedServerId) + connectServer(decodedServerId) + } + }, [decodedServerId, connection?.state, connectServer]) + const handleExecuteTool = async (toolName: string) => { if (!connection) return diff --git a/packages/inspector/src/client/context/McpContext.tsx b/packages/inspector/src/client/context/McpContext.tsx index 284800ce..6b683a70 100644 --- a/packages/inspector/src/client/context/McpContext.tsx +++ b/packages/inspector/src/client/context/McpContext.tsx @@ -24,6 +24,10 @@ interface McpContextType { addConnection: (url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }, transportType?: 'http' | 'sse') => void removeConnection: (id: string) => void getConnection: (id: string) => MCPConnection | undefined + autoConnect: boolean + setAutoConnect: (autoConnect: boolean) => void + connectServer: (id: string) => void + disconnectServer: (id: string) => void } const McpContext = createContext(undefined) @@ -181,8 +185,10 @@ export function McpProvider({ children }: { children: ReactNode }) { const [savedConnections, setSavedConnections] = useState([]) const [activeConnections, setActiveConnections] = useState>(new Map()) const [connectionVersion, setConnectionVersion] = useState(0) + const [autoConnect, setAutoConnectState] = useState(true) + const [manualConnections, setManualConnections] = useState>(new Set()) - // Load saved connections from localStorage on mount + // Load saved connections and auto-connect setting from localStorage on mount useEffect(() => { const saved = localStorage.getItem('mcp-inspector-connections') if (saved) { @@ -223,6 +229,12 @@ export function McpProvider({ children }: { children: ReactNode }) { localStorage.removeItem('mcp-inspector-connections') } } + + // Load auto-connect setting + const autoConnectSetting = localStorage.getItem('mcp-inspector-auto-connect') + if (autoConnectSetting !== null) { + setAutoConnectState(autoConnectSetting === 'true') + } }, []) const updateConnection = useCallback((connection: MCPConnection) => { @@ -276,15 +288,107 @@ export function McpProvider({ children }: { children: ReactNode }) { }, []) const getConnection = useCallback((id: string) => { - return activeConnections.get(id) - }, [activeConnections]) + // First check active connections + const activeConn = activeConnections.get(id) + if (activeConn) { + return activeConn + } + + // If not active, check if it's a saved connection and return placeholder + const savedConn = savedConnections.find(conn => conn.id === id) + if (savedConn) { + return { + id: savedConn.id, + url: savedConn.url, + name: savedConn.name, + state: 'disconnected' as const, + tools: [], + resources: [], + prompts: [], + error: null, + authUrl: null, + callTool: async () => { throw new Error('Not connected') }, + readResource: async () => { throw new Error('Not connected') }, + authenticate: () => {}, + retry: () => {}, + clearStorage: () => {}, + } + } + + return undefined + }, [activeConnections, savedConnections]) + + const setAutoConnect = useCallback((value: boolean) => { + setAutoConnectState(value) + localStorage.setItem('mcp-inspector-auto-connect', String(value)) + + // If disabling auto-connect, clear manual connections + if (!value) { + setManualConnections(new Set()) + } + }, []) + + const connectServer = useCallback((id: string) => { + setManualConnections(prev => new Set(prev).add(id)) + }, []) + + const disconnectServer = useCallback((id: string) => { + setManualConnections((prev) => { + const next = new Set(prev) + next.delete(id) + return next + }) + + setActiveConnections((prev) => { + const next = new Map(prev) + const connection = next.get(id) + if (connection) { + // Clear storage and remove connection + try { + connection.clearStorage() + } + catch (error) { + console.error('Failed to clear storage:', error) + } + next.delete(id) + } + return next + }) + }, []) // Use connectionVersion to force array recreation when connections update const connections = useMemo(() => { - const conns = Array.from(activeConnections.values()) + // Maintain order based on savedConnections, not activeConnections + // This ensures the order doesn't change when a server connects/disconnects + const conns = savedConnections.map((saved) => { + // Use active connection if it exists, otherwise create placeholder + const activeConn = activeConnections.get(saved.id) + if (activeConn) { + return activeConn + } + + // Create a placeholder connection for disconnected servers + return { + id: saved.id, + url: saved.url, + name: saved.name, + state: 'disconnected' as const, + tools: [], + resources: [], + prompts: [], + error: null, + authUrl: null, + callTool: async () => { throw new Error('Not connected') }, + readResource: async () => { throw new Error('Not connected') }, + authenticate: () => {}, + retry: () => {}, + clearStorage: () => {}, + } + }) + console.warn('[McpContext] Connections updated, version:', connectionVersion, 'count:', conns.length, 'states:', conns.map(c => `${c.id}:${c.state}`)) return conns - }, [activeConnections, connectionVersion]) + }, [activeConnections, savedConnections, connectionVersion]) // Memoize the context value to prevent unnecessary re-renders and HMR issues const contextValue = useMemo(() => ({ @@ -292,21 +396,27 @@ export function McpProvider({ children }: { children: ReactNode }) { addConnection, removeConnection, getConnection, - }), [connections, addConnection, removeConnection, getConnection]) + autoConnect, + setAutoConnect, + connectServer, + disconnectServer, + }), [connections, addConnection, removeConnection, getConnection, autoConnect, setAutoConnect, connectServer, disconnectServer]) return ( - {savedConnections.map(saved => ( - removeConnection(saved.id)} - /> - ))} + {savedConnections + .filter(saved => autoConnect || manualConnections.has(saved.id)) + .map(saved => ( + removeConnection(saved.id)} + /> + ))} {children} ) diff --git a/packages/inspector/src/components/ui/switch.tsx b/packages/inspector/src/components/ui/switch.tsx new file mode 100644 index 00000000..7cecd9dc --- /dev/null +++ b/packages/inspector/src/components/ui/switch.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import * as SwitchPrimitives from '@radix-ui/react-switch' + +import { cn } from '@/lib/utils' + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c82159d..71493c7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -2257,6 +2260,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^19.2.0 + react-dom: ^19.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -8343,6 +8359,21 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 From cd5cff9178c93f23104e0c05b6b31929527c4195 Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:35:56 +0200 Subject: [PATCH 4/6] refactor: replace console.log with console.warn for connection status updates - Updated logging in InspectorDashboard and ServerDetail components to use console.warn for better visibility of connection state changes. - Enhanced pending navigation checks to include data presence before navigating or canceling navigation. --- .../client/components/InspectorDashboard.tsx | 20 +++++++++++++------ .../src/client/components/ServerDetail.tsx | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/inspector/src/client/components/InspectorDashboard.tsx b/packages/inspector/src/client/components/InspectorDashboard.tsx index a4f13bc0..300a47a9 100644 --- a/packages/inspector/src/client/components/InspectorDashboard.tsx +++ b/packages/inspector/src/client/components/InspectorDashboard.tsx @@ -161,7 +161,7 @@ export function InspectorDashboard() { const handleServerClick = (connection: any) => { // If disconnected, connect the server if (connection.state === 'disconnected') { - console.log('[InspectorDashboard] Connecting server and setting pending navigation:', connection.id) + 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) @@ -195,18 +195,26 @@ export function InspectorDashboard() { return const connection = connections.find(c => c.id === pendingNavigation) - console.log('[InspectorDashboard] Pending navigation check:', { + 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, }) - if (connection?.state === 'ready') { - console.log('[InspectorDashboard] Navigating to server:', connection.id) + // 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)}`) } - else if (connection?.state === 'failed') { - console.log('[InspectorDashboard] Connection failed, canceling navigation') + // 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]) diff --git a/packages/inspector/src/client/components/ServerDetail.tsx b/packages/inspector/src/client/components/ServerDetail.tsx index 8e11f7fb..69d211c1 100644 --- a/packages/inspector/src/client/components/ServerDetail.tsx +++ b/packages/inspector/src/client/components/ServerDetail.tsx @@ -21,7 +21,7 @@ export function ServerDetail() { // This ensures the server connects even if auto-connect is disabled globally useEffect(() => { if (decodedServerId && connection?.state === 'disconnected') { - console.log('[ServerDetail] Auto-connecting server for details page:', decodedServerId) + console.warn('[ServerDetail] Auto-connecting server for details page:', decodedServerId) connectServer(decodedServerId) } }, [decodedServerId, connection?.state, connectServer]) From 1bbe7675319ecd7d609f5daf8de6fc02718d4c58 Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:33 +0200 Subject: [PATCH 5/6] fix: inspector connector status and transport improvements (#75) * fix status dot * fix: 1 transport per request * chore: version packages and update changelogs --- packages/cli/CHANGELOG.md | 8 +++ packages/cli/package.json | 2 +- packages/inspector/CHANGELOG.md | 8 +++ packages/inspector/package.json | 2 +- .../client/components/InspectorDashboard.tsx | 13 +++-- .../src/client/components/Layout.tsx | 6 +- .../src/client/context/McpContext.tsx | 26 +++++++-- packages/mcp-use/CHANGELOG.md | 8 +++ packages/mcp-use/package.json | 4 +- packages/mcp-use/src/server/mcp-server.ts | 56 +++++++++++++------ pnpm-lock.yaml | 6 ++ 11 files changed, 106 insertions(+), 33 deletions(-) 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..560fefcc 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", diff --git a/packages/inspector/src/client/components/InspectorDashboard.tsx b/packages/inspector/src/client/components/InspectorDashboard.tsx index 40046c70..70d9801d 100644 --- a/packages/inspector/src/client/components/InspectorDashboard.tsx +++ b/packages/inspector/src/client/components/InspectorDashboard.tsx @@ -124,8 +124,13 @@ export function InspectorDashboard() { }, {} as Record), } + // 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' + // For now, use URL as both ID and name - this will need proper implementation - addConnection(url, url, proxyConfig) + addConnection(url, url, proxyConfig, actualTransportType) } const handleClearAllConnections = () => { @@ -222,7 +227,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'} @@ -268,7 +273,7 @@ export function InspectorDashboard() {

{connection.name}

- {connection.error + {connection.error && connection.state !== 'ready' ? ( @@ -399,7 +404,7 @@ export function InspectorDashboard() { Streamable HTTP - WebSocket + Server-Sent Events (SSE)
diff --git a/packages/inspector/src/client/components/Layout.tsx b/packages/inspector/src/client/components/Layout.tsx index 001e3fa6..fea366a1 100644 --- a/packages/inspector/src/client/components/Layout.tsx +++ b/packages/inspector/src/client/components/Layout.tsx @@ -233,7 +233,7 @@ export function Layout({ children }: LayoutProps) { const existing = connections.find(c => c.url === autoConnectUrl) if (!existing) { setIsAutoConnecting(true) - addConnection(autoConnectUrl, 'Local MCP Server') + addConnection(autoConnectUrl, 'Local MCP Server', undefined, 'http') // Navigate immediately but keep loading screen visible a bit longer to avoid flash navigate(`/servers/${encodeURIComponent(autoConnectUrl)}`) setTimeout(() => { @@ -254,7 +254,7 @@ export function Layout({ children }: LayoutProps) { const existing = connections.find(c => c.url === config.autoConnectUrl) if (!existing) { // Auto-connect to the local server - addConnection(config.autoConnectUrl, 'Local MCP Server') + addConnection(config.autoConnectUrl, 'Local MCP Server', undefined, 'http') } } }) @@ -383,7 +383,7 @@ export function Layout({ children }: LayoutProps) { {selectedServer && (
- {selectedServer.error + {selectedServer.error && selectedServer.state !== 'ready' ? ( diff --git a/packages/inspector/src/client/context/McpContext.tsx b/packages/inspector/src/client/context/McpContext.tsx index b3945974..284800ce 100644 --- a/packages/inspector/src/client/context/McpContext.tsx +++ b/packages/inspector/src/client/context/McpContext.tsx @@ -21,7 +21,7 @@ interface MCPConnection { interface McpContextType { connections: MCPConnection[] - addConnection: (url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }) => void + addConnection: (url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }, transportType?: 'http' | 'sse') => void removeConnection: (id: string) => void getConnection: (id: string) => MCPConnection | undefined } @@ -33,12 +33,14 @@ interface SavedConnection { url: string name: string proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' } -function McpConnectionWrapper({ url, name, proxyConfig, onUpdate, onRemove: _onRemove }: { +function McpConnectionWrapper({ url, name, proxyConfig, transportType, onUpdate, onRemove: _onRemove }: { url: string name: string proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record } + transportType?: 'http' | 'sse' onUpdate: (connection: MCPConnection) => void onRemove: () => void }) { @@ -79,6 +81,7 @@ function McpConnectionWrapper({ url, name, proxyConfig, onUpdate, onRemove: _onR url: finalUrl, callbackUrl, customHeaders: Object.keys(customHeaders).length > 0 ? customHeaders : undefined, + transportType: transportType || 'http', // Default to 'http' for Streamable HTTP }) const onUpdateRef = useRef(onUpdate) const prevConnectionRef = useRef(null) @@ -194,12 +197,21 @@ export function McpProvider({ children }: { children: ReactNode }) { && typeof conn.id === 'string' && typeof conn.url === 'string' && typeof conn.name === 'string' + }).map((conn: any) => { + // Migrate existing connections to include transportType + if (!conn.transportType) { + conn.transportType = 'http' // Default to 'http' for Streamable HTTP + } + return conn }) : [] - // If we filtered out any invalid connections, update localStorage - if (validConnections.length !== parsed.length) { - console.warn('Cleaned up invalid connections from localStorage') + // If we filtered out any invalid connections or migrated transport types, update localStorage + const hasChanges = validConnections.length !== parsed.length + || validConnections.some((conn: any) => conn.transportType === 'http' && !parsed.find((p: any) => p.id === conn.id && p.transportType)) + + if (hasChanges) { + console.warn('Updated connections in localStorage with transport type migration') localStorage.setItem('mcp-inspector-connections', JSON.stringify(validConnections)) } @@ -218,13 +230,14 @@ export function McpProvider({ children }: { children: ReactNode }) { setConnectionVersion(v => v + 1) }, []) - const addConnection = useCallback((url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }) => { + const addConnection = useCallback((url: string, name?: string, proxyConfig?: { proxyAddress?: string, proxyToken?: string, customHeaders?: Record }, transportType?: 'http' | 'sse') => { const connectionName = name || url const newConnection: SavedConnection = { id: url, url, name: connectionName, proxyConfig, + transportType: transportType || 'http', // Default to 'http' for Streamable HTTP } setSavedConnections((prev) => { @@ -289,6 +302,7 @@ export function McpProvider({ children }: { children: ReactNode }) { url={saved.url} name={saved.name} proxyConfig={saved.proxyConfig} + transportType={saved.transportType} onUpdate={updateConnection} onRemove={() => removeConnection(saved.id)} /> diff --git a/packages/mcp-use/CHANGELOG.md b/packages/mcp-use/CHANGELOG.md index 99d6075c..3a6db034 100644 --- a/packages/mcp-use/CHANGELOG.md +++ b/packages/mcp-use/CHANGELOG.md @@ -1,5 +1,13 @@ # mcp-use +## 1.0.4 + +### Patch Changes + +- fix: support multiple clients per server +- Updated dependencies + - @mcp-use/inspector@0.3.8 + ## 1.0.3 ### Patch Changes diff --git a/packages/mcp-use/package.json b/packages/mcp-use/package.json index 36fb768c..d7cca0f2 100644 --- a/packages/mcp-use/package.json +++ b/packages/mcp-use/package.json @@ -1,7 +1,7 @@ { "name": "mcp-use", "type": "module", - "version": "1.0.3", + "version": "1.0.4", "packageManager": "pnpm@10.6.1", "description": "Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents and Clients + MCP Servers with support for MCP-UI.", "author": "mcp-use, Inc.", @@ -88,7 +88,7 @@ "example:observability": "tsx examples/client/observability.ts" }, "peerDependencies": { - "@mcp-use/inspector": "^0.3.7", + "@mcp-use/inspector": "^0.3.8", "cors": "^2.8.5", "express": "^4.18.2", "langfuse": "^3.32.0", diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 20b6bb50..7ef56dd9 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -46,7 +46,7 @@ export class McpServer { this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS') - res.header('Access-Control-Allow-Headers', 'Content-Type') + res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, mcp-protocol-version, mcp-session-id, X-Proxy-Token, X-Target-URL') next() }) @@ -296,7 +296,8 @@ export class McpServer { * * Sets up the HTTP transport layer for the MCP server, creating endpoints for * Server-Sent Events (SSE) streaming, POST message handling, and DELETE session cleanup. - * Uses stateless mode for session management, making it suitable for stateless deployments. + * Each request gets its own transport instance to prevent state conflicts between + * concurrent client connections. * * This method is called automatically when the server starts listening and ensures * that MCP clients can communicate with the server over HTTP. @@ -314,30 +315,53 @@ export class McpServer { if (this.mcpMounted) return const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js') - - // Create StreamableHTTPServerTransport in stateless mode - const httpTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined // Stateless mode - }) - - // Connect the MCP server to the transport - await this.server.connect(httpTransport) const endpoint = '/mcp' + // POST endpoint for messages + // Create a new transport for each request to support multiple concurrent clients + this.app.post(endpoint, express.json(), async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) + + res.on('close', () => { + transport.close() + }) + + await this.server.connect(transport) + await transport.handleRequest(req, res, req.body) + }) + // GET endpoint for SSE streaming this.app.get(endpoint, async (req, res) => { - await httpTransport.handleRequest(req, res) - }) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) - // POST endpoint for messages - this.app.post(endpoint, express.json(), async (req, res) => { - await httpTransport.handleRequest(req, res, req.body) + res.on('close', () => { + transport.close() + }) + + await this.server.connect(transport) + await transport.handleRequest(req, res) }) // DELETE endpoint for session cleanup this.app.delete(endpoint, async (req, res) => { - await httpTransport.handleRequest(req, res) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) + + res.on('close', () => { + transport.close() + }) + + await this.server.connect(transport) + await transport.handleRequest(req, res) }) this.mcpMounted = true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c81d68..2c82159d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -521,6 +521,9 @@ importers: '@mcp-ui/server': specifier: ^5.11.0 version: 5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11) + '@modelcontextprotocol/sdk': + specifier: ^1.0.0 + version: 1.20.0 cors: specifier: ^2.8.5 version: 2.8.5 @@ -530,6 +533,9 @@ importers: mcp-use: specifier: workspace:* version: link:../packages/mcp-use + zod: + specifier: ^3.22.0 + version: 3.25.76 devDependencies: '@mcp-use/cli': specifier: workspace:* From a62549f3b17f5855f27d827493f6327ea9f74711 Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:10:43 +0200 Subject: [PATCH 6/6] feat: implement connection tester and auto-switch feature in InspectorDashboard - Added a temporary ConnectionTester component to handle connection attempts with success and failure callbacks. - Introduced an auto-switch feature that allows users to automatically switch between direct and proxy connections if one fails. - Updated the InspectorDashboard to manage pending connection configurations and handle connection success/failure logic. - Enhanced UI to include a switch for enabling/disabling the auto-switch feature, with state persistence in localStorage. --- .../client/components/InspectorDashboard.tsx | 237 ++++++++++++++---- 1 file changed, 188 insertions(+), 49 deletions(-) diff --git a/packages/inspector/src/client/components/InspectorDashboard.tsx b/packages/inspector/src/client/components/InspectorDashboard.tsx index 300a47a9..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, Loader2, RotateCcw, Shield } from 'lucide-react' -import React, { useEffect, useRef, useState } from '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' @@ -17,6 +18,71 @@ 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, autoConnect, setAutoConnect, connectServer, disconnectServer: _disconnectServer } = mcpContext @@ -52,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 - } - - 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 + const autoSwitchSetting = localStorage.getItem('mcp-inspector-auto-switch') + if (autoSwitchSetting !== null) { + setAutoSwitch(autoSwitchSetting === 'true') } - }, [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() @@ -132,9 +170,82 @@ export function InspectorDashboard() { // "WebSocket" in UI means "WebSocket" which uses 'sse' transport const actualTransportType = transportType === 'SSE' ? 'http' : 'sse' - // For now, use URL as both ID and name - this will need proper implementation - addConnection(url, url, proxyConfig, actualTransportType) - } + // 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 @@ -347,6 +458,7 @@ export function InspectorDashboard() {