Update test framework: fix run_tests.py to support all test files, add auto-import-check for test files

This commit is contained in:
qiaoxinjiu
2026-05-09 15:11:30 +08:00
parent eb053a347f
commit eaba8328da
21739 changed files with 2236758 additions and 719 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 ExecuteAutomation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,319 @@
<div align="center" markdown="1">
<table>
<tr>
<td align="center" valign="middle">
<a href="https://mseep.ai/app/executeautomation-mcp-playwright">
<img src="https://mseep.net/pr/executeautomation-mcp-playwright-badge.png" alt="MseeP.ai Security Assessment Badge" height="80"/>
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=mcp-playwright">
<img alt="Warp sponsorship" width="200" src="https://github.com/user-attachments/assets/ab8dd143-b0fd-4904-bdc5-dd7ecac94eae"/>
</a>
</td>
</tr>
<tr>
<td align="center"><sub>MseeP.ai Security Assessment</sub></td>
<td align="center"><sub>Special thanks to <a href="https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=mcp-playwright">Warp, the AI terminal for developers</a></sub></td>
</tr>
</table>
</div>
<hr>
# Playwright MCP Server 🎭
[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/executeautomation/mcp-playwright)](https://archestra.ai/mcp-catalog/executeautomation__mcp-playwright)
[![smithery badge](https://smithery.ai/badge/@executeautomation/playwright-mcp-server)](https://smithery.ai/server/@executeautomation/playwright-mcp-server)
A Model Context Protocol server that provides browser automation capabilities using Playwright. This server enables LLMs to interact with web pages, take screenshots, generate test code, web scrapes the page and execute JavaScript in a real browser environment.
<a href="https://glama.ai/mcp/servers/yh4lgtwgbe"><img width="380" height="200" src="https://glama.ai/mcp/servers/yh4lgtwgbe/badge" alt="mcp-playwright MCP server" /></a>
## ✨ What's New in v1.0.10
### 🎯 Device Emulation with 143 Real Device Presets!
Test your web applications on **real device profiles** with a simple command:
```javascript
// Test on iPhone 13 with automatic user-agent, touch support, and device pixel ratio
await playwright_resize({ device: "iPhone 13" });
// Switch to iPad with landscape orientation
await playwright_resize({ device: "iPad Pro 11", orientation: "landscape" });
// Test desktop view
await playwright_resize({ device: "Desktop Chrome" });
```
**Natural Language Support for AI Assistants:**
- "Test on iPhone 13"
- "Switch to iPad view"
- "Rotate to landscape"
**Supports 143 devices:** iPhone, iPad, Pixel, Galaxy, and Desktop browsers with proper emulation of viewport, user-agent, touch events, and device pixel ratios.
📚 [View Device Quick Reference](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Device-Quick-Reference) | [Prompt Guide](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Resize-Prompts-Guide)
## Screenshot
![Playwright + Claude](image/playwright_claude.png)
## [Documentation](https://executeautomation.github.io/mcp-playwright/) | [API reference](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Supported-Tools)
## Installation
You can install the package using either npm, mcp-get, or Smithery:
Using npm:
```bash
npm install -g @executeautomation/playwright-mcp-server
```
Using mcp-get:
```bash
npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server
```
Using Smithery
To install Playwright MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@executeautomation/playwright-mcp-server):
```bash
npx @smithery/cli install @executeautomation/playwright-mcp-server --client claude
```
Using Claude Code:
```bash
claude mcp add --transport stdio playwright npx @executeautomation/playwright-mcp-server
```
#### Installation in VS Code
Install the Playwright MCP server in VS Code using one of these buttons:
<!--
// Generate using?:
const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["-y", "@executeautomation/playwright-mcp-server"] });
const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
// Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
-->
[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540executeautomation%252Fplaywright-mcp-server%2522%255D%257D)
[<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540executeautomation%252Fplaywright-mcp-server%2522%255D%257D)
Alternatively, you can install the Playwright MCP server using the VS Code CLI:
```bash
# For VS Code
code --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
```
```bash
# For VS Code Insiders
code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
```
After installation, the ExecuteAutomation Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
## Browser Installation
### Automatic Installation (Recommended)
The Playwright MCP Server **automatically installs browser binaries** when you first use it. When the server detects that a browser is missing, it will:
1. Automatically download and install the required browser (Chromium, Firefox, or WebKit)
2. Display installation progress in the console
3. Retry your request once installation completes
**No manual setup required!** Just start using the server, and it handles browser installation for you.
### Manual Installation (Optional)
If you prefer to install browsers manually or encounter any issues with automatic installation:
```bash
# Install all browsers
npx playwright install
# Or install specific browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
```
### Browser Storage Location
Browsers are installed to:
- **Windows:** `%USERPROFILE%\AppData\Local\ms-playwright`
- **macOS:** `~/Library/Caches/ms-playwright`
- **Linux:** `~/.cache/ms-playwright`
## Configuration to use Playwright Server
### Standard Mode (stdio)
This is the **recommended mode for Claude Desktop**.
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}
```
**Note:** In stdio mode, logging is automatically directed to files only (not console) to maintain clean JSON-RPC communication. Logs are written to `~/playwright-mcp-server.log`.
### HTTP Mode (Standalone Server)
When running headed browser on systems without display or from worker processes of IDEs, you can run the MCP server as a standalone HTTP server:
> **Note for Claude Desktop Users:** Claude Desktop currently requires stdio mode (command/args configuration). HTTP mode is recommended for VS Code, custom clients, and remote deployments. See [CLAUDE_DESKTOP_CONFIG.md](CLAUDE_DESKTOP_CONFIG.md) for details.
#### Starting the HTTP Server
```bash
# Using npx
npx @executeautomation/playwright-mcp-server --port 8931
# Or after global installation
playwright-mcp-server --port 8931
```
The server will start and display available endpoints:
```
==============================================
Playwright MCP Server (HTTP Mode)
==============================================
Port: 8931
ENDPOINTS:
- SSE Stream: GET http://localhost:8931/sse
- Messages: POST http://localhost:8931/messages?sessionId=<id>
- MCP (unified): GET http://localhost:8931/mcp
- MCP (unified): POST http://localhost:8931/mcp?sessionId=<id>
- Health Check: GET http://localhost:8931/health
==============================================
```
#### Client Configuration for HTTP Mode
> **⚠️ CRITICAL:** The `"type": "http"` field is **REQUIRED** for HTTP/SSE transport!
**For VS Code GitHub Copilot:**
```json
{
"github.copilot.chat.mcp.servers": {
"playwright": {
"url": "http://localhost:8931/mcp",
"type": "http"
}
}
}
```
**For Custom MCP Clients:**
```json
{
"mcpServers": {
"playwright": {
"url": "http://localhost:8931/mcp",
"type": "http"
}
}
}
```
**Important:** Without `"type": "http"`, the connection will fail.
**For Claude Desktop:** Use stdio mode instead (see Standard Mode above)
#### Use Cases for HTTP Mode
- Running headed browsers on systems without display (e.g., remote servers)
- Integrating with VS Code GitHub Copilot
- Running the server as a background service
- Accessing the server from multiple clients
- Debugging with the `/health` endpoint
- Custom MCP client integrations
**Monitoring:** The server includes a monitoring system that starts on a dynamically allocated port (avoiding conflicts). Check the console output for the actual port.
**Note:** For Claude Desktop, continue using stdio mode (Standard Mode above) for now.
## Troubleshooting
### "No transport found for sessionId" Error
**Symptom:** 400 error with message "Bad Request: No transport found for sessionId"
**Solution:**
1. **Check configuration includes `"type": "http"`**
```json
{
"url": "http://localhost:8931/mcp",
"type": "http" // ← This is REQUIRED!
}
```
2. **Verify server logs show connection:**
```bash
# Should see these in order:
# 1. "Incoming request" - GET /mcp
# 2. "Transport registered" - with sessionId
# 3. "POST message received" - with same sessionId
```
3. **Restart both server and client**
### Connection Issues
- **Server not starting:** Check if port 8931 is available
- **External access blocked:** This is by design (security). Server binds to localhost only
- **For remote access:** Use SSH tunneling:
```bash
ssh -L 8931:localhost:8931 user@remote-server
```
## Testing
This project uses Jest for testing. The tests are located in the `src/__tests__` directory.
### Running Tests
You can run the tests using one of the following commands:
```bash
# Run tests using the custom script (with coverage)
node run-tests.cjs
# Run tests using npm scripts
npm test # Run tests without coverage
npm run test:coverage # Run tests with coverage
npm run test:custom # Run tests with custom script (same as node run-tests.cjs)
```
The test coverage report will be generated in the `coverage` directory.
### Running evals
The evals package loads an mcp client that then runs the index.ts file, so there is no need to rebuild between tests. You can load environment variables by prefixing the npx command. Full documentation can be found [here](https://www.mcpevals.io/docs).
```bash
OPENAI_API_KEY=your-key npx mcp-eval src/evals/evals.ts src/tools/codegen/index.ts
```
## Contributing
When adding new tools, please be mindful of the tool name length. Some clients, like Cursor, have a 60-character limit for the combined server and tool name (`server_name:tool_name`).
Our server name is `playwright-mcp`. Please ensure your tool names are short enough to not exceed this limit.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=executeautomation/mcp-playwright&type=Date)](https://star-history.com/#executeautomation/mcp-playwright&Date)

View File

@@ -0,0 +1,5 @@
import { EvalConfig } from 'mcp-evals';
import { EvalFunction } from "mcp-evals";
declare const config: EvalConfig;
export default config;
export declare const evals: EvalFunction[];

View File

@@ -0,0 +1,41 @@
//evals.ts
import { openai } from "@ai-sdk/openai";
import { grade } from "mcp-evals";
const startCodegenSessionEval = {
name: 'startCodegenSession Evaluation',
description: 'Evaluates the start codegen session tool',
run: async () => {
const result = await grade(openai("gpt-4"), "Please start a new code generation session with an output path of /my/test/path, a testNamePrefix of MyPrefix, and comments enabled. Confirm the session was created successfully.");
return JSON.parse(result);
}
};
const end_codegen_sessionEval = {
name: 'end_codegen_session Evaluation',
description: 'Evaluates the end_codegen_session tool functionality',
run: async () => {
const result = await grade(openai("gpt-4"), "Please end the code generation session with ID session123 and generate the Playwright test code");
return JSON.parse(result);
}
};
const get_codegen_sessionEval = {
name: 'get_codegen_session Tool Evaluation',
description: 'Evaluates the retrieval of code generation session details',
run: async () => {
const result = await grade(openai("gpt-4"), "Please retrieve the code generation session details using session ID abc123.");
return JSON.parse(result);
}
};
const clearCodegenSessionEval = {
name: 'clear_codegen_session Evaluation',
description: 'Evaluates the functionality of clearing a code generation session',
run: async () => {
const result = await grade(openai("gpt-4"), "Please clear the code generation session with the ID testSession_123 to verify removal.");
return JSON.parse(result);
}
};
const config = {
model: openai("gpt-4"),
evals: [startCodegenSessionEval, end_codegen_sessionEval, get_codegen_sessionEval, clearCodegenSessionEval]
};
export default config;
export const evals = [startCodegenSessionEval, end_codegen_sessionEval, get_codegen_sessionEval, clearCodegenSessionEval];

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export declare function startHttpServer(port: number): Promise<void>;

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env node
import express from 'express';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createToolDefinitions } from "./tools.js";
import { setupRequestHandlers } from "./requestHandler.js";
import { Logger, RequestLoggingMiddleware } from "./logging/index.js";
import { MonitoringSystem } from "./monitoring/index.js";
export async function startHttpServer(port) {
// Show immediate feedback that server is starting
process.stdout.write('\n🚀 Starting Playwright MCP Server (HTTP Mode)...\n');
// Initialize logger for HTTP mode (file only - cleaner console output)
const logger = Logger.getInstance({
level: 'info',
format: 'json',
outputs: ['file'], // File only - we have nice formatted console output below
filePath: `${process.env.HOME || '/tmp'}/playwright-mcp-server-http.log`,
maxFileSize: 10485760,
maxFiles: 5
});
const loggingMiddleware = new RequestLoggingMiddleware(logger);
// Initialize monitoring system
const monitoringSystem = new MonitoringSystem({
enabled: true,
metricsInterval: 30000,
healthCheckInterval: 60000,
memoryThreshold: 80,
responseTimeThreshold: 5000
});
const serverInfo = {
name: "playwright-mcp",
version: "1.0.11",
capabilities: {
resources: {},
tools: {},
}
};
// Create Express application
const app = express();
app.use(express.json());
// Log all incoming requests for debugging
app.use((req, res, next) => {
logger.info('Incoming request', {
method: req.method,
path: req.path,
url: req.url,
query: req.query,
headers: {
accept: req.headers['accept'],
contentType: req.headers['content-type']
}
});
next();
});
// Store transports by session ID
const transports = {};
// Create tool definitions
const TOOLS = createToolDefinitions();
// Helper function to create a new MCP server instance
const createMcpServer = () => {
const server = new Server({
name: serverInfo.name,
version: serverInfo.version,
}, {
capabilities: serverInfo.capabilities,
});
// Setup request handlers
setupRequestHandlers(server, TOOLS, monitoringSystem);
return server;
};
// Helper function to handle SSE connection (DRY principle)
const handleSseConnection = async (endpoint, messageEndpoint, req, res) => {
logger.info('SSE connection request received', { endpoint });
try {
const transport = new SSEServerTransport(messageEndpoint, res);
const sessionId = transport.sessionId;
transports[sessionId] = transport;
logger.info('Transport registered', {
sessionId,
endpoint,
messageEndpoint,
activeTransports: Object.keys(transports).length
});
res.on('close', () => {
logger.info('SSE connection closed', { sessionId, endpoint });
delete transports[sessionId];
logger.info('Transport unregistered', {
sessionId,
activeTransports: Object.keys(transports).length
});
});
const server = createMcpServer();
await server.connect(transport);
logger.info('SSE transport connected', { sessionId, endpoint });
}
catch (error) {
logger.error('Error establishing SSE connection', error instanceof Error ? error : new Error(String(error)), { endpoint });
if (!res.headersSent) {
res.status(500).send('Failed to establish SSE connection');
}
}
};
// Helper function to handle POST messages (DRY principle)
const handlePostMessage = async (endpoint, req, res) => {
const sessionId = req.query.sessionId;
logger.info('POST message received', {
endpoint,
sessionId: sessionId || 'missing',
availableTransports: Object.keys(transports),
activeTransports: Object.keys(transports).length
});
if (!sessionId) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: sessionId query parameter required',
},
id: null,
});
return;
}
const transport = transports[sessionId];
if (!transport) {
logger.warn('Message received for unknown session', {
sessionId,
endpoint,
availableTransports: Object.keys(transports),
transportExists: sessionId in transports
});
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No transport found for sessionId',
},
id: null,
});
return;
}
try {
await transport.handlePostMessage(req, res, req.body);
}
catch (error) {
logger.error('Error handling POST message', error instanceof Error ? error : new Error(String(error)), { sessionId, endpoint });
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
};
// Legacy SSE endpoint (for backward compatibility)
app.get('/sse', (req, res) => handleSseConnection('/sse', '/messages', req, res));
app.post('/messages', (req, res) => handlePostMessage('/messages', req, res));
// Unified MCP endpoint (recommended)
app.get('/mcp', (req, res) => handleSseConnection('/mcp', '/mcp', req, res));
app.post('/mcp', (req, res) => handlePostMessage('/mcp', req, res));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
version: serverInfo.version,
activeSessions: Object.keys(transports).length,
});
});
// Start the HTTP server
// SECURITY: Bind to localhost only to prevent external access
const host = '127.0.0.1';
return new Promise((resolve, reject) => {
const httpServer = app.listen(port, host, () => {
logger.info(`Playwright MCP HTTP server listening on ${host}:${port}`, {
host,
port,
endpoints: {
sse: `http://localhost:${port}/sse`,
messages: `http://localhost:${port}/messages`,
mcp: `http://localhost:${port}/mcp`,
health: `http://localhost:${port}/health`,
}
});
console.log(`
==============================================
Playwright MCP Server (HTTP Mode)
==============================================
Listening: ${host}:${port} (localhost only)
Version: ${serverInfo.version}
SECURITY: Server is bound to localhost only.
Not accessible from external networks.
ENDPOINTS:
- SSE Stream: GET http://localhost:${port}/sse
- Messages: POST http://localhost:${port}/messages?sessionId=<id>
- MCP (unified): GET http://localhost:${port}/mcp
- MCP (unified): POST http://localhost:${port}/mcp?sessionId=<id>
- Health Check: GET http://localhost:${port}/health
CLIENT CONFIGURATION:
{
"mcpServers": {
"playwright": {
"url": "http://localhost:${port}/mcp",
"type": "http"
}
}
}
==============================================
`);
// Start monitoring system with dynamic port allocation
monitoringSystem.startMetricsCollection(0).catch(error => {
logger.warn('Failed to start monitoring HTTP server', {
error: error instanceof Error ? error.message : String(error)
});
});
resolve();
});
httpServer.on('error', (error) => {
logger.error('Failed to start HTTP server', error);
reject(error);
});
// Graceful shutdown
const shutdown = async () => {
logger.info('Shutdown signal received');
// Close all active transports
for (const sessionId in transports) {
try {
logger.info('Closing transport', { sessionId });
await transports[sessionId].close();
delete transports[sessionId];
}
catch (error) {
logger.error('Error closing transport', error instanceof Error ? error : new Error(String(error)), { sessionId });
}
}
// Stop monitoring
try {
await monitoringSystem.stopMetricsCollection();
logger.info('Monitoring system stopped');
}
catch (error) {
logger.error('Error stopping monitoring system', error instanceof Error ? error : new Error(String(error)));
}
// Close HTTP server
httpServer.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
logger.warn('Forced shutdown after timeout');
process.exit(1);
}, 5000);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
});
}

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createToolDefinitions } from "./tools.js";
import { setupRequestHandlers } from "./requestHandler.js";
import { Logger, RequestLoggingMiddleware } from "./logging/index.js";
import { MonitoringSystem } from "./monitoring/index.js";
import { startHttpServer } from "./http-server.js";
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const options = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' && i + 1 < args.length) {
options.port = parseInt(args[i + 1], 10);
if (isNaN(options.port) || options.port < 1 || options.port > 65535) {
console.error('Error: --port must be a valid port number (1-65535)');
process.exit(1);
}
}
else if (args[i] === '--help' || args[i] === '-h') {
console.error(`
Playwright MCP Server
USAGE:
playwright-mcp-server [OPTIONS]
OPTIONS:
--port <number> Run in HTTP mode on the specified port
--help, -h Show this help message
EXAMPLES:
# Run in stdio mode (default)
playwright-mcp-server
# Run in HTTP mode on port 8931
playwright-mcp-server --port 8931
HTTP MODE CONFIGURATION:
When running with --port, configure your MCP client:
{
"mcpServers": {
"playwright": {
"url": "http://localhost:8931/mcp",
"type": "http"
}
}
}
`);
process.exit(0);
}
}
return options;
}
async function runServer() {
const options = parseArgs();
// If port is specified, run in HTTP mode
if (options.port) {
// Show immediate feedback
process.stdout.write(`\n⏳ Initializing Playwright MCP Server on port ${options.port}...\n`);
await startHttpServer(options.port);
return;
}
// Otherwise, run in stdio mode (default)
// Initialize logger for stdio mode (file only - no console output to avoid breaking stdio protocol)
const logger = Logger.getInstance({
level: 'info',
format: 'json',
outputs: ['file'], // File only - console would write to stdout and break stdio protocol
filePath: `${process.env.HOME || '/tmp'}/playwright-mcp-server.log`, // Use home directory
maxFileSize: 10485760,
maxFiles: 5
});
const loggingMiddleware = new RequestLoggingMiddleware(logger);
// Initialize monitoring system (disabled in stdio mode to avoid console output)
const monitoringSystem = new MonitoringSystem({
enabled: false, // Disabled in stdio mode - HTTP server output would break stdio protocol
metricsInterval: 30000,
healthCheckInterval: 60000,
memoryThreshold: 80,
responseTimeThreshold: 5000
});
const serverInfo = {
name: "playwright-mcp",
version: "1.0.11",
capabilities: {
resources: {},
tools: {},
}
};
const server = new Server({
name: serverInfo.name,
version: serverInfo.version,
}, {
capabilities: serverInfo.capabilities,
});
// Log server startup
loggingMiddleware.logServerStartup(serverInfo);
// Create tool definitions
const TOOLS = createToolDefinitions();
// Setup request handlers
setupRequestHandlers(server, TOOLS, monitoringSystem);
// Start monitoring system
try {
await monitoringSystem.startMetricsCollection(3001);
logger.info('Monitoring system started', { port: 3001 });
}
catch (error) {
logger.warn('Failed to start monitoring HTTP server', { error: error instanceof Error ? error.message : String(error) });
}
// Graceful shutdown logic
async function shutdown() {
loggingMiddleware.logServerShutdown();
logger.info('Shutdown signal received');
try {
await monitoringSystem.stopMetricsCollection();
logger.info('Monitoring system stopped');
}
catch (error) {
logger.error('Error stopping monitoring system', error instanceof Error ? error : new Error(String(error)));
}
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.on('exit', shutdown);
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception', err, {
category: 'system',
nodeVersion: process.version,
platform: process.platform,
});
});
// Create transport and connect
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('MCP Server connected and ready', {
transport: 'stdio',
toolCount: TOOLS.length,
});
}
runServer().catch(() => {
process.exit(1);
});

