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.
467 lines (466 loc) • 18 kB
JavaScript
import { WebPlugin } from '@capacitor/core';
// MCP SDK imports (ESM)
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
// Import modular managers
import { UtilsManager } from './web/utils';
import { HttpManager } from './web/http';
import { StreamManager } from './web/stream';
import { SSEManager } from './web/sse';
import { WebSocketManager } from './web/websocket';
import { InterceptorManager } from './web/interceptor';
export class CorsBypassWeb extends WebPlugin {
constructor() {
super();
this.proxyServerUrl = null;
this.globalProxyConfig = null;
this.proxyRequestCount = 0;
this.proxyLastSuccessTime = null;
this.proxyLastError = null;
// MCP specific
this.mcpClients = new Map();
this.mcpTransports = new Map();
this.connectionCounter = 0;
// Initialize managers
this.utilsManager = new UtilsManager();
this.httpManager = new HttpManager(this.proxyServerUrl);
this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this));
this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this));
this.wsManager = new WebSocketManager(this.notifyListeners.bind(this));
this.interceptorManager = new InterceptorManager();
// Try to detect if a proxy server is available
this.detectProxyServer();
}
async detectProxyServer() {
const possibleUrls = [
'http://localhost:3002',
'http://127.0.0.1:3002',
'http://localhost:3001',
'http://127.0.0.1:3001',
'http://localhost:8080',
'http://127.0.0.1:8080'
];
for (const url of possibleUrls) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1000);
const response = await fetch(`${url}/health`, {
method: 'GET',
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
this.proxyServerUrl = url;
this.httpManager.setProxyServer(url);
this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this));
this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this));
console.log(`🔧 CORS Proxy server detected at: ${url}`);
break;
}
}
catch (error) {
// Ignore errors, continue checking
}
}
if (!this.proxyServerUrl) {
console.warn('⚠️ No CORS proxy server detected. Some requests may fail due to CORS.');
console.log('💡 To enable full functionality, run: node web-proxy-server.js');
}
}
/**
* Set custom proxy server URL
*/
setProxyServer(url) {
this.proxyServerUrl = url;
this.httpManager.setProxyServer(url);
this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this));
this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this));
console.log(`🔧 Proxy server set to: ${url}`);
}
async request(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.request(options, interceptors);
}
async get(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.get(options, interceptors);
}
async post(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.post(options, interceptors);
}
async put(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.put(options, interceptors);
}
async patch(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.patch(options, interceptors);
}
async delete(options) {
const interceptors = this.interceptorManager.getInterceptorsInternal();
return this.httpManager.delete(options, interceptors);
}
/**
* Streaming HTTP request - supports AI model streaming output
*/
async streamRequest(options) {
return this.streamManager.streamRequest(options);
}
/**
* Cancel streaming request
*/
async cancelStream(options) {
return this.streamManager.cancelStream(options);
}
async startSSE(options) {
return this.sseManager.startSSE(options);
}
async stopSSE(options) {
return this.sseManager.stopSSE(options);
}
async createSSEConnection(options) {
return this.sseManager.createSSEConnection(options);
}
async closeSSEConnection(options) {
return this.sseManager.closeSSEConnection(options);
}
async createWebSocketConnection(options) {
return this.wsManager.createWebSocketConnection(options);
}
async closeWebSocketConnection(options) {
return this.wsManager.closeWebSocketConnection(options);
}
async sendWebSocketMessage(options) {
return this.wsManager.sendWebSocketMessage(options);
}
// ===== MCP Protocol Methods =====
async createMCPClient(options) {
const connectionId = `mcp_${++this.connectionCounter}`;
try {
// Determine transport type and URL
const transport = options.transport || 'streamablehttp';
// Get URL (support both new and legacy config)
let url = options.url;
if (!url && options.sseUrl) {
// Backward compatibility: use sseUrl if url is not provided
url = options.sseUrl;
}
if (!url) {
throw new Error('URL is required for MCP client (provide either "url" or "sseUrl")');
}
// Create transport layer
let mcpTransport;
if (transport === 'streamablehttp') {
// Use new StreamableHTTP transport (recommended)
throw new Error('StreamableHTTP transport should use mcpClientManager. Use @capacitor/cors-bypass-enhanced web managers directly.');
}
else if (transport === 'sse' || options.sseUrl) {
// Legacy SSE transport
if (this.proxyServerUrl && this.utilsManager.isCrossOrigin(url)) {
// Use proxy server
const proxyUrl = `${this.proxyServerUrl}/sse-proxy/${encodeURIComponent(url)}`;
mcpTransport = new SSEClientTransport(new URL(proxyUrl));
}
else {
// Direct connection
mcpTransport = new SSEClientTransport(new URL(url));
}
}
else {
throw new Error(`Unsupported transport type: ${transport}`);
}
// Create MCP client
const client = new Client({
name: options.clientInfo.name,
version: options.clientInfo.version,
}, {
capabilities: {
roots: options.capabilities?.roots ? { listChanged: true } : undefined,
sampling: options.capabilities?.sampling ? {} : undefined,
}
});
// Connect to server
await client.connect(mcpTransport);
// Store client and transport
this.mcpClients.set(connectionId, client);
this.mcpTransports.set(connectionId, mcpTransport);
console.log(`✅ MCP client connected: ${connectionId}`);
return {
connectionId,
status: 'connected',
serverCapabilities: client.getServerCapabilities(),
protocolVersion: '2025-03-26'
};
}
catch (error) {
console.error(`❌ MCP client connection failed:`, error);
throw new Error(`Failed to create MCP client: ${error}`);
}
}
async listMCPResources(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.listResources(options.cursor ? { cursor: options.cursor } : {});
return {
resources: result.resources || [],
nextCursor: result.nextCursor
};
}
catch (error) {
throw new Error(`Failed to list MCP resources: ${error}`);
}
}
async readMCPResource(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.readResource({ uri: options.uri });
return {
uri: options.uri,
mimeType: result.contents?.[0]?.mimeType || 'text/plain',
text: result.contents?.[0]?.text || '',
blob: result.contents?.[0]?.data
};
}
catch (error) {
throw new Error(`Failed to read MCP resource: ${error}`);
}
}
async listMCPTools(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.listTools(options.cursor ? { cursor: options.cursor } : {});
return {
tools: result.tools || [],
nextCursor: result.nextCursor
};
}
catch (error) {
throw new Error(`Failed to list MCP tools: ${error}`);
}
}
async callMCPTool(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.callTool({
name: options.name,
arguments: options.arguments || {}
});
return {
content: result.content || [],
isError: result.isError || false
};
}
catch (error) {
throw new Error(`Failed to call MCP tool: ${error}`);
}
}
async listMCPPrompts(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.listPrompts(options.cursor ? { cursor: options.cursor } : {});
return {
prompts: result.prompts || [],
nextCursor: result.nextCursor
};
}
catch (error) {
throw new Error(`Failed to list MCP prompts: ${error}`);
}
}
async getMCPPrompt(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.getPrompt({
name: options.name,
arguments: options.arguments || {}
});
return {
description: result.description,
messages: result.messages || []
};
}
catch (error) {
throw new Error(`Failed to get MCP prompt: ${error}`);
}
}
async sendMCPSampling(options) {
const client = this.mcpClients.get(options.connectionId);
if (!client) {
throw new Error('MCP client not found');
}
try {
const result = await client.request({
method: options.request.method,
params: options.request.params
});
return result;
}
catch (error) {
throw new Error(`Failed to send MCP sampling request: ${error}`);
}
}
// ==================== Interceptor Management ====================
async addInterceptor(interceptor, options) {
return this.interceptorManager.addInterceptor(interceptor, options);
}
async removeInterceptor(handle) {
return this.interceptorManager.removeInterceptor(handle);
}
async removeAllInterceptors() {
return this.interceptorManager.removeAllInterceptors();
}
async getInterceptors() {
return this.interceptorManager.getInterceptors();
}
// ==================== Proxy Management ====================
/**
* Set global proxy configuration
* Note: On Web platform, proxy is handled through the CORS proxy server
* The proxy config is stored and can be passed to the server for server-side proxying
*/
async setGlobalProxy(config) {
this.globalProxyConfig = config;
// If using a proxy server, we can configure it to use the specified proxy
if (this.proxyServerUrl && config.enabled) {
console.log(`🔧 [Web] Global proxy configured: ${config.type || 'http'}://${config.host}:${config.port}`);
console.log('💡 Note: Web platform proxying requires server-side support.');
}
}
/**
* Get current global proxy configuration
*/
async getGlobalProxy() {
return this.globalProxyConfig;
}
/**
* Clear global proxy configuration
*/
async clearGlobalProxy() {
this.globalProxyConfig = null;
this.proxyLastError = null;
console.log('🔧 [Web] Global proxy configuration cleared');
}
/**
* Test proxy connection
* On Web platform, this tests connectivity through the CORS proxy server
*/
async testProxy(config, testUrl) {
const startTime = Date.now();
const url = testUrl || 'https://www.google.com';
if (!config.enabled || !config.host) {
return {
success: false,
error: 'Proxy configuration is invalid or disabled',
responseTime: 0
};
}
try {
// On Web, we can only test through our proxy server
if (this.proxyServerUrl) {
const response = await fetch(`${this.proxyServerUrl}/proxy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
method: 'HEAD',
proxy: {
enabled: true,
type: config.type || 'http',
host: config.host,
port: config.port,
username: config.username,
password: config.password
}
})
});
const responseTime = Date.now() - startTime;
this.proxyRequestCount++;
if (response.ok) {
this.proxyLastSuccessTime = Date.now();
this.proxyLastError = null;
return {
success: true,
responseTime,
statusCode: response.status
};
}
else {
const error = `HTTP ${response.status}`;
this.proxyLastError = error;
return {
success: false,
responseTime,
statusCode: response.status,
error
};
}
}
else {
// No proxy server available, test direct connection
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
signal: controller.signal
});
clearTimeout(timeoutId);
const responseTime = Date.now() - startTime;
return {
success: true,
responseTime,
statusCode: response.status || 0
};
}
catch (fetchError) {
clearTimeout(timeoutId);
return {
success: false,
responseTime: Date.now() - startTime,
error: fetchError.message || 'Connection failed'
};
}
}
}
catch (error) {
this.proxyLastError = error.message;
return {
success: false,
responseTime: Date.now() - startTime,
error: error.message || 'Proxy test failed'
};
}
}
/**
* Get current proxy status
*/
async getProxyStatus() {
return {
active: this.globalProxyConfig?.enabled ?? false,
config: this.globalProxyConfig ?? undefined,
requestCount: this.proxyRequestCount,
lastError: this.proxyLastError ?? undefined,
lastSuccessTime: this.proxyLastSuccessTime ?? undefined
};
}
}