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
472 lines (469 loc) • 14.3 kB
JavaScript
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");
export { MCPWebSocket, WebSocketStatus, createWebSocketClient };
//# sourceMappingURL=websocket.mjs.map
//# sourceMappingURL=websocket.mjs.map