View File

@@ -0,0 +1,3 @@
export * from './types.js';
export * from './logger.js';
export * from './middleware.js';

View File

@@ -0,0 +1,4 @@
// Logging module exports
export * from './types.js';
export * from './logger.js';
export * from './middleware.js';

View File

@@ -0,0 +1,112 @@
import { LoggerConfig, RequestLogContext, ErrorLogContext } from './types';
/**
* Structured Logger Class
* Provides comprehensive logging with multiple levels and outputs
*/
export declare class Logger {
private config;
private requestId?;
private static instance;
constructor(config: LoggerConfig);
/**
* Get singleton logger instance
* @param config Logger configuration (only used on first call)
* @returns Logger instance
*/
static getInstance(config?: LoggerConfig): Logger;
/**
* Create default logger configuration
* @returns Default configuration
*/
static createDefaultConfig(): LoggerConfig;
/**
* Ensure log directory exists
*/
private ensureLogDirectory;
/**
* Set request ID for request tracing
* @param requestId Request identifier
*/
setRequestId(requestId: string): void;
/**
* Clear request ID
*/
clearRequestId(): void;
/**
* Log debug message
* @param message Log message
* @param context Additional context
*/
debug(message: string, context?: Record<string, any>): void;
/**
* Log info message
* @param message Log message
* @param context Additional context
*/
info(message: string, context?: Record<string, any>): void;
/**
* Log warning message
* @param message Log message
* @param context Additional context
*/
warn(message: string, context?: Record<string, any>): void;
/**
* Log error message
* @param message Log message
* @param error Optional error object
* @param context Additional context
*/
error(message: string, error?: Error, context?: Record<string, any>): void;
/**
* Log request details
* @param message Log message
* @param requestContext Request context
*/
logRequest(message: string, requestContext: RequestLogContext): void;
/**
* Log error with detailed context
* @param message Error message
* @param error Error object
* @param errorContext Error context
*/
logError(message: string, error: Error, errorContext?: ErrorLogContext): void;
/**
* Core logging method
* @param level Log level
* @param message Log message
* @param context Additional context
*/
private log;
/**
* Format log entry based on configuration
* @param entry Log entry
* @returns Formatted string
*/
private formatLogEntry;
/**
* Write to console with appropriate method
* @param level Log level
* @param message Formatted message
*/
private writeToConsole;
/**
* Write to file with rotation support
* @param message Formatted message
*/
private writeToFile;
/**
* Check if log file should be rotated
* @returns Whether rotation is needed
*/
private shouldRotateFile;
/**
* Rotate log file
*/
private rotateLogFile;
/**
* Check if message should be logged based on configured level
* @param level Message level
* @returns Whether to log the message
*/
private shouldLog;
}

View File

@@ -0,0 +1,278 @@
import * as fs from 'fs';
import * as path from 'path';
/**
* Structured Logger Class
* Provides comprehensive logging with multiple levels and outputs
*/
export class Logger {
constructor(config) {
this.config = config;
this.ensureLogDirectory();
}
/**
* Get singleton logger instance
* @param config Logger configuration (only used on first call)
* @returns Logger instance
*/
static getInstance(config) {
if (!Logger.instance) {
if (!config) {
throw new Error('Logger configuration required for first initialization');
}
Logger.instance = new Logger(config);
}
return Logger.instance;
}
/**
* Create default logger configuration
* @returns Default configuration
*/
static createDefaultConfig() {
return {
level: process.env.LOG_LEVEL || 'info',
format: process.env.LOG_FORMAT || 'json',
outputs: process.env.LOG_OUTPUTS ? process.env.LOG_OUTPUTS.split(',') : ['console'],
filePath: process.env.LOG_FILE_PATH || './logs/mcp-server.log',
maxFileSize: parseInt(process.env.LOG_MAX_FILE_SIZE || '10485760'), // 10MB
maxFiles: parseInt(process.env.LOG_MAX_FILES || '5')
};
}
/**
* Ensure log directory exists
*/
ensureLogDirectory() {
if (this.config.outputs.includes('file') && this.config.filePath) {
const logDir = path.dirname(this.config.filePath);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
}
/**
* Set request ID for request tracing
* @param requestId Request identifier
*/
setRequestId(requestId) {
this.requestId = requestId;
}
/**
* Clear request ID
*/
clearRequestId() {
this.requestId = undefined;
}
/**
* Log debug message
* @param message Log message
* @param context Additional context
*/
debug(message, context) {
this.log('debug', message, context);
}
/**
* Log info message
* @param message Log message
* @param context Additional context
*/
info(message, context) {
this.log('info', message, context);
}
/**
* Log warning message
* @param message Log message
* @param context Additional context
*/
warn(message, context) {
this.log('warn', message, context);
}
/**
* Log error message
* @param message Log message
* @param error Optional error object
* @param context Additional context
*/
error(message, error, context) {
const errorContext = {
...context,
...(error && {
errorName: error.name,
errorMessage: error.message,
stack: error.stack
})
};
this.log('error', message, errorContext);
}
/**
* Log request details
* @param message Log message
* @param requestContext Request context
*/
logRequest(message, requestContext) {
this.info(message, requestContext);
}
/**
* Log error with detailed context
* @param message Error message
* @param error Error object
* @param errorContext Error context
*/
logError(message, error, errorContext) {
const context = {
...errorContext,
errorName: error.name,
errorMessage: error.message,
stack: error.stack
};
this.log('error', message, context);
}
/**
* Core logging method
* @param level Log level
* @param message Log message
* @param context Additional context
*/
log(level, message, context) {
if (!this.shouldLog(level)) {
return;
}
const entry = {
timestamp: new Date().toISOString(),
level,
message,
context,
requestId: this.requestId
};
const formattedMessage = this.formatLogEntry(entry);
// Output to configured destinations
this.config.outputs.forEach(output => {
switch (output) {
case 'console':
this.writeToConsole(level, formattedMessage);
break;
case 'file':
this.writeToFile(formattedMessage);
break;
}
});
}
/**
* Format log entry based on configuration
* @param entry Log entry
* @returns Formatted string
*/
formatLogEntry(entry) {
if (this.config.format === 'json') {
return JSON.stringify(entry);
}
else {
// Text format: [timestamp] [level] message [context]
const contextStr = entry.context ? ` ${JSON.stringify(entry.context)}` : '';
const requestIdStr = entry.requestId ? ` [req:${entry.requestId}]` : '';
return `[${entry.timestamp}] [${entry.level.toUpperCase()}]${requestIdStr} ${entry.message}${contextStr}`;
}
}
/**
* Write to console with appropriate method
* @param level Log level
* @param message Formatted message
*/
writeToConsole(level, message) {
switch (level) {
case 'debug':
console.debug(message);
break;
case 'info':
console.info(message);
break;
case 'warn':
console.warn(message);
break;
case 'error':
console.error(message);
break;
}
}
/**
* Write to file with rotation support
* @param message Formatted message
*/
writeToFile(message) {
if (!this.config.filePath) {
return;
}
try {
// Check if file rotation is needed
if (this.shouldRotateFile()) {
this.rotateLogFile();
}
// Append to log file
fs.appendFileSync(this.config.filePath, message + '\n', 'utf8');
}
catch (error) {
// Fallback to console if file writing fails
console.error('Failed to write to log file:', error);
console.log(message);
}
}
/**
* Check if log file should be rotated
* @returns Whether rotation is needed
*/
shouldRotateFile() {
if (!this.config.filePath || !this.config.maxFileSize) {
return false;
}
try {
const stats = fs.statSync(this.config.filePath);
return stats.size >= this.config.maxFileSize;
}
catch {
return false;
}
}
/**
* Rotate log file
*/
rotateLogFile() {
if (!this.config.filePath || !this.config.maxFiles) {
return;
}
try {
const logDir = path.dirname(this.config.filePath);
const logName = path.basename(this.config.filePath, path.extname(this.config.filePath));
const logExt = path.extname(this.config.filePath);
// Rotate existing files
for (let i = this.config.maxFiles - 1; i >= 1; i--) {
const oldFile = path.join(logDir, `${logName}.${i}${logExt}`);
const newFile = path.join(logDir, `${logName}.${i + 1}${logExt}`);
if (fs.existsSync(oldFile)) {
if (i === this.config.maxFiles - 1) {
fs.unlinkSync(oldFile); // Delete oldest file
}
else {
fs.renameSync(oldFile, newFile);
}
}
}
// Move current file to .1
const rotatedFile = path.join(logDir, `${logName}.1${logExt}`);
if (fs.existsSync(this.config.filePath)) {
fs.renameSync(this.config.filePath, rotatedFile);
}
}
catch (error) {
console.error('Failed to rotate log file:', error);
}
}
/**
* Check if message should be logged based on configured level
* @param level Message level
* @returns Whether to log the message
*/
shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error'];
const configLevel = levels.indexOf(this.config.level);
const messageLevel = levels.indexOf(level);
return messageLevel >= configLevel;
}
}

View File

@@ -0,0 +1,86 @@
import { Logger } from './logger';
/**
* Request/Response Logging Middleware
* Provides comprehensive logging for all MCP requests and responses
*/
export declare class RequestLoggingMiddleware {
private logger;
constructor(logger: Logger);
/**
* Generate unique request ID for tracing
* @returns Unique request identifier
*/
generateRequestId(): string;
/**
* Wrap a request handler with logging middleware
* @param handlerName Name of the handler for logging
* @param handler Original handler function
* @returns Wrapped handler with logging
*/
wrapHandler<T, R>(handlerName: string, handler: (request: T) => Promise<R>): (request: T) => Promise<R>;
/**
* Wrap tool call handler with enhanced logging
* @param handler Original tool call handler
* @returns Wrapped handler with tool-specific logging
*/
wrapToolHandler(handler: (name: string, args: any, server: any) => Promise<any>): (name: string, args: any, server: any) => Promise<any>;
/**
* Sanitize request body for logging (remove sensitive data)
* @param request Request object
* @returns Sanitized request data
*/
private sanitizeRequestBody;
/**
* Sanitize tool arguments for logging
* @param toolName Tool name
* @param args Tool arguments
* @returns Sanitized arguments
*/
private sanitizeToolArgs;
/**
* Extract client information from request
* @param request Request object
* @returns Client IP or identifier
*/
private extractClientInfo;
/**
* Categorize errors for better logging
* @param error Error object
* @returns Error category
*/
private categorizeError;
/**
* Categorize tool-specific errors
* @param toolName Tool name
* @param error Error object
* @returns Error category
*/
private categorizeToolError;
/**
* Capture enhanced error context
* @param error Error object
* @param toolName Optional tool name
* @param args Optional tool arguments
* @returns Enhanced error context
*/
captureErrorContext(error: Error, toolName?: string, args?: any): Record<string, any>;
/**
* Capture browser-specific error context
* @param error Error object
* @returns Browser context information
*/
private captureBrowserContext;
/**
* Log system startup information
* @param serverInfo Server information
*/
logServerStartup(serverInfo: {
name: string;
version: string;
capabilities: any;
}): void;
/**
* Log system shutdown information
*/
logServerShutdown(): void;
}

View File

@@ -0,0 +1,350 @@
import { randomUUID } from 'crypto';
/**
* Request/Response Logging Middleware
* Provides comprehensive logging for all MCP requests and responses
*/
export class RequestLoggingMiddleware {
constructor(logger) {
this.logger = logger;
}
/**
* Generate unique request ID for tracing
* @returns Unique request identifier
*/
generateRequestId() {
return randomUUID();
}
/**
* Wrap a request handler with logging middleware
* @param handlerName Name of the handler for logging
* @param handler Original handler function
* @returns Wrapped handler with logging
*/
wrapHandler(handlerName, handler) {
return async (request) => {
const requestId = this.generateRequestId();
const startTime = Date.now();
// Set request ID in logger context
this.logger.setRequestId(requestId);
// Log incoming request
const requestContext = {
method: handlerName,
url: handlerName,
body: this.sanitizeRequestBody(request),
clientIp: this.extractClientInfo(request),
};
this.logger.logRequest(`Incoming ${handlerName} request`, requestContext);
try {
// Execute the original handler
const result = await handler(request);
const duration = Date.now() - startTime;
// Log successful response
const responseContext = {
...requestContext,
statusCode: 200,
duration,
};
this.logger.logRequest(`${handlerName} request completed successfully`, responseContext);
return result;
}
catch (error) {
const duration = Date.now() - startTime;
// Log error response
const errorContext = {
...requestContext,
statusCode: 500,
duration,
};
this.logger.logError(`${handlerName} request failed`, error, {
...errorContext,
category: this.categorizeError(error),
});
throw error;
}
finally {
// Clear request ID from logger context
this.logger.clearRequestId();
}
};
}
/**
* Wrap tool call handler with enhanced logging
* @param handler Original tool call handler
* @returns Wrapped handler with tool-specific logging
*/
wrapToolHandler(handler) {
return async (name, args, server) => {
const requestId = this.generateRequestId();
const startTime = Date.now();
// Set request ID in logger context
this.logger.setRequestId(requestId);
// Log tool execution start
const requestContext = {
method: 'TOOL_CALL',
url: name,
body: this.sanitizeToolArgs(name, args),
};
this.logger.logRequest(`Tool execution started: ${name}`, requestContext);
try {
// Execute the original handler
const result = await handler(name, args, server);
const duration = Date.now() - startTime;
// Log successful tool execution
const responseContext = {
...requestContext,
statusCode: result.isError ? 500 : 200,
duration,
};
if (result.isError) {
this.logger.warn(`Tool execution completed with error: ${name}`, responseContext);
}
else {
this.logger.logRequest(`Tool execution completed successfully: ${name}`, responseContext);
}
return result;
}
catch (error) {
const duration = Date.now() - startTime;
// Log tool execution error
const errorContext = {
...requestContext,
statusCode: 500,
duration,
};
// Capture enhanced error context
const enhancedContext = this.captureErrorContext(error, name, args);
this.logger.logError(`Tool execution failed: ${name}`, error, {
...errorContext,
...enhancedContext,
});
throw error;
}
finally {
// Clear request ID from logger context
this.logger.clearRequestId();
}
};
}
/**
* Sanitize request body for logging (remove sensitive data)
* @param request Request object
* @returns Sanitized request data
*/
sanitizeRequestBody(request) {
if (!request)
return request;
// Create a deep copy to avoid modifying original
const sanitized = JSON.parse(JSON.stringify(request));
// Remove or mask sensitive fields
if (sanitized.params) {
// Remove potential passwords, tokens, etc.
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
sensitiveFields.forEach(field => {
if (sanitized.params[field]) {
sanitized.params[field] = '[REDACTED]';
}
});
}
return sanitized;
}
/**
* Sanitize tool arguments for logging
* @param toolName Tool name
* @param args Tool arguments
* @returns Sanitized arguments
*/
sanitizeToolArgs(toolName, args) {
if (!args)
return args;
// Create a deep copy
const sanitized = JSON.parse(JSON.stringify(args));
// Tool-specific sanitization
switch (toolName) {
case 'playwright_fill':
// Don't log potentially sensitive form data
if (sanitized.text && typeof sanitized.text === 'string' && sanitized.text.length > 100) {
sanitized.text = `[TRUNCATED:${sanitized.text.length}chars]`;
}
break;
case 'playwright_evaluate':
// Truncate long JavaScript code
if (sanitized.script && sanitized.script.length > 500) {
sanitized.script = `[TRUNCATED:${sanitized.script.length}chars]`;
}
break;
case 'playwright_post':
case 'playwright_put':
case 'playwright_patch':
// Sanitize request bodies
if (sanitized.body) {
if (typeof sanitized.body === 'string' && sanitized.body.length > 1000) {
sanitized.body = `[TRUNCATED:${sanitized.body.length}chars]`;
}
else if (typeof sanitized.body === 'object') {
// Remove sensitive fields from request bodies
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
sensitiveFields.forEach(field => {
if (sanitized.body[field]) {
sanitized.body[field] = '[REDACTED]';
}
});
}
}
break;
}
return sanitized;
}
/**
* Extract client information from request
* @param request Request object
* @returns Client IP or identifier
*/
extractClientInfo(request) {
// For MCP over STDIO, we don't have traditional HTTP headers
// This could be enhanced if running over HTTP transport
return 'stdio-client';
}
/**
* Categorize errors for better logging
* @param error Error object
* @returns Error category
*/
categorizeError(error) {
const message = error.message.toLowerCase();
if (message.includes('invalid') || message.includes('validation') || message.includes('required')) {
return 'validation';
}
if (message.includes('browser') || message.includes('page') || message.includes('connection')) {
return 'system';
}
return 'unknown';
}
/**
* Categorize tool-specific errors
* @param toolName Tool name
* @param error Error object
* @returns Error category
*/
categorizeToolError(toolName, error) {
const message = error.message.toLowerCase();
const stack = error.stack?.toLowerCase() || '';
// Validation errors
if (message.includes('invalid') || message.includes('validation') || message.includes('required') ||
message.includes('missing parameter') || message.includes('malformed')) {
return 'validation';
}
// Resource/browser errors
if (message.includes('browser') || message.includes('page') || message.includes('connection') ||
message.includes('closed') || message.includes('disconnected') || message.includes('target') ||
message.includes('protocol error') || message.includes('websocket')) {
return 'resource';
}
// Authentication/Authorization errors
if (message.includes('unauthorized') || message.includes('forbidden') || message.includes('access denied') ||
message.includes('authentication') || message.includes('permission')) {
return 'authentication';
}
// Rate limiting errors
if (message.includes('rate limit') || message.includes('too many requests') || message.includes('throttle')) {
return 'rate_limit';
}
// System/timeout errors
if (toolName.startsWith('playwright_') && (message.includes('timeout') || message.includes('element') ||
message.includes('navigation') || message.includes('waiting') || stack.includes('timeout'))) {
return 'system';
}
return 'unknown';
}
/**
* Capture enhanced error context
* @param error Error object
* @param toolName Optional tool name
* @param args Optional tool arguments
* @returns Enhanced error context
*/
captureErrorContext(error, toolName, args) {
const context = {
errorName: error.name,
errorMessage: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
};
// Add tool-specific context
if (toolName) {
context.toolName = toolName;
context.toolCategory = toolName.split('_')[0]; // e.g., 'playwright' from 'playwright_click'
context.errorCategory = this.categorizeToolError(toolName, error);
// Add sanitized arguments
if (args) {
context.toolArgs = this.sanitizeToolArgs(toolName, args);
}
}
// Add browser-specific context for playwright tools
if (toolName?.startsWith('playwright_')) {
context.browserContext = this.captureBrowserContext(error);
}
return context;
}
/**
* Capture browser-specific error context
* @param error Error object
* @returns Browser context information
*/
captureBrowserContext(error) {
const context = {};
const message = error.message.toLowerCase();
const stack = error.stack?.toLowerCase() || '';
// Detect browser state issues
if (message.includes('closed') || message.includes('disconnected')) {
context.browserState = 'disconnected';
}
else if (message.includes('timeout')) {
context.browserState = 'timeout';
}
else if (message.includes('navigation')) {
context.browserState = 'navigation_error';
}
else {
context.browserState = 'unknown';
}
// Extract timeout information
const timeoutMatch = message.match(/timeout (\d+)ms/);
if (timeoutMatch) {
context.timeoutMs = parseInt(timeoutMatch[1]);
}
// Extract selector information
const selectorMatch = message.match(/selector "([^"]+)"/);
if (selectorMatch) {
context.selector = selectorMatch[1];
}
return context;
}
/**
* Log system startup information
* @param serverInfo Server information
*/
logServerStartup(serverInfo) {
this.logger.info('MCP Server starting up', {
serverName: serverInfo.name,
serverVersion: serverInfo.version,
capabilities: serverInfo.capabilities,
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
});
}
/**
* Log system shutdown information
*/
logServerShutdown() {
this.logger.info('MCP Server shutting down', {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
});
}
}

