UNPKG

capacitor-cors-bypass-enhanced

Version:

Enhanced Capacitor plugin for CORS bypass with HTTP/2, HTTP/3, gRPC, GraphQL, file operations, and advanced networking features. Modular TypeScript definitions for better maintainability.

258 lines (223 loc) 7.54 kB
/** * StreamableHTTP Transport for MCP (Web Implementation) * Implements the new single-endpoint transport protocol * * @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports */ export interface StreamableHTTPCallback { onMessage: (message: any) => void; onError: (error: string) => void; onConnectionStateChange: (state: string) => void; } export class StreamableHTTPTransport { private endpointUrl: string; private callback: StreamableHTTPCallback; private resumable: boolean; private sessionId: string | null = null; private lastSequence: number = 0; private abortController: AbortController | null = null; private currentReader: ReadableStreamDefaultReader<Uint8Array> | null = null; constructor( endpointUrl: string, callback: StreamableHTTPCallback, resumable: boolean = false, sessionId?: string, lastSequence?: number ) { this.endpointUrl = endpointUrl; this.callback = callback; this.resumable = resumable; if (sessionId) this.sessionId = sessionId; if (lastSequence !== undefined) this.lastSequence = lastSequence; } /** * Send a JSON-RPC message to the server */ async sendMessage(message: any, expectStream: boolean = false): Promise<void> { try { this.abortController = new AbortController(); const headers: Record<string, string> = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Mcp-Protocol-Version': '2025-03-26', }; // Add session headers for resumability if (this.resumable && this.sessionId) { headers['Mcp-Session-Id'] = this.sessionId; headers['Mcp-Sequence'] = String(this.lastSequence); } const response = await fetch(this.endpointUrl, { method: 'POST', headers, body: JSON.stringify(message), signal: this.abortController.signal, }); // Extract session ID for resumability const newSessionId = response.headers.get('Mcp-Session-Id'); if (newSessionId) { this.sessionId = newSessionId; } const contentType = response.headers.get('Content-Type') || ''; if (contentType.includes('text/event-stream')) { // Handle SSE stream await this.handleSSEStream(response); } else if (contentType.includes('application/json')) { // Handle single JSON response await this.handleJSONResponse(response); } else if (response.status === 202) { // Accepted (for notifications/responses) this.callback.onConnectionStateChange('accepted'); } else { // Error response const errorBody = await response.text(); this.callback.onError(`HTTP ${response.status}: ${errorBody}`); } } catch (error: any) { if (error.name !== 'AbortError') { this.callback.onError(`Send message failed: ${error.message}`); } } } /** * Open a GET stream to listen for server-initiated messages */ async openListenStream(): Promise<void> { try { this.abortController = new AbortController(); const headers: Record<string, string> = { 'Accept': 'text/event-stream', 'Mcp-Protocol-Version': '2025-03-26', }; // Add session headers for resumability if (this.resumable && this.sessionId) { headers['Mcp-Session-Id'] = this.sessionId; headers['Mcp-Sequence'] = String(this.lastSequence); } const response = await fetch(this.endpointUrl, { method: 'GET', headers, signal: this.abortController.signal, }); if (response.status === 405) { // Method Not Allowed - server doesn't support GET streams this.callback.onConnectionStateChange('get_not_supported'); return; } const contentType = response.headers.get('Content-Type') || ''; if (contentType.includes('text/event-stream')) { await this.handleSSEStream(response); } else { this.callback.onError(`Unexpected content type: ${contentType}`); } } catch (error: any) { if (error.name !== 'AbortError') { this.callback.onError(`Listen stream failed: ${error.message}`); } } } /** * Handle SSE stream response */ private async handleSSEStream(response: Response): Promise<void> { this.callback.onConnectionStateChange('streaming'); if (!response.body) { this.callback.onError('Response body is null'); return; } try { const reader = response.body.getReader(); this.currentReader = reader; const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { this.callback.onConnectionStateChange('stream_closed'); break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; let eventData = ''; for (const line of lines) { if (line.startsWith('data: ')) { eventData += line.substring(6) + '\n'; } else if (line === '') { // Empty line signals end of event if (eventData.trim()) { this.processSSEEvent(eventData.trim()); eventData = ''; } } // Ignore other SSE fields (event, id, retry) and comments } } } catch (error: any) { if (error.name !== 'AbortError') { this.callback.onError(`SSE stream error: ${error.message}`); } } finally { this.currentReader = null; } } /** * Process an SSE event data */ private processSSEEvent(eventData: string): void { try { const message = JSON.parse(eventData); // Update sequence number if present if (message._meta?.sequence !== undefined) { this.lastSequence = message._meta.sequence; } this.callback.onMessage(message); } catch (error: any) { this.callback.onError(`Failed to parse SSE event: ${error.message}`); } } /** * Handle single JSON response */ private async handleJSONResponse(response: Response): Promise<void> { try { const message = await response.json(); // Update sequence number if present if (message._meta?.sequence !== undefined) { this.lastSequence = message._meta.sequence; } this.callback.onMessage(message); } catch (error: any) { this.callback.onError(`Failed to parse JSON response: ${error.message}`); } } /** * Close the transport and cancel any active streams */ close(): void { if (this.abortController) { this.abortController.abort(); } if (this.currentReader) { this.currentReader.cancel(); } this.callback.onConnectionStateChange('closed'); } /** * Get the current session ID */ getSessionId(): string | null { return this.sessionId; } /** * Get the last sequence number */ getLastSequence(): number { return this.lastSequence; } /** * Check if transport is resumable */ isResumable(): boolean { return this.resumable; } }