Spaces:
Paused
Paused
Yury Semikhatsky commited on
chore(extension): use react for connect dialog (#777)
Browse files- extension/connect.html +4 -11
- extension/src/connect.ts +0 -172
- extension/src/ui/connect.css +174 -0
- extension/src/ui/connect.tsx +213 -0
- extension/tsconfig.json +5 -0
- extension/tsconfig.ui.json +18 -0
- extension/vite.config.ts +40 -0
- package-lock.json +0 -0
- package.json +10 -2
- src/extension/cdpRelay.ts +1 -1
- tsconfig.all.json +1 -1
extension/connect.html
CHANGED
|
@@ -17,18 +17,11 @@
|
|
| 17 |
<html>
|
| 18 |
<head>
|
| 19 |
<title>Playwright MCP extension</title>
|
|
|
|
|
|
|
| 20 |
</head>
|
| 21 |
<body>
|
| 22 |
-
<
|
| 23 |
-
<
|
| 24 |
-
<div class="button-row">
|
| 25 |
-
<button id="continue-btn">Continue</button>
|
| 26 |
-
<button id="reject-btn">Reject</button>
|
| 27 |
-
</div>
|
| 28 |
-
<div id="tab-list-container">
|
| 29 |
-
<h4>Select page to expose to MCP server:</h4>
|
| 30 |
-
<div id="tab-list"></div>
|
| 31 |
-
</div>
|
| 32 |
-
<script src="lib/connect.js"></script>
|
| 33 |
</body>
|
| 34 |
</html>
|
|
|
|
| 17 |
<html>
|
| 18 |
<head>
|
| 19 |
<title>Playwright MCP extension</title>
|
| 20 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 21 |
+
<link rel="stylesheet" href="src/ui/connect.css">
|
| 22 |
</head>
|
| 23 |
<body>
|
| 24 |
+
<div id="root"></div>
|
| 25 |
+
<script type="module" src="src/ui/connect.tsx"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</body>
|
| 27 |
</html>
|
extension/src/connect.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Copyright (c) Microsoft Corporation.
|
| 3 |
-
*
|
| 4 |
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
-
* you may not use this file except in compliance with the License.
|
| 6 |
-
* You may obtain a copy of the License at
|
| 7 |
-
*
|
| 8 |
-
* http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
-
*
|
| 10 |
-
* Unless required by applicable law or agreed to in writing, software
|
| 11 |
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
-
* See the License for the specific language governing permissions and
|
| 14 |
-
* limitations under the License.
|
| 15 |
-
*/
|
| 16 |
-
|
| 17 |
-
interface TabInfo {
|
| 18 |
-
id: number;
|
| 19 |
-
windowId: number;
|
| 20 |
-
title: string;
|
| 21 |
-
url: string;
|
| 22 |
-
favIconUrl?: string;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
class ConnectPage {
|
| 26 |
-
private _tabList: HTMLElement;
|
| 27 |
-
private _tabListContainer: HTMLElement;
|
| 28 |
-
private _statusContainer: HTMLElement;
|
| 29 |
-
private _selectedTab: TabInfo | undefined;
|
| 30 |
-
|
| 31 |
-
constructor() {
|
| 32 |
-
this._tabList = document.getElementById('tab-list')!;
|
| 33 |
-
this._tabListContainer = document.getElementById('tab-list-container')!;
|
| 34 |
-
this._statusContainer = document.getElementById('status-container') as HTMLElement;
|
| 35 |
-
this._addButtonHandlers();
|
| 36 |
-
void this._loadTabs();
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
private _addButtonHandlers() {
|
| 40 |
-
const continueBtn = document.getElementById('continue-btn') as HTMLButtonElement;
|
| 41 |
-
const rejectBtn = document.getElementById('reject-btn') as HTMLButtonElement;
|
| 42 |
-
const buttonRow = document.querySelector('.button-row') as HTMLElement;
|
| 43 |
-
|
| 44 |
-
const params = new URLSearchParams(window.location.search);
|
| 45 |
-
const mcpRelayUrl = params.get('mcpRelayUrl');
|
| 46 |
-
|
| 47 |
-
if (!mcpRelayUrl) {
|
| 48 |
-
buttonRow.style.display = 'none';
|
| 49 |
-
this._showStatus('error', 'Missing mcpRelayUrl parameter in URL.');
|
| 50 |
-
return;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
let clientInfo = 'unknown';
|
| 54 |
-
try {
|
| 55 |
-
const client = JSON.parse(params.get('client') || '{}');
|
| 56 |
-
clientInfo = `${client.name}/${client.version}`;
|
| 57 |
-
} catch (e) {
|
| 58 |
-
this._showStatus('error', 'Failed to parse client version.');
|
| 59 |
-
return;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
this._showStatus('connecting', `MCP client "${clientInfo}" is trying to connect. Do you want to continue?`);
|
| 63 |
-
|
| 64 |
-
rejectBtn.addEventListener('click', async () => {
|
| 65 |
-
buttonRow.style.display = 'none';
|
| 66 |
-
this._tabListContainer.style.display = 'none';
|
| 67 |
-
this._showStatus('error', 'Connection rejected. This tab can be closed.');
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
continueBtn.addEventListener('click', async () => {
|
| 71 |
-
buttonRow.style.display = 'none';
|
| 72 |
-
this._tabListContainer.style.display = 'none';
|
| 73 |
-
try {
|
| 74 |
-
const selectedTab = this._selectedTab;
|
| 75 |
-
if (!selectedTab) {
|
| 76 |
-
this._showStatus('error', 'Tab not selected.');
|
| 77 |
-
return;
|
| 78 |
-
}
|
| 79 |
-
const response = await chrome.runtime.sendMessage({
|
| 80 |
-
type: 'connectToMCPRelay',
|
| 81 |
-
mcpRelayUrl,
|
| 82 |
-
tabId: selectedTab.id,
|
| 83 |
-
windowId: selectedTab.windowId,
|
| 84 |
-
});
|
| 85 |
-
if (response?.success)
|
| 86 |
-
this._showStatus('connected', `MCP client "${clientInfo}" connected.`);
|
| 87 |
-
else
|
| 88 |
-
this._showStatus('error', response?.error || `MCP client "${clientInfo}" failed to connect.`);
|
| 89 |
-
} catch (e) {
|
| 90 |
-
this._showStatus('error', `MCP client "${clientInfo}" failed to connect: ${e}`);
|
| 91 |
-
}
|
| 92 |
-
});
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
private async _loadTabs(): Promise<void> {
|
| 96 |
-
try {
|
| 97 |
-
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
|
| 98 |
-
if (response.success)
|
| 99 |
-
this._populateTabList(response.tabs, response.currentTabId);
|
| 100 |
-
else
|
| 101 |
-
this._showStatus('error', 'Failed to load tabs: ' + response.error);
|
| 102 |
-
} catch (error) {
|
| 103 |
-
this._showStatus('error', 'Failed to communicate with background script: ' + error);
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
private _populateTabList(tabs: TabInfo[], currentTabId: number): void {
|
| 108 |
-
this._tabList.replaceChildren();
|
| 109 |
-
this._selectedTab = tabs.find(tab => tab.id === currentTabId);
|
| 110 |
-
|
| 111 |
-
tabs.forEach((tab, index) => {
|
| 112 |
-
const tabElement = this._createTabElement(tab);
|
| 113 |
-
this._tabList.appendChild(tabElement);
|
| 114 |
-
});
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
private _createTabElement(tab: TabInfo): HTMLElement {
|
| 118 |
-
const disabled = tab.url.startsWith('chrome://');
|
| 119 |
-
|
| 120 |
-
const tabInfoDiv = document.createElement('div');
|
| 121 |
-
tabInfoDiv.className = 'tab-info';
|
| 122 |
-
tabInfoDiv.style.padding = '5px';
|
| 123 |
-
if (disabled)
|
| 124 |
-
tabInfoDiv.style.opacity = '0.5';
|
| 125 |
-
|
| 126 |
-
const radioButton = document.createElement('input');
|
| 127 |
-
radioButton.type = 'radio';
|
| 128 |
-
radioButton.name = 'tab-selection';
|
| 129 |
-
radioButton.checked = tab.id === this._selectedTab?.id;
|
| 130 |
-
radioButton.id = `tab-${tab.id}`;
|
| 131 |
-
radioButton.addEventListener('change', e => {
|
| 132 |
-
if (radioButton.checked)
|
| 133 |
-
this._selectedTab = tab;
|
| 134 |
-
});
|
| 135 |
-
if (disabled)
|
| 136 |
-
radioButton.disabled = true;
|
| 137 |
-
|
| 138 |
-
const favicon = document.createElement('img');
|
| 139 |
-
favicon.className = 'tab-favicon';
|
| 140 |
-
if (tab.favIconUrl)
|
| 141 |
-
favicon.src = tab.favIconUrl;
|
| 142 |
-
favicon.alt = '';
|
| 143 |
-
favicon.style.height = '16px';
|
| 144 |
-
favicon.style.width = '16px';
|
| 145 |
-
|
| 146 |
-
const title = document.createElement('span');
|
| 147 |
-
title.style.paddingLeft = '5px';
|
| 148 |
-
title.className = 'tab-title';
|
| 149 |
-
title.textContent = tab.title || 'Untitled';
|
| 150 |
-
|
| 151 |
-
const url = document.createElement('span');
|
| 152 |
-
url.style.paddingLeft = '5px';
|
| 153 |
-
url.className = 'tab-url';
|
| 154 |
-
url.textContent = tab.url;
|
| 155 |
-
|
| 156 |
-
tabInfoDiv.appendChild(radioButton);
|
| 157 |
-
tabInfoDiv.appendChild(favicon);
|
| 158 |
-
tabInfoDiv.appendChild(title);
|
| 159 |
-
tabInfoDiv.appendChild(url);
|
| 160 |
-
|
| 161 |
-
return tabInfoDiv;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
private _showStatus(type: 'connected' | 'error' | 'connecting', message: string) {
|
| 165 |
-
const div = document.createElement('div');
|
| 166 |
-
div.className = `status ${type}`;
|
| 167 |
-
div.textContent = message;
|
| 168 |
-
this._statusContainer.replaceChildren(div);
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
new ConnectPage();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/src/ui/connect.css
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
Copyright (c) Microsoft Corporation.
|
| 3 |
+
|
| 4 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
you may not use this file except in compliance with the License.
|
| 6 |
+
You may obtain a copy of the License at
|
| 7 |
+
|
| 8 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
|
| 10 |
+
Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
See the License for the specific language governing permissions and
|
| 14 |
+
limitations under the License.
|
| 15 |
+
*/
|
| 16 |
+
|
| 17 |
+
body {
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* Base styles */
|
| 23 |
+
.app-container {
|
| 24 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
| 25 |
+
background-color: #ffffff;
|
| 26 |
+
color: #1f2328;
|
| 27 |
+
margin: 0;
|
| 28 |
+
padding: 24px;
|
| 29 |
+
min-height: 100vh;
|
| 30 |
+
font-size: 14px;
|
| 31 |
+
line-height: 1.5;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.content-wrapper {
|
| 35 |
+
max-width: 600px;
|
| 36 |
+
margin: 0 auto;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.main-title {
|
| 40 |
+
font-size: 32px;
|
| 41 |
+
font-weight: 600;
|
| 42 |
+
margin-bottom: 8px;
|
| 43 |
+
color: #1f2328;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Status Banner */
|
| 47 |
+
.status-banner {
|
| 48 |
+
padding: 16px;
|
| 49 |
+
margin-bottom: 24px;
|
| 50 |
+
border-radius: 6px;
|
| 51 |
+
border: 1px solid;
|
| 52 |
+
font-size: 14px;
|
| 53 |
+
font-weight: 500;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.status-banner.connected {
|
| 57 |
+
background-color: #dafbe1;
|
| 58 |
+
border-color: #1a7f37;
|
| 59 |
+
color: #0d5a23;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.status-banner.error {
|
| 63 |
+
background-color: #ffebe9;
|
| 64 |
+
border-color: #da3633;
|
| 65 |
+
color: #a40e26;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.status-banner.connecting {
|
| 69 |
+
background-color: #fff8c5;
|
| 70 |
+
border-color: #d1b500;
|
| 71 |
+
color: #7a5c00;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Buttons */
|
| 75 |
+
.button-container {
|
| 76 |
+
margin-bottom: 24px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.button {
|
| 80 |
+
padding: 8px 16px;
|
| 81 |
+
border-radius: 6px;
|
| 82 |
+
border: 1px solid;
|
| 83 |
+
font-size: 14px;
|
| 84 |
+
font-weight: 500;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
display: inline-flex;
|
| 87 |
+
align-items: center;
|
| 88 |
+
justify-content: center;
|
| 89 |
+
text-decoration: none;
|
| 90 |
+
margin-right: 8px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.button.primary {
|
| 94 |
+
background-color: #2da44e;
|
| 95 |
+
border-color: #2da44e;
|
| 96 |
+
color: #ffffff;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.button.primary:hover {
|
| 100 |
+
background-color: #2c974b;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.button.default {
|
| 104 |
+
background-color: #f6f8fa;
|
| 105 |
+
border-color: #d1d9e0;
|
| 106 |
+
color: #24292f;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.button.default:hover {
|
| 110 |
+
background-color: #f3f4f6;
|
| 111 |
+
border-color: #c7d2da;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Tab selection */
|
| 115 |
+
.tab-section-title {
|
| 116 |
+
font-size: 20px;
|
| 117 |
+
font-weight: 600;
|
| 118 |
+
margin-bottom: 16px;
|
| 119 |
+
color: #1f2328;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.tab-item {
|
| 123 |
+
display: flex;
|
| 124 |
+
align-items: center;
|
| 125 |
+
padding: 12px;
|
| 126 |
+
border: 1px solid #d1d9e0;
|
| 127 |
+
border-radius: 6px;
|
| 128 |
+
margin-bottom: 8px;
|
| 129 |
+
background-color: #ffffff;
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.tab-item.selected {
|
| 134 |
+
background-color: #f6f8fa;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.tab-item.disabled {
|
| 138 |
+
cursor: not-allowed;
|
| 139 |
+
opacity: 0.5;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.tab-radio {
|
| 143 |
+
margin-right: 12px;
|
| 144 |
+
flex-shrink: 0;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.tab-favicon {
|
| 148 |
+
width: 16px;
|
| 149 |
+
height: 16px;
|
| 150 |
+
margin-right: 8px;
|
| 151 |
+
flex-shrink: 0;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.tab-content {
|
| 155 |
+
flex: 1;
|
| 156 |
+
min-width: 0;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.tab-title {
|
| 160 |
+
font-weight: 500;
|
| 161 |
+
color: #1f2328;
|
| 162 |
+
margin-bottom: 2px;
|
| 163 |
+
white-space: nowrap;
|
| 164 |
+
overflow: hidden;
|
| 165 |
+
text-overflow: ellipsis;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.tab-url {
|
| 169 |
+
font-size: 12px;
|
| 170 |
+
color: #656d76;
|
| 171 |
+
white-space: nowrap;
|
| 172 |
+
overflow: hidden;
|
| 173 |
+
text-overflow: ellipsis;
|
| 174 |
+
}
|
extension/src/ui/connect.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Copyright (c) Microsoft Corporation.
|
| 3 |
+
*
|
| 4 |
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
* you may not use this file except in compliance with the License.
|
| 6 |
+
* You may obtain a copy of the License at
|
| 7 |
+
*
|
| 8 |
+
* http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
*
|
| 10 |
+
* Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
* See the License for the specific language governing permissions and
|
| 14 |
+
* limitations under the License.
|
| 15 |
+
*/
|
| 16 |
+
|
| 17 |
+
import React, { useState, useEffect, useCallback } from 'react';
|
| 18 |
+
import { createRoot } from 'react-dom/client';
|
| 19 |
+
import './connect.css';
|
| 20 |
+
|
| 21 |
+
interface TabInfo {
|
| 22 |
+
id: number;
|
| 23 |
+
windowId: number;
|
| 24 |
+
title: string;
|
| 25 |
+
url: string;
|
| 26 |
+
favIconUrl?: string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type StatusType = 'connected' | 'error' | 'connecting';
|
| 30 |
+
|
| 31 |
+
const ConnectApp: React.FC = () => {
|
| 32 |
+
const [tabs, setTabs] = useState<TabInfo[]>([]);
|
| 33 |
+
const [selectedTab, setSelectedTab] = useState<TabInfo | undefined>();
|
| 34 |
+
const [status, setStatus] = useState<{ type: StatusType; message: string } | null>(null);
|
| 35 |
+
const [showButtons, setShowButtons] = useState(true);
|
| 36 |
+
const [showTabList, setShowTabList] = useState(true);
|
| 37 |
+
const [clientInfo, setClientInfo] = useState('unknown');
|
| 38 |
+
const [mcpRelayUrl, setMcpRelayUrl] = useState('');
|
| 39 |
+
|
| 40 |
+
useEffect(() => {
|
| 41 |
+
const params = new URLSearchParams(window.location.search);
|
| 42 |
+
const relayUrl = params.get('mcpRelayUrl');
|
| 43 |
+
|
| 44 |
+
if (!relayUrl) {
|
| 45 |
+
setShowButtons(false);
|
| 46 |
+
setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' });
|
| 47 |
+
return;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
setMcpRelayUrl(relayUrl);
|
| 51 |
+
|
| 52 |
+
try {
|
| 53 |
+
const client = JSON.parse(params.get('client') || '{}');
|
| 54 |
+
const info = `${client.name}/${client.version}`;
|
| 55 |
+
setClientInfo(info);
|
| 56 |
+
setStatus({
|
| 57 |
+
type: 'connecting',
|
| 58 |
+
message: `MCP client "${info}" is trying to connect. Do you want to continue?`
|
| 59 |
+
});
|
| 60 |
+
} catch (e) {
|
| 61 |
+
setStatus({ type: 'error', message: 'Failed to parse client version.' });
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
void loadTabs();
|
| 66 |
+
}, []);
|
| 67 |
+
|
| 68 |
+
const loadTabs = async () => {
|
| 69 |
+
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
|
| 70 |
+
if (response.success) {
|
| 71 |
+
setTabs(response.tabs);
|
| 72 |
+
const currentTab = response.tabs.find((tab: TabInfo) => tab.id === response.currentTabId);
|
| 73 |
+
setSelectedTab(currentTab);
|
| 74 |
+
} else {
|
| 75 |
+
setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error });
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
const handleContinue = useCallback(async () => {
|
| 80 |
+
setShowButtons(false);
|
| 81 |
+
setShowTabList(false);
|
| 82 |
+
|
| 83 |
+
if (!selectedTab) {
|
| 84 |
+
setStatus({ type: 'error', message: 'Tab not selected.' });
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
try {
|
| 89 |
+
const response = await chrome.runtime.sendMessage({
|
| 90 |
+
type: 'connectToMCPRelay',
|
| 91 |
+
mcpRelayUrl,
|
| 92 |
+
tabId: selectedTab.id,
|
| 93 |
+
windowId: selectedTab.windowId,
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (response?.success) {
|
| 97 |
+
setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` });
|
| 98 |
+
} else {
|
| 99 |
+
setStatus({
|
| 100 |
+
type: 'error',
|
| 101 |
+
message: response?.error || `MCP client "${clientInfo}" failed to connect.`
|
| 102 |
+
});
|
| 103 |
+
}
|
| 104 |
+
} catch (e) {
|
| 105 |
+
setStatus({
|
| 106 |
+
type: 'error',
|
| 107 |
+
message: `MCP client "${clientInfo}" failed to connect: ${e}`
|
| 108 |
+
});
|
| 109 |
+
}
|
| 110 |
+
}, [selectedTab, clientInfo, mcpRelayUrl]);
|
| 111 |
+
|
| 112 |
+
const handleReject = useCallback(() => {
|
| 113 |
+
setShowButtons(false);
|
| 114 |
+
setShowTabList(false);
|
| 115 |
+
setStatus({ type: 'error', message: 'Connection rejected. This tab can be closed.' });
|
| 116 |
+
}, []);
|
| 117 |
+
|
| 118 |
+
return (
|
| 119 |
+
<div className='app-container'>
|
| 120 |
+
<div className='content-wrapper'>
|
| 121 |
+
<h1 className='main-title'>
|
| 122 |
+
Playwright MCP Extension
|
| 123 |
+
</h1>
|
| 124 |
+
|
| 125 |
+
{status && <StatusBanner type={status.type} message={status.message} />}
|
| 126 |
+
|
| 127 |
+
{showButtons && (
|
| 128 |
+
<div className='button-container'>
|
| 129 |
+
<Button variant='primary' onClick={handleContinue}>
|
| 130 |
+
Continue
|
| 131 |
+
</Button>
|
| 132 |
+
<Button variant='default' onClick={handleReject}>
|
| 133 |
+
Reject
|
| 134 |
+
</Button>
|
| 135 |
+
</div>
|
| 136 |
+
)}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
{showTabList && (
|
| 140 |
+
<div>
|
| 141 |
+
<h2 className='tab-section-title'>
|
| 142 |
+
Select page to expose to MCP server:
|
| 143 |
+
</h2>
|
| 144 |
+
<div>
|
| 145 |
+
{tabs.map(tab => (
|
| 146 |
+
<TabItem
|
| 147 |
+
key={tab.id}
|
| 148 |
+
tab={tab}
|
| 149 |
+
isSelected={selectedTab?.id === tab.id}
|
| 150 |
+
onSelect={() => setSelectedTab(tab)}
|
| 151 |
+
/>
|
| 152 |
+
))}
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
)}
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
);
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
const StatusBanner: React.FC<{ type: StatusType; message: string }> = ({ type, message }) => {
|
| 162 |
+
return <div className={`status-banner ${type}`}>{message}</div>;
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const Button: React.FC<{ variant: 'primary' | 'default'; onClick: () => void; children: React.ReactNode }> = ({
|
| 166 |
+
variant,
|
| 167 |
+
onClick,
|
| 168 |
+
children
|
| 169 |
+
}) => {
|
| 170 |
+
return (
|
| 171 |
+
<button className={`button ${variant}`} onClick={onClick}>
|
| 172 |
+
{children}
|
| 173 |
+
</button>
|
| 174 |
+
);
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
const TabItem: React.FC<{ tab: TabInfo; isSelected: boolean; onSelect: () => void }> = ({
|
| 178 |
+
tab,
|
| 179 |
+
isSelected,
|
| 180 |
+
onSelect
|
| 181 |
+
}) => {
|
| 182 |
+
const disabled = tab.url.startsWith('chrome://');
|
| 183 |
+
|
| 184 |
+
const className = `tab-item ${isSelected ? 'selected' : ''} ${disabled ? 'disabled' : ''}`.trim();
|
| 185 |
+
|
| 186 |
+
return (
|
| 187 |
+
<div className={className} onClick={disabled ? undefined : onSelect}>
|
| 188 |
+
<input
|
| 189 |
+
type='radio'
|
| 190 |
+
className='tab-radio'
|
| 191 |
+
checked={isSelected}
|
| 192 |
+
disabled={disabled}
|
| 193 |
+
/>
|
| 194 |
+
<img
|
| 195 |
+
src={tab.favIconUrl || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect width="16" height="16" fill="%23f6f8fa"/></svg>'}
|
| 196 |
+
alt=''
|
| 197 |
+
className='tab-favicon'
|
| 198 |
+
/>
|
| 199 |
+
<div className='tab-content'>
|
| 200 |
+
<div className='tab-title'>{tab.title || 'Untitled'}</div>
|
| 201 |
+
<div className='tab-url'>{tab.url}</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
);
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
// Initialize the React app
|
| 209 |
+
const container = document.getElementById('root');
|
| 210 |
+
if (container) {
|
| 211 |
+
const root = createRoot(container);
|
| 212 |
+
root.render(<ConnectApp />);
|
| 213 |
+
}
|
extension/tsconfig.json
CHANGED
|
@@ -8,8 +8,13 @@
|
|
| 8 |
"rootDir": "src",
|
| 9 |
"outDir": "./lib",
|
| 10 |
"resolveJsonModule": true,
|
|
|
|
|
|
|
| 11 |
},
|
| 12 |
"include": [
|
| 13 |
"src",
|
| 14 |
],
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
|
|
|
| 8 |
"rootDir": "src",
|
| 9 |
"outDir": "./lib",
|
| 10 |
"resolveJsonModule": true,
|
| 11 |
+
"jsx": "react-jsx",
|
| 12 |
+
"jsxImportSource": "react"
|
| 13 |
},
|
| 14 |
"include": [
|
| 15 |
"src",
|
| 16 |
],
|
| 17 |
+
"exclude": [
|
| 18 |
+
"src/ui",
|
| 19 |
+
]
|
| 20 |
}
|
extension/tsconfig.ui.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ESNext",
|
| 4 |
+
"esModuleInterop": true,
|
| 5 |
+
"moduleResolution": "node",
|
| 6 |
+
"strict": true,
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"rootDir": "src",
|
| 9 |
+
"outDir": "./lib",
|
| 10 |
+
"resolveJsonModule": true,
|
| 11 |
+
"jsx": "react-jsx",
|
| 12 |
+
"jsxImportSource": "react",
|
| 13 |
+
"noEmit": true,
|
| 14 |
+
},
|
| 15 |
+
"include": [
|
| 16 |
+
"src/ui",
|
| 17 |
+
],
|
| 18 |
+
}
|
extension/vite.config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Copyright (c) Microsoft Corporation.
|
| 3 |
+
*
|
| 4 |
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
* you may not use this file except in compliance with the License.
|
| 6 |
+
* You may obtain a copy of the License at
|
| 7 |
+
*
|
| 8 |
+
* http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
*
|
| 10 |
+
* Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
* See the License for the specific language governing permissions and
|
| 14 |
+
* limitations under the License.
|
| 15 |
+
*/
|
| 16 |
+
|
| 17 |
+
import { resolve } from 'path';
|
| 18 |
+
import { defineConfig } from 'vite';
|
| 19 |
+
import react from '@vitejs/plugin-react';
|
| 20 |
+
|
| 21 |
+
// https://vitejs.dev/config/
|
| 22 |
+
export default defineConfig({
|
| 23 |
+
plugins: [react()],
|
| 24 |
+
base: '/lib/ui/',
|
| 25 |
+
build: {
|
| 26 |
+
outDir: resolve(__dirname, 'lib/ui'),
|
| 27 |
+
emptyOutDir: true,
|
| 28 |
+
minify: false,
|
| 29 |
+
rollupOptions: {
|
| 30 |
+
input: resolve(__dirname, 'connect.html'),
|
| 31 |
+
output: {
|
| 32 |
+
manualChunks: undefined,
|
| 33 |
+
inlineDynamicImports: true,
|
| 34 |
+
entryFileNames: '[name].js',
|
| 35 |
+
chunkFileNames: '[name].js',
|
| 36 |
+
assetFileNames: '[name].[ext]'
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
});
|
package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -17,12 +17,13 @@
|
|
| 17 |
"license": "Apache-2.0",
|
| 18 |
"scripts": {
|
| 19 |
"build": "tsc",
|
| 20 |
-
"build:extension": "tsc --project extension",
|
| 21 |
"lint": "npm run update-readme && eslint . && tsc --noEmit",
|
| 22 |
"lint-fix": "eslint . --fix",
|
| 23 |
"update-readme": "node utils/update-readme.js",
|
| 24 |
"watch": "tsc --watch",
|
| 25 |
-
"watch:extension": "tsc --watch --project extension",
|
|
|
|
| 26 |
"test": "playwright test",
|
| 27 |
"ctest": "playwright test --project=chrome",
|
| 28 |
"ftest": "playwright test --project=firefox",
|
|
@@ -58,14 +59,21 @@
|
|
| 58 |
"@types/chrome": "^0.0.315",
|
| 59 |
"@types/debug": "^4.1.12",
|
| 60 |
"@types/node": "^22.13.10",
|
|
|
|
|
|
|
| 61 |
"@types/ws": "^8.18.1",
|
| 62 |
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
| 63 |
"@typescript-eslint/parser": "^8.26.1",
|
| 64 |
"@typescript-eslint/utils": "^8.26.1",
|
|
|
|
| 65 |
"eslint": "^9.19.0",
|
| 66 |
"eslint-plugin-import": "^2.31.0",
|
|
|
|
|
|
|
| 67 |
"eslint-plugin-notice": "^1.0.0",
|
| 68 |
"openai": "^5.10.2",
|
|
|
|
|
|
|
| 69 |
"typescript": "^5.8.2"
|
| 70 |
},
|
| 71 |
"bin": {
|
|
|
|
| 17 |
"license": "Apache-2.0",
|
| 18 |
"scripts": {
|
| 19 |
"build": "tsc",
|
| 20 |
+
"build:extension": "tsc --project extension && tsc --project extension && vite build extension",
|
| 21 |
"lint": "npm run update-readme && eslint . && tsc --noEmit",
|
| 22 |
"lint-fix": "eslint . --fix",
|
| 23 |
"update-readme": "node utils/update-readme.js",
|
| 24 |
"watch": "tsc --watch",
|
| 25 |
+
"watch:extension": "tsc --watch --project extension & tsc --watch --project extension/tsconfig.ui.json & vite build extension --watch",
|
| 26 |
+
"dev:extension": "vite build --watch",
|
| 27 |
"test": "playwright test",
|
| 28 |
"ctest": "playwright test --project=chrome",
|
| 29 |
"ftest": "playwright test --project=firefox",
|
|
|
|
| 59 |
"@types/chrome": "^0.0.315",
|
| 60 |
"@types/debug": "^4.1.12",
|
| 61 |
"@types/node": "^22.13.10",
|
| 62 |
+
"@types/react": "^18.2.66",
|
| 63 |
+
"@types/react-dom": "^18.2.22",
|
| 64 |
"@types/ws": "^8.18.1",
|
| 65 |
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
| 66 |
"@typescript-eslint/parser": "^8.26.1",
|
| 67 |
"@typescript-eslint/utils": "^8.26.1",
|
| 68 |
+
"esbuild": "^0.20.1",
|
| 69 |
"eslint": "^9.19.0",
|
| 70 |
"eslint-plugin-import": "^2.31.0",
|
| 71 |
+
"vite": "^5.0.0",
|
| 72 |
+
"@vitejs/plugin-react": "^4.0.0",
|
| 73 |
"eslint-plugin-notice": "^1.0.0",
|
| 74 |
"openai": "^5.10.2",
|
| 75 |
+
"react": "^18.2.0",
|
| 76 |
+
"react-dom": "^18.2.0",
|
| 77 |
"typescript": "^5.8.2"
|
| 78 |
},
|
| 79 |
"bin": {
|
src/extension/cdpRelay.ts
CHANGED
|
@@ -103,7 +103,7 @@ export class CDPRelayServer {
|
|
| 103 |
private async _connectBrowser(clientInfo: { name: string, version: string }) {
|
| 104 |
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
| 105 |
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
|
| 106 |
-
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
|
| 107 |
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
|
| 108 |
url.searchParams.set('client', JSON.stringify(clientInfo));
|
| 109 |
const href = url.toString();
|
|
|
|
| 103 |
private async _connectBrowser(clientInfo: { name: string, version: string }) {
|
| 104 |
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
| 105 |
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
|
| 106 |
+
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/lib/ui/connect.html');
|
| 107 |
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
|
| 108 |
url.searchParams.set('client', JSON.stringify(clientInfo));
|
| 109 |
const href = url.toString();
|
tsconfig.all.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
{
|
| 2 |
"extends": "./tsconfig.json",
|
| 3 |
-
"include": ["**/*.ts", "**/*.js"],
|
| 4 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"extends": "./tsconfig.json",
|
| 3 |
+
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
|
| 4 |
}
|