mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
chore(extension): page selector for MCP (#750)
This commit is contained in:
parent
c72d0320f4
commit
bd34e9d7e9
@ -19,14 +19,16 @@
|
||||
<title>Playwright MCP extension</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h3>Playwright MCP extension</h3>
|
||||
</div>
|
||||
<h3>Playwright MCP extension</h3>
|
||||
<div id="status-container"></div>
|
||||
<div class="button-row">
|
||||
<button id="continue-btn">Continue</button>
|
||||
<button id="reject-btn">Reject</button>
|
||||
</div>
|
||||
<div id="tab-list-container">
|
||||
<h4>Select page to expose to MCP server:</h4>
|
||||
<div id="tab-list"></div>
|
||||
</div>
|
||||
<script src="lib/connect.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -19,6 +19,10 @@ import { RelayConnection, debugLog } from './relayConnection.js';
|
||||
type PageMessage = {
|
||||
type: 'connectToMCPRelay';
|
||||
mcpRelayUrl: string;
|
||||
tabId: number;
|
||||
windowId: number;
|
||||
} | {
|
||||
type: 'getTabs';
|
||||
};
|
||||
|
||||
class TabShareExtension {
|
||||
@ -35,22 +39,22 @@ class TabShareExtension {
|
||||
private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
|
||||
switch (message.type) {
|
||||
case 'connectToMCPRelay':
|
||||
const tabId = sender.tab?.id;
|
||||
if (!tabId) {
|
||||
sendResponse({ success: false, error: 'No tab id' });
|
||||
return true;
|
||||
}
|
||||
this._connectTab(sender.tab!, message.mcpRelayUrl!).then(
|
||||
this._connectTab(message.tabId, message.windowId, message.mcpRelayUrl!).then(
|
||||
() => sendResponse({ success: true }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true; // Return true to indicate that the response will be sent asynchronously
|
||||
case 'getTabs':
|
||||
this._getTabs().then(
|
||||
tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _connectTab(tab: chrome.tabs.Tab, mcpRelayUrl: string): Promise<void> {
|
||||
private async _connectTab(tabId: number, windowId: number, mcpRelayUrl: string): Promise<void> {
|
||||
try {
|
||||
debugLog(`Connecting tab ${tab.id} to bridge at ${mcpRelayUrl}`);
|
||||
debugLog(`Connecting tab ${tabId} to bridge at ${mcpRelayUrl}`);
|
||||
const socket = new WebSocket(mcpRelayUrl);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.onopen = () => resolve();
|
||||
@ -58,7 +62,7 @@ class TabShareExtension {
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 5000);
|
||||
});
|
||||
|
||||
const connection = new RelayConnection(socket, tab.id!);
|
||||
const connection = new RelayConnection(socket, tabId);
|
||||
const connectionClosed = (m: string) => {
|
||||
debugLog(m);
|
||||
if (this._activeConnection === connection) {
|
||||
@ -71,14 +75,14 @@ class TabShareExtension {
|
||||
this._activeConnection = connection;
|
||||
|
||||
await Promise.all([
|
||||
this._setConnectedTabId(tab.id!),
|
||||
chrome.tabs.update(tab.id!, { active: true }),
|
||||
chrome.windows.update(tab.windowId, { focused: true }),
|
||||
this._setConnectedTabId(tabId),
|
||||
chrome.tabs.update(tabId, { active: true }),
|
||||
chrome.windows.update(windowId, { focused: true }),
|
||||
]);
|
||||
debugLog(`Connected to MCP bridge`);
|
||||
} catch (error: any) {
|
||||
await this._setConnectedTabId(null);
|
||||
debugLog(`Failed to connect tab ${tab.id}:`, error.message);
|
||||
debugLog(`Failed to connect tab ${tabId}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -110,6 +114,11 @@ class TabShareExtension {
|
||||
if (changeInfo.status === 'complete' && this._connectedTabId === tabId)
|
||||
await this._setConnectedTabId(tabId);
|
||||
}
|
||||
|
||||
private async _getTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
return tabs;
|
||||
}
|
||||
}
|
||||
|
||||
new TabShareExtension();
|
||||
|
@ -14,57 +14,158 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const statusContainer = document.getElementById('status-container') as HTMLElement;
|
||||
const continueBtn = document.getElementById('continue-btn') as HTMLButtonElement;
|
||||
const rejectBtn = document.getElementById('reject-btn') as HTMLButtonElement;
|
||||
const buttonRow = document.querySelector('.button-row') as HTMLElement;
|
||||
interface TabInfo {
|
||||
id: number;
|
||||
windowId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
favIconUrl?: string;
|
||||
}
|
||||
|
||||
function showStatus(type: 'connected' | 'error' | 'connecting', message: string) {
|
||||
class ConnectPage {
|
||||
private _tabList: HTMLElement;
|
||||
private _tabListContainer: HTMLElement;
|
||||
private _statusContainer: HTMLElement;
|
||||
private _selectedTab: TabInfo | undefined;
|
||||
|
||||
constructor() {
|
||||
this._tabList = document.getElementById('tab-list')!;
|
||||
this._tabListContainer = document.getElementById('tab-list-container')!;
|
||||
this._statusContainer = document.getElementById('status-container') as HTMLElement;
|
||||
this._addButtonHandlers();
|
||||
void this._loadTabs();
|
||||
}
|
||||
|
||||
private _addButtonHandlers() {
|
||||
const continueBtn = document.getElementById('continue-btn') as HTMLButtonElement;
|
||||
const rejectBtn = document.getElementById('reject-btn') as HTMLButtonElement;
|
||||
const buttonRow = document.querySelector('.button-row') as HTMLElement;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mcpRelayUrl = params.get('mcpRelayUrl');
|
||||
|
||||
if (!mcpRelayUrl) {
|
||||
buttonRow.style.display = 'none';
|
||||
this._showStatus('error', 'Missing mcpRelayUrl parameter in URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
let clientInfo = 'unknown';
|
||||
try {
|
||||
const client = JSON.parse(params.get('client') || '{}');
|
||||
clientInfo = `${client.name}/${client.version}`;
|
||||
} catch (e) {
|
||||
this._showStatus('error', 'Failed to parse client version.');
|
||||
return;
|
||||
}
|
||||
|
||||
this._showStatus('connecting', `MCP client "${clientInfo}" is trying to connect. Do you want to continue?`);
|
||||
|
||||
rejectBtn.addEventListener('click', async () => {
|
||||
buttonRow.style.display = 'none';
|
||||
this._tabListContainer.style.display = 'none';
|
||||
this._showStatus('error', 'Connection rejected. This tab can be closed.');
|
||||
});
|
||||
|
||||
continueBtn.addEventListener('click', async () => {
|
||||
buttonRow.style.display = 'none';
|
||||
try {
|
||||
const selectedTab = this._selectedTab;
|
||||
if (!selectedTab) {
|
||||
this._showStatus('error', 'Tab not selected.');
|
||||
return;
|
||||
}
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'connectToMCPRelay',
|
||||
mcpRelayUrl,
|
||||
tabId: selectedTab.id,
|
||||
windowId: selectedTab.windowId,
|
||||
});
|
||||
if (response?.success)
|
||||
this._showStatus('connected', `MCP client "${clientInfo}" connected.`);
|
||||
else
|
||||
this._showStatus('error', response?.error || `MCP client "${clientInfo}" failed to connect.`);
|
||||
} catch (e) {
|
||||
this._showStatus('error', `MCP client "${clientInfo}" failed to connect: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadTabs(): Promise<void> {
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
|
||||
if (response.success)
|
||||
this._populateTabList(response.tabs, response.currentTabId);
|
||||
else
|
||||
this._showStatus('error', 'Failed to load tabs: ' + response.error);
|
||||
} catch (error) {
|
||||
this._showStatus('error', 'Failed to communicate with background script: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
private _populateTabList(tabs: TabInfo[], currentTabId: number): void {
|
||||
this._tabList.replaceChildren();
|
||||
this._selectedTab = tabs.find(tab => tab.id === currentTabId);
|
||||
|
||||
tabs.forEach((tab, index) => {
|
||||
const tabElement = this._createTabElement(tab);
|
||||
this._tabList.appendChild(tabElement);
|
||||
});
|
||||
}
|
||||
|
||||
private _createTabElement(tab: TabInfo): HTMLElement {
|
||||
const disabled = tab.url.startsWith('chrome://');
|
||||
|
||||
const tabInfoDiv = document.createElement('div');
|
||||
tabInfoDiv.className = 'tab-info';
|
||||
tabInfoDiv.style.padding = '5px';
|
||||
if (disabled)
|
||||
tabInfoDiv.style.opacity = '0.5';
|
||||
|
||||
const radioButton = document.createElement('input');
|
||||
radioButton.type = 'radio';
|
||||
radioButton.name = 'tab-selection';
|
||||
radioButton.checked = tab.id === this._selectedTab?.id;
|
||||
radioButton.id = `tab-${tab.id}`;
|
||||
radioButton.addEventListener('change', e => {
|
||||
if (radioButton.checked)
|
||||
this._selectedTab = tab;
|
||||
});
|
||||
if (disabled)
|
||||
radioButton.disabled = true;
|
||||
|
||||
const favicon = document.createElement('img');
|
||||
favicon.className = 'tab-favicon';
|
||||
if (tab.favIconUrl)
|
||||
favicon.src = tab.favIconUrl;
|
||||
favicon.alt = '';
|
||||
favicon.style.height = '16px';
|
||||
favicon.style.width = '16px';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.style.paddingLeft = '5px';
|
||||
title.className = 'tab-title';
|
||||
title.textContent = tab.title || 'Untitled';
|
||||
|
||||
const url = document.createElement('span');
|
||||
url.style.paddingLeft = '5px';
|
||||
url.className = 'tab-url';
|
||||
url.textContent = tab.url;
|
||||
|
||||
tabInfoDiv.appendChild(radioButton);
|
||||
tabInfoDiv.appendChild(favicon);
|
||||
tabInfoDiv.appendChild(title);
|
||||
tabInfoDiv.appendChild(url);
|
||||
|
||||
return tabInfoDiv;
|
||||
}
|
||||
|
||||
private _showStatus(type: 'connected' | 'error' | 'connecting', message: string) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `status ${type}`;
|
||||
div.textContent = message;
|
||||
statusContainer.replaceChildren(div);
|
||||
this._statusContainer.replaceChildren(div);
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mcpRelayUrl = params.get('mcpRelayUrl');
|
||||
|
||||
if (!mcpRelayUrl) {
|
||||
buttonRow.style.display = 'none';
|
||||
showStatus('error', 'Missing mcpRelayUrl parameter in URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
let clientInfo = 'unknown';
|
||||
try {
|
||||
const client = JSON.parse(params.get('client') || '{}');
|
||||
clientInfo = `${client.name}/${client.version}`;
|
||||
} catch (e) {
|
||||
showStatus('error', 'Failed to parse client version.');
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus('connecting', `MCP client "${clientInfo}" is trying to connect. Do you want to continue?`);
|
||||
|
||||
rejectBtn.addEventListener('click', async () => {
|
||||
buttonRow.style.display = 'none';
|
||||
showStatus('error', 'Connection rejected. This tab can be closed.');
|
||||
});
|
||||
|
||||
continueBtn.addEventListener('click', async () => {
|
||||
buttonRow.style.display = 'none';
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'connectToMCPRelay',
|
||||
mcpRelayUrl
|
||||
});
|
||||
if (response?.success)
|
||||
showStatus('connected', `MCP client "${clientInfo}" connected.`);
|
||||
else
|
||||
showStatus('error', response?.error || `MCP client "${clientInfo}" failed to connect.`);
|
||||
} catch (e) {
|
||||
showStatus('error', `MCP client "${clientInfo}" failed to connect: ${e}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
new ConnectPage();
|
||||
|
Loading…
x
Reference in New Issue
Block a user