View File

@@ -0,0 +1,68 @@
/**
* Log Entry Structure
*/
export interface LogEntry {
/** ISO timestamp string */
timestamp: string;
/** Log level */
level: 'debug' | 'info' | 'warn' | 'error';
/** Log message */
message: string;
/** Additional context data */
context?: Record<string, any>;
/** Request ID for tracing */
requestId?: string;
/** User ID for attribution */
userId?: string;
/** Error stack trace if applicable */
stack?: string;
}
/**
* Logger Configuration
*/
export interface LoggerConfig {
/** Minimum log level to output */
level: 'debug' | 'info' | 'warn' | 'error';
/** Output format */
format: 'json' | 'text';
/** Output destinations */
outputs: ('console' | 'file')[];
/** File path for file output */
filePath?: string;
/** Maximum file size before rotation */
maxFileSize?: number;
/** Maximum number of log files to keep */
maxFiles?: number;
}
/**
* Request Logging Context
*/
export interface RequestLogContext {
/** HTTP method */
method?: string;
/** Request URL or path */
url?: string;
/** Request headers */
headers?: Record<string, string>;
/** Request body (sanitized) */
body?: any;
/** Response status code */
statusCode?: number;
/** Request duration in milliseconds */
duration?: number;
/** Client IP address */
clientIp?: string;
/** User agent */
userAgent?: string;
}
/**
* Error Logging Context
*/
export interface ErrorLogContext extends RequestLogContext {
/** Error name */
errorName?: string;
/** Error code */
errorCode?: string;
/** Error category */
category?: 'validation' | 'authentication' | 'authorization' | 'rate_limit' | 'resource' | 'system' | 'unknown';
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,2 @@
export * from './types.js';
export * from './system.js';

View File

@@ -0,0 +1,3 @@
// Monitoring module exports
export * from './types.js';
export * from './system.js';

View File

@@ -0,0 +1,75 @@
import { MonitoringConfig, Metrics, HealthStatus } from './types';
/**
* System Monitoring Class
* Tracks performance metrics and system health
*/
export declare class MonitoringSystem {
private config;
private metrics;
private requestHistory;
private startTime;
private metricsInterval?;
private healthCheckInterval?;
private httpServer?;
private app?;
private actualPort?;
constructor(config: MonitoringConfig);
/**
* Record a request for metrics tracking
* @param duration Request duration in milliseconds
* @param success Whether the request was successful
* @param category Optional request category
*/
recordRequest(duration: number, success: boolean, category?: string): void;
/**
* Record a rate limit violation
*/
recordRateLimitViolation(): void;
/**
* Update active browser count
* @param count Current number of active browsers
*/
updateActiveBrowsers(count: number): void;
/**
* Update active connections count
* @param count Current number of active SSE connections
*/
updateActiveConnections(count: number): void;
/**
* Get current system metrics
* @returns Current metrics snapshot
*/
getMetrics(): Metrics;
/**
* Get system health status
* @returns Current health status
*/
getHealthStatus(): HealthStatus;
/**
* Start metrics collection and HTTP server for health checks
* @param port Optional port for HTTP server (default: 3001)
*/
startMetricsCollection(port?: number): Promise<void>;
/**
* Start HTTP server for health check endpoints
* @param port Port to listen on
*/
private startHttpServer;
/**
* Stop metrics collection and HTTP server
*/
stopMetricsCollection(): Promise<void>;
/**
* Update system-level metrics
*/
private updateSystemMetrics;
/**
* Update average response time based on recent requests
*/
private updateAverageResponseTime;
/**
* Get the actual port the monitoring server is listening on
* @returns The port number, or undefined if server not started
*/
getMonitoringPort(): number | undefined;
}

View File

@@ -0,0 +1,214 @@
import express from 'express';
/**
* System Monitoring Class
* Tracks performance metrics and system health
*/
export class MonitoringSystem {
constructor(config) {
this.requestHistory = [];
this.config = config;
this.startTime = Date.now();
this.metrics = {
requestCount: 0,
errorCount: 0,
averageResponseTime: 0,
activeBrowsers: 0,
memoryUsage: 0,
uptime: 0,
activeConnections: 0,
rateLimitViolations: 0
};
}
/**
* Record a request for metrics tracking
* @param duration Request duration in milliseconds
* @param success Whether the request was successful
* @param category Optional request category
*/
recordRequest(duration, success, category) {
const requestMetric = {
duration,
success,
timestamp: Date.now(),
category
};
this.requestHistory.push(requestMetric);
this.metrics.requestCount++;
if (!success) {
this.metrics.errorCount++;
}
// Keep only recent requests for average calculation (last 1000)
if (this.requestHistory.length > 1000) {
this.requestHistory = this.requestHistory.slice(-1000);
}
this.updateAverageResponseTime();
}
/**
* Record a rate limit violation
*/
recordRateLimitViolation() {
this.metrics.rateLimitViolations++;
}
/**
* Update active browser count
* @param count Current number of active browsers
*/
updateActiveBrowsers(count) {
this.metrics.activeBrowsers = count;
}
/**
* Update active connections count
* @param count Current number of active SSE connections
*/
updateActiveConnections(count) {
this.metrics.activeConnections = count;
}
/**
* Get current system metrics
* @returns Current metrics snapshot
*/
getMetrics() {
this.updateSystemMetrics();
return { ...this.metrics };
}
/**
* Get system health status
* @returns Current health status
*/
getHealthStatus() {
const checks = {};
// Memory check
const memoryUsage = process.memoryUsage();
const memoryUsagePercent = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
checks.memory = {
status: memoryUsagePercent > this.config.memoryThreshold ? 'warn' : 'pass',
description: `Memory usage: ${memoryUsagePercent.toFixed(2)}%`,
data: { memoryUsage, threshold: this.config.memoryThreshold }
};
// Response time check
const avgResponseTime = this.metrics.averageResponseTime;
checks.responseTime = {
status: avgResponseTime > this.config.responseTimeThreshold ? 'warn' : 'pass',
description: `Average response time: ${avgResponseTime.toFixed(2)}ms`,
data: { averageResponseTime: avgResponseTime, threshold: this.config.responseTimeThreshold }
};
// Error rate check
const errorRate = this.metrics.requestCount > 0 ? (this.metrics.errorCount / this.metrics.requestCount) * 100 : 0;
checks.errorRate = {
status: errorRate > 10 ? 'warn' : errorRate > 5 ? 'warn' : 'pass',
description: `Error rate: ${errorRate.toFixed(2)}%`,
data: { errorRate, errorCount: this.metrics.errorCount, requestCount: this.metrics.requestCount }
};
// Overall status
const hasFailures = Object.values(checks).some(check => check.status === 'fail');
const hasWarnings = Object.values(checks).some(check => check.status === 'warn');
const status = hasFailures ? 'unhealthy' : hasWarnings ? 'degraded' : 'healthy';
return {
status,
checks,
timestamp: Date.now(),
version: process.env.npm_package_version
};
}
/**
* Start metrics collection and HTTP server for health checks
* @param port Optional port for HTTP server (default: 3001)
*/
async startMetricsCollection(port = 3001) {
if (!this.config.enabled)
return;
this.metricsInterval = setInterval(() => {
this.updateSystemMetrics();
}, this.config.metricsInterval);
this.healthCheckInterval = setInterval(() => {
// Health checks are performed on-demand via getHealthStatus()
// This interval can be used for proactive health monitoring
}, this.config.healthCheckInterval);
// Start HTTP server for health check endpoints
await this.startHttpServer(port);
}
/**
* Start HTTP server for health check endpoints
* @param port Port to listen on
*/
async startHttpServer(port) {
this.app = express();
this.app.use(express.json());
// Health check endpoint
this.app.get('/health', (req, res) => {
const healthStatus = this.getHealthStatus();
const statusCode = healthStatus.status === 'healthy' ? 200 :
healthStatus.status === 'degraded' ? 200 : 503;
res.status(statusCode).json(healthStatus);
});
// Metrics endpoint
this.app.get('/metrics', (req, res) => {
const metrics = this.getMetrics();
res.json(metrics);
});
// Ready endpoint (simple health check)
this.app.get('/ready', (req, res) => {
res.json({ status: 'ready', timestamp: Date.now() });
});
return new Promise((resolve, reject) => {
this.httpServer = this.app.listen(port, () => {
// Get the actual port assigned (important when using port 0 for dynamic allocation)
const address = this.httpServer.address();
this.actualPort = typeof address === 'object' && address !== null ? address.port : port;
console.log(`Monitoring HTTP server listening on port ${this.actualPort}`);
console.log(`Health check: http://localhost:${this.actualPort}/health`);
console.log(`Metrics: http://localhost:${this.actualPort}/metrics`);
resolve();
});
this.httpServer.on('error', (error) => {
reject(error);
});
});
}
/**
* Stop metrics collection and HTTP server
*/
async stopMetricsCollection() {
if (this.metricsInterval) {
clearInterval(this.metricsInterval);
this.metricsInterval = undefined;
}
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = undefined;
}
if (this.httpServer) {
return new Promise((resolve) => {
this.httpServer.close(() => {
console.log('Monitoring HTTP server stopped');
resolve();
});
});
}
}
/**
* Update system-level metrics
*/
updateSystemMetrics() {
this.metrics.uptime = Date.now() - this.startTime;
this.metrics.memoryUsage = process.memoryUsage().heapUsed;
}
/**
* Update average response time based on recent requests
*/
updateAverageResponseTime() {
if (this.requestHistory.length === 0) {
this.metrics.averageResponseTime = 0;
return;
}
const totalTime = this.requestHistory.reduce((sum, req) => sum + req.duration, 0);
this.metrics.averageResponseTime = totalTime / this.requestHistory.length;
}
/**
* Get the actual port the monitoring server is listening on
* @returns The port number, or undefined if server not started
*/
getMonitoringPort() {
return this.actualPort;
}
}

View File

@@ -0,0 +1,77 @@
/**
* System Metrics Interface
*/
export interface Metrics {
/** Total number of requests processed */
requestCount: number;
/** Total number of errors encountered */
errorCount: number;
/** Average response time in milliseconds */
averageResponseTime: number;
/** Number of active browser instances */
activeBrowsers: number;
/** Memory usage in bytes */
memoryUsage: number;
/** Server uptime in milliseconds */
uptime: number;
/** Number of active SSE connections */
activeConnections: number;
/** Rate limit violations count */
rateLimitViolations: number;
}
/**
* Health Check Status
*/
export interface HealthStatus {
/** Overall system status */
status: 'healthy' | 'degraded' | 'unhealthy';
/** Individual health checks */
checks: Record<string, HealthCheck>;
/** Status timestamp */
timestamp: number;
/** System version */
version?: string;
}
/**
* Individual Health Check
*/
export interface HealthCheck {
/** Check status */
status: 'pass' | 'fail' | 'warn';
/** Check description */
description?: string;
/** Check duration in milliseconds */
duration?: number;
/** Additional check data */
data?: Record<string, any>;
}
/**
* Performance Metrics Configuration
*/
export interface MonitoringConfig {
/** Enable metrics collection */
enabled: boolean;
/** Metrics collection interval in milliseconds */
metricsInterval: number;
/** Health check interval in milliseconds */
healthCheckInterval: number;
/** Memory usage threshold for warnings (percentage) */
memoryThreshold: number;
/** Response time threshold for warnings (milliseconds) */
responseTimeThreshold: number;
}
/**
* Request Performance Data
*/
export interface RequestMetrics {
/** Request duration in milliseconds */
duration: number;
/** Whether request was successful */
success: boolean;
/** Request timestamp */
timestamp: number;
/** Request type/category */
category?: string;
/** Response size in bytes */
responseSize?: number;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,2 @@
export * from './types.js';
export * from './limiter.js';

View File

@@ -0,0 +1,3 @@
// Rate limiting module exports
export * from './types.js';
export * from './limiter.js';

View File

@@ -0,0 +1,107 @@
import { RateLimitConfig, RateLimitResult, RateLimitStats, ResourceLimitConfig } from './types';
/**
* Rate Limiter Class
* Implements request throttling and resource protection
*/
export declare class RateLimiter {
private config;
private store;
private categoryConfigs;
private stats;
private cleanupInterval?;
constructor(config: RateLimitConfig);
/**
* Set rate limit configuration for a specific category
* @param category Rate limit category
* @param config Category-specific configuration
*/
setCategoryConfig(category: string, config: RateLimitConfig): void;
/**
* Check if request is within rate limits
* @param key Rate limit key (usually IP or user ID)
* @param category Request category
* @returns Rate limit check result
*/
checkLimit(key: string, category?: string): RateLimitResult;
/**
* Reset rate limits for a specific key
* @param key Rate limit key to reset
*/
resetLimits(key: string): void;
/**
* Reset all rate limits
*/
resetAllLimits(): void;
/**
* Get rate limiter statistics
* @returns Current statistics
*/
getStats(): RateLimitStats;
/**
* Get current entries count
* @returns Number of active rate limit entries
*/
getActiveEntries(): number;
/**
* Check if a key is currently rate limited
* @param key Rate limit key
* @param category Request category
* @returns Whether the key is rate limited
*/
isRateLimited(key: string, category?: string): boolean;
/**
* Get remaining requests for a key
* @param key Rate limit key
* @param category Request category
* @returns Number of remaining requests
*/
getRemainingRequests(key: string, category?: string): number;
/**
* Start cleanup interval to remove expired entries
*/
private startCleanup;
/**
* Stop cleanup interval
*/
stopCleanup(): void;
/**
* Remove expired rate limit entries
*/
private cleanup;
}
/**
* Resource Limiter Class
* Manages system resource limits
*/
export declare class ResourceLimiter {
private config;
private activeBrowsers;
constructor(config: ResourceLimitConfig);
/**
* Check if browser instance can be created
* @returns Whether browser creation is allowed
*/
canCreateBrowser(): boolean;
/**
* Register a new browser instance
*/
registerBrowser(): void;
/**
* Unregister a browser instance
*/
unregisterBrowser(): void;
/**
* Get current browser count
*/
getActiveBrowserCount(): number;
/**
* Check current memory usage
* @returns Whether memory usage is within limits
*/
checkMemoryUsage(): boolean;
/**
* Get current memory usage percentage
* @returns Memory usage as percentage of limit
*/
getMemoryUsagePercent(): number;
}

View File

@@ -0,0 +1,208 @@
import { RateLimitCategory } from './types';
/**
* Rate Limiter Class
* Implements request throttling and resource protection
*/
export class RateLimiter {
constructor(config) {
this.store = new Map();
this.categoryConfigs = new Map();
this.config = config;
this.stats = {
totalRequests: 0,
blockedRequests: 0,
activeEntries: 0,
byCategory: {},
timestamp: Date.now()
};
// Start cleanup interval to remove expired entries
this.startCleanup();
}
/**
* Set rate limit configuration for a specific category
* @param category Rate limit category
* @param config Category-specific configuration
*/
setCategoryConfig(category, config) {
this.categoryConfigs.set(category, config);
}
/**
* Check if request is within rate limits
* @param key Rate limit key (usually IP or user ID)
* @param category Request category
* @returns Rate limit check result
*/
checkLimit(key, category = RateLimitCategory.GENERAL) {
const config = this.categoryConfigs.get(category) || this.config;
const now = Date.now();
const windowStart = now - config.windowMs;
// Get or create entry for this key
let entry = this.store.get(key);
if (!entry || entry.windowStart < windowStart) {
// Create new window
entry = {
count: 0,
windowStart: now,
firstRequest: now
};
this.store.set(key, entry);
}
// Update statistics
this.stats.totalRequests++;
if (!this.stats.byCategory[category]) {
this.stats.byCategory[category] = { requests: 0, blocked: 0 };
}
this.stats.byCategory[category].requests++;
// Check if limit exceeded
const allowed = entry.count < config.maxRequests;
if (allowed) {
entry.count++;
}
else {
this.stats.blockedRequests++;
this.stats.byCategory[category].blocked++;
}
const resetTime = entry.windowStart + config.windowMs;
const retryAfter = allowed ? undefined : Math.ceil((resetTime - now) / 1000);
return {
allowed,
remaining: Math.max(0, config.maxRequests - entry.count),
resetTime,
retryAfter,
limit: config.maxRequests,
current: entry.count
};
}
/**
* Reset rate limits for a specific key
* @param key Rate limit key to reset
*/
resetLimits(key) {
this.store.delete(key);
}
/**
* Reset all rate limits
*/
resetAllLimits() {
this.store.clear();
}
/**
* Get rate limiter statistics
* @returns Current statistics
*/
getStats() {
this.stats.activeEntries = this.store.size;
this.stats.timestamp = Date.now();
return { ...this.stats };
}
/**
* Get current entries count
* @returns Number of active rate limit entries
*/
getActiveEntries() {
return this.store.size;
}
/**
* Check if a key is currently rate limited
* @param key Rate limit key
* @param category Request category
* @returns Whether the key is rate limited
*/
isRateLimited(key, category = RateLimitCategory.GENERAL) {
const result = this.checkLimit(key, category);
return !result.allowed;
}
/**
* Get remaining requests for a key
* @param key Rate limit key
* @param category Request category
* @returns Number of remaining requests
*/
getRemainingRequests(key, category = RateLimitCategory.GENERAL) {
const result = this.checkLimit(key, category);
return result.remaining;
}
/**
* Start cleanup interval to remove expired entries
*/
startCleanup() {
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 60000); // Cleanup every minute
}
/**
* Stop cleanup interval
*/
stopCleanup() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
}
/**
* Remove expired rate limit entries
*/
cleanup() {
const now = Date.now();
const expiredKeys = [];
for (const [key, entry] of this.store.entries()) {
// Remove entries older than the longest window
const maxWindow = Math.max(this.config.windowMs, ...Array.from(this.categoryConfigs.values()).map(c => c.windowMs));
if (entry.windowStart < now - maxWindow) {
expiredKeys.push(key);
}
}
expiredKeys.forEach(key => this.store.delete(key));
}
}
/**
* Resource Limiter Class
* Manages system resource limits
*/
export class ResourceLimiter {
constructor(config) {
this.activeBrowsers = 0;
this.config = config;
}
/**
* Check if browser instance can be created
* @returns Whether browser creation is allowed
*/
canCreateBrowser() {
return this.activeBrowsers < this.config.maxBrowsers;
}
/**
* Register a new browser instance
*/
registerBrowser() {
this.activeBrowsers++;
}
/**
* Unregister a browser instance
*/
unregisterBrowser() {
this.activeBrowsers = Math.max(0, this.activeBrowsers - 1);
}
/**
* Get current browser count
*/
getActiveBrowserCount() {
return this.activeBrowsers;
}
/**
* Check current memory usage
* @returns Whether memory usage is within limits
*/
checkMemoryUsage() {
const memoryUsage = process.memoryUsage();
return memoryUsage.heapUsed < this.config.maxMemoryUsage;
}
/**
* Get current memory usage percentage
* @returns Memory usage as percentage of limit
*/
getMemoryUsagePercent() {
const memoryUsage = process.memoryUsage();
return (memoryUsage.heapUsed / this.config.maxMemoryUsage) * 100;
}
}

View File

