Skip to main content
Glama
bridge.ts10.6 kB
/** * PinePaper Browser Bridge * * postMessage interface for communication between PinePaper Studio (iframe/window) * and the MCP server or external clients. * * This bridge enables: * 1. Direct browser-to-PinePaper communication (no MCP required) * 2. MCP server to PinePaper communication via postMessage relay * 3. Cross-origin iframe communication for embedded PinePaper */ // ============================================================================= // TYPES // ============================================================================= export interface BridgeMessage { type: 'mcp-request' | 'mcp-response' | 'execute-code' | 'code-result' | 'ping' | 'pong'; id: string; payload: unknown; source: 'mcp-bridge' | 'pinepaper-studio'; } export interface BridgeConfig { targetOrigin: string; targetWindow?: Window; timeout?: number; onReady?: () => void; onError?: (error: Error) => void; } export interface PendingRequest { resolve: (value: unknown) => void; reject: (error: Error) => void; timeout: ReturnType<typeof setTimeout>; } // ============================================================================= // BROWSER BRIDGE CLASS // ============================================================================= /** * PinePaper Browser Bridge * * Provides a postMessage-based interface for communicating with PinePaper Studio. * Can be used standalone in the browser or integrated with an MCP server. */ export class PinePaperBridge { private config: Required<BridgeConfig>; private pendingRequests: Map<string, PendingRequest> = new Map(); private isReady = false; private readyPromise: Promise<void>; private readyResolve!: () => void; private messageHandler: (event: MessageEvent) => void; constructor(config: BridgeConfig) { this.config = { targetWindow: window.parent, timeout: 30000, onReady: () => {}, onError: () => {}, ...config, }; this.readyPromise = new Promise((resolve) => { this.readyResolve = resolve; }); // Bind message handler this.messageHandler = this.handleMessage.bind(this); window.addEventListener('message', this.messageHandler); // Send initial ping this.ping(); } /** * Wait for the bridge to be ready */ async waitForReady(): Promise<void> { return this.readyPromise; } /** * Check if bridge is connected */ get connected(): boolean { return this.isReady; } /** * Send a ping to establish connection */ private ping(): void { this.postMessage({ type: 'ping', id: this.generateId(), payload: { timestamp: Date.now() }, source: 'mcp-bridge', }); } /** * Handle incoming messages */ private handleMessage(event: MessageEvent): void { // Validate origin if (this.config.targetOrigin !== '*' && event.origin !== this.config.targetOrigin) { return; } const message = event.data as BridgeMessage; if (!message || !message.type || message.source === 'mcp-bridge') { return; } switch (message.type) { case 'pong': this.handlePong(); break; case 'mcp-response': case 'code-result': this.handleResponse(message); break; default: // Ignore unknown message types break; } } /** * Handle pong response (connection established) */ private handlePong(): void { if (!this.isReady) { this.isReady = true; this.readyResolve(); this.config.onReady(); } } /** * Handle response messages */ private handleResponse(message: BridgeMessage): void { const pending = this.pendingRequests.get(message.id); if (!pending) { return; } clearTimeout(pending.timeout); this.pendingRequests.delete(message.id); const payload = message.payload as { success?: boolean; error?: string; result?: unknown }; if (payload.error) { pending.reject(new Error(payload.error)); } else { pending.resolve(payload.result); } } /** * Send a message to PinePaper Studio */ private postMessage(message: BridgeMessage): void { this.config.targetWindow.postMessage(message, this.config.targetOrigin); } /** * Generate unique message ID */ private generateId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Execute code in PinePaper Studio */ async executeCode(code: string): Promise<unknown> { if (!this.isReady) { await this.waitForReady(); } const id = this.generateId(); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error('Request timeout')); }, this.config.timeout); this.pendingRequests.set(id, { resolve, reject, timeout }); this.postMessage({ type: 'execute-code', id, payload: { code }, source: 'mcp-bridge', }); }); } /** * Send an MCP tool request to PinePaper Studio */ async sendToolRequest(toolName: string, args: Record<string, unknown>): Promise<unknown> { if (!this.isReady) { await this.waitForReady(); } const id = this.generateId(); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error('Request timeout')); }, this.config.timeout); this.pendingRequests.set(id, { resolve, reject, timeout }); this.postMessage({ type: 'mcp-request', id, payload: { tool: toolName, arguments: args }, source: 'mcp-bridge', }); }); } /** * Clean up resources */ destroy(): void { window.removeEventListener('message', this.messageHandler); // Cancel all pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error('Bridge destroyed')); this.pendingRequests.delete(id); } } } // ============================================================================= // PINEPAPER STUDIO HANDLER (runs inside PinePaper) // ============================================================================= /** * Message handler for PinePaper Studio side * * Install this in PinePaper Studio to handle incoming bridge messages. */ export class PinePaperStudioHandler { private allowedOrigins: string[]; private messageHandler: (event: MessageEvent) => void; constructor(allowedOrigins: string[] = ['*']) { this.allowedOrigins = allowedOrigins; this.messageHandler = this.handleMessage.bind(this); window.addEventListener('message', this.messageHandler); } /** * Handle incoming messages */ private handleMessage(event: MessageEvent): void { // Validate origin if ( !this.allowedOrigins.includes('*') && !this.allowedOrigins.includes(event.origin) ) { return; } const message = event.data as BridgeMessage; if (!message || !message.type || message.source !== 'mcp-bridge') { return; } switch (message.type) { case 'ping': this.handlePing(event.source as Window, event.origin); break; case 'execute-code': this.handleExecuteCode(message, event.source as Window, event.origin); break; case 'mcp-request': this.handleMCPRequest(message, event.source as Window, event.origin); break; } } /** * Handle ping message */ private handlePing(source: Window, origin: string): void { source.postMessage( { type: 'pong', id: `pong_${Date.now()}`, payload: { timestamp: Date.now() }, source: 'pinepaper-studio', } as BridgeMessage, origin ); } /** * Handle code execution request */ private async handleExecuteCode( message: BridgeMessage, source: Window, origin: string ): Promise<void> { const { code } = message.payload as { code: string }; try { // Execute the code in PinePaper context // eslint-disable-next-line no-eval const result = await eval(`(async () => { ${code} })()`); source.postMessage( { type: 'code-result', id: message.id, payload: { success: true, result }, source: 'pinepaper-studio', } as BridgeMessage, origin ); } catch (error) { source.postMessage( { type: 'code-result', id: message.id, payload: { success: false, error: error instanceof Error ? error.message : 'Unknown error', }, source: 'pinepaper-studio', } as BridgeMessage, origin ); } } /** * Handle MCP tool request */ private async handleMCPRequest( message: BridgeMessage, source: Window, origin: string ): Promise<void> { const { tool, arguments: args } = message.payload as { tool: string; arguments: Record<string, unknown>; }; try { // Import and use the handler const { handleToolCall } = await import('../tools/handlers.js'); const result = await handleToolCall(tool, args); source.postMessage( { type: 'mcp-response', id: message.id, payload: { success: true, result }, source: 'pinepaper-studio', } as BridgeMessage, origin ); } catch (error) { source.postMessage( { type: 'mcp-response', id: message.id, payload: { success: false, error: error instanceof Error ? error.message : 'Unknown error', }, source: 'pinepaper-studio', } as BridgeMessage, origin ); } } /** * Clean up resources */ destroy(): void { window.removeEventListener('message', this.messageHandler); } } // ============================================================================= // FACTORY FUNCTIONS // ============================================================================= /** * Create a bridge to communicate with PinePaper Studio */ export function createBridge(config: BridgeConfig): PinePaperBridge { return new PinePaperBridge(config); } /** * Create a handler for PinePaper Studio side */ export function createStudioHandler(allowedOrigins?: string[]): PinePaperStudioHandler { return new PinePaperStudioHandler(allowedOrigins); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pinepaper/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server