@@ -38,8 +38,8 @@ export interface McpResponseMessage {
3838 status : number ;
3939 statusText : string ;
4040 headers : Record < string , string > ;
41- /** Transferred across `postMessage` so streaming ( SSE) bodies don't get buffered . */
42- body : ReadableStream < Uint8Array > | null ;
41+ /** `ReadableStream` on Chromium (streams SSE); buffered `ArrayBuffer` on browsers without transferable streams . */
42+ body : ReadableStream < Uint8Array > | ArrayBuffer | null ;
4343}
4444
4545/** Carries the SW↔Worker `MessagePort` transfer that wires MCP traffic over a dedicated channel. */
@@ -55,6 +55,17 @@ export function isMcpInitPortMessage(value: unknown): value is McpInitPortMessag
5555 return Object ( value ) === value && ( value as { type ?: unknown } ) . type === MCP_INIT_PORT_MESSAGE_TYPE ;
5656}
5757
58+ /** Probes whether the current realm can transfer `ReadableStream` via `postMessage` (Chromium yes; Firefox / Safari no). */
59+ export function canTransferReadableStream ( ) : boolean {
60+ try {
61+ const probe = new ReadableStream ( ) ;
62+ structuredClone ( probe , { transfer : [ probe ] } ) ;
63+ return true ;
64+ } catch {
65+ return false ;
66+ }
67+ }
68+
5869/** Minimal Worker scope contract — defined locally so the package compiles without the `webworker` lib. */
5970export interface McpBridgeScope {
6071 addEventListener ( type : 'message' , listener : ( event : MessageEvent ) => void ) : void ;
@@ -73,6 +84,8 @@ export class McpWorkerBridge implements Disposable {
7384 protected readonly listener = ( event : MessageEvent ) : void => this . onMessage ( event ) ;
7485 /** Serialise dispatches — `BrowserMcpRequestContext` is a single-slot store, not concurrency-safe. */
7586 protected dispatchChain : Promise < void > = Promise . resolve ( ) ;
87+ protected readonly streamTransferable : boolean = canTransferReadableStream ( ) ;
88+ protected bufferingWarned = false ;
7689
7790 constructor ( protected readonly scope : McpBridgeScope = self as unknown as McpBridgeScope ) {
7891 this . launcherReady = new Promise < AbstractMcpServerLauncher > ( resolve => {
@@ -146,7 +159,7 @@ export class McpWorkerBridge implements Disposable {
146159 status : response . status ,
147160 statusText : response . statusText ,
148161 headers,
149- body : response . body
162+ body : await this . encodeBody ( response )
150163 } ) ;
151164 } catch ( err : unknown ) {
152165 const message = err instanceof Error ? err . message : String ( err ) ;
@@ -156,13 +169,28 @@ export class McpWorkerBridge implements Disposable {
156169 status : 500 ,
157170 statusText : 'Internal Server Error' ,
158171 headers : { 'content-type' : 'text/plain' } ,
159- body : new Response ( `MCP worker bridge error: ${ message } ` ) . body
172+ body : await this . encodeBody ( new Response ( `MCP worker bridge error: ${ message } ` ) )
160173 } ) ;
161174 }
162175 }
163176
177+ /** Stream on Chromium; buffer to `ArrayBuffer` (still transferable) on browsers without transferable streams. */
178+ protected async encodeBody ( response : Response ) : Promise < ReadableStream < Uint8Array > | ArrayBuffer | null > {
179+ if ( this . streamTransferable ) {
180+ return response . body ;
181+ }
182+ if ( ! this . bufferingWarned ) {
183+ this . bufferingWarned = true ;
184+ console . warn (
185+ 'McpWorkerBridge: ReadableStream is not transferable in this browser — buffering MCP response bodies. ' +
186+ 'Chunked / SSE streaming will deliver as a single chunk; non-streaming MCP calls are unaffected.'
187+ ) ;
188+ }
189+ return response . arrayBuffer ( ) ;
190+ }
191+
164192 protected reply ( port : MessagePort | undefined , message : McpResponseMessage ) : void {
165- const transfer = message . body ? [ message . body ] : [ ] ;
193+ const transfer : Transferable [ ] = message . body ? [ message . body ] : [ ] ;
166194 if ( port ) {
167195 port . postMessage ( message , transfer ) ;
168196 } else {
0 commit comments