@@ -0,0 +1,89 @@
/**
* Rate Limit Configuration
*/
export interface RateLimitConfig {
/** Time window in milliseconds */
windowMs: number;
/** Maximum requests allowed in the window */
maxRequests: number;
/** Maximum concurrent browser instances */
maxBrowsers: number;
/** Skip counting successful requests */
skipSuccessfulRequests: boolean;
/** Function to generate rate limit key from request */
keyGenerator: (req: any) => string;
/** Custom message for rate limit exceeded */
message?: string;
/** Headers to include in rate limit response */
standardHeaders?: boolean;
}
/**
* Rate Limit Check Result
*/
export interface RateLimitResult {
/** Whether the request is allowed */
allowed: boolean;
/** Number of requests remaining in current window */
remaining: number;
/** Timestamp when the rate limit resets */
resetTime: number;
/** Seconds to wait before retrying (if not allowed) */
retryAfter?: number;
/** Total requests allowed in window */
limit: number;
/** Current request count in window */
current: number;
}
/**
* Rate Limit Store Entry
*/
export interface RateLimitEntry {
/** Request count in current window */
count: number;
/** Window start timestamp */
windowStart: number;
/** First request timestamp in window */
firstRequest: number;
}
/**
* Rate Limit Categories
*/
export declare enum RateLimitCategory {
BROWSER = "browser",
API = "api",
SCREENSHOT = "screenshot",
NAVIGATION = "navigation",
INTERACTION = "interaction",
GENERAL = "general"
}
/**
* Rate Limit Statistics
*/
export interface RateLimitStats {
/** Total requests processed */
totalRequests: number;
/** Total requests blocked */
blockedRequests: number;
/** Active rate limit entries */
activeEntries: number;
/** Statistics by category */
byCategory: Record<string, {
requests: number;
blocked: number;
}>;
/** Current timestamp */
timestamp: number;
}
/**
* Resource Limit Configuration
*/
export interface ResourceLimitConfig {
/** Maximum concurrent browser instances */
maxBrowsers: number;
/** Maximum memory usage in bytes */
maxMemoryUsage: number;
/** Maximum CPU usage percentage */
maxCpuUsage: number;
/** Enable resource monitoring */
enableMonitoring: boolean;
}

View File

@@ -0,0 +1,12 @@
/**
* Rate Limit Categories
*/
export var RateLimitCategory;
(function (RateLimitCategory) {
RateLimitCategory["BROWSER"] = "browser";
RateLimitCategory["API"] = "api";
RateLimitCategory["SCREENSHOT"] = "screenshot";
RateLimitCategory["NAVIGATION"] = "navigation";
RateLimitCategory["INTERACTION"] = "interaction";
RateLimitCategory["GENERAL"] = "general";
})(RateLimitCategory || (RateLimitCategory = {}));

View File

@@ -0,0 +1,4 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { MonitoringSystem } from "./monitoring/index.js";
export declare function setupRequestHandlers(server: Server, tools: Tool[], monitoringSystem?: MonitoringSystem): void;

View File

@@ -0,0 +1,79 @@
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolHandler.js";
import { Logger, RequestLoggingMiddleware } from "./logging/index.js";
export function setupRequestHandlers(server, tools, monitoringSystem) {
// Initialize logger and middleware
const logger = Logger.getInstance(Logger.createDefaultConfig());
const loggingMiddleware = new RequestLoggingMiddleware(logger);
// Helper function to wrap handlers with monitoring
const wrapWithMonitoring = (handler, category) => {
return (async (...args) => {
const startTime = Date.now();
let success = true;
try {
const result = await handler(...args);
return result;
}
catch (error) {
success = false;
throw error;
}
finally {
if (monitoringSystem) {
const duration = Date.now() - startTime;
monitoringSystem.recordRequest(duration, success, category);
}
}
});
};
// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, loggingMiddleware.wrapHandler('ListResources', wrapWithMonitoring(async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(getScreenshots().keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}), 'ListResources')));
// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, loggingMiddleware.wrapHandler('ReadResource', wrapWithMonitoring(async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
const logs = getConsoleLogs().join("\n");
return {
contents: [{
uri,
mimeType: "text/plain",
text: logs,
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = getScreenshots().get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
}, 'ReadResource')));
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, loggingMiddleware.wrapHandler('ListTools', wrapWithMonitoring(async () => ({
tools: tools,
}), 'ListTools')));
// Call tool handler with enhanced tool logging
const wrappedToolHandler = loggingMiddleware.wrapToolHandler(handleToolCall);
server.setRequestHandler(CallToolRequestSchema, loggingMiddleware.wrapHandler('CallTool', wrapWithMonitoring(async (request) => wrappedToolHandler(request.params.name, request.params.arguments ?? {}, server), 'CallTool')));
}

View File

@@ -0,0 +1,2 @@
export * from './types';
export * from './server';

View File

@@ -0,0 +1,3 @@
// SSE (Server-Sent Events) module exports
export * from './types';
export * from './server';

View File

@@ -0,0 +1,46 @@
import { SSEServerConfig, SSEEvent } from './types';
/**
* Server-Sent Events Server Class
* Handles real-time event streaming to connected clients
*/
export declare class SSEServer {
private config;
private clients;
private server;
private actualPort?;
constructor(config: SSEServerConfig);
/**
* Start the SSE server
* @param config Server configuration
* @returns Promise resolving to the actual port number used
*/
start(config: SSEServerConfig): Promise<number>;
/**
* Broadcast an event to all connected clients
* @param event Event to broadcast
*/
broadcast(event: SSEEvent): void;
/**
* Send an event to a specific client
* @param clientId Target client ID
* @param event Event to send
*/
sendToClient(clientId: string, event: SSEEvent): void;
/**
* Get list of connected client IDs
* @returns Array of client IDs
*/
getConnectedClients(): string[];
/**
* Stop the SSE server and cleanup resources
*/
stop(): Promise<void>;
/**
* Get the actual port the server is running on
*/
getPort(): number | undefined;
/**
* Get connected clients count
*/
getClientCount(): number;
}

View File

@@ -0,0 +1,63 @@
/**
* Server-Sent Events Server Class
* Handles real-time event streaming to connected clients
*/
export class SSEServer {
constructor(config) {
this.clients = new Map();
this.config = config;
}
/**
* Start the SSE server
* @param config Server configuration
* @returns Promise resolving to the actual port number used
*/
async start(config) {
this.config = { ...this.config, ...config };
// Implementation will be added in later tasks
throw new Error('SSEServer.start() implementation pending');
}
/**
* Broadcast an event to all connected clients
* @param event Event to broadcast
*/
broadcast(event) {
// Implementation will be added in later tasks
throw new Error('SSEServer.broadcast() implementation pending');
}
/**
* Send an event to a specific client
* @param clientId Target client ID
* @param event Event to send
*/
sendToClient(clientId, event) {
// Implementation will be added in later tasks
throw new Error('SSEServer.sendToClient() implementation pending');
}
/**
* Get list of connected client IDs
* @returns Array of client IDs
*/
getConnectedClients() {
return Array.from(this.clients.keys());
}
/**
* Stop the SSE server and cleanup resources
*/
async stop() {
// Implementation will be added in later tasks
throw new Error('SSEServer.stop() implementation pending');
}
/**
* Get the actual port the server is running on
*/
getPort() {
return this.actualPort;
}
/**
* Get connected clients count
*/
getClientCount() {
return this.clients.size;
}
}

View File

@@ -0,0 +1,53 @@
/**
* SSE Server Configuration Interface
*/
export interface SSEServerConfig {
/** Port to bind the SSE server to. If not specified, will auto-detect available port */
port?: number;
/** Maximum number of concurrent SSE connections */
maxConnections: number;
/** Interval in milliseconds for sending heartbeat messages */
heartbeatInterval: number;
/** Allowed CORS origins for SSE connections */
corsOrigins: string[];
}
/**
* SSE Event Structure
*/
export interface SSEEvent {
/** Event type identifier */
type: string;
/** Event payload data */
data: any;
/** Event timestamp */
timestamp: number;
/** Optional event ID for client-side deduplication */
id?: string;
}
/**
* SSE Event Types Enumeration
*/
export declare enum SSEEventType {
BROWSER_LAUNCHED = "browser.launched",
BROWSER_CLOSED = "browser.closed",
NAVIGATION_START = "navigation.start",
NAVIGATION_COMPLETE = "navigation.complete",
TOOL_EXECUTION_START = "tool.execution.start",
TOOL_EXECUTION_COMPLETE = "tool.execution.complete",
ERROR_OCCURRED = "error.occurred",
SYSTEM_STATUS = "system.status",
RATE_LIMIT_EXCEEDED = "rate_limit.exceeded"
}
/**
* SSE Client Connection Information
*/
export interface SSEClient {
/** Unique client identifier */
id: string;
/** Connection timestamp */
connectedAt: number;
/** Client IP address */
ipAddress: string;
/** User agent string */
userAgent?: string;
}

View File

@@ -0,0 +1,15 @@
/**
* SSE Event Types Enumeration
*/
export var SSEEventType;
(function (SSEEventType) {
SSEEventType["BROWSER_LAUNCHED"] = "browser.launched";
SSEEventType["BROWSER_CLOSED"] = "browser.closed";
SSEEventType["NAVIGATION_START"] = "navigation.start";
SSEEventType["NAVIGATION_COMPLETE"] = "navigation.complete";
SSEEventType["TOOL_EXECUTION_START"] = "tool.execution.start";
SSEEventType["TOOL_EXECUTION_COMPLETE"] = "tool.execution.complete";
SSEEventType["ERROR_OCCURRED"] = "error.occurred";
SSEEventType["SYSTEM_STATUS"] = "system.status";
SSEEventType["RATE_LIMIT_EXCEEDED"] = "rate_limit.exceeded";
})(SSEEventType || (SSEEventType = {}));

View File

@@ -0,0 +1,39 @@
import type { Page } from 'playwright';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
/**
* Resets browser and page variables
* Used when browser is closed
*/
export declare function resetBrowserState(): void;
/**
* Sets the provided page to the global page variable
* @param newPage The Page object to set as the global page
*/
export declare function setGlobalPage(newPage: Page): void;
interface BrowserSettings {
viewport?: {
width?: number;
height?: number;
};
userAgent?: string;
headless?: boolean;
browserType?: 'chromium' | 'firefox' | 'webkit';
}
declare function registerConsoleMessage(page: any): Promise<void>;
/**
* Ensures a browser is launched and returns the page
*/
export declare function ensureBrowser(browserSettings?: BrowserSettings): Promise<Page>;
/**
* Main handler for tool calls
*/
export declare function handleToolCall(name: string, args: any, server: any): Promise<CallToolResult>;
/**
* Get console logs
*/
export declare function getConsoleLogs(): string[];
/**
* Get screenshots
*/
export declare function getScreenshots(): Map<string, string>;
export { registerConsoleMessage };

View File

