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