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
881 lines (875 loc) • 27.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
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;
exports.ArrowConnectionStatus = ArrowConnectionStatus;
exports.MCPArrowClient = MCPArrowClient;
exports.createArrowClient = createArrowClient;
exports.default = arrow_ipc_default;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map