@@ -0,0 +1,636 @@
import { chromium, firefox, webkit, request } from 'playwright';
import { BROWSER_TOOLS, API_TOOLS } from './tools.js';
import { ActionRecorder } from './tools/codegen/recorder.js';
import { spawn } from 'child_process';
import { startCodegenSession, endCodegenSession, getCodegenSession, clearCodegenSession } from './tools/codegen/index.js';
import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool, ExpectResponseTool, AssertResponseTool, CustomUserAgentTool, ResizeTool } from './tools/browser/index.js';
import { ClickTool, IframeClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, IframeFillTool, UploadFileTool } from './tools/browser/interaction.js';
import { VisibleTextTool, VisibleHtmlTool } from './tools/browser/visiblePage.js';
import { GetRequestTool, PostRequestTool, PutRequestTool, PatchRequestTool, DeleteRequestTool } from './tools/api/requests.js';
import { GoBackTool, GoForwardTool } from './tools/browser/navigation.js';
import { DragTool, PressKeyTool } from './tools/browser/interaction.js';
import { SaveAsPdfTool } from './tools/browser/output.js';
import { ClickAndSwitchTabTool } from './tools/browser/interaction.js';
// Global state
let browser;
let page;
let currentBrowserType = 'chromium';
/**
* Resets browser and page variables
* Used when browser is closed
*/
export function resetBrowserState() {
browser = undefined;
page = undefined;
currentBrowserType = 'chromium';
}
/**
* Sets the provided page to the global page variable
* @param newPage The Page object to set as the global page
*/
export function setGlobalPage(newPage) {
page = newPage;
page.bringToFront(); // Bring the new tab to the front
}
// Tool instances
let screenshotTool;
let navigationTool;
let closeBrowserTool;
let consoleLogsTool;
let clickTool;
let iframeClickTool;
let iframeFillTool;
let fillTool;
let selectTool;
let hoverTool;
let uploadFileTool;
let evaluateTool;
let expectResponseTool;
let assertResponseTool;
let customUserAgentTool;
let visibleTextTool;
let visibleHtmlTool;
let resizeTool;
let getRequestTool;
let postRequestTool;
let putRequestTool;
let patchRequestTool;
let deleteRequestTool;
// Add these variables at the top with other tool declarations
let goBackTool;
let goForwardTool;
let dragTool;
let pressKeyTool;
let saveAsPdfTool;
let clickAndSwitchTabTool;
async function registerConsoleMessage(page) {
page.on("console", (msg) => {
if (consoleLogsTool) {
const type = msg.type();
const text = msg.text();
// "Unhandled Rejection In Promise" we injected
if (text.startsWith("[Playwright]")) {
const payload = text.replace("[Playwright]", "");
if (consoleLogsTool) {
consoleLogsTool.registerConsoleMessage("exception", payload);
}
}
else {
if (consoleLogsTool) {
consoleLogsTool.registerConsoleMessage(type, text);
}
}
}
});
// Uncaught exception
page.on("pageerror", (error) => {
if (consoleLogsTool) {
const message = error.message;
const stack = error.stack || "";
consoleLogsTool.registerConsoleMessage("exception", `${message}\n${stack}`);
}
});
// Unhandled rejection in promise
await page.addInitScript(() => {
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
const message = typeof reason === "object" && reason !== null
? reason.message || JSON.stringify(reason)
: String(reason);
const stack = reason?.stack || "";
// Use console.error get "Unhandled Rejection In Promise"
console.error(`[Playwright][Unhandled Rejection In Promise] ${message}\n${stack}`);
});
});
}
/**
* Attempts to install browsers automatically
*/
async function installBrowsers(browserType = 'chromium') {
return new Promise((resolve) => {
console.error(`[Playwright MCP] Attempting to install ${browserType} browser...`);
const installProcess = spawn('npx', ['playwright', 'install', browserType], {
stdio: ['ignore', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
installProcess.stdout?.on('data', (data) => {
output += data.toString();
});
installProcess.stderr?.on('data', (data) => {
errorOutput += data.toString();
});
installProcess.on('close', (code) => {
if (code === 0) {
console.error(`[Playwright MCP] Successfully installed ${browserType} browser`);
resolve({
success: true,
message: `Successfully installed ${browserType} browser. Please try your request again.`
});
}
else {
console.error(`[Playwright MCP] Failed to install browser: ${errorOutput}`);
resolve({
success: false,
message: `Failed to automatically install ${browserType} browser. Please run: npx playwright install ${browserType}`
});
}
});
installProcess.on('error', (error) => {
console.error(`[Playwright MCP] Error during browser installation: ${error.message}`);
resolve({
success: false,
message: `Error during installation: ${error.message}. Please run: npx playwright install ${browserType}`
});
});
// Timeout after 2 minutes
setTimeout(() => {
installProcess.kill();
resolve({
success: false,
message: `Browser installation timed out. Please run manually: npx playwright install ${browserType}`
});
}, 120000);
});
}
/**
* Ensures a browser is launched and returns the page
*/
export async function ensureBrowser(browserSettings) {
try {
// Check if browser exists but is disconnected
if (browser && !browser.isConnected()) {
try {
await browser.close().catch(() => { });
}
catch (e) {
// Ignore errors when closing disconnected browser
}
// Reset browser and page references
resetBrowserState();
}
// Launch new browser if needed
if (!browser) {
const { viewport, userAgent, headless = false, browserType = 'chromium' } = browserSettings ?? {};
// If browser type is changing, force a new browser instance
if (browser && currentBrowserType !== browserType) {
try {
await browser.close().catch(() => { });
}
catch (e) {
// Ignore errors
}
resetBrowserState();
}
// Use the appropriate browser engine
let browserInstance;
switch (browserType) {
case 'firefox':
browserInstance = firefox;
break;
case 'webkit':
browserInstance = webkit;
break;
case 'chromium':
default:
browserInstance = chromium;
break;
}
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
try {
browser = await browserInstance.launch({
headless,
executablePath: executablePath
});
currentBrowserType = browserType;
}
catch (launchError) {
// Check if error is due to missing browser executable
if (launchError.message?.includes("Executable doesn't exist") ||
launchError.message?.includes("Failed to launch") ||
launchError.message?.includes("browserType.launch")) {
console.error(`[Playwright MCP] Browser not found, attempting auto-installation...`);
const installResult = await installBrowsers(browserType);
if (installResult.success) {
// Try launching again after installation
browser = await browserInstance.launch({
headless,
executablePath: executablePath
});
currentBrowserType = browserType;
}
else {
throw new Error(installResult.message);
}
}
else {
throw launchError;
}
}
// Add cleanup logic when browser is disconnected
browser.on('disconnected', () => {
browser = undefined;
page = undefined;
});
const context = await browser.newContext({
...userAgent && { userAgent },
viewport: {
width: viewport?.width ?? 1280,
height: viewport?.height ?? 720,
},
deviceScaleFactor: 1,
});
page = await context.newPage();
// Register console message handler
await registerConsoleMessage(page);
}
// Verify page is still valid
if (!page || page.isClosed()) {
// Create a new page if the current one is invalid
const context = browser.contexts()[0] || await browser.newContext();
page = await context.newPage();
// Re-register console message handler
await registerConsoleMessage(page);
}
return page;
}
catch (error) {
// If something went wrong, clean up completely and retry once
try {
if (browser) {
await browser.close().catch(() => { });
}
}
catch (e) {
// Ignore errors during cleanup
}
resetBrowserState();
// Check if error is due to missing browser, if so attempt install
const errorMessage = error.message;
if (errorMessage?.includes("Executable doesn't exist") ||
errorMessage?.includes("Failed to launch") ||
errorMessage?.includes("browserType.launch")) {
const { browserType = 'chromium' } = browserSettings ?? {};
console.error(`[Playwright MCP] Browser not found in retry, attempting auto-installation...`);
const installResult = await installBrowsers(browserType);
if (!installResult.success) {
throw new Error(installResult.message);
}
// If installation successful, continue to retry
}
// Try one more time from scratch
const { viewport, userAgent, headless = false, browserType = 'chromium' } = browserSettings ?? {};
// Use the appropriate browser engine
let browserInstance;
switch (browserType) {
case 'firefox':
browserInstance = firefox;
break;
case 'webkit':
browserInstance = webkit;
break;
case 'chromium':
default:
browserInstance = chromium;
break;
}
browser = await browserInstance.launch({ headless });
currentBrowserType = browserType;
browser.on('disconnected', () => {
browser = undefined;
page = undefined;
});
const context = await browser.newContext({
...userAgent && { userAgent },
viewport: {
width: viewport?.width ?? 1280,
height: viewport?.height ?? 720,
},
deviceScaleFactor: 1,
});
page = await context.newPage();
await registerConsoleMessage(page);
return page;
}
}
/**
* Creates a new API request context
*/
async function ensureApiContext(url) {
return await request.newContext({
baseURL: url,
});
}
/**
* Initialize all tool instances
*/
function initializeTools(server) {
// Browser tools
if (!screenshotTool)
screenshotTool = new ScreenshotTool(server);
if (!navigationTool)
navigationTool = new NavigationTool(server);
if (!closeBrowserTool)
closeBrowserTool = new CloseBrowserTool(server);
if (!consoleLogsTool)
consoleLogsTool = new ConsoleLogsTool(server);
if (!clickTool)
clickTool = new ClickTool(server);
if (!iframeClickTool)
iframeClickTool = new IframeClickTool(server);
if (!iframeFillTool)
iframeFillTool = new IframeFillTool(server);
if (!fillTool)
fillTool = new FillTool(server);
if (!selectTool)
selectTool = new SelectTool(server);
if (!hoverTool)
hoverTool = new HoverTool(server);
if (!uploadFileTool)
uploadFileTool = new UploadFileTool(server);
if (!evaluateTool)
evaluateTool = new EvaluateTool(server);
if (!expectResponseTool)
expectResponseTool = new ExpectResponseTool(server);
if (!assertResponseTool)
assertResponseTool = new AssertResponseTool(server);
if (!customUserAgentTool)
customUserAgentTool = new CustomUserAgentTool(server);
if (!visibleTextTool)
visibleTextTool = new VisibleTextTool(server);
if (!visibleHtmlTool)
visibleHtmlTool = new VisibleHtmlTool(server);
if (!resizeTool)
resizeTool = new ResizeTool(server);
// API tools
if (!getRequestTool)
getRequestTool = new GetRequestTool(server);
if (!postRequestTool)
postRequestTool = new PostRequestTool(server);
if (!putRequestTool)
putRequestTool = new PutRequestTool(server);
if (!patchRequestTool)
patchRequestTool = new PatchRequestTool(server);
if (!deleteRequestTool)
deleteRequestTool = new DeleteRequestTool(server);
// Initialize new tools
if (!goBackTool)
goBackTool = new GoBackTool(server);
if (!goForwardTool)
goForwardTool = new GoForwardTool(server);
if (!dragTool)
dragTool = new DragTool(server);
if (!pressKeyTool)
pressKeyTool = new PressKeyTool(server);
if (!saveAsPdfTool)
saveAsPdfTool = new SaveAsPdfTool(server);
if (!clickAndSwitchTabTool)
clickAndSwitchTabTool = new ClickAndSwitchTabTool(server);
}
/**
* Main handler for tool calls
*/
export async function handleToolCall(name, args, server) {
// Initialize tools
initializeTools(server);
try {
// Handle codegen tools
switch (name) {
case 'start_codegen_session':
return await handleCodegenResult(startCodegenSession.handler(args));
case 'end_codegen_session':
return await handleCodegenResult(endCodegenSession.handler(args));
case 'get_codegen_session':
return await handleCodegenResult(getCodegenSession.handler(args));
case 'clear_codegen_session':
return await handleCodegenResult(clearCodegenSession.handler(args));
}
// Record tool action if there's an active session
const recorder = ActionRecorder.getInstance();
const activeSession = recorder.getActiveSession();
if (activeSession && name !== 'playwright_close') {
recorder.recordAction(name, args);
}
// Special case for browser close to ensure it always works
if (name === "playwright_close") {
if (browser) {
try {
if (browser.isConnected()) {
await browser.close().catch(e => console.error("Error closing browser:", e));
}
}
catch (error) {
console.error("Error during browser close in handler:", error);
}
finally {
resetBrowserState();
}
return {
content: [{
type: "text",
text: "Browser closed successfully",
}],
isError: false,
};
}
return {
content: [{
type: "text",
text: "No browser instance to close",
}],
isError: false,
};
}
// Check if we have a disconnected browser that needs cleanup
if (browser && !browser.isConnected() && BROWSER_TOOLS.includes(name)) {
try {
await browser.close().catch(() => { }); // Ignore errors
}
catch (e) {
// Ignore any errors during cleanup
}
resetBrowserState();
}
// Prepare context based on tool requirements
const context = {
server
};
// Set up browser if needed
if (BROWSER_TOOLS.includes(name)) {
const browserSettings = {
viewport: {
width: args.width,
height: args.height
},
userAgent: name === "playwright_custom_user_agent" ? args.userAgent : undefined,
headless: args.headless,
browserType: args.browserType || 'chromium'
};
try {
context.page = await ensureBrowser(browserSettings);
context.browser = browser;
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to initialize browser: ${error.message}. Please try again.`,
}],
isError: true,
};
}
}
// Set up API context if needed
if (API_TOOLS.includes(name)) {
try {
context.apiContext = await ensureApiContext(args.url);
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to initialize API context: ${error.message}`,
}],
isError: true,
};
}
}
// Route to appropriate tool
switch (name) {
// Browser tools
case "playwright_navigate":
return await navigationTool.execute(args, context);
case "playwright_screenshot":
return await screenshotTool.execute(args, context);
case "playwright_resize":
return await resizeTool.execute(args, context);
case "playwright_close":
return await closeBrowserTool.execute(args, context);
case "playwright_console_logs":
return await consoleLogsTool.execute(args, context);
case "playwright_click":
return await clickTool.execute(args, context);
case "playwright_iframe_click":
return await iframeClickTool.execute(args, context);
case "playwright_iframe_fill":
return await iframeFillTool.execute(args, context);
case "playwright_fill":
return await fillTool.execute(args, context);
case "playwright_select":
return await selectTool.execute(args, context);
case "playwright_hover":
return await hoverTool.execute(args, context);
case "playwright_upload_file":
return await uploadFileTool.execute(args, context);
case "playwright_evaluate":
return await evaluateTool.execute(args, context);
case "playwright_expect_response":
return await expectResponseTool.execute(args, context);
case "playwright_assert_response":
return await assertResponseTool.execute(args, context);
case "playwright_custom_user_agent":
return await customUserAgentTool.execute(args, context);
case "playwright_get_visible_text":
return await visibleTextTool.execute(args, context);
case "playwright_get_visible_html":
return await visibleHtmlTool.execute(args, context);
// API tools
case "playwright_get":
return await getRequestTool.execute(args, context);
case "playwright_post":
return await postRequestTool.execute(args, context);
case "playwright_put":
return await putRequestTool.execute(args, context);
case "playwright_patch":
return await patchRequestTool.execute(args, context);
case "playwright_delete":
return await deleteRequestTool.execute(args, context);
// New tools
case "playwright_go_back":
return await goBackTool.execute(args, context);
case "playwright_go_forward":
return await goForwardTool.execute(args, context);
case "playwright_drag":
return await dragTool.execute(args, context);
case "playwright_press_key":
return await pressKeyTool.execute(args, context);
case "playwright_save_as_pdf":
return await saveAsPdfTool.execute(args, context);
case "playwright_click_and_switch_tab":
return await clickAndSwitchTabTool.execute(args, context);
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
catch (error) {
// Handle browser-specific errors at the top level
if (BROWSER_TOOLS.includes(name)) {
const errorMessage = error.message;
if (errorMessage.includes("Target page, context or browser has been closed") ||
errorMessage.includes("Browser has been disconnected") ||
errorMessage.includes("Target closed") ||
errorMessage.includes("Protocol error") ||
errorMessage.includes("Connection closed")) {
// Reset browser state if it's a connection issue
resetBrowserState();
return {
content: [{
type: "text",
text: `Browser connection error: ${errorMessage}. Browser state has been reset, please try again.`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: error instanceof Error ? error.message : String(error),
}],
isError: true,
};
}
}
/**
* Helper function to handle codegen tool results
*/
async function handleCodegenResult(resultPromise) {
try {
const result = await resultPromise;
return {
content: [{
type: "text",
text: JSON.stringify(result),
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: error instanceof Error ? error.message : String(error),
}],
isError: true,
};
}
}
/**
* Get console logs
*/
export function getConsoleLogs() {
return consoleLogsTool?.getConsoleLogs() ?? [];
}
/**
* Get screenshots
*/
export function getScreenshots() {
return screenshotTool?.getScreenshots() ?? new Map();
}
export { registerConsoleMessage };

View File

@@ -0,0 +1,673 @@
export declare function createToolDefinitions(): [{
readonly name: "start_codegen_session";
readonly description: "Start a new code generation session to record Playwright actions";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly options: {
readonly type: "object";
readonly description: "Code generation options";
readonly properties: {
readonly outputPath: {
readonly type: "string";
readonly description: "Directory path where generated tests will be saved (use absolute path)";
};
readonly testNamePrefix: {
readonly type: "string";
readonly description: "Prefix to use for generated test names (default: 'GeneratedTest')";
};
readonly includeComments: {
readonly type: "boolean";
readonly description: "Whether to include descriptive comments in generated tests";
};
};
readonly required: readonly ["outputPath"];
};
};
readonly required: ["options"];
};
}, {
readonly name: "end_codegen_session";
readonly description: "End a code generation session and generate the test file";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly sessionId: {
readonly type: "string";
readonly description: "ID of the session to end";
};
};
readonly required: ["sessionId"];
};
}, {
readonly name: "get_codegen_session";
readonly description: "Get information about a code generation session";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly sessionId: {
readonly type: "string";
readonly description: "ID of the session to retrieve";
};
};
readonly required: ["sessionId"];
};
}, {
readonly name: "clear_codegen_session";
readonly description: "Clear a code generation session without generating a test";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly sessionId: {
readonly type: "string";
readonly description: "ID of the session to clear";
};
};
readonly required: ["sessionId"];
};
}, {
readonly name: "playwright_navigate";
readonly description: "Navigate to a URL";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to navigate to the website specified";
};
readonly browserType: {
readonly type: "string";
readonly description: "Browser type to use (chromium, firefox, webkit). Defaults to chromium";
readonly enum: readonly ["chromium", "firefox", "webkit"];
};
readonly width: {
readonly type: "number";
readonly description: "Viewport width in pixels (default: 1280)";
};
readonly height: {
readonly type: "number";
readonly description: "Viewport height in pixels (default: 720)";
};
readonly timeout: {
readonly type: "number";
readonly description: "Navigation timeout in milliseconds";
};
readonly waitUntil: {
readonly type: "string";
readonly description: "Navigation wait condition";
};
readonly headless: {
readonly type: "boolean";
readonly description: "Run browser in headless mode (default: false)";
};
};
readonly required: ["url"];
};
}, {
readonly name: "playwright_screenshot";
readonly description: "Take a screenshot of the current page or a specific element";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly name: {
readonly type: "string";
readonly description: "Name for the screenshot";
};
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for element to screenshot";
};
readonly width: {
readonly type: "number";
readonly description: "Width in pixels (default: 800)";
};
readonly height: {
readonly type: "number";
readonly description: "Height in pixels (default: 600)";
};
readonly storeBase64: {
readonly type: "boolean";
readonly description: "Store screenshot in base64 format (default: true)";
};
readonly fullPage: {
readonly type: "boolean";
readonly description: "Store screenshot of the entire page (default: false)";
};
readonly savePng: {
readonly type: "boolean";
readonly description: "Save screenshot as PNG file (default: false)";
};
readonly downloadsDir: {
readonly type: "string";
readonly description: "Custom downloads directory path (default: user's Downloads folder)";
};
};
readonly required: ["name"];
};
}, {
readonly name: "playwright_click";
readonly description: "Click an element on the page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for the element to click";
};
};
readonly required: ["selector"];
};
}, {
readonly name: "playwright_iframe_click";
readonly description: "Click an element in an iframe on the page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly iframeSelector: {
readonly type: "string";
readonly description: "CSS selector for the iframe containing the element to click";
};
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for the element to click";
};
};
readonly required: ["iframeSelector", "selector"];
};
}, {
readonly name: "playwright_iframe_fill";
readonly description: "Fill an element in an iframe on the page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly iframeSelector: {
readonly type: "string";
readonly description: "CSS selector for the iframe containing the element to fill";
};
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for the element to fill";
};
readonly value: {
readonly type: "string";
readonly description: "Value to fill";
};
};
readonly required: ["iframeSelector", "selector", "value"];
};
}, {
readonly name: "playwright_fill";
readonly description: "fill out an input field";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for input field";
};
readonly value: {
readonly type: "string";
readonly description: "Value to fill";
};
};
readonly required: ["selector", "value"];
};
}, {
readonly name: "playwright_select";
readonly description: "Select an element on the page with Select tag";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for element to select";
};
readonly value: {
readonly type: "string";
readonly description: "Value to select";
};
};
readonly required: ["selector", "value"];
};
}, {
readonly name: "playwright_hover";
readonly description: "Hover an element on the page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for element to hover";
};
};
readonly required: ["selector"];
};
}, {
readonly name: "playwright_upload_file";
readonly description: "Upload a file to an input[type='file'] element on the page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for the file input element";
};
readonly filePath: {
readonly type: "string";
readonly description: "Absolute path to the file to upload";
};
};
readonly required: ["selector", "filePath"];
};
}, {
readonly name: "playwright_evaluate";
readonly description: "Execute JavaScript in the browser console";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly script: {
readonly type: "string";
readonly description: "JavaScript code to execute";
};
};
readonly required: ["script"];
};
}, {
readonly name: "playwright_console_logs";
readonly description: "Retrieve console logs from the browser with filtering options";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly type: {
readonly type: "string";
readonly description: "Type of logs to retrieve (all, error, warning, log, info, debug, exception)";
readonly enum: readonly ["all", "error", "warning", "log", "info", "debug", "exception"];
};
readonly search: {
readonly type: "string";
readonly description: "Text to search for in logs (handles text with square brackets)";
};
readonly limit: {
readonly type: "number";
readonly description: "Maximum number of logs to return";
};
readonly clear: {
readonly type: "boolean";
readonly description: "Whether to clear logs after retrieval (default: false)";
};
};
readonly required: [];
};
}, {
readonly name: "playwright_resize";
readonly description: "Resize the browser viewport using manual dimensions or device presets. Supports 143+ device presets including iPhone, iPad, Android devices, and desktop browsers with proper user-agent and touch emulation.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly device: {
readonly type: "string";
readonly description: "Device preset name (e.g., 'iPhone 13', 'iPad Pro 11', 'Pixel 7', 'Galaxy S24', 'Desktop Chrome'). Automatically configures viewport, user-agent, and device capabilities. Use playwright.devices to see all available devices.";
};
readonly width: {
readonly type: "number";
readonly description: "Viewport width in pixels (for manual resize without device preset)";
};
readonly height: {
readonly type: "number";
readonly description: "Viewport height in pixels (for manual resize without device preset)";
};
readonly orientation: {
readonly type: "string";
readonly description: "Device orientation: 'portrait' or 'landscape' (only applies when using device preset)";
readonly enum: readonly ["portrait", "landscape"];
};
};
readonly required: [];
};
}, {
readonly name: "playwright_close";
readonly description: "Close the browser and release all resources";
readonly inputSchema: {
readonly type: "object";
readonly properties: {};
readonly required: [];
};
}, {
readonly name: "playwright_get";
readonly description: "Perform an HTTP GET request";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to perform GET operation";
};
readonly token: {
readonly type: "string";
readonly description: "Bearer token for authorization";
};
readonly headers: {
readonly type: "object";
readonly description: "Additional headers to include in the request";
readonly additionalProperties: {
readonly type: "string";
};
};
};
readonly required: ["url"];
};
}, {
readonly name: "playwright_post";
readonly description: "Perform an HTTP POST request";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to perform POST operation";
};
readonly value: {
readonly type: "string";
readonly description: "Data to post in the body";
};
readonly token: {
readonly type: "string";
readonly description: "Bearer token for authorization";
};
readonly headers: {
readonly type: "object";
readonly description: "Additional headers to include in the request";
readonly additionalProperties: {
readonly type: "string";
};
};
};
readonly required: ["url", "value"];
};
}, {
readonly name: "playwright_put";
readonly description: "Perform an HTTP PUT request";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to perform PUT operation";
};
readonly value: {
readonly type: "string";
readonly description: "Data to PUT in the body";
};
readonly token: {
readonly type: "string";
readonly description: "Bearer token for authorization";
};
readonly headers: {
readonly type: "object";
readonly description: "Additional headers to include in the request";
readonly additionalProperties: {
readonly type: "string";
};
};
};
readonly required: ["url", "value"];
};
}, {
readonly name: "playwright_patch";
readonly description: "Perform an HTTP PATCH request";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to perform PATCH operation";
};
readonly value: {
readonly type: "string";
readonly description: "Data to PATCH in the body";
};
readonly token: {
readonly type: "string";
readonly description: "Bearer token for authorization";
};
readonly headers: {
readonly type: "object";
readonly description: "Additional headers to include in the request";
readonly additionalProperties: {
readonly type: "string";
};
};
};
readonly required: ["url", "value"];
};
}, {
readonly name: "playwright_delete";
readonly description: "Perform an HTTP DELETE request";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly url: {
readonly type: "string";
readonly description: "URL to perform DELETE operation";
};
readonly token: {
readonly type: "string";
readonly description: "Bearer token for authorization";
};
readonly headers: {
readonly type: "object";
readonly description: "Additional headers to include in the request";
readonly additionalProperties: {
readonly type: "string";
};
};
};
readonly required: ["url"];
};
}, {
readonly name: "playwright_expect_response";
readonly description: "Ask Playwright to start waiting for a HTTP response. This tool initiates the wait operation but does not wait for its completion.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly id: {
readonly type: "string";
readonly description: "Unique & arbitrary identifier to be used for retrieving this response later with `Playwright_assert_response`.";
};
readonly url: {
readonly type: "string";
readonly description: "URL pattern to match in the response.";
};
};
readonly required: ["id", "url"];
};
}, {
readonly name: "playwright_assert_response";
readonly description: "Wait for and validate a previously initiated HTTP response wait operation.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly id: {
readonly type: "string";
readonly description: "Identifier of the HTTP response initially expected using `Playwright_expect_response`.";
};
readonly value: {
readonly type: "string";
readonly description: "Data to expect in the body of the HTTP response. If provided, the assertion will fail if this value is not found in the response body.";
};
};
readonly required: ["id"];
};
}, {
readonly name: "playwright_custom_user_agent";
readonly description: "Set a custom User Agent for the browser";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly userAgent: {
readonly type: "string";
readonly description: "Custom User Agent for the Playwright browser instance";
};
};
readonly required: ["userAgent"];
};
}, {
readonly name: "playwright_get_visible_text";
readonly description: "Get the visible text content of the current page";
readonly inputSchema: {
readonly type: "object";
readonly properties: {};
readonly required: [];
};
}, {
readonly name: "playwright_get_visible_html";
readonly description: "Get the HTML content of the current page. By default, all <script> tags are removed from the output unless removeScripts is explicitly set to false.";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector to limit the HTML to a specific container";
};
readonly removeScripts: {
readonly type: "boolean";
readonly description: "Remove all script tags from the HTML (default: true)";
};
readonly removeComments: {
readonly type: "boolean";
readonly description: "Remove all HTML comments (default: false)";
};
readonly removeStyles: {
readonly type: "boolean";
readonly description: "Remove all style tags from the HTML (default: false)";
};
readonly removeMeta: {
readonly type: "boolean";
readonly description: "Remove all meta tags from the HTML (default: false)";
};
readonly cleanHtml: {
readonly type: "boolean";
readonly description: "Perform comprehensive HTML cleaning (default: false)";
};
readonly minify: {
readonly type: "boolean";
readonly description: "Minify the HTML output (default: false)";
};
readonly maxLength: {
readonly type: "number";
readonly description: "Maximum number of characters to return (default: 20000)";
};
};
readonly required: [];
};
}, {
readonly name: "playwright_go_back";
readonly description: "Navigate back in browser history";
readonly inputSchema: {
readonly type: "object";
readonly properties: {};
readonly required: [];
};
}, {
readonly name: "playwright_go_forward";
readonly description: "Navigate forward in browser history";
readonly inputSchema: {
readonly type: "object";
readonly properties: {};
readonly required: [];
};
}, {
readonly name: "playwright_drag";
readonly description: "Drag an element to a target location";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly sourceSelector: {
readonly type: "string";
readonly description: "CSS selector for the element to drag";
};
readonly targetSelector: {
readonly type: "string";
readonly description: "CSS selector for the target location";
};
};
readonly required: ["sourceSelector", "targetSelector"];
};
}, {
readonly name: "playwright_press_key";
readonly description: "Press a keyboard key";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly key: {
readonly type: "string";
readonly description: "Key to press (e.g. 'Enter', 'ArrowDown', 'a')";
};
readonly selector: {
readonly type: "string";
readonly description: "Optional CSS selector to focus before pressing key";
};
};
readonly required: ["key"];
};
}, {
readonly name: "playwright_save_as_pdf";
readonly description: "Save the current page as a PDF file";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly outputPath: {
readonly type: "string";
readonly description: "Directory path where PDF will be saved";
};
readonly filename: {
readonly type: "string";
readonly description: "Name of the PDF file (default: page.pdf)";
};
readonly format: {
readonly type: "string";
readonly description: "Page format (e.g. 'A4', 'Letter')";
};
readonly printBackground: {
readonly type: "boolean";
readonly description: "Whether to print background graphics";
};
readonly margin: {
readonly type: "object";
readonly description: "Page margins";
readonly properties: {
readonly top: {
readonly type: "string";
};
readonly right: {
readonly type: "string";
};
readonly bottom: {
readonly type: "string";
};
readonly left: {
readonly type: "string";
};
};
};
};
readonly required: ["outputPath"];
};
}, {
readonly name: "playwright_click_and_switch_tab";
readonly description: "Click a link and switch to the newly opened tab";
readonly inputSchema: {
readonly type: "object";
readonly properties: {
readonly selector: {
readonly type: "string";
readonly description: "CSS selector for the link to click";
};
};
readonly required: ["selector"];
};
}];
export declare const BROWSER_TOOLS: string[];
export declare const API_TOOLS: string[];
export declare const CODEGEN_TOOLS: string[];
export declare const tools: string[];

View File

