diff --git a/.gitignore b/.gitignore index 137cd90..8a245a5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ dist *.orig temp *.tsbuildinfo + +# Log files +logs/ +*.log diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 40ed4ea..d9f2810 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { PenpotMcpServer } from "./PenpotMcpServer"; -import { createLogger } from "./logger"; +import { createLogger, logFilePath } from "./logger"; /** * Entry point for Penpot MCP Server @@ -10,18 +10,20 @@ import { createLogger } from "./logger"; * gracefully and ensuring proper process termination. * * Usage: - * - Default port: node dist/index.js (runs on 4401) - * - Custom port: node dist/index.js --port * - Help: node dist/index.js --help + * - Default configuration: runs on port 4401, logs to mcp-server/logs at info level */ - async function main(): Promise { const logger = createLogger("main"); - try { - // Parse command line arguments for port configuration - const args = process.argv.slice(2); - let port = 4401; // Default port + // log the file path early so it appears before any potential errors + logger.info(`Logging to file: ${logFilePath}`); + + try { + const args = process.argv.slice(2); + let port = 4401; // default port + + // parse command line arguments for (let i = 0; i < args.length; i++) { if (args[i] === "--port" || args[i] === "-p") { if (i + 1 < args.length) { @@ -32,10 +34,20 @@ async function main(): Promise { logger.info("Invalid port number. Using default port 4401."); } } + } else if (args[i] === "--log-level" || args[i] === "-l") { + if (i + 1 < args.length) { + process.env.LOG_LEVEL = args[i + 1]; + } + } else if (args[i] === "--log-dir") { + if (i + 1 < args.length) { + process.env.LOG_DIR = args[i + 1]; + } } else if (args[i] === "--help" || args[i] === "-h") { logger.info("Usage: node dist/index.js [options]"); logger.info("Options:"); logger.info(" --port, -p Port number for the HTTP/SSE server (default: 4401)"); + logger.info(" --log-level, -l Log level: trace, debug, info, warn, error (default: info)"); + logger.info(" --log-dir Directory for log files (default: mcp-server/logs)"); logger.info(" --help, -h Show this help message"); process.exit(0); } @@ -44,7 +56,7 @@ async function main(): Promise { const server = new PenpotMcpServer(port); await server.start(); - // Keep the process alive + // keep the process alive process.on("SIGINT", async () => { logger.info("Received SIGINT, shutting down gracefully..."); await server.stop(); diff --git a/mcp-server/src/logger.ts b/mcp-server/src/logger.ts index bf6f730..05580c6 100644 --- a/mcp-server/src/logger.ts +++ b/mcp-server/src/logger.ts @@ -1,27 +1,71 @@ import pino from "pino"; +import { join, resolve } from "path"; /** - * Logger instance configured for console output with metadata. + * Configuration for log file location and level. + */ +const LOG_DIR = process.env.LOG_DIR || "logs"; +const LOG_LEVEL = process.env.LOG_LEVEL || "info"; + +/** + * Generates a timestamped log file name. * - * Configured to output to console only with level, full timestamp, origin, and message. + * @returns Log file name + */ +function generateLogFileName(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const seconds = String(now.getSeconds()).padStart(2, "0"); + return `penpot-mcp-${year}${month}${day}-${hours}${minutes}${seconds}.log`; +} + +/** + * Absolute path to the log file being written. + */ +export const logFilePath = resolve(join(LOG_DIR, generateLogFileName())); + +/** + * Logger instance configured for both console and file output with metadata. + * + * Both console and file output use pretty formatting for human readability. + * Console output includes colors, while file output is plain text. */ export const logger = pino({ - level: "info", + level: LOG_LEVEL, timestamp: pino.stdTimeFunctions.isoTime, - formatters: { - level: (label) => { - return { level: label }; - }, - }, transport: { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", - ignore: "pid,hostname", - messageFormat: "{msg}", - levelFirst: true, - }, + targets: [ + { + // console transport with pretty formatting + target: "pino-pretty", + level: LOG_LEVEL, + options: { + colorize: true, + translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", + ignore: "pid,hostname", + messageFormat: "{msg}", + levelFirst: true, + }, + }, + { + // file transport with pretty formatting (same as console) + target: "pino-pretty", + level: LOG_LEVEL, + options: { + destination: logFilePath, + colorize: false, + translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", + ignore: "pid,hostname", + messageFormat: "{msg}", + levelFirst: true, + mkdir: true, + }, + }, + ], }, });