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.
223 lines (222 loc) • 7.78 kB
JavaScript
/**
* StreamableHTTP Transport for MCP (Web Implementation)
* Implements the new single-endpoint transport protocol
*
* @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
*/
export class StreamableHTTPTransport {
constructor(endpointUrl, callback, resumable = false, sessionId, lastSequence) {
this.sessionId = null;
this.lastSequence = 0;
this.abortController = null;
this.currentReader = null;
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, expectStream = false) {
try {
this.abortController = new AbortController();
const headers = {
'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) {
if (error.name !== 'AbortError') {
this.callback.onError(`Send message failed: ${error.message}`);
}
}
}
/**
* Open a GET stream to listen for server-initiated messages
*/
async openListenStream() {
try {
this.abortController = new AbortController();
const headers = {
'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) {
if (error.name !== 'AbortError') {
this.callback.onError(`Listen stream failed: ${error.message}`);
}
}
}
/**
* Handle SSE stream response
*/
async handleSSEStream(response) {
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) {
if (error.name !== 'AbortError') {
this.callback.onError(`SSE stream error: ${error.message}`);
}
}
finally {
this.currentReader = null;
}
}
/**
* Process an SSE event data
*/
processSSEEvent(eventData) {
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) {
this.callback.onError(`Failed to parse SSE event: ${error.message}`);
}
}
/**
* Handle single JSON response
*/
async handleJSONResponse(response) {
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) {
this.callback.onError(`Failed to parse JSON response: ${error.message}`);
}
}
/**
* Close the transport and cancel any active streams
*/
close() {
if (this.abortController) {
this.abortController.abort();
}
if (this.currentReader) {
this.currentReader.cancel();
}
this.callback.onConnectionStateChange('closed');
}
/**
* Get the current session ID
*/
getSessionId() {
return this.sessionId;
}
/**
* Get the last sequence number
*/
getLastSequence() {
return this.lastSequence;
}
/**
* Check if transport is resumable
*/
isResumable() {
return this.resumable;
}
}