@@ -0,0 +1,542 @@
export function createToolDefinitions() {
return [
// Codegen tools
{
name: "start_codegen_session",
description: "Start a new code generation session to record Playwright actions",
inputSchema: {
type: "object",
properties: {
options: {
type: "object",
description: "Code generation options",
properties: {
outputPath: {
type: "string",
description: "Directory path where generated tests will be saved (use absolute path)"
},
testNamePrefix: {
type: "string",
description: "Prefix to use for generated test names (default: 'GeneratedTest')"
},
includeComments: {
type: "boolean",
description: "Whether to include descriptive comments in generated tests"
}
},
required: ["outputPath"]
}
},
required: ["options"]
}
},
{
name: "end_codegen_session",
description: "End a code generation session and generate the test file",
inputSchema: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to end"
}
},
required: ["sessionId"]
}
},
{
name: "get_codegen_session",
description: "Get information about a code generation session",
inputSchema: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to retrieve"
}
},
required: ["sessionId"]
}
},
{
name: "clear_codegen_session",
description: "Clear a code generation session without generating a test",
inputSchema: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to clear"
}
},
required: ["sessionId"]
}
},
{
name: "playwright_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to navigate to the website specified" },
browserType: { type: "string", description: "Browser type to use (chromium, firefox, webkit). Defaults to chromium", enum: ["chromium", "firefox", "webkit"] },
width: { type: "number", description: "Viewport width in pixels (default: 1280)" },
height: { type: "number", description: "Viewport height in pixels (default: 720)" },
timeout: { type: "number", description: "Navigation timeout in milliseconds" },
waitUntil: { type: "string", description: "Navigation wait condition" },
headless: { type: "boolean", description: "Run browser in headless mode (default: false)" }
},
required: ["url"],
},
},
{
name: "playwright_screenshot",
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: { type: "string", description: "CSS selector for element to screenshot" },
width: { type: "number", description: "Width in pixels (default: 800)" },
height: { type: "number", description: "Height in pixels (default: 600)" },
storeBase64: { type: "boolean", description: "Store screenshot in base64 format (default: true)" },
fullPage: { type: "boolean", description: "Store screenshot of the entire page (default: false)" },
savePng: { type: "boolean", description: "Save screenshot as PNG file (default: false)" },
downloadsDir: { type: "string", description: "Custom downloads directory path (default: user's Downloads folder)" },
},
required: ["name"],
},
},
{
name: "playwright_click",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for the element to click" },
},
required: ["selector"],
},
},
{
name: "playwright_iframe_click",
description: "Click an element in an iframe on the page",
inputSchema: {
type: "object",
properties: {
iframeSelector: { type: "string", description: "CSS selector for the iframe containing the element to click" },
selector: { type: "string", description: "CSS selector for the element to click" },
},
required: ["iframeSelector", "selector"],
},
},
{
name: "playwright_iframe_fill",
description: "Fill an element in an iframe on the page",
inputSchema: {
type: "object",
properties: {
iframeSelector: { type: "string", description: "CSS selector for the iframe containing the element to fill" },
selector: { type: "string", description: "CSS selector for the element to fill" },
value: { type: "string", description: "Value to fill" },
},
required: ["iframeSelector", "selector", "value"],
},
},
{
name: "playwright_fill",
description: "fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for input field" },
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: "playwright_select",
description: "Select an element on the page with Select tag",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to select" },
value: { type: "string", description: "Value to select" },
},
required: ["selector", "value"],
},
},
{
name: "playwright_hover",
description: "Hover an element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to hover" },
},
required: ["selector"],
},
},
{
name: "playwright_upload_file",
description: "Upload a file to an input[type='file'] element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for the file input element" },
filePath: { type: "string", description: "Absolute path to the file to upload" }
},
required: ["selector", "filePath"],
},
},
{
name: "playwright_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
{
name: "playwright_console_logs",
description: "Retrieve console logs from the browser with filtering options",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
description: "Type of logs to retrieve (all, error, warning, log, info, debug, exception)",
enum: ["all", "error", "warning", "log", "info", "debug", "exception"]
},
search: {
type: "string",
description: "Text to search for in logs (handles text with square brackets)"
},
limit: {
type: "number",
description: "Maximum number of logs to return"
},
clear: {
type: "boolean",
description: "Whether to clear logs after retrieval (default: false)"
}
},
required: [],
},
},
{
name: "playwright_resize",
description: "Resize the browser viewport using manual dimensions or device presets. Supports 143+ device presets including iPhone, iPad, Android devices, and desktop browsers with proper user-agent and touch emulation.",
inputSchema: {
type: "object",
properties: {
device: {
type: "string",
description: "Device preset name (e.g., 'iPhone 13', 'iPad Pro 11', 'Pixel 7', 'Galaxy S24', 'Desktop Chrome'). Automatically configures viewport, user-agent, and device capabilities. Use playwright.devices to see all available devices."
},
width: {
type: "number",
description: "Viewport width in pixels (for manual resize without device preset)"
},
height: {
type: "number",
description: "Viewport height in pixels (for manual resize without device preset)"
},
orientation: {
type: "string",
description: "Device orientation: 'portrait' or 'landscape' (only applies when using device preset)",
enum: ["portrait", "landscape"]
}
},
required: [],
},
},
{
name: "playwright_close",
description: "Close the browser and release all resources",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "playwright_get",
description: "Perform an HTTP GET request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform GET operation" },
token: { type: "string", description: "Bearer token for authorization" },
headers: {
type: "object",
description: "Additional headers to include in the request",
additionalProperties: { type: "string" }
}
},
required: ["url"],
},
},
{
name: "playwright_post",
description: "Perform an HTTP POST request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform POST operation" },
value: { type: "string", description: "Data to post in the body" },
token: { type: "string", description: "Bearer token for authorization" },
headers: {
type: "object",
description: "Additional headers to include in the request",
additionalProperties: { type: "string" }
}
},
required: ["url", "value"],
},
},
{
name: "playwright_put",
description: "Perform an HTTP PUT request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform PUT operation" },
value: { type: "string", description: "Data to PUT in the body" },
token: { type: "string", description: "Bearer token for authorization" },
headers: {
type: "object",
description: "Additional headers to include in the request",
additionalProperties: { type: "string" }
}
},
required: ["url", "value"],
},
},
{
name: "playwright_patch",
description: "Perform an HTTP PATCH request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform PATCH operation" },
value: { type: "string", description: "Data to PATCH in the body" },
token: { type: "string", description: "Bearer token for authorization" },
headers: {
type: "object",
description: "Additional headers to include in the request",
additionalProperties: { type: "string" }
}
},
required: ["url", "value"],
},
},
{
name: "playwright_delete",
description: "Perform an HTTP DELETE request",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to perform DELETE operation" },
token: { type: "string", description: "Bearer token for authorization" },
headers: {
type: "object",
description: "Additional headers to include in the request",
additionalProperties: { type: "string" }
}
},
required: ["url"],
},
},
{
name: "playwright_expect_response",
description: "Ask Playwright to start waiting for a HTTP response. This tool initiates the wait operation but does not wait for its completion.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Unique & arbitrary identifier to be used for retrieving this response later with `Playwright_assert_response`." },
url: { type: "string", description: "URL pattern to match in the response." }
},
required: ["id", "url"],
},
},
{
name: "playwright_assert_response",
description: "Wait for and validate a previously initiated HTTP response wait operation.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Identifier of the HTTP response initially expected using `Playwright_expect_response`." },
value: { type: "string", description: "Data to expect in the body of the HTTP response. If provided, the assertion will fail if this value is not found in the response body." }
},
required: ["id"],
},
},
{
name: "playwright_custom_user_agent",
description: "Set a custom User Agent for the browser",
inputSchema: {
type: "object",
properties: {
userAgent: { type: "string", description: "Custom User Agent for the Playwright browser instance" }
},
required: ["userAgent"],
},
},
{
name: "playwright_get_visible_text",
description: "Get the visible text content of the current page",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "playwright_get_visible_html",
description: "Get the HTML content of the current page. By default, all <script> tags are removed from the output unless removeScripts is explicitly set to false.",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector to limit the HTML to a specific container" },
removeScripts: { type: "boolean", description: "Remove all script tags from the HTML (default: true)" },
removeComments: { type: "boolean", description: "Remove all HTML comments (default: false)" },
removeStyles: { type: "boolean", description: "Remove all style tags from the HTML (default: false)" },
removeMeta: { type: "boolean", description: "Remove all meta tags from the HTML (default: false)" },
cleanHtml: { type: "boolean", description: "Perform comprehensive HTML cleaning (default: false)" },
minify: { type: "boolean", description: "Minify the HTML output (default: false)" },
maxLength: { type: "number", description: "Maximum number of characters to return (default: 20000)" }
},
required: [],
},
},
{
name: "playwright_go_back",
description: "Navigate back in browser history",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "playwright_go_forward",
description: "Navigate forward in browser history",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "playwright_drag",
description: "Drag an element to a target location",
inputSchema: {
type: "object",
properties: {
sourceSelector: { type: "string", description: "CSS selector for the element to drag" },
targetSelector: { type: "string", description: "CSS selector for the target location" }
},
required: ["sourceSelector", "targetSelector"],
},
},
{
name: "playwright_press_key",
description: "Press a keyboard key",
inputSchema: {
type: "object",
properties: {
key: { type: "string", description: "Key to press (e.g. 'Enter', 'ArrowDown', 'a')" },
selector: { type: "string", description: "Optional CSS selector to focus before pressing key" }
},
required: ["key"],
},
},
{
name: "playwright_save_as_pdf",
description: "Save the current page as a PDF file",
inputSchema: {
type: "object",
properties: {
outputPath: { type: "string", description: "Directory path where PDF will be saved" },
filename: { type: "string", description: "Name of the PDF file (default: page.pdf)" },
format: { type: "string", description: "Page format (e.g. 'A4', 'Letter')" },
printBackground: { type: "boolean", description: "Whether to print background graphics" },
margin: {
type: "object",
description: "Page margins",
properties: {
top: { type: "string" },
right: { type: "string" },
bottom: { type: "string" },
left: { type: "string" }
}
}
},
required: ["outputPath"],
},
},
{
name: "playwright_click_and_switch_tab",
description: "Click a link and switch to the newly opened tab",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for the link to click" },
},
required: ["selector"],
},
},
];
}
// Browser-requiring tools for conditional browser launch
export const BROWSER_TOOLS = [
"playwright_navigate",
"playwright_screenshot",
"playwright_click",
"playwright_iframe_click",
"playwright_iframe_fill",
"playwright_fill",
"playwright_select",
"playwright_hover",
"playwright_upload_file",
"playwright_evaluate",
"playwright_resize",
"playwright_close",
"playwright_expect_response",
"playwright_assert_response",
"playwright_custom_user_agent",
"playwright_get_visible_text",
"playwright_get_visible_html",
"playwright_go_back",
"playwright_go_forward",
"playwright_drag",
"playwright_press_key",
"playwright_save_as_pdf",
"playwright_click_and_switch_tab"
];
// API Request tools for conditional launch
export const API_TOOLS = [
"playwright_get",
"playwright_post",
"playwright_put",
"playwright_delete",
"playwright_patch"
];
// Codegen tools
export const CODEGEN_TOOLS = [
'start_codegen_session',
'end_codegen_session',
'get_codegen_session',
'clear_codegen_session'
];
// All available tools
export const tools = [
...BROWSER_TOOLS,
...API_TOOLS,
...CODEGEN_TOOLS
];

View File

