@executeautomation/playwright-mcp-server
Version:
Model Context Protocol servers for Playwright
264 lines (260 loc) ⢠10.1 kB
JavaScript
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);
});
}