Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { ConnectedTabGroup, cleanupStalePlaywrightGroups, isNonDebuggableUrl } f
type PageMessage = {
type: 'connectionRequested';
mcpRelayUrl: string;
protocolVersion: number;
} | {
type: 'getTabs';
} | {
Expand Down Expand Up @@ -56,10 +55,9 @@ class PlaywrightExtension {
private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
switch (message.type) {
case 'connectionRequested':
this._pendingConnections.create(sender.tab!.id!, message.mcpRelayUrl, message.protocolVersion).then(
() => sendResponse({ success: true }),
(error: any) => sendResponse({ success: false, error: error.message }));
return true;
this._pendingConnections.create(sender.tab!.id!, message.mcpRelayUrl);
sendResponse({ success: true });
return false;
case 'getTabs':
this._getTabs().then(
tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
Expand Down
81 changes: 11 additions & 70 deletions packages/extension/src/pendingConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,96 +16,37 @@

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

interface PendingEntry {
connect(): Promise<RelayConnection>;
close(reason: string): void;
}

class EagerPending implements PendingEntry {
private _connection: RelayConnection;
onclose?: () => void;

static async create(mcpRelayUrl: string, protocolVersion: number): Promise<EagerPending> {
const connection = await openRelayConnection(mcpRelayUrl, protocolVersion);
return new EagerPending(connection);
}

private constructor(connection: RelayConnection) {
this._connection = connection;
this._connection.onclose = () => this.onclose?.();
}

async connect(): Promise<RelayConnection> {
return this._connection;
}

close(reason: string): void {
this._connection.close(reason);
}
}

class DeferredPending implements PendingEntry {
constructor(private _mcpRelayUrl: string, private _protocolVersion: number) {}

async connect(): Promise<RelayConnection> {
return openRelayConnection(this._mcpRelayUrl, this._protocolVersion);
}

close(_reason: string): void {
}
}

// Relay URLs recorded by `connectionRequested`, keyed by the connect page tab
// id. The relay WebSocket opens lazily in `take` once the user clicks Allow.
export class PendingConnections {
private _map = new Map<number, PendingEntry>();
private _map = new Map<number, string>();

constructor() {
chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));
chrome.tabs.onRemoved.addListener(tabId => this._map.delete(tabId));
}

// v1 opens the relay WS eagerly — the daemon expects a prompt connection.
// v2 records only the descriptor; the WS opens lazily in `take` once the
// user clicks Allow.
async create(selectorTabId: number, mcpRelayUrl: string, protocolVersion: number): Promise<void> {
if (protocolVersion !== 1) {
this._map.set(selectorTabId, new DeferredPending(mcpRelayUrl, protocolVersion));
return;
}
const entry = await EagerPending.create(mcpRelayUrl, protocolVersion);
entry.onclose = () => {
if (this._map.get(selectorTabId) !== entry)
return;
this._map.delete(selectorTabId);
chrome.tabs.sendMessage(selectorTabId, { type: 'pendingConnectionClosed' }).catch(() => {});
};
this._map.set(selectorTabId, entry);
create(selectorTabId: number, mcpRelayUrl: string): void {
this._map.set(selectorTabId, mcpRelayUrl);
}

async take(selectorTabId: number): Promise<RelayConnection | undefined> {
const entry = this._map.get(selectorTabId);
if (!entry)
const mcpRelayUrl = this._map.get(selectorTabId);
if (mcpRelayUrl === undefined)
return undefined;
this._map.delete(selectorTabId);
return entry.connect();
}

private _onTabRemoved(tabId: number): void {
const entry = this._map.get(tabId);
if (!entry)
return;
this._map.delete(tabId);
entry.close('Browser tab closed');
return openRelayConnection(mcpRelayUrl);
}
}

async function openRelayConnection(mcpRelayUrl: string, protocolVersion: number): Promise<RelayConnection> {
async function openRelayConnection(mcpRelayUrl: string): Promise<RelayConnection> {
try {
const socket = new WebSocket(mcpRelayUrl);
await new Promise<void>((resolve, reject) => {
socket.onopen = () => resolve();
socket.onerror = () => reject(new Error('WebSocket error'));
setTimeout(() => reject(new Error('Connection timeout')), 5000);
});
return new RelayConnection(socket, protocolVersion);
return new RelayConnection(socket);
} catch (error: any) {
const message = `Failed to connect to MCP relay: ${error.message}`;
debugLog(message);
Expand Down
203 changes: 0 additions & 203 deletions packages/extension/src/protocolHandlers.ts

This file was deleted.

Loading
Loading