Skip to main content
Glama
senseisven

MCP Remote macOS Control Server

by senseisven
useSocket.ts6.7 kB
'use client' import { useEffect, useState, useRef, useCallback } from 'react' import { io, Socket } from 'socket.io-client' import { ConnectionStatus } from '@/types/chat' import { useChatStore } from '@/stores/chatStore' const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001' export function useSocket() { const [socket, setSocket] = useState<Socket | null>(null) const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>({ connected: false, connecting: false }) const { addMessage, setTyping, updateMessage } = useChatStore() const reconnectAttempts = useRef(0) const maxReconnectAttempts = 5 const reconnectTimeout = useRef<NodeJS.Timeout>() // Cleanup function const cleanup = useCallback(() => { if (socket) { socket.removeAllListeners() socket.disconnect() } if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current) } setSocket(null) }, [socket]) const connectSocket = useCallback(() => { // Clean up existing connection cleanup() setConnectionStatus({ connected: false, connecting: true }) const newSocket = io(BACKEND_URL, { transports: ['websocket', 'polling'], timeout: 10000, forceNew: true, reconnection: false // We'll handle reconnection manually }) // Connection events newSocket.on('connect', () => { console.log('Connected to server') setConnectionStatus({ connected: true, connecting: false }) reconnectAttempts.current = 0 setSocket(newSocket) }) newSocket.on('disconnect', (reason) => { console.log('Disconnected from server:', reason) setConnectionStatus({ connected: false, connecting: false }) setSocket(null) // Attempt reconnection if disconnect wasn't intentional if (reason !== 'io client disconnect' && reconnectAttempts.current < maxReconnectAttempts) { scheduleReconnect() } }) newSocket.on('connect_error', (error) => { console.error('Connection error:', error) setConnectionStatus({ connected: false, connecting: false, error: 'Failed to connect to server' }) if (reconnectAttempts.current < maxReconnectAttempts) { scheduleReconnect() } else { setConnectionStatus({ connected: false, connecting: false, error: 'Unable to connect after multiple attempts' }) } }) // Chat message handlers newSocket.on('chat_response', (data) => { addMessage({ id: Date.now().toString(), type: data.type || 'assistant', content: data.message, timestamp: data.timestamp || Date.now(), imageData: data.imageData, toolName: data.toolName }) }) // Typing indicators newSocket.on('typing_start', () => { setTyping(true) }) newSocket.on('typing_stop', () => { setTyping(false) }) // Tool execution events newSocket.on('tool_execution_start', (data) => { addMessage({ id: `tool-${Date.now()}`, type: 'system', content: `🔧 Executing: ${data.toolName?.replace('remote_macos_', '').replace('_', ' ') || 'tool'}`, timestamp: Date.now(), toolName: data.toolName }) }) newSocket.on('tool_execution_progress', (data) => { addMessage({ id: `progress-${Date.now()}`, type: 'system', content: `⚡ ${data.message}`, timestamp: Date.now(), toolName: data.toolName }) }) newSocket.on('tool_execution_complete', (data) => { if (!data.success) { addMessage({ id: Date.now().toString(), type: 'error', content: `❌ Tool execution failed: ${data.error || 'Unknown error'}`, timestamp: Date.now(), toolName: data.toolName }) } }) // Error handling newSocket.on('error', (error) => { console.error('Socket error:', error) addMessage({ id: Date.now().toString(), type: 'error', content: `⚠️ Connection error: ${error.message || 'Unknown error'}`, timestamp: Date.now() }) }) // Health check response newSocket.on('health_check_response', (data) => { console.log('Health check response:', data) }) return newSocket }, [addMessage, setTyping, cleanup]) const scheduleReconnect = useCallback(() => { reconnectAttempts.current += 1 const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current - 1), 10000) // Exponential backoff, max 10s console.log(`Scheduling reconnection attempt ${reconnectAttempts.current}/${maxReconnectAttempts} in ${delay}ms`) setConnectionStatus({ connected: false, connecting: true, error: `Reconnecting... (${reconnectAttempts.current}/${maxReconnectAttempts})` }) reconnectTimeout.current = setTimeout(() => { if (reconnectAttempts.current <= maxReconnectAttempts) { connectSocket() } }, delay) }, [connectSocket]) // Manual reconnect function const reconnect = useCallback(() => { reconnectAttempts.current = 0 connectSocket() }, [connectSocket]) // Health check function const healthCheck = useCallback(() => { if (socket?.connected) { socket.emit('health_check') } }, [socket]) // Send message with error handling const sendMessage = useCallback((message: string, sessionId: string = 'default-session') => { if (!socket?.connected) { addMessage({ id: Date.now().toString(), type: 'error', content: '❌ Not connected to server. Please wait for reconnection.', timestamp: Date.now() }) return false } try { socket.emit('chat_message', { message, sessionId }) return true } catch (error) { console.error('Error sending message:', error) addMessage({ id: Date.now().toString(), type: 'error', content: '❌ Failed to send message. Please try again.', timestamp: Date.now() }) return false } }, [socket, addMessage]) useEffect(() => { connectSocket() // Periodic health check const healthCheckInterval = setInterval(() => { healthCheck() }, 30000) // Every 30 seconds return () => { clearInterval(healthCheckInterval) cleanup() } }, []) // Only run once on mount return { socket, connectionStatus, reconnect, healthCheck, sendMessage, isConnected: socket?.connected || false } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/senseisven/mcp_macos'

If you have feedback or need assistance with the MCP directory API, please join our Discord server