Skip to content

Commit 10c815f

Browse files
committed
feat(extension): multi-instance support - each relay connection is isolated
- background.ts: replace single _activeConnection with _connections Map keyed by relay URL - Each relay URL (UUID-based) gets its own ConnectionState with its own tab set - Multiple simultaneous MCP instances no longer conflict - status.html shows all active connections, each with their own tab lists - disconnect accepts optional mcpRelayUrl to target specific instance - getConnectionStatus returns full connections array (with legacy compat fields) - Bump version to 0.0.68.3
1 parent 6a36dd2 commit 10c815f

2 files changed

Lines changed: 137 additions & 109 deletions

File tree

packages/extension/src/background.ts

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616

1717
import { RelayConnection, debugLog } from './relayConnection';
1818

19+
type ConnectionState = {
20+
connection: RelayConnection;
21+
connectedTabId: number;
22+
playwrightTabIds: Set<number>;
23+
mcpRelayUrl: string;
24+
};
25+
1926
type 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

3543
class 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

packages/extension/src/ui/status.tsx

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,23 @@ import { Button, TabItem } from './tabItem';
2121
import type { TabInfo } from './tabItem';
2222
import { AuthTokenSection } from './authToken';
2323

24-
interface ConnectionStatus {
25-
isConnected: boolean;
26-
connectedTabId: number | null;
24+
type ConnectionInfo = {
25+
mcpRelayUrl: string;
26+
connectedTabId: number;
27+
playwrightTabIds: number[];
2728
connectedTab?: TabInfo;
2829
playwrightTabs: TabInfo[];
29-
}
30+
};
3031

3132
const StatusApp: React.FC = () => {
32-
const [status, setStatus] = useState<ConnectionStatus>({
33-
isConnected: false,
34-
connectedTabId: null,
35-
playwrightTabs: [],
36-
});
33+
const [connections, setConnections] = useState<ConnectionInfo[]>([]);
3734

3835
useEffect(() => {
3936
void loadStatus();
4037
}, []);
4138

4239
const loadStatus = async () => {
43-
const { connectedTabId, playwrightTabIds = [] } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
40+
const { connections: rawConnections = [] } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
4441

4542
const fetchTab = async (id: number): Promise<TabInfo | null> => {
4643
try {
@@ -51,68 +48,72 @@ const StatusApp: React.FC = () => {
5148
}
5249
};
5350

54-
const connectedTab = connectedTabId ? await fetchTab(connectedTabId) ?? undefined : undefined;
55-
const playwrightTabs = (await Promise.all((playwrightTabIds as number[]).map(fetchTab))).filter((t): t is TabInfo => t !== null);
56-
57-
setStatus({
58-
isConnected: !!connectedTabId,
59-
connectedTabId,
60-
connectedTab,
61-
playwrightTabs,
62-
});
51+
const resolved: ConnectionInfo[] = await Promise.all(
52+
rawConnections.map(async (c: { mcpRelayUrl: string, connectedTabId: number, playwrightTabIds: number[] }) => {
53+
const connectedTab = await fetchTab(c.connectedTabId) ?? undefined;
54+
const playwrightTabs = (await Promise.all(c.playwrightTabIds.map(fetchTab))).filter((t): t is TabInfo => t !== null);
55+
return { ...c, connectedTab, playwrightTabs };
56+
})
57+
);
58+
setConnections(resolved);
6359
};
6460

6561
const openTab = async (tabId: number) => {
6662
await chrome.tabs.update(tabId, { active: true });
6763
window.close();
6864
};
6965

70-
const disconnect = async () => {
71-
await chrome.runtime.sendMessage({ type: 'disconnect' });
72-
window.close();
66+
const disconnect = async (mcpRelayUrl: string) => {
67+
await chrome.runtime.sendMessage({ type: 'disconnect', mcpRelayUrl });
68+
void loadStatus();
7369
};
7470

7571
return (
7672
<div className='app-container'>
7773
<div className='content-wrapper'>
78-
{status.isConnected && status.connectedTab ? (
79-
<div>
80-
<div className='tab-section-title'>
81-
Page with connected MCP client:
82-
</div>
83-
<div>
84-
<TabItem
85-
tab={status.connectedTab}
86-
button={
87-
<Button variant='primary' onClick={disconnect}>
88-
Disconnect
89-
</Button>
90-
}
91-
onClick={() => openTab(status.connectedTabId!)}
92-
/>
93-
</div>
94-
</div>
95-
) : (
74+
{connections.length === 0 ? (
9675
<div className='status-banner'>
9776
No MCP clients are currently connected.
9877
</div>
99-
)}
100-
{status.playwrightTabs.length > 0 && (
101-
<div>
102-
<div className='tab-section-title'>
103-
Playwright managed tabs:
104-
</div>
105-
<div>
106-
{status.playwrightTabs.map(tab => (
78+
) : connections.map((c, i) => (
79+
<div key={c.mcpRelayUrl}>
80+
{connections.length > 1 && (
81+
<div className='tab-section-title'>
82+
Instance {i + 1}:
83+
</div>
84+
)}
85+
{c.connectedTab && (
86+
<div>
87+
<div className='tab-section-title'>
88+
Page with connected MCP client:
89+
</div>
10790
<TabItem
108-
key={tab.id}
109-
tab={tab}
110-
onClick={() => openTab(tab.id)}
91+
tab={c.connectedTab}
92+
button={
93+
<Button variant='primary' onClick={() => disconnect(c.mcpRelayUrl)}>
94+
Disconnect
95+
</Button>
96+
}
97+
onClick={() => openTab(c.connectedTabId)}
11198
/>
112-
))}
113-
</div>
99+
</div>
100+
)}
101+
{c.playwrightTabs.length > 0 && (
102+
<div>
103+
<div className='tab-section-title'>
104+
Playwright managed tabs:
105+
</div>
106+
{c.playwrightTabs.map(tab => (
107+
<TabItem
108+
key={tab.id}
109+
tab={tab}
110+
onClick={() => openTab(tab.id)}
111+
/>
112+
))}
113+
</div>
114+
)}
114115
</div>
115-
)}
116+
))}
116117
<AuthTokenSection />
117118
</div>
118119
</div>

0 commit comments

Comments
 (0)