Spaces:
Running
Running
| "use client"; | |
| import React, { createContext, useContext, useRef } from "react"; | |
| import { useLocalStorage } from "@/lib/hooks/use-local-storage"; | |
| import { STORAGE_KEYS } from "@/lib/constants"; | |
| import { startSandbox, stopSandbox } from "@/app/actions"; | |
| // Define types for MCP server | |
| export interface KeyValuePair { | |
| key: string; | |
| value: string; | |
| } | |
| export type ServerStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; | |
| export interface MCPServer { | |
| id: string; | |
| name: string; | |
| url: string; | |
| type: 'sse' | 'stdio'; | |
| command?: string; | |
| args?: string[]; | |
| env?: KeyValuePair[]; | |
| headers?: KeyValuePair[]; | |
| description?: string; | |
| status?: ServerStatus; | |
| errorMessage?: string; | |
| sandboxUrl?: string; // Store the sandbox URL directly on the server object | |
| } | |
| // Type for processed MCP server config for API | |
| export interface MCPServerApi { | |
| type: 'sse'; | |
| url: string; | |
| headers?: KeyValuePair[]; | |
| } | |
| interface MCPContextType { | |
| mcpServers: MCPServer[]; | |
| setMcpServers: (servers: MCPServer[]) => void; | |
| selectedMcpServers: string[]; | |
| setSelectedMcpServers: (serverIds: string[]) => void; | |
| mcpServersForApi: MCPServerApi[]; | |
| startServer: (serverId: string) => Promise<boolean>; | |
| stopServer: (serverId: string) => Promise<boolean>; | |
| updateServerStatus: (serverId: string, status: ServerStatus, errorMessage?: string) => void; | |
| getActiveServersForApi: () => MCPServerApi[]; | |
| } | |
| const MCPContext = createContext<MCPContextType | undefined>(undefined); | |
| // Helper function to wait for server readiness | |
| async function waitForServerReady(url: string, maxAttempts = 20, timeout = 3000) { | |
| console.log(`Checking server readiness at ${url}, will try ${maxAttempts} times`); | |
| for (let i = 0; i < maxAttempts; i++) { | |
| try { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), timeout); | |
| const response = await fetch(url, { signal: controller.signal }); | |
| clearTimeout(timeoutId); | |
| if (response.status === 200) { | |
| console.log(`Server ready at ${url} after ${i + 1} attempts`); | |
| return true; | |
| } | |
| console.log(`Server not ready yet (attempt ${i + 1}), status: ${response.status}`); | |
| } catch (error) { | |
| console.log(`Server connection failed (attempt ${i + 1}): ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| // Wait before next attempt with progressive backoff | |
| const waitTime = Math.min(1000 * (i + 1), 5000); // Start with 1s, increase each time, max 5s | |
| console.log(`Waiting ${waitTime}ms before next attempt`); | |
| await new Promise(resolve => setTimeout(resolve, waitTime)); | |
| } | |
| console.log(`Server failed to become ready after ${maxAttempts} attempts`); | |
| return false; | |
| } | |
| export function MCPProvider({ children }: { children: React.ReactNode }) { | |
| const [mcpServers, setMcpServers] = useLocalStorage<MCPServer[]>( | |
| STORAGE_KEYS.MCP_SERVERS, | |
| [] | |
| ); | |
| const [selectedMcpServers, setSelectedMcpServers] = useLocalStorage<string[]>( | |
| STORAGE_KEYS.SELECTED_MCP_SERVERS, | |
| [] | |
| ); | |
| // Create a ref to track active servers and avoid unnecessary re-renders | |
| const activeServersRef = useRef<Record<string, boolean>>({}); | |
| // Helper to get a server by ID | |
| const getServerById = (serverId: string): MCPServer | undefined => { | |
| return mcpServers.find(server => server.id === serverId); | |
| }; | |
| // Update server status | |
| const updateServerStatus = (serverId: string, status: ServerStatus, errorMessage?: string) => { | |
| setMcpServers(currentServers => | |
| currentServers.map(server => | |
| server.id === serverId | |
| ? { ...server, status, errorMessage: errorMessage || undefined } | |
| : server | |
| ) | |
| ); | |
| }; | |
| // Update server with sandbox URL | |
| const updateServerSandboxUrl = (serverId: string, sandboxUrl: string) => { | |
| console.log(`Storing sandbox URL for server ${serverId}: ${sandboxUrl}`); | |
| // Update in memory and force save to localStorage | |
| setMcpServers(currentServers => { | |
| const updatedServers = currentServers.map(server => | |
| server.id === serverId | |
| ? { ...server, sandboxUrl, status: 'connected' as ServerStatus } | |
| : server | |
| ); | |
| // Log the updated servers to verify the changes are there | |
| console.log('Updated server with sandbox URL:', | |
| updatedServers.find(s => s.id === serverId)); | |
| // Return the updated servers to set in state and localStorage | |
| return updatedServers; | |
| }); | |
| }; | |
| // Get active servers formatted for API usage | |
| const getActiveServersForApi = (): MCPServerApi[] => { | |
| return selectedMcpServers | |
| .map(id => getServerById(id)) | |
| .filter((server): server is MCPServer => !!server && server.status === 'connected') | |
| .map(server => ({ | |
| type: 'sse', | |
| url: server.type === 'stdio' && server.sandboxUrl ? server.sandboxUrl : server.url, | |
| headers: server.headers | |
| })); | |
| }; | |
| // Start a server | |
| const startServer = async (serverId: string): Promise<boolean> => { | |
| const server = getServerById(serverId); | |
| if (!server) return false; | |
| // Mark server as connecting | |
| updateServerStatus(serverId, 'connecting'); | |
| try { | |
| // For HTTP or SSE servers, just check if the endpoint is available | |
| if (server.type === 'sse') { | |
| const isReady = await waitForServerReady(server.url); | |
| updateServerStatus(serverId, isReady ? 'connected' : 'error', | |
| isReady ? undefined : 'Could not connect to server'); | |
| // Update active servers ref | |
| if (isReady) { | |
| activeServersRef.current[serverId] = true; | |
| } | |
| return isReady; | |
| } | |
| // For stdio servers, start a sandbox | |
| if (server.type === 'stdio' && server.command && server.args?.length) { | |
| // Check if we already have a valid sandbox URL | |
| if (server.sandboxUrl) { | |
| try { | |
| const isReady = await waitForServerReady(server.sandboxUrl); | |
| if (isReady) { | |
| updateServerStatus(serverId, 'connected'); | |
| activeServersRef.current[serverId] = true; | |
| return true; | |
| } | |
| } catch { | |
| // If sandbox check fails, we'll create a new one | |
| } | |
| } | |
| // Create a new sandbox | |
| const { url } = await startSandbox({ | |
| id: serverId, | |
| command: server.command, | |
| args: server.args, | |
| env: server.env, | |
| }); | |
| // Wait for the server to become ready | |
| const isReady = await waitForServerReady(url); | |
| if (isReady) { | |
| // Store the sandbox URL and update status - do this first! | |
| console.log(`Server ${serverId} started successfully, storing sandbox URL: ${url}`); | |
| updateServerSandboxUrl(serverId, url); | |
| // Mark as active | |
| activeServersRef.current[serverId] = true; | |
| return true; | |
| } else { | |
| // Failed to start | |
| updateServerStatus(serverId, 'error', 'Server failed to start'); | |
| // Clean up sandbox | |
| try { | |
| await stopSandbox(serverId); | |
| } catch (error) { | |
| console.error(`Failed to stop non-responsive sandbox ${serverId}:`, error); | |
| } | |
| return false; | |
| } | |
| } | |
| // If we get here, something is misconfigured | |
| updateServerStatus(serverId, 'error', 'Invalid server configuration'); | |
| return false; | |
| } catch (error) { | |
| // Handle any unexpected errors | |
| console.error(`Error starting server ${serverId}:`, error); | |
| updateServerStatus(serverId, 'error', | |
| `Error: ${error instanceof Error ? error.message : String(error)}`); | |
| return false; | |
| } | |
| }; | |
| // Stop a server | |
| const stopServer = async (serverId: string): Promise<boolean> => { | |
| const server = getServerById(serverId); | |
| if (!server) return false; | |
| try { | |
| // For stdio servers with sandbox, stop the sandbox | |
| if (server.type === 'stdio' && server.sandboxUrl) { | |
| try { | |
| await stopSandbox(serverId); | |
| console.log(`Stopped sandbox for server ${serverId}`); | |
| // Mark as not active | |
| delete activeServersRef.current[serverId]; | |
| } catch (error) { | |
| console.error(`Error stopping sandbox for server ${serverId}:`, error); | |
| } | |
| } | |
| // Update server status | |
| updateServerStatus(serverId, 'disconnected'); | |
| return true; | |
| } catch (error) { | |
| console.error(`Error stopping server ${serverId}:`, error); | |
| return false; | |
| } | |
| }; | |
| // Calculate mcpServersForApi based on current state | |
| const mcpServersForApi = getActiveServersForApi(); | |
| return ( | |
| <MCPContext.Provider | |
| value={{ | |
| mcpServers, | |
| setMcpServers, | |
| selectedMcpServers, | |
| setSelectedMcpServers, | |
| mcpServersForApi, | |
| startServer, | |
| stopServer, | |
| updateServerStatus, | |
| getActiveServersForApi | |
| }} | |
| > | |
| {children} | |
| </MCPContext.Provider> | |
| ); | |
| } | |
| export function useMCP() { | |
| const context = useContext(MCPContext); | |
| if (context === undefined) { | |
| throw new Error("useMCP must be used within an MCPProvider"); | |
| } | |
| return context; | |
| } |