1616
1717import { RelayConnection , debugLog } from './relayConnection' ;
1818
19+ type ConnectionState = {
20+ connection : RelayConnection ;
21+ connectedTabId : number ;
22+ playwrightTabIds : Set < number > ;
23+ mcpRelayUrl : string ;
24+ } ;
25+
1926type PageMessage = {
2027 type : 'connectToMCPRelay' ;
2128 mcpRelayUrl : string ;
@@ -30,13 +37,12 @@ type PageMessage = {
3037 type : 'getConnectionStatus' ;
3138} | {
3239 type : 'disconnect' ;
40+ mcpRelayUrl ?: string ;
3341} ;
3442
3543class TabShareExtension {
36- private _activeConnection : RelayConnection | undefined ;
37- private _connectedTabId : number | null = null ;
38- private _playwrightTabIds = new Set < number > ( ) ;
39- private _pendingTabSelection = new Map < number , { connection : RelayConnection , timerId ?: number } > ( ) ;
44+ private _connections = new Map < string , ConnectionState > ( ) ;
45+ private _pendingTabSelection = new Map < number , { connection : RelayConnection , mcpRelayUrl : string , timerId ?: number } > ( ) ;
4046
4147 constructor ( ) {
4248 chrome . tabs . onRemoved . addListener ( this . _onTabRemoved . bind ( this ) ) ;
@@ -68,12 +74,18 @@ class TabShareExtension {
6874 return true ; // Return true to indicate that the response will be sent asynchronously
6975 case 'getConnectionStatus' :
7076 sendResponse ( {
71- connectedTabId : this . _connectedTabId ,
72- playwrightTabIds : [ ...this . _playwrightTabIds ] ,
77+ connections : [ ...this . _connections . values ( ) ] . map ( s => ( {
78+ mcpRelayUrl : s . mcpRelayUrl ,
79+ connectedTabId : s . connectedTabId ,
80+ playwrightTabIds : [ ...s . playwrightTabIds ] ,
81+ } ) ) ,
82+ // Legacy fields for backward compat: first connection's tabId
83+ connectedTabId : [ ...this . _connections . values ( ) ] [ 0 ] ?. connectedTabId ?? null ,
84+ playwrightTabIds : [ ...this . _connections . values ( ) ] . flatMap ( s => [ ...s . playwrightTabIds ] ) ,
7385 } ) ;
7486 return false ;
7587 case 'disconnect' :
76- this . _disconnect ( ) . then (
88+ this . _disconnect ( message . mcpRelayUrl ) . then (
7789 ( ) => sendResponse ( { success : true } ) ,
7890 ( error : any ) => sendResponse ( { success : false , error : error . message } ) ) ;
7991 return true ;
@@ -97,7 +109,7 @@ class TabShareExtension {
97109 this . _pendingTabSelection . delete ( selectorTabId ) ;
98110 // TODO: show error in the selector tab?
99111 } ;
100- this . _pendingTabSelection . set ( selectorTabId , { connection } ) ;
112+ this . _pendingTabSelection . set ( selectorTabId , { connection, mcpRelayUrl } ) ;
101113 debugLog ( `Connected to MCP relay` ) ;
102114 } catch ( error : any ) {
103115 const message = `Failed to connect to MCP relay: ${ error . message } ` ;
@@ -109,58 +121,61 @@ class TabShareExtension {
109121 private async _connectTab ( selectorTabId : number , tabId : number , windowId : number , mcpRelayUrl : string ) : Promise < void > {
110122 try {
111123 debugLog ( `Connecting tab ${ tabId } to relay at ${ mcpRelayUrl } ` ) ;
112- try {
113- this . _activeConnection ?. close ( 'Another connection is requested' ) ;
114- } catch ( error : any ) {
115- debugLog ( `Error closing active connection:` , error ) ;
116- }
117- await this . _setConnectedTabId ( null ) ;
118124
119- this . _activeConnection = this . _pendingTabSelection . get ( selectorTabId ) ?. connection ;
120- if ( ! this . _activeConnection )
125+ const pending = this . _pendingTabSelection . get ( selectorTabId ) ;
126+ if ( ! pending )
121127 throw new Error ( 'No active MCP relay connection' ) ;
122128 this . _pendingTabSelection . delete ( selectorTabId ) ;
123129
124- this . _activeConnection . setTabId ( tabId ) ;
125- this . _activeConnection . onclose = ( ) => {
130+ const connection = pending . connection ;
131+ const relayUrl = pending . mcpRelayUrl ;
132+
133+ // Close any existing connection on the same relay URL.
134+ const existing = this . _connections . get ( relayUrl ) ;
135+ if ( existing ) {
136+ existing . connection . close ( 'Another connection is requested' ) ;
137+ this . _connections . delete ( relayUrl ) ;
138+ }
139+
140+ const state : ConnectionState = {
141+ connection,
142+ connectedTabId : tabId ,
143+ playwrightTabIds : new Set ( ) ,
144+ mcpRelayUrl : relayUrl ,
145+ } ;
146+ this . _connections . set ( relayUrl , state ) ;
147+
148+ connection . setTabId ( tabId ) ;
149+ connection . onclose = ( ) => {
126150 debugLog ( 'MCP connection closed' ) ;
127- this . _activeConnection = undefined ;
128- void this . _setConnectedTabId ( null ) ;
129- for ( const pwTabId of this . _playwrightTabIds )
151+ if ( this . _connections . get ( relayUrl ) ?. connection === connection )
152+ this . _connections . delete ( relayUrl ) ;
153+ void this . _updateBadge ( state . connectedTabId , { text : '' } ) ;
154+ for ( const pwTabId of state . playwrightTabIds )
130155 void this . _updateBadge ( pwTabId , { text : '' } ) ;
131- this . _playwrightTabIds . clear ( ) ;
156+ state . playwrightTabIds . clear ( ) ;
132157 } ;
133- this . _activeConnection . onPlaywrightTabCreated = ( pwTabId : number ) => {
134- this . _playwrightTabIds . add ( pwTabId ) ;
158+ connection . onPlaywrightTabCreated = ( pwTabId : number ) => {
159+ state . playwrightTabIds . add ( pwTabId ) ;
135160 void this . _updateBadge ( pwTabId , { text : '✓' , color : '#1976D2' , title : 'Playwright managed tab' } ) ;
136161 } ;
137- this . _activeConnection . onPlaywrightTabRemoved = ( pwTabId : number ) => {
138- this . _playwrightTabIds . delete ( pwTabId ) ;
162+ connection . onPlaywrightTabRemoved = ( pwTabId : number ) => {
163+ state . playwrightTabIds . delete ( pwTabId ) ;
139164 void this . _updateBadge ( pwTabId , { text : '' } ) ;
140165 } ;
141166
142167 await Promise . all ( [
143- this . _setConnectedTabId ( tabId ) ,
168+ this . _updateBadge ( tabId , { text : '✓' , color : '#4CAF50' , title : 'Connected to MCP client' } ) ,
144169 chrome . tabs . update ( tabId , { active : true } ) ,
145170 chrome . windows . update ( windowId , { focused : true } ) ,
146171 ] ) ;
147172 debugLog ( `Connected to MCP bridge` ) ;
148173 } catch ( error : any ) {
149- await this . _setConnectedTabId ( null ) ;
150174 debugLog ( `Failed to connect tab ${ tabId } :` , error . message ) ;
151175 throw error ;
152176 }
153177 }
154178
155- private async _setConnectedTabId ( tabId : number | null ) : Promise < void > {
156- const oldTabId = this . _connectedTabId ;
157- this . _connectedTabId = tabId ;
158- if ( oldTabId && oldTabId !== tabId )
159- await this . _updateBadge ( oldTabId , { text : '' } ) ;
160- if ( tabId )
161- await this . _updateBadge ( tabId , { text : '✓' , color : '#4CAF50' , title : 'Connected to MCP client' } ) ;
162- }
163-
164179 private async _updateBadge ( tabId : number , { text, color, title } : { text : string ; color ?: string , title ?: string } ) : Promise < void > {
165180 try {
166181 await chrome . action . setBadgeText ( { tabId, text } ) ;
@@ -173,21 +188,23 @@ class TabShareExtension {
173188 }
174189
175190 private async _onTabRemoved ( tabId : number ) : Promise < void > {
176- const pendingConnection = this . _pendingTabSelection . get ( tabId ) ?. connection ;
191+ const pendingConnection = [ ... this . _pendingTabSelection . entries ( ) ] . find ( ( [ k ] ) => k === tabId ) ?. [ 1 ] ;
177192 if ( pendingConnection ) {
178193 this . _pendingTabSelection . delete ( tabId ) ;
179- pendingConnection . close ( 'Browser tab closed' ) ;
194+ pendingConnection . connection . close ( 'Browser tab closed' ) ;
180195 return ;
181196 }
182- if ( this . _playwrightTabIds . has ( tabId ) ) {
183- this . _playwrightTabIds . delete ( tabId ) ;
184- return ;
197+ for ( const [ relayUrl , state ] of this . _connections ) {
198+ if ( state . playwrightTabIds . has ( tabId ) ) {
199+ state . playwrightTabIds . delete ( tabId ) ;
200+ return ;
201+ }
202+ if ( state . connectedTabId === tabId ) {
203+ state . connection . close ( 'Browser tab closed' ) ;
204+ this . _connections . delete ( relayUrl ) ;
205+ return ;
206+ }
185207 }
186- if ( this . _connectedTabId !== tabId )
187- return ;
188- this . _activeConnection ?. close ( 'Browser tab closed' ) ;
189- this . _activeConnection = undefined ;
190- this . _connectedTabId = null ;
191208 }
192209
193210 private _onTabActivated ( activeInfo : chrome . tabs . TabActiveInfo ) {
@@ -212,10 +229,12 @@ class TabShareExtension {
212229 }
213230
214231 private _onTabUpdated ( tabId : number , changeInfo : chrome . tabs . TabChangeInfo , tab : chrome . tabs . Tab ) {
215- if ( this . _connectedTabId === tabId )
216- void this . _setConnectedTabId ( tabId ) ;
217- if ( this . _playwrightTabIds . has ( tabId ) )
218- void this . _updateBadge ( tabId , { text : '✓' , color : '#1976D2' , title : 'Playwright managed tab' } ) ;
232+ for ( const state of this . _connections . values ( ) ) {
233+ if ( state . connectedTabId === tabId )
234+ void this . _updateBadge ( tabId , { text : '✓' , color : '#4CAF50' , title : 'Connected to MCP client' } ) ;
235+ if ( state . playwrightTabIds . has ( tabId ) )
236+ void this . _updateBadge ( tabId , { text : '✓' , color : '#1976D2' , title : 'Playwright managed tab' } ) ;
237+ }
219238 }
220239
221240 private async _getTabs ( ) : Promise < chrome . tabs . Tab [ ] > {
@@ -230,10 +249,18 @@ class TabShareExtension {
230249 } ) ;
231250 }
232251
233- private async _disconnect ( ) : Promise < void > {
234- this . _activeConnection ?. close ( 'User disconnected' ) ;
235- this . _activeConnection = undefined ;
236- await this . _setConnectedTabId ( null ) ;
252+ private async _disconnect ( mcpRelayUrl ?: string ) : Promise < void > {
253+ if ( mcpRelayUrl ) {
254+ const state = this . _connections . get ( mcpRelayUrl ) ;
255+ if ( state ) {
256+ state . connection . close ( 'User disconnected' ) ;
257+ this . _connections . delete ( mcpRelayUrl ) ;
258+ }
259+ } else {
260+ for ( const state of this . _connections . values ( ) )
261+ state . connection . close ( 'User disconnected' ) ;
262+ this . _connections . clear ( ) ;
263+ }
237264 }
238265}
239266
0 commit comments