UNPKG

ragatanga-mcp-sdk

Version:

SDK for integrating with the Ragatanga Management Control Plane (MCP) with Next.js 15, React 19, WebSocket and Arrow IPC support

476 lines (472 loc) 14.3 kB
'use strict'; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/client/paths.ts function createFullUrl(baseUrl, path) { const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; const normalizedPath = path.startsWith("/") ? path : `/${path}`; return `${normalizedBaseUrl}${normalizedPath}`; } __name(createFullUrl, "createFullUrl"); // src/client/websocket.ts var WebSocketStatus = /* @__PURE__ */ ((WebSocketStatus2) => { WebSocketStatus2[WebSocketStatus2["CONNECTING"] = 0] = "CONNECTING"; WebSocketStatus2[WebSocketStatus2["CONNECTED"] = 1] = "CONNECTED"; WebSocketStatus2[WebSocketStatus2["CLOSING"] = 2] = "CLOSING"; WebSocketStatus2[WebSocketStatus2["CLOSED"] = 3] = "CLOSED"; WebSocketStatus2[WebSocketStatus2["RECONNECTING"] = 4] = "RECONNECTING"; WebSocketStatus2[WebSocketStatus2["ERROR"] = 5] = "ERROR"; return WebSocketStatus2; })(WebSocketStatus || {}); var _MCPWebSocket = class _MCPWebSocket { /** * Creates a new WebSocket client */ constructor(options) { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.reconnectDelay = 1e3; this.handlers = {}; this.status = 3 /* CLOSED */; this.messageQueue = []; this.subscriptions = /* @__PURE__ */ new Map(); this.heartbeatDelay = 15e3; // 15 seconds to match server's interval this.connectionId = ""; this.binarySupport = false; this.connectionPath = "/resolve_ws"; this.autoReconnect = true; this.lastConnectionTime = 0; this.connectionRetryTimer = null; this.manualClose = false; let wsBaseUrl = options.baseUrl || "https://api.mcp.ai"; wsBaseUrl = wsBaseUrl.replace(/^http/, "ws"); this.baseUrl = wsBaseUrl; this.apiKey = options.apiKey; this.token = options.token; this.tenantId = options.tenantId; this.autoReconnect = options.autoReconnect !== false; if (options.maxReconnectAttempts !== void 0) { this.maxReconnectAttempts = options.maxReconnectAttempts; } } /** * Connects to the MCP WebSocket endpoint */ connect(path = "/resolve_ws", handlers = {}) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.warn("WebSocket is already connected. Disconnect first to reconnect."); return; } this.connectionPath = path; this.manualClose = false; this.setStatus(0 /* CONNECTING */); this.handlers = handlers; this.lastConnectionTime = Date.now(); const url = new URL(createFullUrl(this.baseUrl, path)); this.connectionId = this.generateConnectionId(); if (this.apiKey) { url.searchParams.append("apiKey", this.apiKey); } if (this.token) { url.searchParams.append("token", this.token); } if (this.tenantId) { url.searchParams.append("tenantId", this.tenantId); } url.searchParams.append("connectionId", this.connectionId); try { this.ws = new WebSocket(url.toString()); this.binarySupport = typeof this.ws.binaryType !== "undefined"; if (this.binarySupport) { this.ws.binaryType = "arraybuffer"; } this.ws.onopen = this.handleOpen.bind(this); this.ws.onmessage = this.handleMessage.bind(this); this.ws.onerror = this.handleError.bind(this); this.ws.onclose = this.handleClose.bind(this); } catch (error) { this.handleConnectionError(error); } } /** * Update and notify about status changes */ setStatus(newStatus) { if (this.status !== newStatus) { this.status = newStatus; if (this.handlers.onStatusChange) { try { this.handlers.onStatusChange(newStatus); } catch (error) { console.error("Error in WebSocket status change handler:", error); } } } } /** * Handle WebSocket open event */ handleOpen(event) { console.log("WebSocket connection opened"); this.setStatus(1 /* CONNECTED */); this.reconnectAttempts = 0; if (this.handlers.onOpen) { try { this.handlers.onOpen(event); } catch (error) { console.error("Error in WebSocket open handler:", error); } } this.startHeartbeat(); this.processQueue(); } /** * Handle WebSocket message event */ handleMessage(event) { let data; let messageType; try { if (this.binarySupport && event.data instanceof ArrayBuffer) { const view = new Uint8Array(event.data); const textDecoder = new TextDecoder(); data = textDecoder.decode(view); } else { data = JSON.parse(event.data); messageType = data.type; } if (this.handlers.onMessage) { try { this.handlers.onMessage(event.data, messageType); } catch (error) { console.error("Error in WebSocket message handler:", error); } } if (messageType && this.handlers.onMessageType && this.handlers.onMessageType[messageType]) { try { this.handlers.onMessageType[messageType](data); } catch (error) { console.error(`Error in WebSocket message type handler for ${messageType}:`, error); } } if (messageType && this.subscriptions.has(messageType)) { const callbacks = this.subscriptions.get(messageType); if (callbacks) { callbacks.forEach((callback) => { try { callback(data); } catch (error) { console.error(`Error in WebSocket subscription callback for ${messageType}:`, error); } }); } } if (messageType === "heartbeat" && this.lastHeartbeatTime) { const latency = Date.now() - this.lastHeartbeatTime; if (this.handlers.onHeartbeat) { try { this.handlers.onHeartbeat(latency); } catch (error) { console.error("Error in WebSocket heartbeat handler:", error); } } } } catch (error) { console.error("Error processing WebSocket message:", error); } } /** * Handle WebSocket error event */ handleError(event) { console.error("WebSocket error:", event); this.setStatus(5 /* ERROR */); if (this.handlers.onError) { try { this.handlers.onError(event); } catch (error) { console.error("Error in WebSocket error handler:", error); } } } /** * Handle WebSocket close event */ handleClose(event) { console.log(`WebSocket closed with code ${event.code}, reason: ${event.reason}`); this.setStatus(3 /* CLOSED */); this.stopHeartbeat(); if (this.handlers.onClose) { try { this.handlers.onClose(event); } catch (error) { console.error("Error in WebSocket close handler:", error); } } if (!this.manualClose && this.autoReconnect) { this.attemptReconnect(); } } /** * Handle connection initialization error */ handleConnectionError(error) { console.error("WebSocket connection error:", error); this.setStatus(5 /* ERROR */); if (this.autoReconnect) { this.attemptReconnect(); } } /** * Send a message over the WebSocket connection */ send(message) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.queueMessage(message); if (!this.ws || this.ws.readyState === WebSocket.CLOSED) { this.connect(this.connectionPath, this.handlers); } return false; } try { if (!message.id) { message.id = this.generateMessageId(); } this.ws.send(JSON.stringify(message)); return true; } catch (error) { console.error("Error sending WebSocket message:", error); this.queueMessage(message); return false; } } /** * Queue a message for later sending */ queueMessage(message) { if (this.messageQueue.length >= 100) { console.warn("WebSocket message queue is full, dropping oldest message"); this.messageQueue.shift(); } this.messageQueue.push(message); } /** * Process the message queue */ processQueue() { if (this.ws && this.ws.readyState === WebSocket.OPEN && this.messageQueue.length > 0) { console.log(`Processing ${this.messageQueue.length} queued WebSocket messages`); const queue = [...this.messageQueue]; this.messageQueue = []; for (const message of queue) { if (!this.send(message)) { break; } } } } /** * Subscribe to a specific message type */ subscribe(type, callback) { const id = this.generateSubscriptionId(); if (!this.subscriptions.has(type)) { this.subscriptions.set(type, /* @__PURE__ */ new Set()); } const callbacks = this.subscriptions.get(type); callbacks.add(callback); const unsubscribe = /* @__PURE__ */ __name(() => { if (this.subscriptions.has(type)) { const callbacks2 = this.subscriptions.get(type); callbacks2.delete(callback); if (callbacks2.size === 0) { this.subscriptions.delete(type); } } }, "unsubscribe"); return { id, type, callback, unsubscribe }; } /** * Send a request to resolve a URI */ resolveUri(uri, depth = 1) { return this.send({ type: "resolve", uri, depth }); } /** * Close the WebSocket connection */ disconnect() { this.manualClose = true; this.stopHeartbeat(); if (this.connectionRetryTimer) { clearTimeout(this.connectionRetryTimer); this.connectionRetryTimer = null; } if (this.ws) { try { if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { this.setStatus(2 /* CLOSING */); this.ws.close(1e3, "Client disconnect"); } } catch (error) { console.error("Error closing WebSocket:", error); } finally { this.ws = null; this.setStatus(3 /* CLOSED */); } } } /** * Get the current WebSocket state */ getState() { if (!this.ws) { return 3 /* CLOSED */; } if (this.status !== 4 /* RECONNECTING */ && this.status !== 5 /* ERROR */) { switch (this.ws.readyState) { case WebSocket.CONNECTING: return 0 /* CONNECTING */; case WebSocket.OPEN: return 1 /* CONNECTED */; case WebSocket.CLOSING: return 2 /* CLOSING */; case WebSocket.CLOSED: return 3 /* CLOSED */; default: return 3 /* CLOSED */; } } return this.status; } /** * Attempt to reconnect with exponential backoff */ attemptReconnect() { if (this.connectionRetryTimer) { clearTimeout(this.connectionRetryTimer); this.connectionRetryTimer = null; } if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error(`WebSocket maximum reconnection attempts (${this.maxReconnectAttempts}) reached`); return; } if (this.manualClose) { return; } this.reconnectAttempts++; const baseDelay = this.reconnectDelay; const exponentialDelay = baseDelay * Math.pow(1.5, this.reconnectAttempts - 1); const maxDelay = 3e4; const cappedDelay = Math.min(exponentialDelay, maxDelay); const jitter = cappedDelay * 0.2; const delay = cappedDelay + (Math.random() * jitter * 2 - jitter); this.setStatus(4 /* RECONNECTING */); console.log(`WebSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); this.connectionRetryTimer = setTimeout(() => { this.connect(this.connectionPath, this.handlers); }, delay); } /** * Start the heartbeat timer */ startHeartbeat() { this.stopHeartbeat(); this.heartbeatInterval = window.setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.lastHeartbeatTime = Date.now(); this.send({ type: "heartbeat", timestamp: this.lastHeartbeatTime }); this.heartbeatTimeout = window.setTimeout(() => { console.warn("WebSocket heartbeat timeout"); if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.disconnect(); if (this.autoReconnect) { this.connect(this.connectionPath, this.handlers); } } }, 5e3); } }, this.heartbeatDelay); } /** * Stop the heartbeat timer */ stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = void 0; } if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = void 0; } } /** * Set the heartbeat interval */ setHeartbeatInterval(intervalMs) { if (intervalMs < 1e3) { console.warn("Heartbeat interval too small, setting to 1000ms"); intervalMs = 1e3; } this.heartbeatDelay = intervalMs; if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.startHeartbeat(); } } /** * Set the maximum number of reconnect attempts */ setMaxReconnectAttempts(attempts) { this.maxReconnectAttempts = attempts; } /** * Set the reconnect delay */ setReconnectDelay(delayMs) { this.reconnectDelay = delayMs; } /** * Generate a unique subscription ID */ generateSubscriptionId() { return `sub_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; } /** * Generate a unique message ID */ generateMessageId() { return `msg_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; } /** * Generate a unique connection ID */ generateConnectionId() { return `conn_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; } }; __name(_MCPWebSocket, "MCPWebSocket"); var MCPWebSocket = _MCPWebSocket; function createWebSocketClient(options) { return new MCPWebSocket(options); } __name(createWebSocketClient, "createWebSocketClient"); exports.MCPWebSocket = MCPWebSocket; exports.WebSocketStatus = WebSocketStatus; exports.createWebSocketClient = createWebSocketClient; //# sourceMappingURL=websocket.js.map //# sourceMappingURL=websocket.js.map