chore: remove extension code (#667)

This commit is contained in:
Pavel Feldman
2025-07-14 10:52:38 -07:00
committed by GitHub
parent 7fca8f50f8
commit 128474b4aa
28 changed files with 8 additions and 1408 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,40 +0,0 @@
{
"manifest_version": 3,
"name": "Playwright MCP Bridge",
"version": "1.0.0",
"description": "Share browser tabs with Playwright MCP server through CDP bridge",
"permissions": [
"debugger",
"activeTab",
"tabs",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "lib/background.js",
"type": "module"
},
"action": {
"default_title": "Share tab with Playwright MCP",
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}

View File

@@ -1,173 +0,0 @@
<!--
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
width: 320px;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
margin: 0;
}
.header {
margin-bottom: 16px;
text-align: center;
}
.header h3 {
margin: 0 0 8px 0;
color: #333;
}
.section {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #555;
}
input[type="url"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
input[type="url"]:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.button {
background: #4CAF50;
color: white;
border: none;
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
width: 100%;
margin-top: 8px;
}
.button:hover {
background: #45a049;
}
.button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.button.disconnect {
background: #f44336;
}
.button.disconnect:hover {
background: #da190b;
}
.status {
padding: 12px;
border-radius: 4px;
margin-bottom: 16px;
text-align: center;
}
.status.connected {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #4caf50;
}
.status.error {
background: #ffebee;
color: #c62828;
border: 1px solid #f44336;
}
.status.warning {
background: #fff3e0;
color: #ef6c00;
border: 1px solid #ff9800;
}
.tab-info {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
margin-bottom: 16px;
}
.tab-title {
font-weight: 500;
margin-bottom: 4px;
color: #333;
}
.tab-url {
font-size: 12px;
color: #666;
word-break: break-all;
}
.focus-button {
background: #2196F3;
margin-top: 8px;
}
.focus-button:hover {
background: #1976D2;
}
.small-text {
font-size: 12px;
color: #666;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="header">
<h3>Playwright MCP Bridge</h3>
</div>
<div id="status-container"></div>
<div class="section">
<label for="bridge-url">Bridge Server URL:</label>
<input type="url" id="bridge-url" disabled placeholder="ws://localhost:9223/extension" />
<div class="small-text">Enter the WebSocket URL of your MCP bridge server</div>
</div>
<div id="action-container">
<button id="connect-btn" class="button">Share This Tab</button>
</div>
<script src="lib/popup.js"></script>
</body>
</html>

View File

@@ -1,199 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RelayConnection, debugLog } from './relayConnection.js';
/**
* Simple Chrome Extension that pumps CDP messages between chrome.debugger and WebSocket
*/
type PopupMessage = {
type: 'getStatus' | 'connect' | 'disconnect';
tabId: number;
bridgeUrl?: string;
};
type SendResponse = (response: any) => void;
class TabShareExtension {
private activeConnections: Map<number, RelayConnection>;
constructor() {
this.activeConnections = new Map(); // tabId -> connection
// Remove page action click handler since we now use popup
chrome.tabs.onRemoved.addListener(this.onTabRemoved.bind(this));
// Handle messages from popup
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
}
/**
* Handle messages from popup
*/
onMessage(message: PopupMessage, sender: chrome.runtime.MessageSender, sendResponse: SendResponse): boolean {
switch (message.type) {
case 'getStatus':
this.getStatus(message.tabId, sendResponse);
return true; // Will respond asynchronously
case 'connect':
this.connectTab(message.tabId, message.bridgeUrl!).then(
() => sendResponse({ success: true }),
(error: Error) => sendResponse({ success: false, error: error.message })
);
return true; // Will respond asynchronously
case 'disconnect':
this.disconnectTab(message.tabId).then(
() => sendResponse({ success: true }),
(error: Error) => sendResponse({ success: false, error: error.message })
);
return true; // Will respond asynchronously
}
return false;
}
/**
* Get connection status for popup
*/
getStatus(requestedTabId: number, sendResponse: SendResponse): void {
const isConnected = this.activeConnections.size > 0;
let activeTabId: number | null = null;
if (isConnected) {
const [tabId] = this.activeConnections.entries().next().value as [number, RelayConnection];
activeTabId = tabId;
// Get tab info
chrome.tabs.get(tabId, tab => {
if (chrome.runtime.lastError) {
sendResponse({
isConnected: false,
error: 'Active tab not found'
});
} else {
sendResponse({
isConnected: true,
activeTabId,
activeTabInfo: {
title: tab.title,
url: tab.url
}
});
}
});
} else {
sendResponse({
isConnected: false,
activeTabId: null,
activeTabInfo: null
});
}
}
/**
* Connect a tab to the bridge server
*/
async connectTab(tabId: number, bridgeUrl: string): Promise<void> {
try {
debugLog(`Connecting tab ${tabId} to bridge at ${bridgeUrl}`);
// Connect to bridge server
const socket = new WebSocket(bridgeUrl);
await new Promise<void>((resolve, reject) => {
socket.onopen = () => resolve();
socket.onerror = () => reject(new Error('WebSocket error'));
setTimeout(() => reject(new Error('Connection timeout')), 5000);
});
const info = this._createConnection(tabId, socket);
// Store connection
this.activeConnections.set(tabId, info);
await this._updateUI(tabId, { text: '●', color: '#4CAF50', title: 'Disconnect from Playwright MCP' });
debugLog(`Tab ${tabId} connected successfully`);
} catch (error: any) {
debugLog(`Failed to connect tab ${tabId}:`, error.message);
await this._cleanupConnection(tabId);
// Show error to user
await this._updateUI(tabId, { text: '!', color: '#F44336', title: `Connection failed: ${error.message}` });
throw error;
}
}
private async _updateUI(tabId: number, { text, color, title }: { text: string; color: string | null; title: string }): Promise<void> {
await chrome.action.setBadgeText({ tabId, text });
if (color)
await chrome.action.setBadgeBackgroundColor({ tabId, color });
await chrome.action.setTitle({ tabId, title });
}
private _createConnection(tabId: number, socket: WebSocket): RelayConnection {
const connection = new RelayConnection(tabId, socket);
socket.onclose = () => {
debugLog(`WebSocket closed for tab ${tabId}`);
void this.disconnectTab(tabId);
};
socket.onerror = error => {
debugLog(`WebSocket error for tab ${tabId}:`, error);
void this.disconnectTab(tabId);
};
return connection;
}
/**
* Disconnect a tab from the bridge
*/
async disconnectTab(tabId: number): Promise<void> {
await this._cleanupConnection(tabId);
await this._updateUI(tabId, { text: '', color: null, title: 'Share tab with Playwright MCP' });
debugLog(`Tab ${tabId} disconnected`);
}
/**
* Clean up connection resources
*/
async _cleanupConnection(tabId: number): Promise<void> {
const connection = this.activeConnections.get(tabId);
if (!connection)
return;
this.activeConnections.delete(tabId);
// Close WebSocket
connection.close();
// Detach debugger
try {
await connection.detachDebugger();
} catch (error) {
// Ignore detach errors - might already be detached
debugLog('Error while detaching debugger:', error);
}
}
/**
* Handle tab removal
*/
async onTabRemoved(tabId: number): Promise<void> {
if (this.activeConnections.has(tabId))
await this._cleanupConnection(tabId);
}
}
new TabShareExtension();

View File

@@ -1,243 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PopupController {
private currentTab: chrome.tabs.Tab | null;
private readonly bridgeUrlInput: HTMLInputElement;
private readonly connectBtn: HTMLButtonElement;
private readonly statusContainer: HTMLElement;
private readonly actionContainer: HTMLElement;
constructor() {
this.currentTab = null;
this.bridgeUrlInput = document.getElementById('bridge-url') as HTMLInputElement;
this.connectBtn = document.getElementById('connect-btn') as HTMLButtonElement;
this.statusContainer = document.getElementById('status-container') as HTMLElement;
this.actionContainer = document.getElementById('action-container') as HTMLElement;
void this.init();
}
async init(): Promise<void> {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
this.currentTab = tab;
// Load saved bridge URL
const result = await chrome.storage.sync.get(['bridgeUrl']);
const savedUrl = result.bridgeUrl || 'ws://localhost:9223/extension';
if (this.bridgeUrlInput) {
this.bridgeUrlInput.value = savedUrl;
this.bridgeUrlInput.disabled = false;
}
// Set up event listeners
if (this.bridgeUrlInput)
this.bridgeUrlInput.addEventListener('input', this.onUrlChange.bind(this));
if (this.connectBtn)
this.connectBtn.addEventListener('click', this.onConnectClick.bind(this));
// Update UI based on current state
await this.updateUI();
}
async updateUI(): Promise<void> {
if (!this.currentTab?.id)
return;
// Get connection status from background script
const response = await chrome.runtime.sendMessage({
type: 'getStatus',
tabId: this.currentTab.id
});
const { isConnected, activeTabId, activeTabInfo, error } = response as {
isConnected: boolean;
activeTabId: number | undefined;
activeTabInfo?: { title?: string; url?: string };
error?: string;
};
if (!this.statusContainer || !this.actionContainer)
return;
this.statusContainer.innerHTML = '';
this.actionContainer.innerHTML = '';
if (error) {
this.showStatus('error', `Error: ${error}`);
this.showConnectButton();
} else if (isConnected && activeTabId === this.currentTab.id) {
// Current tab is connected
this.showStatus('connected', 'This tab is currently shared with MCP server');
this.showDisconnectButton();
} else if (isConnected && activeTabId !== this.currentTab.id) {
// Another tab is connected
this.showStatus('warning', 'Another tab is already sharing the CDP session');
this.showActiveTabInfo(activeTabInfo);
this.showFocusButton(activeTabId);
} else {
// No connection
this.showConnectButton();
}
}
showStatus(type: string, message: string): void {
if (!this.statusContainer)
return;
const statusDiv = document.createElement('div');
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
this.statusContainer.appendChild(statusDiv);
}
showConnectButton(): void {
if (!this.actionContainer)
return;
this.actionContainer.innerHTML = `
<button id="connect-btn" class="button">Share This Tab</button>
`;
const connectBtn = document.getElementById('connect-btn') as HTMLButtonElement | null;
if (connectBtn) {
connectBtn.addEventListener('click', this.onConnectClick.bind(this));
// Disable if URL is invalid
const isValidUrl = this.bridgeUrlInput ? this.isValidWebSocketUrl(this.bridgeUrlInput.value) : false;
connectBtn.disabled = !isValidUrl;
}
}
showDisconnectButton(): void {
if (!this.actionContainer)
return;
this.actionContainer.innerHTML = `
<button id="disconnect-btn" class="button disconnect">Stop Sharing</button>
`;
const disconnectBtn = document.getElementById('disconnect-btn') as HTMLButtonElement | null;
if (disconnectBtn)
disconnectBtn.addEventListener('click', this.onDisconnectClick.bind(this));
}
showActiveTabInfo(tabInfo?: { title?: string; url?: string }): void {
if (!tabInfo || !this.statusContainer)
return;
const tabDiv = document.createElement('div');
tabDiv.className = 'tab-info';
tabDiv.innerHTML = `
<div class="tab-title">${tabInfo.title || 'Unknown Tab'}</div>
<div class="tab-url">${tabInfo.url || ''}</div>
`;
this.statusContainer.appendChild(tabDiv);
}
showFocusButton(activeTabId?: number): void {
if (!this.actionContainer)
return;
this.actionContainer.innerHTML = `
<button id="focus-btn" class="button focus-button">Switch to Shared Tab</button>
`;
const focusBtn = document.getElementById('focus-btn') as HTMLButtonElement | null;
if (focusBtn && activeTabId !== undefined)
focusBtn.addEventListener('click', () => this.onFocusClick(activeTabId));
}
onUrlChange(): void {
if (!this.bridgeUrlInput)
return;
const isValid = this.isValidWebSocketUrl(this.bridgeUrlInput.value);
const connectBtn = document.getElementById('connect-btn') as HTMLButtonElement | null;
if (connectBtn)
connectBtn.disabled = !isValid;
// Save URL to storage
if (isValid)
void chrome.storage.sync.set({ bridgeUrl: this.bridgeUrlInput.value });
}
async onConnectClick(): Promise<void> {
if (!this.bridgeUrlInput || !this.currentTab?.id)
return;
const url = this.bridgeUrlInput.value.trim();
if (!this.isValidWebSocketUrl(url)) {
this.showStatus('error', 'Please enter a valid WebSocket URL');
return;
}
// Save URL to storage
await chrome.storage.sync.set({ bridgeUrl: url });
// Send connect message to background script
const response = await chrome.runtime.sendMessage({
type: 'connect',
tabId: this.currentTab.id,
bridgeUrl: url
});
if (response.success)
await this.updateUI();
else
this.showStatus('error', response.error || 'Failed to connect');
}
async onDisconnectClick(): Promise<void> {
if (!this.currentTab?.id)
return;
const response = await chrome.runtime.sendMessage({
type: 'disconnect',
tabId: this.currentTab.id
});
if (response.success)
await this.updateUI();
else
this.showStatus('error', response.error || 'Failed to disconnect');
}
async onFocusClick(activeTabId: number): Promise<void> {
try {
await chrome.tabs.update(activeTabId, { active: true });
window.close(); // Close popup after switching
} catch (error) {
this.showStatus('error', 'Failed to switch to tab');
}
}
isValidWebSocketUrl(url: string): boolean {
if (!url)
return false;
try {
const parsed = new URL(url);
return parsed.protocol === 'ws:' || parsed.protocol === 'wss:';
} catch {
return false;
}
}
}
// Initialize popup when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new PopupController();
});

