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,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 {};