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

1 lines 13.3 kB
{"version":3,"sources":["../../src/client/sse.ts"],"names":["SSEConnectionStatus"],"mappings":";AA8BY,IAAA,mBAAA,qBAAAA,oBAAL,KAAA;AACL,EAAAA,qBAAA,cAAe,CAAA,GAAA,cAAA;AACf,EAAAA,qBAAA,YAAa,CAAA,GAAA,YAAA;AACb,EAAAA,qBAAA,WAAY,CAAA,GAAA,WAAA;AACZ,EAAAA,qBAAA,cAAe,CAAA,GAAA,cAAA;AACf,EAAAA,qBAAA,OAAQ,CAAA,GAAA,OAAA;AALE,EAAAA,OAAAA,oBAAAA;AAAA,CAAA,EAAA,mBAAA,IAAA,EAAA","file":"sse.mjs","sourcesContent":["import { MCPOptions } from './types';\nimport { MCPError } from '../errors';\n\ntype EventCallback = (event: MessageEvent) => void;\n\n/**\n * Options for the SSE Client\n */\nexport interface SSEOptions extends MCPOptions {\n // Endpoint for SSE events, defaults to \"/events\"\n eventsEndpoint?: string;\n // Timeout in ms before reconnecting after connection loss\n reconnectTimeout?: number;\n // Maximum number of reconnection attempts\n maxReconnectAttempts?: number;\n // Additional SSE-specific options\n sseOptions?: EventSourceInit;\n // Event types to listen for (used for filtering)\n eventTypes?: string[];\n // Heartbeat interval in ms (default: 30000)\n heartbeatInterval?: number;\n // Connection timeout in ms (default: 10000)\n connectionTimeout?: number;\n // Whether to enable auto-reconnect (default: true)\n autoReconnect?: boolean;\n}\n\n/**\n * Connection status for SSE client\n */\nexport enum SSEConnectionStatus {\n DISCONNECTED = 'disconnected',\n CONNECTING = 'connecting',\n CONNECTED = 'connected',\n RECONNECTING = 'reconnecting',\n ERROR = 'error'\n}\n\n/**\n * SSE Client for handling Server-Sent Events\n */\nexport class MCPSSEClient {\n private baseUrl: string;\n private apiKey?: string;\n private token?: string;\n private tenantId?: string;\n private eventSource: EventSource | null = null;\n private eventHandlers: Map<string, Set<EventCallback>> = new Map();\n private reconnectTimeout: number;\n private maxReconnectAttempts: number;\n private reconnectAttempts: number = 0;\n private eventsEndpoint: string;\n private sseOptions: EventSourceInit | undefined;\n private status: SSEConnectionStatus = SSEConnectionStatus.DISCONNECTED;\n private eventTypes: string[] | undefined;\n private lastEventId: string | null = null;\n private heartbeatInterval: number;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private connectionTimeout: number;\n private connectionTimer: ReturnType<typeof setTimeout> | null = null;\n private autoReconnect: boolean;\n private statusHandlers: Set<(status: SSEConnectionStatus) => void> = new Set();\n\n /**\n * Create a new SSE client\n */\n constructor(options: SSEOptions) {\n this.baseUrl = options.baseUrl || 'https://api.mcp.ai';\n this.apiKey = options.apiKey;\n this.token = options.token;\n this.tenantId = options.tenantId;\n this.reconnectTimeout = options.reconnectTimeout || 3000;\n this.maxReconnectAttempts = options.maxReconnectAttempts || 10;\n this.eventsEndpoint = options.eventsEndpoint || '/events';\n this.sseOptions = options.sseOptions;\n this.eventTypes = options.eventTypes;\n this.heartbeatInterval = options.heartbeatInterval || 30000;\n this.connectionTimeout = options.connectionTimeout || 10000;\n this.autoReconnect = options.autoReconnect !== false;\n }\n\n /**\n * Get the current connection status\n */\n getStatus(): SSEConnectionStatus {\n return this.status;\n }\n\n /**\n * Register a handler for connection status changes\n */\n onStatusChange(handler: (status: SSEConnectionStatus) => void): () => void {\n this.statusHandlers.add(handler);\n\n // Call immediately with current status\n handler(this.status);\n\n // Return function to remove handler\n return () => {\n this.statusHandlers.delete(handler);\n };\n }\n\n /**\n * Update connection status and notify handlers\n */\n private setStatus(status: SSEConnectionStatus): void {\n if (this.status !== status) {\n this.status = status;\n\n // Notify all status handlers\n for (const handler of this.statusHandlers) {\n try {\n handler(status);\n } catch (error) {\n console.error('Error in SSE status handler:', error);\n }\n }\n }\n }\n\n /**\n * Connect to the SSE endpoint\n */\n connect(): void {\n if (this.eventSource) {\n this.disconnect();\n }\n\n this.setStatus(SSEConnectionStatus.CONNECTING);\n this.reconnectAttempts = 0;\n\n const url = new URL(`${this.baseUrl}${this.eventsEndpoint}`);\n\n // Add auth parameters\n if (this.apiKey) {\n url.searchParams.append('apiKey', this.apiKey);\n }\n if (this.token) {\n url.searchParams.append('token', this.token);\n }\n if (this.tenantId) {\n url.searchParams.append('tenantId', this.tenantId);\n }\n\n // Add event types filter if specified\n if (this.eventTypes && this.eventTypes.length > 0) {\n url.searchParams.append('events', this.eventTypes.join(','));\n }\n\n // Add last event ID for resuming\n if (this.lastEventId) {\n url.searchParams.append('lastEventId', this.lastEventId);\n }\n\n try {\n // Set connection timeout\n this.connectionTimer = setTimeout(() => {\n if (this.status === SSEConnectionStatus.CONNECTING) {\n console.warn('SSE connection timeout');\n this.setStatus(SSEConnectionStatus.ERROR);\n this.disconnect();\n if (this.autoReconnect) {\n this.attemptReconnect();\n }\n }\n }, this.connectionTimeout);\n\n // Create the EventSource\n this.eventSource = new EventSource(url.toString(), this.sseOptions);\n\n // Set up event handlers\n this.eventSource.onopen = this.handleOpen.bind(this);\n this.eventSource.onerror = this.handleError.bind(this);\n this.eventSource.onmessage = this.handleMessage.bind(this);\n\n // Register for specific event types and existing handlers\n if (this.eventTypes) {\n for (const eventType of this.eventTypes) {\n this.eventSource.addEventListener(eventType, this.handleEvent.bind(this));\n }\n }\n\n // Register all event types from handlers\n for (const eventType of this.eventHandlers.keys()) {\n this.eventSource.addEventListener(eventType, this.handleEvent.bind(this));\n }\n } catch (error) {\n console.error('Error connecting to SSE endpoint:', error);\n this.handleConnectionError(error);\n }\n }\n\n /**\n * Disconnect from the SSE endpoint\n */\n disconnect(): void {\n // Clear timers\n if (this.connectionTimer) {\n clearTimeout(this.connectionTimer);\n this.connectionTimer = null;\n }\n\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n\n if (this.status !== SSEConnectionStatus.ERROR) {\n this.setStatus(SSEConnectionStatus.DISCONNECTED);\n }\n }\n }\n\n /**\n * Register an event handler for a specific event type\n */\n on(eventType: string, callback: EventCallback): () => void {\n if (!this.eventHandlers.has(eventType)) {\n this.eventHandlers.set(eventType, new Set());\n\n // If connected, add the event listener\n if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) {\n this.eventSource.addEventListener(eventType, this.handleEvent.bind(this));\n }\n }\n\n const handlers = this.eventHandlers.get(eventType)!;\n handlers.add(callback);\n\n // Return a function to remove this handler\n return () => {\n if (this.eventHandlers.has(eventType)) {\n const handlers = this.eventHandlers.get(eventType)!;\n handlers.delete(callback);\n\n if (handlers.size === 0) {\n this.eventHandlers.delete(eventType);\n }\n }\n };\n }\n\n /**\n * Remove all event handlers for a specific event type\n */\n off(eventType: string): void {\n this.eventHandlers.delete(eventType);\n }\n\n /**\n * Attempt to reconnect with exponential backoff\n */\n private attemptReconnect(): void {\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n console.error(`SSE maximum reconnection attempts (${this.maxReconnectAttempts}) reached`);\n this.setStatus(SSEConnectionStatus.ERROR);\n return;\n }\n\n this.reconnectAttempts++;\n this.setStatus(SSEConnectionStatus.RECONNECTING);\n\n // Calculate backoff time (with jitter)\n const baseDelay = this.reconnectTimeout;\n const exponentialDelay = baseDelay * Math.pow(1.5, this.reconnectAttempts - 1);\n const jitter = 0.2 * exponentialDelay;\n const delay = exponentialDelay + (Math.random() * jitter);\n\n console.log(`SSE reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);\n\n setTimeout(() => {\n this.connect();\n }, delay);\n }\n\n /**\n * Start heartbeat to detect connection issues\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n }\n\n this.heartbeatTimer = setInterval(() => {\n if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) {\n // Dispatch a heartbeat check event\n this.dispatchEvent('heartbeat', new MessageEvent('heartbeat', {\n data: JSON.stringify({\n timestamp: Date.now()\n })\n }));\n } else if (this.eventSource && this.eventSource.readyState === EventSource.CLOSED) {\n // Connection is unexpectedly closed\n console.warn('SSE connection unexpectedly closed during heartbeat check');\n this.disconnect();\n if (this.autoReconnect) {\n this.attemptReconnect();\n }\n }\n }, this.heartbeatInterval);\n }\n\n /**\n * Handle SSE connection open\n */\n private handleOpen(event: Event): void {\n // Clear connection timeout\n if (this.connectionTimer) {\n clearTimeout(this.connectionTimer);\n this.connectionTimer = null;\n }\n\n console.log('SSE connection opened');\n this.setStatus(SSEConnectionStatus.CONNECTED);\n this.reconnectAttempts = 0;\n\n // Start heartbeat\n this.startHeartbeat();\n\n // Dispatch the open event\n this.dispatchEvent('open', new MessageEvent('open'));\n }\n\n /**\n * Handle SSE connection error\n */\n private handleError(event: Event): void {\n console.error('SSE connection error:', event);\n\n // Dispatch the error event\n this.dispatchEvent('error', new MessageEvent('error'));\n\n // Set error status\n this.setStatus(SSEConnectionStatus.ERROR);\n\n // Attempt to reconnect\n if (this.autoReconnect) {\n this.disconnect();\n this.attemptReconnect();\n }\n }\n\n /**\n * Handle connection initialization error\n */\n private handleConnectionError(error: unknown): void {\n // Clear connection timeout\n if (this.connectionTimer) {\n clearTimeout(this.connectionTimer);\n this.connectionTimer = null;\n }\n\n // Set error status\n this.setStatus(SSEConnectionStatus.ERROR);\n\n // Dispatch the error event\n this.dispatchEvent('error', new MessageEvent('error', {\n data: error instanceof Error ? error.message : String(error)\n }));\n\n // Attempt to reconnect\n if (this.autoReconnect) {\n this.attemptReconnect();\n }\n }\n\n /**\n * Handle generic message event\n */\n private handleMessage(event: MessageEvent): void {\n // Store the last event ID for resuming\n if (event.lastEventId) {\n this.lastEventId = event.lastEventId;\n }\n\n // Parse the data if it's JSON\n try {\n const data = JSON.parse(event.data);\n\n // If the data has a type field, dispatch to that specific event type\n if (data.type && typeof data.type === 'string') {\n this.dispatchEvent(data.type, new MessageEvent(data.type, {\n data: data.payload || data,\n lastEventId: event.lastEventId\n }));\n }\n } catch (e) {\n // Not JSON, just pass through the raw data\n }\n\n // Reset reconnect attempts on successful message\n this.reconnectAttempts = 0;\n\n // Dispatch to the generic message handler\n this.dispatchEvent('message', event);\n }\n\n /**\n * Handle specific event type\n */\n private handleEvent(event: MessageEvent): void {\n // Dispatch to handlers for this event type\n this.dispatchEvent(event.type, event);\n }\n\n /**\n * Dispatch an event to all registered handlers\n */\n private dispatchEvent(eventType: string, event: MessageEvent): void {\n if (this.eventHandlers.has(eventType)) {\n const handlers = this.eventHandlers.get(eventType)!;\n for (const handler of handlers) {\n try {\n handler(event);\n } catch (error) {\n console.error(`Error in SSE event handler for ${eventType}:`, error);\n }\n }\n }\n }\n}\n\n/**\n * Create a new SSE client\n */\nexport function createSSEClient(options: SSEOptions): MCPSSEClient {\n return new MCPSSEClient(options);\n}\n"]}