View File

@@ -1,166 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function debugLog(...args: unknown[]): void {
const enabled = true;
if (enabled) {
// eslint-disable-next-line no-console
console.log('[Extension]', ...args);
}
}
type ProtocolCommand = {
id: number;
method: string;
params?: any;
};
type ProtocolResponse = {
id?: number;
method?: string;
params?: any;
result?: any;
error?: string;
};
export class RelayConnection {
private _debuggee: chrome.debugger.Debuggee;
private _rootSessionId: string;
private _ws: WebSocket;
private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void;
private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void;
constructor(tabId: number, ws: WebSocket) {
this._debuggee = { tabId };
this._rootSessionId = `pw-tab-${tabId}`;
this._ws = ws;
this._ws.onmessage = this._onMessage.bind(this);
// Store listeners for cleanup
this._eventListener = this._onDebuggerEvent.bind(this);
this._detachListener = this._onDebuggerDetach.bind(this);
chrome.debugger.onEvent.addListener(this._eventListener);
chrome.debugger.onDetach.addListener(this._detachListener);
}
close(message?: string): void {
chrome.debugger.onEvent.removeListener(this._eventListener);
chrome.debugger.onDetach.removeListener(this._detachListener);
this._ws.close(1000, message || 'Connection closed');
}
async detachDebugger(): Promise<void> {
await chrome.debugger.detach(this._debuggee);
}
private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void {
if (source.tabId !== this._debuggee.tabId)
return;
debugLog('Forwarding CDP event:', method, params);
const sessionId = source.sessionId || this._rootSessionId;
this._sendMessage({
method: 'forwardCDPEvent',
params: {
sessionId,
method,
params,
},
});
}
private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void {
if (source.tabId !== this._debuggee.tabId)
return;
this._sendMessage({
method: 'detachedFromTab',
params: {
tabId: this._debuggee.tabId,
reason,
},
});
}
private _onMessage(event: MessageEvent): void {
this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e));
}
private async _onMessageAsync(event: MessageEvent): Promise<void> {
let message: ProtocolCommand;
try {
message = JSON.parse(event.data);
} catch (error: any) {
debugLog('Error parsing message:', error);
this._sendError(-32700, `Error parsing message: ${error.message}`);
return;
}
debugLog('Received message:', message);
const response: ProtocolResponse = {
id: message.id,
};
try {
response.result = await this._handleCommand(message);
} catch (error: any) {
debugLog('Error handling command:', error);
response.error = error.message;
}
debugLog('Sending response:', response);
this._sendMessage(response);
}
private async _handleCommand(message: ProtocolCommand): Promise<any> {
if (message.method === 'attachToTab') {
debugLog('Attaching debugger to tab:', this._debuggee);
await chrome.debugger.attach(this._debuggee, '1.3');
const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo');
return {
sessionId: this._rootSessionId,
targetInfo: result?.targetInfo,
};
}
if (message.method === 'detachFromTab') {
debugLog('Detaching debugger from tab:', this._debuggee);
return await this.detachDebugger();
}
if (message.method === 'forwardCDPCommand') {
const { sessionId, method, params } = message.params;
debugLog('CDP command:', method, params);
const debuggerSession: chrome.debugger.DebuggerSession = { ...this._debuggee };
// Pass session id, unless it's the root session.
if (sessionId && sessionId !== this._rootSessionId)
debuggerSession.sessionId = sessionId;
// Forward CDP command to chrome.debugger
return await chrome.debugger.sendCommand(
debuggerSession,
method,
params
);
}
}
private _sendError(code: number, message: string): void {
this._sendMessage({
error: {
code,
message,
},
});
}
private _sendMessage(message: any): void {
this._ws.send(JSON.stringify(message));
}
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"strict": true,
"module": "ESNext",
"rootDir": "src",
"outDir": "./lib",
"resolveJsonModule": true,
},
"include": [
"src",
],
}