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

874 lines (870 loc) 27 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/errors/index.ts var _MCPError = class _MCPError extends Error { constructor(message, options) { super(message); this.name = "MCPError"; this.status = options?.status; this.code = options?.code; this.cause = options?.cause; Object.setPrototypeOf(this, _MCPError.prototype); } /** * Creates a formatted error message with all available details */ toFormattedString() { const parts = [ `[${this.name}] ${this.message}`, this.code ? `Code: ${this.code}` : null, this.status ? `Status: ${this.status}` : null, this.cause ? `Cause: ${this.cause instanceof Error ? this.cause.message : String(this.cause)}` : null ].filter(Boolean); return parts.join(" | "); } }; __name(_MCPError, "MCPError"); var MCPError = _MCPError; var _WebSocketError = class _WebSocketError extends MCPError { constructor(message, options) { super(message, { code: options?.code || "WEBSOCKET_ERROR", cause: options?.cause }); this.name = "WebSocketError"; this.event = options?.event; Object.setPrototypeOf(this, _WebSocketError.prototype); } }; __name(_WebSocketError, "WebSocketError"); var WebSocketError = _WebSocketError; // 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/arrow-ipc.ts var ArrowConnectionStatus = /* @__PURE__ */ ((ArrowConnectionStatus2) => { ArrowConnectionStatus2[ArrowConnectionStatus2["DISCONNECTED"] = 0] = "DISCONNECTED"; ArrowConnectionStatus2[ArrowConnectionStatus2["CONNECTING"] = 1] = "CONNECTING"; ArrowConnectionStatus2[ArrowConnectionStatus2["CONNECTED"] = 2] = "CONNECTED"; ArrowConnectionStatus2[ArrowConnectionStatus2["CLOSING"] = 3] = "CLOSING"; ArrowConnectionStatus2[ArrowConnectionStatus2["ERROR"] = 4] = "ERROR"; ArrowConnectionStatus2[ArrowConnectionStatus2["RECONNECTING"] = 5] = "RECONNECTING"; return ArrowConnectionStatus2; })(ArrowConnectionStatus || {}); var _MCPArrowClient = class _MCPArrowClient { /** * Creates a new Arrow IPC client */ constructor(options) { this.ws = null; this.status = 0 /* DISCONNECTED */; this.handlers = {}; this.messageQueue = []; this.pendingRequests = /* @__PURE__ */ new Map(); this.defaultTimeout = 3e4; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1e3; this.autoReconnect = true; this.connectionPath = "/arrow"; this.connectionRetryTimer = null; this.manualClose = false; this.lastConnectionTime = 0; this.nextMessageId = 1; // Heartbeat properties this.heartbeatInterval = 15e3; // 15 seconds this.heartbeatTimer = null; this.missedHeartbeats = 0; this.maxMissedHeartbeats = 3; this.lastHeartbeatResponse = 0; /** * Event listener map for custom events */ this.eventListeners = /* @__PURE__ */ new Map(); 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; } if (options.defaultTimeout) { this.defaultTimeout = options.defaultTimeout; } if (options.streamTimeout) { this.streamTimeout = options.streamTimeout; } } /** * Add an event listener for a specific event type */ addEventListener(event, handler) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, /* @__PURE__ */ new Set()); } this.eventListeners.get(event)?.add(handler); } /** * Remove an event listener */ removeEventListener(event, handler) { if (this.eventListeners.has(event)) { this.eventListeners.get(event)?.delete(handler); } } /** * Trigger event handlers for a specific event */ triggerEvent(event, message) { if (this.eventListeners.has(event)) { for (const handler of this.eventListeners.get(event) || []) { try { handler(message); } catch (error) { console.error(`Error in ${event} event handler:`, error); } } } } /** * Connect to the Arrow IPC WebSocket endpoint */ connect(path = "/arrow", handlers = {}) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.warn("Arrow WebSocket is already connected. Disconnect first to reconnect."); return; } this.connectionPath = path; this.manualClose = false; this.setStatus(1 /* CONNECTING */); this.handlers = handlers; this.lastConnectionTime = Date.now(); const url = new URL(createFullUrl(this.baseUrl, path)); 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); } try { this.ws = new WebSocket(url.toString()); 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 Arrow status change handler:", error); } } } } /** * Handle WebSocket open event */ handleOpen(event) { console.log("Arrow WebSocket connection opened"); this.setStatus(2 /* CONNECTED */); this.reconnectAttempts = 0; if (this.handlers.onOpen) { try { this.handlers.onOpen(event); } catch (error) { console.error("Error in Arrow open handler:", error); } } this.processQueue(); this.startHeartbeat(); } /** * Start heartbeat mechanism to keep connection alive */ startHeartbeat() { if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); this.heartbeatTimer = null; } this.missedHeartbeats = 0; this.heartbeatTimer = setInterval(() => { this.sendHeartbeat(); }, this.heartbeatInterval); this.sendHeartbeat(); } /** * Stop heartbeat mechanism */ stopHeartbeat() { if (this.heartbeatTimer) { clearTimeout(this.heartbeatTimer); this.heartbeatTimer = null; } } /** * Send a heartbeat ping */ sendHeartbeat() { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { return; } if (this.missedHeartbeats >= this.maxMissedHeartbeats) { console.warn(`Missed ${this.missedHeartbeats} heartbeats, reconnecting...`); try { this.ws.close(); } catch (e) { console.error("Error closing WebSocket during heartbeat failure:", e); } if (this.autoReconnect && !this.manualClose) { this.attemptReconnect(); } return; } this.missedHeartbeats++; try { console.debug("Ping sent"); const pingMessage = { jsonrpc: "2.0", method: "ping", id: `ping-${Date.now()}` }; this.ws.send(JSON.stringify(pingMessage)); } catch (error) { console.error("Error sending heartbeat:", error); } } /** * Handle a WebSocket message event */ handleMessage(event) { try { this.missedHeartbeats = 0; this.lastHeartbeatResponse = Date.now(); if (event.data instanceof ArrayBuffer) { const dataSize = event.data.byteLength; console.log(`Received binary data (${dataSize} bytes)`); if (this.handlers.onBinaryMessage) { try { this.handlers.onBinaryMessage(event.data); } catch (error) { console.error("Error in binary message handler:", error); } } try { const message = this.decodeMessage(event.data); if (message.method === "ping") { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const pongMessage = { jsonrpc: "2.0", method: "pong", id: message.id }; try { this.ws.send(JSON.stringify(pongMessage)); } catch (e) { console.warn("Failed to send pong response:", e); } } return; } if (message.error) { console.warn("Received error message:", message.error); } this.triggerEvent("message", message); this.processMessage(message); } catch (decodeError) { console.error("Error decoding binary message:", decodeError); } } else if (typeof event.data === "string") { try { const message = JSON.parse(event.data); if (message.method === "pong" || message.id && String(message.id).startsWith("ping-")) { console.debug("Pong received"); return; } if (this.handlers.onMessage) { try { this.handlers.onMessage(message); } catch (error) { console.error("Error in message handler:", error); } } this.triggerEvent("message", message); this.processMessage(message); } catch (error) { console.error("Error parsing JSON message:", error, "Raw data:", event.data); } } else { console.warn("Received unsupported message type:", typeof event.data); } } catch (error) { console.error("Error handling WebSocket message:", error); } } /** * Handle WebSocket error event */ handleError(event) { console.error("Arrow WebSocket error:", event); this.setStatus(4 /* ERROR */); if (this.handlers.onError) { try { this.handlers.onError(event); } catch (error) { console.error("Error in Arrow error handler:", error); } } if (this.autoReconnect && !this.manualClose) { this.attemptReconnect(); } } /** * Handle WebSocket close event */ handleClose(event) { console.log("Arrow WebSocket connection closed:", event); this.setStatus(0 /* DISCONNECTED */); this.stopHeartbeat(); if (this.handlers.onClose) { try { this.handlers.onClose(event); } catch (error) { console.error("Error in Arrow close handler:", error); } } for (const [id, request] of this.pendingRequests.entries()) { this.pendingRequests.delete(id); if (request.timer) { clearTimeout(request.timer); } request.reject(new WebSocketError("Connection closed", { code: `CLOSE_${event.code}`, cause: event })); } if (this.autoReconnect && !this.manualClose && event.code !== 1e3) { this.attemptReconnect(); } } /** * Handle connection error */ handleConnectionError(error) { console.error("Arrow WebSocket connection error:", error); this.setStatus(4 /* ERROR */); if (this.autoReconnect && !this.manualClose) { this.attemptReconnect(); } } /** * Send a message over the WebSocket */ async sendArrowMessage(message) { if (message.id === void 0) { message.id = this.nextMessageId++; } const id = String(message.id); return new Promise((resolve, reject) => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.queueMessage({ ...message }); const timer = setTimeout(() => { this.pendingRequests.delete(id); reject(new WebSocketError("WebSocket not connected", { code: "CONNECTION_ERROR" })); }, this.defaultTimeout); this.pendingRequests.set(id, { resolve, reject, timer }); if (this.status !== 1 /* CONNECTING */ && this.autoReconnect && !this.manualClose) { this.attemptReconnect(); } return; } try { const data = this.encodeMessage(message); const timer = setTimeout(() => { this.pendingRequests.delete(id); reject(new MCPError("Request timed out", { code: "TIMEOUT" })); }, this.defaultTimeout); this.pendingRequests.set(id, { resolve, reject, timer }); this.ws.send(data); } catch (error) { reject(new MCPError("Failed to send message", { cause: error })); } }); } /** * Encode a message to binary format */ encodeMessage(message) { try { if ("binaryData" in message && message.binaryData instanceof Uint8Array && message.binaryData) { const { binaryData, ...jsonPart } = message; const jsonString = JSON.stringify(jsonPart); const headerDelimiter = new Uint8Array([0, 65, 82, 82, 79, 87, 58]); const encoder2 = new TextEncoder(); const jsonBytes = encoder2.encode(jsonString); const totalLength = jsonBytes.length + headerDelimiter.length + binaryData.length; const result = new Uint8Array(totalLength); result.set(jsonBytes, 0); result.set(headerDelimiter, jsonBytes.length); result.set(binaryData, jsonBytes.length + headerDelimiter.length); return result.buffer; } const encoder = new TextEncoder(); const encodedData = encoder.encode(JSON.stringify(message)); return encodedData.buffer; } catch (error) { console.error("Error encoding message:", error); throw new MCPError("Failed to encode message", { code: "ENCODING_ERROR", cause: error }); } } /** * Decode an Arrow IPC message from binary data */ decodeMessage(buffer) { try { const decoder = new TextDecoder(); const text = decoder.decode(new Uint8Array(buffer)); try { const jsonMessage = JSON.parse(text); if (jsonMessage && typeof jsonMessage === "object") { return jsonMessage; } } catch (jsonError) { console.debug("Not valid JSON, trying binary parse"); } const headerDelimiter = new Uint8Array([0, 65, 82, 82, 79, 87, 58]); const data = new Uint8Array(buffer); console.debug(`Received binary data (${data.length} bytes)`); let delimiterIndex = -1; for (let i = 0; i <= data.length - headerDelimiter.length; i++) { let match = true; for (let j = 0; j < headerDelimiter.length; j++) { if (data[i + j] !== headerDelimiter[j]) { match = false; break; } } if (match) { delimiterIndex = i; break; } } let message; if (delimiterIndex > 0) { const headerPart = new Uint8Array(buffer, 0, delimiterIndex); const headerText = decoder.decode(headerPart); try { message = JSON.parse(headerText); } catch (e) { console.error("Failed to parse Arrow IPC header:", e); message = { jsonrpc: "2.0" }; } const binaryDataStart = delimiterIndex + headerDelimiter.length; const binaryData = new Uint8Array(buffer, binaryDataStart); message.binaryData = binaryData; } else { if (data.length < 10) { console.debug("Received likely heartbeat/ping message"); message = { jsonrpc: "2.0", method: "ping", id: String(Date.now()) }; } else { console.warn(`Received unrecognized binary format (${data.length} bytes): ${data.slice(0, 20)}`); message = { jsonrpc: "2.0", id: String(Date.now()), binaryData: data }; } } return message; } catch (error) { console.error("Error decoding message:", error); return { jsonrpc: "2.0", error: { code: -32700, message: "Failed to decode binary message" } }; } } /** * Queue a message for later */ queueMessage(message) { console.log("Queueing message for later:", message); this.messageQueue.push(message); if (this.status !== 1 /* CONNECTING */ && this.autoReconnect && !this.manualClose) { this.attemptReconnect(); } } /** * Process the message queue */ processQueue() { if (this.messageQueue.length === 0) { return; } console.log(`Processing ${this.messageQueue.length} queued messages`); const queue = [...this.messageQueue]; this.messageQueue = []; for (const message of queue) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.sendArrowMessage(message).catch(console.error); } else { this.messageQueue.push(message); } } } /** * Attempt to reconnect to the WebSocket */ attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error(`Maximum reconnect attempts (${this.maxReconnectAttempts}) reached. Giving up.`); return; } if (this.status === 1 /* CONNECTING */ || this.status === 5 /* RECONNECTING */) { return; } this.setStatus(5 /* RECONNECTING */); const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts), 3e4); this.reconnectAttempts++; console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); this.connectionRetryTimer = setTimeout(() => { this.connectionRetryTimer = null; this.connect(this.connectionPath, this.handlers); }, delay); } /** * Generate a unique message ID */ generateMessageId() { return this.nextMessageId++; } /** * Get server capabilities */ async getCapabilities() { return this.sendArrowMessage({ jsonrpc: "2.0", method: "capabilities", params: {}, id: this.generateMessageId() }); } /** * List available resources */ async listResources() { return this.sendArrowMessage({ jsonrpc: "2.0", method: "listResources", params: {}, id: this.generateMessageId() }); } /** * Get a specific resource */ async getResource(uri) { return this.sendArrowMessage({ jsonrpc: "2.0", method: "getResource", params: { uri }, id: this.generateMessageId() }); } /** * List available tools */ async listTools() { return this.sendArrowMessage({ jsonrpc: "2.0", method: "listTools", params: {}, id: this.generateMessageId() }); } /** * Get the current connection status */ getStatus() { return this.status; } /** * Disconnect from the WebSocket */ disconnect() { if (!this.ws) { return; } console.log("Disconnecting Arrow WebSocket"); this.manualClose = true; this.stopHeartbeat(); this.setStatus(3 /* CLOSING */); try { this.ws.close(); } catch (error) { console.error("Error closing WebSocket:", error); } finally { this.ws = null; this.setStatus(0 /* DISCONNECTED */); } if (this.connectionRetryTimer) { clearTimeout(this.connectionRetryTimer); this.connectionRetryTimer = null; } } /** * Send a request to a model (like Claude) through the MCP server */ async sendRequest(options) { const startTime = performance.now(); let serializationTime = 0; let networkStart = 0; let networkEnd = 0; let deserializationTime = 0; try { const serializationStart = performance.now(); const method = options.method || "POST"; const responseType = options.responseType || "json"; const isStreaming = options.onChunk && options.body?.stream; const params = { path: options.path, method, body: options.body || {}, responseType, stream: isStreaming }; serializationTime = performance.now() - serializationStart; networkStart = performance.now(); if (isStreaming) { const messageId = this.generateMessageId(); const streamingPromise = new Promise((resolve, reject) => { const streamHandler = /* @__PURE__ */ __name((message) => { if (message.id === messageId && message.method === "streamChunk") { options.onChunk?.(message.params); } else if (message.id === messageId && message.method === "streamEnd") { this.removeEventListener("message", streamHandler); networkEnd = performance.now(); resolve(message.params?.result || {}); } else if (message.id === messageId && message.error) { this.removeEventListener("message", streamHandler); reject(new MCPError("Streaming error", { code: "STREAM_ERROR", cause: message.error })); } }, "streamHandler"); this.addEventListener("message", streamHandler); const timer = setTimeout(() => { this.removeEventListener("message", streamHandler); reject(new MCPError("Stream request timed out", { code: "TIMEOUT" })); }, this.streamTimeout || 6e4); this.sendArrowMessage({ jsonrpc: "2.0", method: "startStream", params, id: messageId }).catch((error) => { this.removeEventListener("message", streamHandler); clearTimeout(timer); reject(error); }); }); const response = await streamingPromise; deserializationTime = 0; const result = { data: response.data || {}, status: response.status || 200, headers: response.headers || {} }; if (options.metrics) { result.metrics = { totalTime: performance.now() - startTime, serializationTime, networkTime: networkEnd - networkStart, deserializationTime }; } return result; } else { const response = await this.sendArrowMessage({ jsonrpc: "2.0", method: "sendRequest", params, id: this.generateMessageId() }); networkEnd = performance.now(); const deserializationStart = performance.now(); const result = { data: response.data || {}, status: response.status || 200, headers: response.headers || {} }; deserializationTime = performance.now() - deserializationStart; if (options.metrics) { result.metrics = { totalTime: performance.now() - startTime, serializationTime, networkTime: networkEnd - networkStart, deserializationTime }; } return result; } } catch (error) { console.error("Error in sendRequest:", error); throw error; } } /** * Check server health */ async checkHealth() { try { const healthUrl = new URL(this.baseUrl.replace("ws://", "http://").replace("wss://", "https://")); healthUrl.pathname = "/health"; const response = await fetch(healthUrl.toString(), { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok) { const data = await response.json(); return data.status === "ok"; } if (this.status !== 2 /* CONNECTED */) { return false; } await this.getCapabilities(); return true; } catch (error) { console.error("Health check failed:", error); return false; } } /** * Get server connection status as string */ getConnectionStatusText() { switch (this.status) { case 2 /* CONNECTED */: return "Connected"; case 1 /* CONNECTING */: return "Connecting"; case 0 /* DISCONNECTED */: return "Disconnected"; case 4 /* ERROR */: return "Error"; case 5 /* RECONNECTING */: return "Reconnecting"; case 3 /* CLOSING */: return "Closing"; default: return "Unknown"; } } /** * Process an incoming message */ processMessage(message) { const id = String(message.id || ""); const pendingRequest = this.pendingRequests.get(id); if (pendingRequest) { if (pendingRequest.timer) { clearTimeout(pendingRequest.timer); } this.pendingRequests.delete(id); if (message.error) { pendingRequest.reject(new MCPError(message.error.message || "Unknown error", { code: String(message.error.code), cause: message.error.data })); } else { let result = message.result; if (message.binaryData && message.binaryData.length > 0) { try { if (!result) { result = {}; } if (typeof result === "object") { result.arrowData = message.binaryData.buffer; if (message.params?.schema) { result.schema = message.params.schema; } } } catch (error) { console.warn("Error processing Arrow binary data:", error); } } pendingRequest.resolve(result); } } if (message.method === "streamChunk" || message.method === "streamEnd") { return; } if (this.handlers.onMessage) { try { this.handlers.onMessage(message); } catch (error) { console.error("Error in message handler:", error); } } } }; __name(_MCPArrowClient, "MCPArrowClient"); var MCPArrowClient = _MCPArrowClient; function createArrowClient(options) { return new MCPArrowClient(options); } __name(createArrowClient, "createArrowClient"); var arrow_ipc_default = MCPArrowClient; export { ArrowConnectionStatus, MCPArrowClient, createArrowClient, arrow_ipc_default as default }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map