mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2026-02-01 20:23:38 +00:00
chore: use mcp implementation in Playwright (#992)
This commit is contained in:
@@ -1,206 +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.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.app-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #1f2328;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Status Banner */
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-banner.connected {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.status-banner.connected::before {
|
||||
content: "\2705";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-banner.error {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.status-banner.error::before {
|
||||
content: "\274C";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button-container {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
margin-right: 8px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background-color: #f8f9fa;
|
||||
color: #3c4043;
|
||||
border: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background-color: #f1f3f4;
|
||||
border-color: #dadce0;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.1);
|
||||
}
|
||||
|
||||
.button.default {
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.button.default:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.button.reject {
|
||||
background-color: #da3633;
|
||||
color: #ffffff;
|
||||
border: 1px solid #da3633;
|
||||
}
|
||||
|
||||
.button.reject:hover {
|
||||
background-color: #c73836;
|
||||
border-color: #c73836;
|
||||
}
|
||||
|
||||
/* Tab selection */
|
||||
.tab-section-title {
|
||||
padding-left: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 12px;
|
||||
color: #656d76;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-item.selected {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.tab-item.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tab-radio {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
font-weight: 500;
|
||||
color: #1f2328;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tab-url {
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Link-style button */
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0066cc;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
@@ -1,29 +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>
|
||||
<title>Playwright MCP extension</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="../../icons/icon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="../../icons/icon-16.png">
|
||||
<link rel="stylesheet" href="connect.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="connect.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,233 +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 React, { useState, useEffect, useCallback } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Button, TabItem } from './tabItem';
|
||||
import type { TabInfo } from './tabItem';
|
||||
|
||||
type Status =
|
||||
| { type: 'connecting'; message: string }
|
||||
| { type: 'connected'; message: string }
|
||||
| { type: 'error'; message: string }
|
||||
| { type: 'error'; versionMismatch: { extensionVersion: string; } };
|
||||
|
||||
const SUPPORTED_PROTOCOL_VERSION = 1;
|
||||
|
||||
const ConnectApp: React.FC = () => {
|
||||
const [tabs, setTabs] = useState<TabInfo[]>([]);
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [showButtons, setShowButtons] = useState(true);
|
||||
const [showTabList, setShowTabList] = useState(true);
|
||||
const [clientInfo, setClientInfo] = useState('unknown');
|
||||
const [mcpRelayUrl, setMcpRelayUrl] = useState('');
|
||||
const [newTab, setNewTab] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const relayUrl = params.get('mcpRelayUrl');
|
||||
|
||||
if (!relayUrl) {
|
||||
setShowButtons(false);
|
||||
setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setMcpRelayUrl(relayUrl);
|
||||
|
||||
try {
|
||||
const client = JSON.parse(params.get('client') || '{}');
|
||||
const info = `${client.name}/${client.version}`;
|
||||
setClientInfo(info);
|
||||
setStatus({
|
||||
type: 'connecting',
|
||||
message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?`
|
||||
});
|
||||
} catch (e) {
|
||||
setStatus({ type: 'error', message: 'Failed to parse client version.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);
|
||||
const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
|
||||
if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {
|
||||
const extensionVersion = chrome.runtime.getManifest().version;
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
setStatus({
|
||||
type: 'error',
|
||||
versionMismatch: {
|
||||
extensionVersion,
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void connectToMCPRelay(relayUrl);
|
||||
|
||||
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
|
||||
if (params.get('newTab') === 'true') {
|
||||
setNewTab(true);
|
||||
setShowTabList(false);
|
||||
} else {
|
||||
void loadTabs();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleReject = useCallback((message: string) => {
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
setStatus({ type: 'error', message });
|
||||
}, []);
|
||||
|
||||
const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {
|
||||
|
||||
const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl });
|
||||
if (!response.success)
|
||||
handleReject(response.error);
|
||||
}, [handleReject]);
|
||||
|
||||
const loadTabs = useCallback(async () => {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
|
||||
if (response.success)
|
||||
setTabs(response.tabs);
|
||||
else
|
||||
setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error });
|
||||
}, []);
|
||||
|
||||
const handleConnectToTab = useCallback(async (tab?: TabInfo) => {
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'connectToTab',
|
||||
mcpRelayUrl,
|
||||
tabId: tab?.id,
|
||||
windowId: tab?.windowId,
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` });
|
||||
} else {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
message: response?.error || `MCP client "${clientInfo}" failed to connect.`
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
message: `MCP client "${clientInfo}" failed to connect: ${e}`
|
||||
});
|
||||
}
|
||||
}, [clientInfo, mcpRelayUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (message: any) => {
|
||||
if (message.type === 'connectionTimeout')
|
||||
handleReject('Connection timed out.');
|
||||
};
|
||||
chrome.runtime.onMessage.addListener(listener);
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(listener);
|
||||
};
|
||||
}, [handleReject]);
|
||||
|
||||
return (
|
||||
<div className='app-container'>
|
||||
<div className='content-wrapper'>
|
||||
{status && (
|
||||
<div className='status-container'>
|
||||
<StatusBanner status={status} />
|
||||
{showButtons && (
|
||||
<div className='button-container'>
|
||||
{newTab ? (
|
||||
<>
|
||||
<Button variant='primary' onClick={() => handleConnectToTab()}>
|
||||
Allow
|
||||
</Button>
|
||||
<Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTabList && (
|
||||
<div>
|
||||
<div className='tab-section-title'>
|
||||
Select page to expose to MCP server:
|
||||
</div>
|
||||
<div>
|
||||
{tabs.map(tab => (
|
||||
<TabItem
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
button={
|
||||
<Button variant='primary' onClick={() => handleConnectToTab(tab)}>
|
||||
Connect
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => {
|
||||
const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';
|
||||
const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest';
|
||||
return (
|
||||
<div>
|
||||
Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '}
|
||||
<a href={latestReleaseUrl}>Click here</a> to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '}
|
||||
See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
|
||||
return (
|
||||
<div className={`status-banner ${status.type}`}>
|
||||
{'versionMismatch' in status ? (
|
||||
<VersionMismatchError
|
||||
extensionVersion={status.versionMismatch.extensionVersion}
|
||||
/>
|
||||
) : (
|
||||
status.message
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize the React app
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<ConnectApp />);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Playwright MCP Bridge Status</title>
|
||||
<link rel="stylesheet" href="connect.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="status.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,110 +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 React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Button, TabItem } from './tabItem';
|
||||
|
||||
import type { TabInfo } from './tabItem';
|
||||
|
||||
interface ConnectionStatus {
|
||||
isConnected: boolean;
|
||||
connectedTabId: number | null;
|
||||
connectedTab?: TabInfo;
|
||||
}
|
||||
|
||||
const StatusApp: React.FC = () => {
|
||||
const [status, setStatus] = useState<ConnectionStatus>({
|
||||
isConnected: false,
|
||||
connectedTabId: null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, []);
|
||||
|
||||
const loadStatus = async () => {
|
||||
// Get current connection status from background script
|
||||
const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
|
||||
if (connectedTabId) {
|
||||
const tab = await chrome.tabs.get(connectedTabId);
|
||||
setStatus({
|
||||
isConnected: true,
|
||||
connectedTabId,
|
||||
connectedTab: {
|
||||
id: tab.id!,
|
||||
windowId: tab.windowId!,
|
||||
title: tab.title!,
|
||||
url: tab.url!,
|
||||
favIconUrl: tab.favIconUrl
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus({
|
||||
isConnected: false,
|
||||
connectedTabId: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openConnectedTab = async () => {
|
||||
if (!status.connectedTabId)
|
||||
return;
|
||||
await chrome.tabs.update(status.connectedTabId, { active: true });
|
||||
window.close();
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
await chrome.runtime.sendMessage({ type: 'disconnect' });
|
||||
window.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='app-container'>
|
||||
<div className='content-wrapper'>
|
||||
{status.isConnected && status.connectedTab ? (
|
||||
<div>
|
||||
<div className='tab-section-title'>
|
||||
Page with connected MCP client:
|
||||
</div>
|
||||
<div>
|
||||
<TabItem
|
||||
tab={status.connectedTab}
|
||||
button={
|
||||
<Button variant='primary' onClick={disconnect}>
|
||||
Disconnect
|
||||
</Button>
|
||||
}
|
||||
onClick={openConnectedTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='status-banner'>
|
||||
No MCP clients are currently connected.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize the React app
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<StatusApp />);
|
||||
}
|
||||
@@ -1,67 +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 React from 'react';
|
||||
|
||||
export interface TabInfo {
|
||||
id: number;
|
||||
windowId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
favIconUrl?: string;
|
||||
}
|
||||
|
||||
export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({
|
||||
variant,
|
||||
onClick,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<button className={`button ${variant}`} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export interface TabItemProps {
|
||||
tab: TabInfo;
|
||||
onClick?: () => void;
|
||||
button?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabItem: React.FC<TabItemProps> = ({
|
||||
tab,
|
||||
onClick,
|
||||
button
|
||||
}) => {
|
||||
return (
|
||||
<div className='tab-item' onClick={onClick} style={onClick ? { cursor: 'pointer' } : undefined}>
|
||||
<img
|
||||
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>'}
|
||||
alt=''
|
||||
className='tab-favicon'
|
||||
/>
|
||||
<div className='tab-content'>
|
||||
<div className='tab-title'>
|
||||
{tab.title || 'Untitled'}
|
||||
</div>
|
||||
<div className='tab-url'>{tab.url}</div>
|
||||
</div>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Help VSCode to find right tsconfig file.
|
||||
{
|
||||
"extends": "../../tsconfig.ui.json"
|
||||
}
|
||||
Reference in New Issue
Block a user