Skip to main content
Glama
websocket.ts11.9 kB
import WebSocket from 'ws'; import { v4 as uuidv4 } from 'uuid'; import { ITransport } from './ITransport.js'; interface PendingRequest { resolve: (value: any) => void; reject: (error: Error) => void; timeout: NodeJS.Timeout; } interface JsonRpcRequest { jsonrpc: '2.0'; id: string; method: string; params: any; } interface JsonRpcResponse { jsonrpc: '2.0'; id: string; result?: any; error?: { code: number; message: string; data?: any; }; } /** * WebSocket transport for Unity Editor communication * Uses JSON-RPC 2.0 protocol for request/response correlation */ export class WebSocketTransport implements ITransport { private ws: WebSocket | null = null; private url: string; private pendingRequests: Map<string, PendingRequest> = new Map(); private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 1000; private requestTimeout: number; private isConnecting = false; private connectionPromise: Promise<void> | null = null; constructor(port: number = 8090, requestTimeout: number = 30000) { this.url = `ws://localhost:${port}`; this.requestTimeout = requestTimeout; } async connect(): Promise<void> { // If already connecting, return the existing promise if (this.isConnecting && this.connectionPromise) { return this.connectionPromise; } // If already connected, return immediately if (this.ws && this.ws.readyState === WebSocket.OPEN) { return Promise.resolve(); } console.error(`[Unity MCP] Attempting to connect to Unity Editor at ${this.url}`); console.error('[Unity MCP] Make sure Unity Editor is running with MCP WebSocket server started'); console.error('[Unity MCP] Check Unity Console for: "[Unity MCP] WebSocket server started on port..."'); this.isConnecting = true; this.connectionPromise = new Promise((resolve, reject) => { try { this.ws = new WebSocket(this.url); const connectTimeout = setTimeout(() => { if (this.ws) { this.ws.close(); } this.isConnecting = false; const errorMsg = `WebSocket connection timeout to ${this.url}. ` + `Make sure Unity Editor is running and MCP WebSocket server is started. ` + `Check Unity Console (Window > General > Console) for server status.`; console.error(`[Unity MCP] ${errorMsg}`); reject(new Error(errorMsg)); }, 10000); this.ws.on('open', () => { clearTimeout(connectTimeout); this.reconnectAttempts = 0; this.isConnecting = false; console.error(`[Unity MCP] WebSocket connected to ${this.url}`); resolve(); }); this.ws.on('message', (data: WebSocket.Data) => { console.error('[Unity MCP] WebSocket.on("message") event fired'); const messageStr = data.toString(); console.error('[Unity MCP] Raw message received, type:', typeof data, 'length:', messageStr.length); this.handleMessage(messageStr); }); this.ws.on('error', (error) => { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`[Unity MCP] WebSocket connection error: ${errorMsg}`); console.error(`[Unity MCP] Failed to connect to ${this.url}`); console.error('[Unity MCP] Troubleshooting:'); console.error(' 1. Is Unity Editor running?'); console.error(' 2. Is MCP WebSocket server started? (Check Unity Console)'); console.error(' 3. Is the port correct? (Default: 8090)'); console.error(' 4. Check Unity > Tools > Unity MCP > Control Panel'); clearTimeout(connectTimeout); this.isConnecting = false; if (this.ws?.readyState === WebSocket.CONNECTING) { reject(new Error(`Failed to connect to Unity Editor: ${errorMsg}. ` + `Make sure Unity is running with MCP WebSocket server started.`)); } }); this.ws.on('close', (code, reason) => { console.error('[Unity MCP] WebSocket disconnected', 'code:', code, 'reason:', reason?.toString()); console.error('[Unity MCP] Pending requests at disconnect:', Array.from(this.pendingRequests.keys())); this.handleDisconnect(); }); } catch (error) { this.isConnecting = false; reject(error); } }); return this.connectionPromise; } async disconnect(): Promise<void> { if (this.ws) { // Reject all pending requests for (const pending of this.pendingRequests.values()) { clearTimeout(pending.timeout); pending.reject(new Error('WebSocket disconnected')); } this.pendingRequests.clear(); this.ws.close(); this.ws = null; } this.isConnecting = false; this.connectionPromise = null; } private handleDisconnect(): void { // Reject all pending requests for (const pending of this.pendingRequests.values()) { clearTimeout(pending.timeout); pending.reject(new Error('WebSocket connection closed')); } this.pendingRequests.clear(); // Auto-reconnect logic if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * this.reconnectAttempts; console.error( `[Unity MCP] Attempting reconnect ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms` ); setTimeout(() => { this.connect().catch((err) => { console.error('[Unity MCP] Reconnection failed:', err.message); }); }, delay); } } private handleMessage(data: string): void { console.error('[Unity MCP] WebSocket received message, length:', data.length); console.error('[Unity MCP] Message preview:', data.substring(0, Math.min(200, data.length))); try { const response: JsonRpcResponse = JSON.parse(data); console.error('[Unity MCP] Parsed JSON successfully, id:', response.id); console.error('[Unity MCP] Response has error?', !!response.error, 'has result?', !!response.result); if (!response.id) { console.error('[Unity MCP] Received message without ID:', data); return; } const pending = this.pendingRequests.get(response.id); if (!pending) { console.error('[Unity MCP] Received response for unknown request:', response.id); console.error('[Unity MCP] Pending requests:', Array.from(this.pendingRequests.keys())); console.error('[Unity MCP] Full response:', JSON.stringify(response, null, 2)); return; } console.error('[Unity MCP] Found pending request, resolving...'); // Clear timeout clearTimeout(pending.timeout); this.pendingRequests.delete(response.id); // Handle error or result if (response.error) { console.error('[Unity MCP] Response has error:', JSON.stringify(response.error, null, 2)); pending.reject( new Error(`Unity error: ${response.error.message} (code: ${response.error.code})`) ); } else if (response.result !== undefined) { console.error('[Unity MCP] Response has result, type:', typeof response.result); console.error('[Unity MCP] Result preview:', JSON.stringify(response.result).substring(0, 200)); pending.resolve(response.result); } else { console.error('[Unity MCP] Response has neither error nor result!', JSON.stringify(response, null, 2)); pending.reject(new Error('Invalid JSON-RPC response: missing both error and result')); } } catch (error) { console.error('[Unity MCP] Error parsing WebSocket message:', error); console.error('[Unity MCP] Raw data that failed to parse:', data); // Try to find and reject any pending requests that might be waiting // This is a fallback in case the response format is unexpected if (this.pendingRequests.size > 0) { console.error('[Unity MCP] Attempting to reject oldest pending request as fallback'); const oldestId = Array.from(this.pendingRequests.keys())[0]; const oldest = this.pendingRequests.get(oldestId); if (oldest) { clearTimeout(oldest.timeout); this.pendingRequests.delete(oldestId); oldest.reject(new Error(`Failed to parse Unity response: ${error instanceof Error ? error.message : String(error)}`)); } } } } async request<T = any>(endpoint: string, params: any): Promise<T> { // Ensure we're connected if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { await this.connect(); } if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { const state = this.ws ? (this.ws.readyState === WebSocket.CONNECTING ? 'CONNECTING' : this.ws.readyState === WebSocket.CLOSING ? 'CLOSING' : this.ws.readyState === WebSocket.CLOSED ? 'CLOSED' : 'UNKNOWN') : 'NULL'; throw new Error(`WebSocket not connected (state: ${state}). ` + `Make sure Unity Editor is running with MCP WebSocket server started on port ${this.url.split(':').pop()}`); } const id = uuidv4(); // Convert HTTP-style endpoint to method name // e.g., '/editor/select' -> 'unity_editor_select' const method = this.endpointToMethod(endpoint); const request: JsonRpcRequest = { jsonrpc: '2.0', id, method, params, }; console.error('[Unity MCP] Sending request:', JSON.stringify(request).substring(0, 200)); console.error('[Unity MCP] Request ID:', id, 'Method:', method, 'Timeout:', this.requestTimeout, 'ms'); return new Promise<T>((resolve, reject) => { const timeout = setTimeout(() => { const wsState = this.ws ? this.ws.readyState : 'null'; const wsStateName = this.ws ? (this.ws.readyState === WebSocket.OPEN ? 'OPEN' : this.ws.readyState === WebSocket.CONNECTING ? 'CONNECTING' : this.ws.readyState === WebSocket.CLOSING ? 'CLOSING' : this.ws.readyState === WebSocket.CLOSED ? 'CLOSED' : 'UNKNOWN') : 'NULL'; console.error('[Unity MCP] REQUEST TIMEOUT!', 'ID:', id, 'Method:', method); console.error('[Unity MCP] WebSocket state:', wsStateName, `(${wsState})`); console.error('[Unity MCP] Pending requests at timeout:', Array.from(this.pendingRequests.keys())); console.error('[Unity MCP] Is connected?', this.ws && this.ws.readyState === WebSocket.OPEN); this.pendingRequests.delete(id); reject(new Error(`Request timeout: ${method} (WebSocket state: ${wsStateName})`)); }, this.requestTimeout); this.pendingRequests.set(id, { resolve, reject, timeout }); try { this.ws!.send(JSON.stringify(request)); console.error('[Unity MCP] Message sent to WebSocket successfully'); } catch (error) { console.error('[Unity MCP] Failed to send message to WebSocket:', error); clearTimeout(timeout); this.pendingRequests.delete(id); reject(error); } }); } private endpointToMethod(endpoint: string): string { // Convert '/editor/select' to 'editor_select' // Remove leading slash and replace remaining slashes with underscores const cleaned = endpoint.replace(/^\//, '').replace(/\//g, '_'); return cleaned; } async isConnected(): Promise<boolean> { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; } getType(): 'websocket' { return 'websocket'; } }

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/muammar-yacoob/unity-mcp'

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