@@ -0,0 +1,33 @@
import type { APIRequestContext } from 'playwright';
import { ToolHandler, ToolContext, ToolResponse } from '../common/types.js';
/**
* Base class for all API-based tools
* Provides common functionality and error handling
*/
export declare abstract class ApiToolBase implements ToolHandler {
protected server: any;
constructor(server: any);
/**
* Main execution method that all tools must implement
*/
abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Ensures an API context is available and returns it
* @param context The tool context containing apiContext
* @returns The apiContext or null if not available
*/
protected ensureApiContext(context: ToolContext): APIRequestContext | null;
/**
* Validates that an API context is available and returns an error response if not
* @param context The tool context
* @returns Either null if apiContext is available, or an error response
*/
protected validateApiContextAvailable(context: ToolContext): ToolResponse | null;
/**
* Safely executes an API operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
protected safeExecute(context: ToolContext, operation: (apiContext: APIRequestContext) => Promise<ToolResponse>): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,49 @@
import { createErrorResponse } from '../common/types.js';
/**
* Base class for all API-based tools
* Provides common functionality and error handling
*/
export class ApiToolBase {
constructor(server) {
this.server = server;
}
/**
* Ensures an API context is available and returns it
* @param context The tool context containing apiContext
* @returns The apiContext or null if not available
*/
ensureApiContext(context) {
if (!context.apiContext) {
return null;
}
return context.apiContext;
}
/**
* Validates that an API context is available and returns an error response if not
* @param context The tool context
* @returns Either null if apiContext is available, or an error response
*/
validateApiContextAvailable(context) {
if (!this.ensureApiContext(context)) {
return createErrorResponse("API context not initialized");
}
return null;
}
/**
* Safely executes an API operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
async safeExecute(context, operation) {
const apiError = this.validateApiContextAvailable(context);
if (apiError)
return apiError;
try {
return await operation(context.apiContext);
}
catch (error) {
return createErrorResponse(`API operation failed: ${error.message}`);
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './base.js';
export * from './requests.js';

View File

@@ -0,0 +1,3 @@
export * from './base.js';
export * from './requests.js';
// TODO: Add exports for other API tools as they are implemented

View File

@@ -0,0 +1,61 @@
import { ApiToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Base arguments for all API requests
*/
export interface BaseRequestArgs {
url: string;
token?: string;
headers?: Record<string, string>;
}
/**
* Arguments for requests with body (POST, PUT, PATCH)
*/
export interface RequestWithBodyArgs extends BaseRequestArgs {
value: string | object;
}
/**
* Tool for making GET requests
*/
export declare class GetRequestTool extends ApiToolBase {
/**
* Execute the GET request tool
*/
execute(args: BaseRequestArgs, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for making POST requests
*/
export declare class PostRequestTool extends ApiToolBase {
/**
* Execute the POST request tool
*/
execute(args: RequestWithBodyArgs, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for making PUT requests
*/
export declare class PutRequestTool extends ApiToolBase {
/**
* Execute the PUT request tool
*/
execute(args: RequestWithBodyArgs, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for making PATCH requests
*/
export declare class PatchRequestTool extends ApiToolBase {
/**
* Execute the PATCH request tool
*/
execute(args: RequestWithBodyArgs, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for making DELETE requests
*/
export declare class DeleteRequestTool extends ApiToolBase {
/**
* Execute the DELETE request tool
*/
execute(args: BaseRequestArgs, context: ToolContext): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,252 @@
import { ApiToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
/**
* Helper function to safely parse JSON string or return the value as-is
* @param value The value to parse (can be string or object)
* @returns Parsed JSON object or the original value
*/
function parseJsonSafely(value) {
if (typeof value === 'string') {
try {
return JSON.parse(value);
}
catch (error) {
// Log warning for debugging
console.warn('Failed to parse JSON, using raw string:', error instanceof Error ? error.message : 'Unknown error');
return value;
}
}
return value;
}
/**
* Helper function to build request headers with optional token and custom headers
* @param token Optional Bearer token for authorization
* @param customHeaders Optional custom headers to include
* @param includeContentType Whether to include Content-Type: application/json header
* @returns Merged headers object
*/
function buildHeaders(token, customHeaders, includeContentType = false) {
const headers = {};
if (includeContentType) {
headers['Content-Type'] = 'application/json';
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (customHeaders) {
// Warn if both token and Authorization header are provided
if (token && customHeaders['Authorization']) {
console.warn('Both token and Authorization header provided. Custom Authorization header will override token.');
}
Object.assign(headers, customHeaders);
}
return headers;
}
/**
* Validate headers are all strings
* @param headers Headers to validate
* @returns Error message if invalid, null if valid
*/
function validateHeaders(headers) {
if (!headers)
return null;
for (const [key, value] of Object.entries(headers)) {
if (typeof value !== 'string') {
return `Header '${key}' must be a string, got ${typeof value}`;
}
}
return null;
}
/**
* Tool for making GET requests
*/
export class GetRequestTool extends ApiToolBase {
/**
* Execute the GET request tool
*/
async execute(args, context) {
return this.safeExecute(context, async (apiContext) => {
// Validate headers
const headerError = validateHeaders(args.headers);
if (headerError) {
return createErrorResponse(headerError);
}
const response = await apiContext.get(args.url, {
headers: buildHeaders(args.token, args.headers)
});
let responseText;
try {
responseText = await response.text();
}
catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`GET request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making POST requests
*/
export class PostRequestTool extends ApiToolBase {
/**
* Execute the POST request tool
*/
async execute(args, context) {
return this.safeExecute(context, async (apiContext) => {
// Validate headers
const headerError = validateHeaders(args.headers);
if (headerError) {
return createErrorResponse(headerError);
}
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
}
catch (error) {
return createErrorResponse(`Failed to parse request body: ${error.message}`);
}
}
const response = await apiContext.post(args.url, {
data: parseJsonSafely(args.value),
headers: buildHeaders(args.token, args.headers, true)
});
let responseText;
try {
responseText = await response.text();
}
catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`POST request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making PUT requests
*/
export class PutRequestTool extends ApiToolBase {
/**
* Execute the PUT request tool
*/
async execute(args, context) {
return this.safeExecute(context, async (apiContext) => {
// Validate headers
const headerError = validateHeaders(args.headers);
if (headerError) {
return createErrorResponse(headerError);
}
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
}
catch (error) {
return createErrorResponse(`Failed to parse request body: ${error.message}`);
}
}
const response = await apiContext.put(args.url, {
data: parseJsonSafely(args.value),
headers: buildHeaders(args.token, args.headers, true)
});
let responseText;
try {
responseText = await response.text();
}
catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`PUT request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making PATCH requests
*/
export class PatchRequestTool extends ApiToolBase {
/**
* Execute the PATCH request tool
*/
async execute(args, context) {
return this.safeExecute(context, async (apiContext) => {
// Validate headers
const headerError = validateHeaders(args.headers);
if (headerError) {
return createErrorResponse(headerError);
}
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
}
catch (error) {
return createErrorResponse(`Failed to parse request body: ${error.message}`);
}
}
const response = await apiContext.patch(args.url, {
data: parseJsonSafely(args.value),
headers: buildHeaders(args.token, args.headers, true)
});
let responseText;
try {
responseText = await response.text();
}
catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`PATCH request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making DELETE requests
*/
export class DeleteRequestTool extends ApiToolBase {
/**
* Execute the DELETE request tool
*/
async execute(args, context) {
return this.safeExecute(context, async (apiContext) => {
// Validate headers
const headerError = validateHeaders(args.headers);
if (headerError) {
return createErrorResponse(headerError);
}
const response = await apiContext.delete(args.url, {
headers: buildHeaders(args.token, args.headers)
});
let responseText;
try {
responseText = await response.text();
}
catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`DELETE request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}

View File

@@ -0,0 +1,33 @@
import type { Page } from 'playwright';
import { ToolHandler, ToolContext, ToolResponse } from '../common/types.js';
/**
* Base class for all browser-based tools
* Provides common functionality and error handling
*/
export declare abstract class BrowserToolBase implements ToolHandler {
protected server: any;
constructor(server: any);
/**
* Main execution method that all tools must implement
*/
abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Ensures a page is available and returns it
* @param context The tool context containing browser and page
* @returns The page or null if not available
*/
protected ensurePage(context: ToolContext): Page | null;
/**
* Validates that a page is available and returns an error response if not
* @param context The tool context
* @returns Either null if page is available, or an error response
*/
protected validatePageAvailable(context: ToolContext): ToolResponse | null;
/**
* Safely executes a browser operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
protected safeExecute(context: ToolContext, operation: (page: Page) => Promise<ToolResponse>): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,72 @@
import { createErrorResponse } from '../common/types.js';
/**
* Base class for all browser-based tools
* Provides common functionality and error handling
*/
export class BrowserToolBase {
constructor(server) {
this.server = server;
}
/**
* Ensures a page is available and returns it
* @param context The tool context containing browser and page
* @returns The page or null if not available
*/
ensurePage(context) {
if (!context.page) {
return null;
}
return context.page;
}
/**
* Validates that a page is available and returns an error response if not
* @param context The tool context
* @returns Either null if page is available, or an error response
*/
validatePageAvailable(context) {
if (!this.ensurePage(context)) {
return createErrorResponse("Browser page not initialized!");
}
return null;
}
/**
* Safely executes a browser operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
async safeExecute(context, operation) {
const pageError = this.validatePageAvailable(context);
if (pageError)
return pageError;
try {
// Verify browser is connected before proceeding
if (context.browser && !context.browser.isConnected()) {
// If browser exists but is disconnected, reset state
const { resetBrowserState } = await import('../../toolHandler.js');
resetBrowserState();
return createErrorResponse("Browser is disconnected. Please retry the operation.");
}
// Check if page is closed
if (context.page.isClosed()) {
return createErrorResponse("Page is closed. Please retry the operation.");
}
return await operation(context.page);
}
catch (error) {
const errorMessage = error.message;
// Check for common browser disconnection errors
if (errorMessage.includes("Target page, context or browser has been closed") ||
errorMessage.includes("Target closed") ||
errorMessage.includes("Browser has been disconnected") ||
errorMessage.includes("Protocol error") ||
errorMessage.includes("Connection closed")) {
// Reset browser state on connection issues
const { resetBrowserState } = await import('../../toolHandler.js');
resetBrowserState();
return createErrorResponse(`Browser connection error: ${errorMessage}. Connection has been reset - please retry the operation.`);
}
return createErrorResponse(`Operation failed: ${errorMessage}`);
}
}
}

View File

@@ -0,0 +1,26 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for retrieving and filtering console logs from the browser
*/
export declare class ConsoleLogsTool extends BrowserToolBase {
private consoleLogs;
/**
* Register a console message
* @param type The type of console message
* @param text The text content of the message
*/
registerConsoleMessage(type: string, text: string): void;
/**
* Execute the console logs tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Get all console logs
*/
getConsoleLogs(): string[];
/**
* Clear all console logs
*/
clearConsoleLogs(): void;
}

View File

@@ -0,0 +1,66 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse } from '../common/types.js';
/**
* Tool for retrieving and filtering console logs from the browser
*/
export class ConsoleLogsTool extends BrowserToolBase {
constructor() {
super(...arguments);
this.consoleLogs = [];
}
/**
* Register a console message
* @param type The type of console message
* @param text The text content of the message
*/
registerConsoleMessage(type, text) {
const logEntry = `[${type}] ${text}`;
this.consoleLogs.push(logEntry);
}
/**
* Execute the console logs tool
*/
async execute(args, context) {
// No need to use safeExecute here as we don't need to interact with the page
// We're just filtering and returning logs that are already stored
let logs = [...this.consoleLogs];
// Filter by type if specified
if (args.type && args.type !== 'all') {
logs = logs.filter(log => log.startsWith(`[${args.type}]`));
}
// Filter by search text if specified
if (args.search) {
logs = logs.filter(log => log.includes(args.search));
}
// Limit the number of logs if specified
if (args.limit && args.limit > 0) {
logs = logs.slice(-args.limit);
}
// Clear logs if requested
if (args.clear) {
this.consoleLogs = [];
}
// Format the response
if (logs.length === 0) {
return createSuccessResponse("No console logs matching the criteria");
}
else {
return createSuccessResponse([
`Retrieved ${logs.length} console log(s):`,
...logs
]);
}
}
/**
* Get all console logs
*/
getConsoleLogs() {
return this.consoleLogs;
}
/**
* Clear all console logs
*/
clearConsoleLogs() {
this.consoleLogs = [];
}
}

View File

@@ -0,0 +1,8 @@
export * from './base.js';
export * from './screenshot.js';
export * from './navigation.js';
export * from './console.js';
export * from './interaction.js';
export * from './response.js';
export * from './useragent.js';
export * from './resize.js';

View File

@@ -0,0 +1,10 @@
export * from './base.js';
export * from './screenshot.js';
export * from './navigation.js';
export * from './console.js';
export * from './interaction.js';
export * from './response.js';
export * from './useragent.js';
export * from './resize.js';
// TODO: Add exports for other browser tools as they are implemented
// export * from './interaction.js';

View File

@@ -0,0 +1,104 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for clicking elements on the page
*/
export declare class ClickTool extends BrowserToolBase {
/**
* Execute the click tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for clicking a link and switching to the new tab
*/
export declare class ClickAndSwitchTabTool extends BrowserToolBase {
/**
* Execute the click and switch tab tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for clicking elements inside iframes
*/
export declare class IframeClickTool extends BrowserToolBase {
/**
* Execute the iframe click tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for filling elements inside iframes
*/
export declare class IframeFillTool extends BrowserToolBase {
/**
* Execute the iframe fill tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for filling form fields
*/
export declare class FillTool extends BrowserToolBase {
/**
* Execute the fill tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for selecting options from dropdown menus
*/
export declare class SelectTool extends BrowserToolBase {
/**
* Execute the select tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for hovering over elements
*/
export declare class HoverTool extends BrowserToolBase {
/**
* Execute the hover tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for uploading files
*/
export declare class UploadFileTool extends BrowserToolBase {
/**
* Execute the upload file tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for executing JavaScript in the browser
*/
export declare class EvaluateTool extends BrowserToolBase {
/**
* Execute the evaluate tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for dragging elements on the page
*/
export declare class DragTool extends BrowserToolBase {
/**
* Execute the drag tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for pressing keyboard keys
*/
export declare class PressKeyTool extends BrowserToolBase {
/**
* Execute the key press tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for switching browser tabs
*/

View File

@@ -0,0 +1,235 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
import { setGlobalPage } from '../../toolHandler.js';
/**
* Tool for clicking elements on the page
*/
export class ClickTool extends BrowserToolBase {
/**
* Execute the click tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.click(args.selector);
return createSuccessResponse(`Clicked element: ${args.selector}`);
});
}
}
/**
* Tool for clicking a link and switching to the new tab
*/
export class ClickAndSwitchTabTool extends BrowserToolBase {
/**
* Execute the click and switch tab tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
// Listen for a new tab to open
const [newPage] = await Promise.all([
//context.browser.waitForEvent('page'), // Wait for a new page (tab) to open
page.context().waitForEvent('page'), // Wait for a new page (tab) to open
page.click(args.selector), // Click the link that opens the new tab
]);
// Wait for the new page to load
await newPage.waitForLoadState('domcontentloaded');
// Switch control to the new tab
setGlobalPage(newPage);
//page= newPage; // Update the current page to the new tab
//context.page = newPage;
//context.page.bringToFront(); // Bring the new tab to the front
return createSuccessResponse(`Clicked link and switched to new tab: ${newPage.url()}`);
//return createSuccessResponse(`Clicked link and switched to new tab: ${context.page.url()}`);
});
}
}
/**
* Tool for clicking elements inside iframes
*/
export class IframeClickTool extends BrowserToolBase {
/**
* Execute the iframe click tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const frame = page.frameLocator(args.iframeSelector);
if (!frame) {
return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
}
await frame.locator(args.selector).click();
return createSuccessResponse(`Clicked element ${args.selector} inside iframe ${args.iframeSelector}`);
});
}
}
/**
* Tool for filling elements inside iframes
*/
export class IframeFillTool extends BrowserToolBase {
/**
* Execute the iframe fill tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const frame = page.frameLocator(args.iframeSelector);
if (!frame) {
return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
}
await frame.locator(args.selector).fill(args.value);
return createSuccessResponse(`Filled element ${args.selector} inside iframe ${args.iframeSelector} with: ${args.value}`);
});
}
}
/**
* Tool for filling form fields
*/
export class FillTool extends BrowserToolBase {
/**
* Execute the fill tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.fill(args.selector, args.value);
return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for selecting options from dropdown menus
*/
export class SelectTool extends BrowserToolBase {
/**
* Execute the select tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.selectOption(args.selector, args.value);
return createSuccessResponse(`Selected ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for hovering over elements
*/
export class HoverTool extends BrowserToolBase {
/**
* Execute the hover tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.hover(args.selector);
return createSuccessResponse(`Hovered ${args.selector}`);
});
}
}
/**
* Tool for uploading files
*/
export class UploadFileTool extends BrowserToolBase {
/**
* Execute the upload file tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.setInputFiles(args.selector, args.filePath);
return createSuccessResponse(`Uploaded file '${args.filePath}' to '${args.selector}'`);
});
}
}
/**
* Tool for executing JavaScript in the browser
*/
export class EvaluateTool extends BrowserToolBase {
/**
* Execute the evaluate tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const result = await page.evaluate(args.script);
// Convert result to string for display
let resultStr;
try {
resultStr = JSON.stringify(result, null, 2);
}
catch (error) {
resultStr = String(result);
}
return createSuccessResponse([
`Executed JavaScript:`,
`${args.script}`,
`Result:`,
`${resultStr}`
]);
});
}
}
/**
* Tool for dragging elements on the page
*/
export class DragTool extends BrowserToolBase {
/**
* Execute the drag tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const sourceElement = await page.waitForSelector(args.sourceSelector);
const targetElement = await page.waitForSelector(args.targetSelector);
const sourceBound = await sourceElement.boundingBox();
const targetBound = await targetElement.boundingBox();
if (!sourceBound || !targetBound) {
return createErrorResponse("Could not get element positions for drag operation");
}
await page.mouse.move(sourceBound.x + sourceBound.width / 2, sourceBound.y + sourceBound.height / 2);
await page.mouse.down();
await page.mouse.move(targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2);
await page.mouse.up();
return createSuccessResponse(`Dragged element from ${args.sourceSelector} to ${args.targetSelector}`);
});
}
}
/**
* Tool for pressing keyboard keys
*/
export class PressKeyTool extends BrowserToolBase {
/**
* Execute the key press tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
if (args.selector) {
await page.waitForSelector(args.selector);
await page.focus(args.selector);
}
await page.keyboard.press(args.key);
return createSuccessResponse(`Pressed key: ${args.key}`);
});
}
}
/**
* Tool for switching browser tabs
*/
// export class SwitchTabTool extends BrowserToolBase {
// /**
// * Switch the tab to the specified index
// */
// async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// return this.safeExecute(context, async (page) => {
// const tabs = await browser.page;
// // Validate the tab index
// const tabIndex = Number(args.index);
// if (isNaN(tabIndex)) {
// return createErrorResponse(`Invalid tab index: ${args.index}. It must be a number.`);
// }
// if (tabIndex >= 0 && tabIndex < tabs.length) {
// await tabs[tabIndex].bringToFront();
// return createSuccessResponse(`Switched to tab with index ${tabIndex}`);
// } else {
// return createErrorResponse(
// `Tab index out of range: ${tabIndex}. Available tabs: 0 to ${tabs.length - 1}.`
// );
// }
// });
// }
// }

View File

@@ -0,0 +1,38 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for navigating to URLs
*/
export declare class NavigationTool extends BrowserToolBase {
/**
* Execute the navigation tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for closing the browser
*/
export declare class CloseBrowserTool extends BrowserToolBase {
/**
* Execute the close browser tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for navigating back in browser history
*/
export declare class GoBackTool extends BrowserToolBase {
/**
* Execute the go back tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for navigating forward in browser history
*/
export declare class GoForwardTool extends BrowserToolBase {
/**
* Execute the go forward tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,106 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
import { resetBrowserState } from '../../toolHandler.js';
/**
* Tool for navigating to URLs
*/
export class NavigationTool extends BrowserToolBase {
/**
* Execute the navigation tool
*/
async execute(args, context) {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse("Browser is not connected. The connection has been reset - please retry your navigation.");
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse("Page is not available or has been closed. Please retry your navigation.");
}
return this.safeExecute(context, async (page) => {
try {
await page.goto(args.url, {
timeout: args.timeout || 30000,
waitUntil: args.waitUntil || "load"
});
return createSuccessResponse(`Navigated to ${args.url}`);
}
catch (error) {
const errorMessage = error.message;
// Check for common disconnection errors
if (errorMessage.includes("Target page, context or browser has been closed") ||
errorMessage.includes("Target closed") ||
errorMessage.includes("Browser has been disconnected")) {
// Reset browser state to force recreation on next attempt
resetBrowserState();
return createErrorResponse(`Browser connection issue: ${errorMessage}. Connection has been reset - please retry your navigation.`);
}
// For other errors, return the standard error
throw error;
}
});
}
}
/**
* Tool for closing the browser
*/
export class CloseBrowserTool extends BrowserToolBase {
/**
* Execute the close browser tool
*/
async execute(args, context) {
if (context.browser) {
try {
// Check if browser is still connected
if (context.browser.isConnected()) {
await context.browser.close().catch(error => {
console.error("Error while closing browser:", error);
});
}
else {
console.error("Browser already disconnected, cleaning up state");
}
}
catch (error) {
console.error("Error during browser close operation:", error);
// Continue with resetting state even if close fails
}
finally {
// Always reset the global browser and page references
resetBrowserState();
}
return createSuccessResponse("Browser closed successfully");
}
return createSuccessResponse("No browser instance to close");
}
}
/**
* Tool for navigating back in browser history
*/
export class GoBackTool extends BrowserToolBase {
/**
* Execute the go back tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.goBack();
return createSuccessResponse("Navigated back in browser history");
});
}
}
/**
* Tool for navigating forward in browser history
*/
export class GoForwardTool extends BrowserToolBase {
/**
* Execute the go forward tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
await page.goForward();
return createSuccessResponse("Navigated forward in browser history");
});
}
}

View File

@@ -0,0 +1,11 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for saving page as PDF
*/
export declare class SaveAsPdfTool extends BrowserToolBase {
/**
* Execute the save as PDF tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,29 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse } from '../common/types.js';
import * as path from 'path';
/**
* Tool for saving page as PDF
*/
export class SaveAsPdfTool extends BrowserToolBase {
/**
* Execute the save as PDF tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const filename = args.filename || 'page.pdf';
const options = {
path: path.resolve(args.outputPath || '.', filename),
format: args.format || 'A4',
printBackground: args.printBackground !== false,
margin: args.margin || {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
};
await page.pdf(options);
return createSuccessResponse(`Saved page as PDF: ${options.path}`);
});
}
}

View File

@@ -0,0 +1,11 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for resizing the browser viewport with device preset support
*/
export declare class ResizeTool extends BrowserToolBase {
/**
* Execute the resize tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,99 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
import { devices } from 'playwright';
/**
* Tool for resizing the browser viewport with device preset support
*/
export class ResizeTool extends BrowserToolBase {
/**
* Execute the resize tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
let width;
let height;
let deviceName;
let shouldSetUserAgent = false;
let userAgent;
let isMobile = false;
let hasTouch = false;
let deviceScaleFactor = 1;
// Check if using device preset
if (args.device) {
const device = devices[args.device];
if (!device) {
// List some popular devices for suggestions
const popularDevices = [
'iPhone 13', 'iPhone 13 Pro', 'iPhone 14', 'iPhone 15',
'iPad Pro 11', 'iPad Pro 12.9',
'Pixel 5', 'Pixel 7',
'Galaxy S9+', 'Galaxy S24',
'Desktop Chrome', 'Desktop Firefox', 'Desktop Safari'
];
return createErrorResponse(`Device "${args.device}" not found. Popular devices: ${popularDevices.join(', ')}. ` +
`Use playwright.devices to see all ${Object.keys(devices).length} available devices.`);
}
// Extract device properties
width = device.viewport.width;
height = device.viewport.height;
deviceName = args.device;
shouldSetUserAgent = true;
userAgent = device.userAgent;
isMobile = device.isMobile || false;
hasTouch = device.hasTouch || false;
deviceScaleFactor = device.deviceScaleFactor || 1;
// Handle orientation
if (args.orientation === 'landscape' && width < height) {
[width, height] = [height, width];
}
else if (args.orientation === 'portrait' && width > height) {
[width, height] = [height, width];
}
}
else {
// Manual dimensions
width = args.width;
height = args.height;
// Check if dimensions are provided
if (width === undefined || height === undefined) {
return createErrorResponse("Either 'device' parameter or both 'width' and 'height' parameters are required");
}
// Validate dimensions
if (width <= 0 || height <= 0) {
throw new Error("Width and height must be positive integers");
}
if (width > 7680 || height > 4320) {
throw new Error("Width and height must not exceed 7680x4320 (8K resolution)");
}
}
// Apply viewport resize
await page.setViewportSize({ width, height });
// If using device preset, also update user agent and touch settings
if (shouldSetUserAgent && userAgent) {
await page.setExtraHTTPHeaders({
'User-Agent': userAgent
});
}
// Construct success message
let message = deviceName
? `Browser viewport resized to ${deviceName} (${width}x${height})`
: `Browser viewport resized to ${width}x${height}`;
if (args.orientation) {
message += ` in ${args.orientation} orientation`;
}
if (deviceName) {
const features = [];
if (isMobile)
features.push('mobile');
if (hasTouch)
features.push('touch');
if (deviceScaleFactor > 1)
features.push(`${deviceScaleFactor}x scale`);
if (features.length > 0) {
message += ` [${features.join(', ')}]`;
}
}
return createSuccessResponse(message);
});
}
}

View File

@@ -0,0 +1,29 @@
import { BrowserToolBase } from './base.js';
import type { ToolContext, ToolResponse } from '../common/types.js';
interface ExpectResponseArgs {
id: string;
url: string;
}
interface AssertResponseArgs {
id: string;
value?: string;
}
/**
* Tool for setting up response wait operations
*/
export declare class ExpectResponseTool extends BrowserToolBase {
/**
* Execute the expect response tool
*/
execute(args: ExpectResponseArgs, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for asserting and validating responses
*/
export declare class AssertResponseTool extends BrowserToolBase {
/**
* Execute the assert response tool
*/
execute(args: AssertResponseArgs, context: ToolContext): Promise<ToolResponse>;
}
export {};

View File

@@ -0,0 +1,67 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
const responsePromises = new Map();
/**
* Tool for setting up response wait operations
*/
export class ExpectResponseTool extends BrowserToolBase {
/**
* Execute the expect response tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
if (!args.id || !args.url) {
return createErrorResponse("Missing required parameters: id and url must be provided");
}
const responsePromise = page.waitForResponse(args.url);
responsePromises.set(args.id, responsePromise);
return createSuccessResponse(`Started waiting for response with ID ${args.id}`);
});
}
}
/**
* Tool for asserting and validating responses
*/
export class AssertResponseTool extends BrowserToolBase {
/**
* Execute the assert response tool
*/
async execute(args, context) {
return this.safeExecute(context, async () => {
if (!args.id) {
return createErrorResponse("Missing required parameter: id must be provided");
}
const responsePromise = responsePromises.get(args.id);
if (!responsePromise) {
return createErrorResponse(`No response wait operation found with ID: ${args.id}`);
}
try {
const response = await responsePromise;
const body = await response.json();
if (args.value) {
const bodyStr = JSON.stringify(body);
if (!bodyStr.includes(args.value)) {
const messages = [
`Response body does not contain expected value: ${args.value}`,
`Actual body: ${bodyStr}`
];
return createErrorResponse(messages.join('\n'));
}
}
const messages = [
`Response assertion for ID ${args.id} successful`,
`URL: ${response.url()}`,
`Status: ${response.status()}`,
`Body: ${JSON.stringify(body, null, 2)}`
];
return createSuccessResponse(messages.join('\n'));
}
catch (error) {
return createErrorResponse(`Failed to assert response: ${error.message}`);
}
finally {
responsePromises.delete(args.id);
}
});
}
}

View File

@@ -0,0 +1,16 @@
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse } from '../common/types.js';
/**
* Tool for taking screenshots of pages or elements
*/
export declare class ScreenshotTool extends BrowserToolBase {
private screenshots;
/**
* Execute the screenshot tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Get all stored screenshots
*/
getScreenshots(): Map<string, string>;
}

View File

@@ -0,0 +1,66 @@
import fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { BrowserToolBase } from './base.js';
import { createSuccessResponse } from '../common/types.js';
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
/**
* Tool for taking screenshots of pages or elements
*/
export class ScreenshotTool extends BrowserToolBase {
constructor() {
super(...arguments);
this.screenshots = new Map();
}
/**
* Execute the screenshot tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
const screenshotOptions = {
type: args.type || "png",
fullPage: !!args.fullPage
};
if (args.selector) {
const element = await page.$(args.selector);
if (!element) {
return {
content: [{
type: "text",
text: `Element not found: ${args.selector}`,
}],
isError: true
};
}
screenshotOptions.element = element;
}
// Generate output path
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${args.name || 'screenshot'}-${timestamp}.png`;
const downloadsDir = args.downloadsDir || defaultDownloadsPath;
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const outputPath = path.join(downloadsDir, filename);
screenshotOptions.path = outputPath;
const screenshot = await page.screenshot(screenshotOptions);
const base64Screenshot = screenshot.toString('base64');
const messages = [`Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
// Handle base64 storage
if (args.storeBase64 !== false) {
this.screenshots.set(args.name || 'screenshot', base64Screenshot);
this.server.notification({
method: "notifications/resources/list_changed",
});
messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
}
return createSuccessResponse(messages);
});
}
/**
* Get all stored screenshots
*/
getScreenshots() {
return this.screenshots;
}
}

View File

@@ -0,0 +1,15 @@
import { BrowserToolBase } from './base.js';
import type { ToolContext, ToolResponse } from '../common/types.js';
interface CustomUserAgentArgs {
userAgent: string;
}
/**
* Tool for validating custom User Agent settings
*/
export declare class CustomUserAgentTool extends BrowserToolBase {
/**
* Execute the custom user agent tool
*/
execute(args: CustomUserAgentArgs, context: ToolContext): Promise<ToolResponse>;
}
export {};

View File

@@ -0,0 +1,32 @@
import { BrowserToolBase } from './base.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
/**
* Tool for validating custom User Agent settings
*/
export class CustomUserAgentTool extends BrowserToolBase {
/**
* Execute the custom user agent tool
*/
async execute(args, context) {
return this.safeExecute(context, async (page) => {
if (!args.userAgent) {
return createErrorResponse("Missing required parameter: userAgent must be provided");
}
try {
const currentUserAgent = await page.evaluate(() => navigator.userAgent);
if (currentUserAgent !== args.userAgent) {
const messages = [
"Page was already initialized with a different User Agent.",
`Requested: ${args.userAgent}`,
`Current: ${currentUserAgent}`
];
return createErrorResponse(messages.join('\n'));
}
return createSuccessResponse("User Agent validation successful");
}
catch (error) {
return createErrorResponse(`Failed to validate User Agent: ${error.message}`);
}
});
}
}

View File

@@ -0,0 +1,20 @@
import { ToolContext, ToolResponse } from "../common/types.js";
import { BrowserToolBase } from "./base.js";
/**
* Tool for getting the visible text content of the current page
*/
export declare class VisibleTextTool extends BrowserToolBase {
/**
* Execute the visible text page tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
/**
* Tool for getting the visible HTML content of the current page
*/
export declare class VisibleHtmlTool extends BrowserToolBase {
/**
* Execute the visible HTML page tool
*/
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}

View File

@@ -0,0 +1,168 @@
import { resetBrowserState } from "../../toolHandler.js";
import { createErrorResponse, createSuccessResponse } from "../common/types.js";
import { BrowserToolBase } from "./base.js";
/**
* Tool for getting the visible text content of the current page
*/
export class VisibleTextTool extends BrowserToolBase {
/**
* Execute the visible text page tool
*/
async execute(args, context) {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse("Browser is not connected. The connection has been reset - please retry your navigation.");
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse("Page is not available or has been closed. Please retry your navigation.");
}
return this.safeExecute(context, async (page) => {
try {
const visibleText = await page.evaluate(() => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
const style = window.getComputedStyle(node.parentElement);
return (style.display !== "none" && style.visibility !== "hidden")
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
let text = "";
let node;
while ((node = walker.nextNode())) {
const trimmedText = node.textContent?.trim();
if (trimmedText) {
text += trimmedText + "\n";
}
}
return text.trim();
});
// Truncate logic
const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
let output = visibleText;
let truncated = false;
if (output.length > maxLength) {
output = output.slice(0, maxLength) + '\n[Output truncated due to size limits]';
truncated = true;
}
return createSuccessResponse(`Visible text content:\n${output}`);
}
catch (error) {
return createErrorResponse(`Failed to get visible text content: ${error.message}`);
}
});
}
}
/**
* Tool for getting the visible HTML content of the current page
*/
export class VisibleHtmlTool extends BrowserToolBase {
/**
* Execute the visible HTML page tool
*/
async execute(args, context) {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse("Browser is not connected. The connection has been reset - please retry your navigation.");
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse("Page is not available or has been closed. Please retry your navigation.");
}
return this.safeExecute(context, async (page) => {
try {
const { selector, removeComments, removeStyles, removeMeta, minify, cleanHtml } = args;
// Default removeScripts to true unless explicitly set to false
const removeScripts = args.removeScripts === false ? false : true;
// Get the HTML content
let htmlContent;
if (selector) {
// If a selector is provided, get only the HTML for that element
const element = await page.$(selector);
if (!element) {
return createErrorResponse(`Element with selector "${selector}" not found`);
}
htmlContent = await page.evaluate((el) => el.outerHTML, element);
}
else {
// Otherwise get the full page HTML
htmlContent = await page.content();
}
// Determine if we need to apply filters
const shouldRemoveScripts = removeScripts || cleanHtml;
const shouldRemoveComments = removeComments || cleanHtml;
const shouldRemoveStyles = removeStyles || cleanHtml;
const shouldRemoveMeta = removeMeta || cleanHtml;
// Apply filters in the browser context
if (shouldRemoveScripts || shouldRemoveComments || shouldRemoveStyles || shouldRemoveMeta || minify) {
htmlContent = await page.evaluate(({ html, removeScripts, removeComments, removeStyles, removeMeta, minify }) => {
// Create a DOM parser to work with the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Remove script tags if requested
if (removeScripts) {
const scripts = doc.querySelectorAll('script');
scripts.forEach(script => script.remove());
}
// Remove style tags if requested
if (removeStyles) {
const styles = doc.querySelectorAll('style');
styles.forEach(style => style.remove());
}
// Remove meta tags if requested
if (removeMeta) {
const metaTags = doc.querySelectorAll('meta');
metaTags.forEach(meta => meta.remove());
}
// Remove HTML comments if requested
if (removeComments) {
const removeComments = (node) => {
const childNodes = node.childNodes;
for (let i = childNodes.length - 1; i >= 0; i--) {
const child = childNodes[i];
if (child.nodeType === 8) { // 8 is for comment nodes
node.removeChild(child);
}
else if (child.nodeType === 1) { // 1 is for element nodes
removeComments(child);
}
}
};
removeComments(doc.documentElement);
}
// Get the processed HTML
let result = doc.documentElement.outerHTML;
// Minify if requested
if (minify) {
// Simple minification: remove extra whitespace
result = result.replace(/>\s+</g, '><').trim();
}
return result;
}, {
html: htmlContent,
removeScripts: shouldRemoveScripts,
removeComments: shouldRemoveComments,
removeStyles: shouldRemoveStyles,
removeMeta: shouldRemoveMeta,
minify
});
}
// Truncate logic
const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
let output = htmlContent;
if (output.length > maxLength) {
output = output.slice(0, maxLength) + '\n<!-- Output truncated due to size limits -->';
}
return createSuccessResponse(`HTML content:\n${output}`);
}
catch (error) {
return createErrorResponse(`Failed to get visible HTML content: ${error.message}`);
}
});
}
}

View File

@@ -0,0 +1,21 @@
import { CodegenOptions, CodegenResult, CodegenSession } from './types.js';
export declare class PlaywrightGenerator {
private static readonly DEFAULT_OPTIONS;
private options;
constructor(options?: CodegenOptions);
private validateOptions;
generateTest(session: CodegenSession): Promise<CodegenResult>;
private createTestCase;
private convertActionToStep;
private generateNavigateStep;
private generateFillStep;
private generateClickStep;
private generateScreenshotStep;
private generateExpectResponseStep;
private generateAssertResponseStep;
private generateHoverStep;
private generateSelectStep;
private generateCustomUserAgentStep;
private generateTestCode;
private getOutputFilePath;
}

View File

@@ -0,0 +1,158 @@
import * as path from 'path';
export class PlaywrightGenerator {
constructor(options = {}) {
this.validateOptions(options);
this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options };
}
validateOptions(options) {
if (options.outputPath && typeof options.outputPath !== 'string') {
throw new Error('outputPath must be a string');
}
if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') {
throw new Error('testNamePrefix must be a string');
}
if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') {
throw new Error('includeComments must be a boolean');
}
}
async generateTest(session) {
if (!session || !Array.isArray(session.actions)) {
throw new Error('Invalid session data');
}
const testCase = this.createTestCase(session);
const testCode = this.generateTestCode(testCase);
const filePath = this.getOutputFilePath(session);
return {
testCode,
filePath,
sessionId: session.id,
};
}
createTestCase(session) {
const testCase = {
name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`,
steps: [],
imports: new Set(['test', 'expect']),
};
for (const action of session.actions) {
const step = this.convertActionToStep(action);
if (step) {
testCase.steps.push(step);
}
}
return testCase;
}
convertActionToStep(action) {
const { toolName, parameters } = action;
switch (toolName) {
case 'playwright_navigate':
return this.generateNavigateStep(parameters);
case 'playwright_fill':
return this.generateFillStep(parameters);
case 'playwright_click':
return this.generateClickStep(parameters);
case 'playwright_screenshot':
return this.generateScreenshotStep(parameters);
case 'playwright_expect_response':
return this.generateExpectResponseStep(parameters);
case 'playwright_assert_response':
return this.generateAssertResponseStep(parameters);
case 'playwright_hover':
return this.generateHoverStep(parameters);
case 'playwright_select':
return this.generateSelectStep(parameters);
case 'playwright_custom_user_agent':
return this.generateCustomUserAgentStep(parameters);
default:
console.warn(`Unsupported tool: ${toolName}`);
return null;
}
}
generateNavigateStep(parameters) {
const { url, waitUntil } = parameters;
const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : '';
return `
// Navigate to URL
await page.goto('${url}'${options});`;
}
generateFillStep(parameters) {
const { selector, value } = parameters;
return `
// Fill input field
await page.fill('${selector}', '${value}');`;
}
generateClickStep(parameters) {
const { selector } = parameters;
return `
// Click element
await page.click('${selector}');`;
}
generateScreenshotStep(parameters) {
const { name, fullPage = false, path } = parameters;
const options = [];
if (fullPage)
options.push('fullPage: true');
if (path)
options.push(`path: '${path}'`);
const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
return `
// Take screenshot
await page.screenshot({ path: '${name}.png'${optionsStr} });`;
}
generateExpectResponseStep(parameters) {
const { url, id } = parameters;
return `
// Wait for response
const ${id}Response = page.waitForResponse('${url}');`;
}
generateAssertResponseStep(parameters) {
const { id, value } = parameters;
const assertion = value
? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');`
: `\n expect(${id}Response.ok()).toBeTruthy();`;
return `
// Assert response${assertion}`;
}
generateHoverStep(parameters) {
const { selector } = parameters;
return `
// Hover over element
await page.hover('${selector}');`;
}
generateSelectStep(parameters) {
const { selector, value } = parameters;
return `
// Select option
await page.selectOption('${selector}', '${value}');`;
}
generateCustomUserAgentStep(parameters) {
const { userAgent } = parameters;
return `
// Set custom user agent
await context.setUserAgent('${userAgent}');`;
}
generateTestCode(testCase) {
const imports = Array.from(testCase.imports)
.map(imp => `import { ${imp} } from '@playwright/test';`)
.join('\n');
return `
${imports}
test('${testCase.name}', async ({ page, context }) => {
${testCase.steps.join('\n')}
});`;
}
getOutputFilePath(session) {
if (!session.id) {
throw new Error('Session ID is required');
}
const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`;
return path.resolve(this.options.outputPath, fileName);
}
}
PlaywrightGenerator.DEFAULT_OPTIONS = {
outputPath: 'tests',
testNamePrefix: 'MCP',
includeComments: true,
};

View File

@@ -0,0 +1,11 @@
import { Tool } from '../../types.js';
import type { Browser, Page } from 'playwright';
declare global {
var browser: Browser | undefined;
var page: Page | undefined;
}
export declare const startCodegenSession: Tool;
export declare const endCodegenSession: Tool;
export declare const getCodegenSession: Tool;
export declare const clearCodegenSession: Tool;
export declare const codegenTools: Tool[];

View File

@@ -0,0 +1,187 @@
import { ActionRecorder } from './recorder.js';
import { PlaywrightGenerator } from './generator.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Helper function to get workspace root path
const getWorkspaceRoot = () => {
return process.cwd();
};
const DEFAULT_OPTIONS = {
outputPath: path.join(getWorkspaceRoot(), 'e2e'),
testNamePrefix: 'Test',
includeComments: true
};
export const startCodegenSession = {
name: 'start_codegen_session',
description: 'Start a new code generation session to record MCP tool actions',
parameters: {
type: 'object',
properties: {
options: {
type: 'object',
description: 'Code generation options',
properties: {
outputPath: { type: 'string' },
testNamePrefix: { type: 'string' },
includeComments: { type: 'boolean' }
}
}
}
},
handler: async ({ options = {} }) => {
try {
// Merge provided options with defaults
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
// Ensure output path is absolute and normalized
const workspaceRoot = getWorkspaceRoot();
const outputPath = path.isAbsolute(mergedOptions.outputPath)
? mergedOptions.outputPath
: path.join(workspaceRoot, mergedOptions.outputPath);
mergedOptions.outputPath = outputPath;
// Ensure output directory exists
try {
await fs.mkdir(outputPath, { recursive: true });
}
catch (mkdirError) {
throw new Error(`Failed to create output directory: ${mkdirError.message}`);
}
const sessionId = ActionRecorder.getInstance().startSession();
// Store options with the session
const recorder = ActionRecorder.getInstance();
const session = recorder.getSession(sessionId);
if (session) {
session.options = mergedOptions;
}
return {
sessionId,
options: mergedOptions,
message: `Started codegen session. Tests will be generated in: ${outputPath}`
};
}
catch (error) {
throw new Error(`Failed to start codegen session: ${error.message}`);
}
}
};
export const endCodegenSession = {
name: 'end_codegen_session',
description: 'End the current code generation session and generate Playwright test',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to end'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
try {
const recorder = ActionRecorder.getInstance();
const session = recorder.endSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
if (!session.options) {
throw new Error(`Session ${sessionId} has no options configured`);
}
const generator = new PlaywrightGenerator(session.options);
const result = await generator.generateTest(session);
// Double check output directory exists
const outputDir = path.dirname(result.filePath);
await fs.mkdir(outputDir, { recursive: true });
// Write test file
try {
await fs.writeFile(result.filePath, result.testCode, 'utf-8');
}
catch (writeError) {
throw new Error(`Failed to write test file: ${writeError.message}`);
}
// Close Playwright browser and cleanup
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
}
catch (browserError) {
console.warn('Failed to close browser:', browserError.message);
}
finally {
global.browser = undefined;
global.page = undefined;
}
const absolutePath = path.resolve(result.filePath);
return {
filePath: absolutePath,
outputDirectory: outputDir,
testCode: result.testCode,
message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}`
};
}
catch (error) {
// Ensure browser cleanup even on error
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
}
catch {
// Ignore cleanup errors
}
finally {
global.browser = undefined;
global.page = undefined;
}
throw new Error(`Failed to end codegen session: ${error.message}`);
}
}
};
export const getCodegenSession = {
name: 'get_codegen_session',
description: 'Get information about a code generation session',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to retrieve'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
const session = ActionRecorder.getInstance().getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
return session;
}
};
export const clearCodegenSession = {
name: 'clear_codegen_session',
description: 'Clear a code generation session',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to clear'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
const success = ActionRecorder.getInstance().clearSession(sessionId);
if (!success) {
throw new Error(`Session ${sessionId} not found`);
}
return { success };
}
};
export const codegenTools = [
startCodegenSession,
endCodegenSession,
getCodegenSession,
clearCodegenSession
];

View File

@@ -0,0 +1,14 @@
import { CodegenSession } from './types';
export declare class ActionRecorder {
private static instance;
private sessions;
private activeSession;
private constructor();
static getInstance(): ActionRecorder;
startSession(): string;
endSession(sessionId: string): CodegenSession | null;
recordAction(toolName: string, parameters: Record<string, unknown>, result?: unknown): void;
getSession(sessionId: string): CodegenSession | null;
getActiveSession(): CodegenSession | null;
clearSession(sessionId: string): boolean;
}

View File

@@ -0,0 +1,62 @@
import { v4 as uuidv4 } from 'uuid';
export class ActionRecorder {
constructor() {
this.sessions = new Map();
this.activeSession = null;
}
static getInstance() {
if (!ActionRecorder.instance) {
ActionRecorder.instance = new ActionRecorder();
}
return ActionRecorder.instance;
}
startSession() {
const sessionId = uuidv4();
this.sessions.set(sessionId, {
id: sessionId,
actions: [],
startTime: Date.now(),
});
this.activeSession = sessionId;
return sessionId;
}
endSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.endTime = Date.now();
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return session;
}
return null;
}
recordAction(toolName, parameters, result) {
if (!this.activeSession) {
return;
}
const session = this.sessions.get(this.activeSession);
if (!session) {
return;
}
const action = {
toolName,
parameters,
timestamp: Date.now(),
result,
};
session.actions.push(action);
}
getSession(sessionId) {
return this.sessions.get(sessionId) || null;
}
getActiveSession() {
return this.activeSession ? this.sessions.get(this.activeSession) : null;
}
clearSession(sessionId) {
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return this.sessions.delete(sessionId);
}
}

View File

@@ -0,0 +1,28 @@
export interface CodegenAction {
toolName: string;
parameters: Record<string, unknown>;
timestamp: number;
result?: unknown;
}
export interface CodegenSession {
id: string;
actions: CodegenAction[];
startTime: number;
endTime?: number;
options?: CodegenOptions;
}
export interface PlaywrightTestCase {
name: string;
steps: string[];
imports: Set<string>;
}
export interface CodegenOptions {
outputPath?: string;
testNamePrefix?: string;
includeComments?: boolean;
}
export interface CodegenResult {
testCode: string;
filePath: string;
sessionId: string;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,17 @@
import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js';
import type { Page, Browser, APIRequestContext } from 'playwright';
export interface ToolContext {
page?: Page;
browser?: Browser;
apiContext?: APIRequestContext;
server?: any;
}
export interface ToolResponse extends CallToolResult {
content: (TextContent | ImageContent)[];
isError: boolean;
}
export interface ToolHandler {
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
export declare function createErrorResponse(message: string): ToolResponse;
export declare function createSuccessResponse(message: string | string[]): ToolResponse;

View File

@@ -0,0 +1,20 @@
// Helper functions for creating responses
export function createErrorResponse(message) {
return {
content: [{
type: "text",
text: message
}],
isError: true
};
}
export function createSuccessResponse(message) {
const messages = Array.isArray(message) ? message : [message];
return {
content: messages.map(msg => ({
type: "text",
text: msg
})),
isError: false
};
}

View File

@@ -0,0 +1,5 @@
export * from './common/types.js';
export * from './browser/index.js';
export * from './api/index.js';
export declare const BROWSER_TOOLS: string[];
export declare const API_TOOLS: string[];

View File

@@ -0,0 +1,26 @@
export * from './common/types.js';
export * from './browser/index.js';
export * from './api/index.js';
// Tool type constants
export const BROWSER_TOOLS = [
"playwright_navigate",
"playwright_screenshot",
"playwright_click",
"playwright_iframe_click",
"playwright_iframe_fill",
"playwright_fill",
"playwright_select",
"playwright_hover",
"playwright_evaluate",
"playwright_console_logs",
"playwright_close",
"playwright_get_visible_text",
"playwright_get_visible_html"
];
export const API_TOOLS = [
"playwright_get",
"playwright_post",
"playwright_put",
"playwright_patch",
"playwright_delete"
];

View File

@@ -0,0 +1,16 @@
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
export interface Tool {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
handler: (args: any) => Promise<any>;
}
export interface ToolCall {
name: string;
parameters: Record<string, unknown>;
result?: CallToolResult;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,58 @@
{
"name": "@executeautomation/playwright-mcp-server",
"version": "1.0.12",
"description": "Model Context Protocol servers for Playwright",
"license": "MIT",
"author": "ExecuteAutomation, Ltd (https://executeautomation.com)",
"homepage": "https://executeautomation.github.io/mcp-playwright/",
"bugs": "https://github.com/executeautomation/mcp-playwright/issues",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"playwright-mcp-server": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"test": "jest --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
"test:coverage": "jest --coverage --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
"test:custom": "node run-tests.cjs"
},
"repository": {
"type": "git",
"url": "https://github.com/executeautomation/mcp-playwright.git"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.24.3",
"@playwright/browser-chromium": "1.57.0",
"@playwright/browser-firefox": "1.57.0",
"@playwright/browser-webkit": "1.57.0",
"@playwright/test": "^1.57.0",
"cors": "^2.8.5",
"express": "^4.21.1",
"mcp-evals": "^2.0.1",
"playwright": "1.57.0",
"uuid": "11.1.0"
},
"keywords": [
"playwright",
"automation",
"AI",
"Claude MCP"
],
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.14",
"@types/node": "^20.10.5",
"jest": "^29.7.0",
"jest-playwright-preset": "4.0.0",
"shx": "^0.3.4",
"ts-jest": "^29.2.6",
"typescript": "^5.8.3"
}
}