refactor: Replace logger with MCPLogger for improved logging compliance and add MCPLogger class for stderr logging

This commit is contained in:
Simon Larsen
2025-06-27 16:47:45 +01:00
parent 99de9fbd3d
commit f62a01594d
6 changed files with 140 additions and 26 deletions

View File

@@ -8,17 +8,20 @@ import {
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import logger from "Common/Server/Utils/Logger";
import dotenv from "dotenv";
import DynamicToolGenerator from "./Utils/DynamicToolGenerator";
import OneUptimeApiService, { OneUptimeApiConfig } from "./Services/OneUptimeApiService";
import { McpToolInfo, OneUptimeToolCallArgs } from "./Types/McpTypes";
import OneUptimeOperation from "./Types/OneUptimeOperation";
import MCPLogger from "./Utils/MCPLogger";
// Load environment variables
// Load environment variables (suppress console output)
const originalConsoleLog = console.log;
console.log = () => {}; // Temporarily disable console.log
dotenv.config();
console.log = originalConsoleLog; // Restore console.log
logger.info("OneUptime MCP Server is starting...");
MCPLogger.info("OneUptime MCP Server is starting...");
class OneUptimeMCPServer {
private server: Server;
@@ -55,15 +58,15 @@ class OneUptimeMCPServer {
};
OneUptimeApiService.initialize(config);
logger.info("OneUptime API Service initialized");
MCPLogger.info("OneUptime API Service initialized");
}
private generateTools(): void {
try {
this.tools = DynamicToolGenerator.generateAllTools();
logger.info(`Generated ${this.tools.length} OneUptime MCP tools`);
MCPLogger.info(`Generated ${this.tools.length} OneUptime MCP tools`);
} catch (error) {
logger.error(`Failed to generate tools: ${error}`);
MCPLogger.error(`Failed to generate tools: ${error}`);
throw error;
}
}
@@ -77,7 +80,7 @@ class OneUptimeMCPServer {
inputSchema: tool.inputSchema,
}));
logger.info(`Listing ${mcpTools.length} available tools`);
MCPLogger.info(`Listing ${mcpTools.length} available tools`);
return { tools: mcpTools };
});
@@ -95,7 +98,7 @@ class OneUptimeMCPServer {
);
}
logger.info(`Executing tool: ${name} for model: ${tool.modelName}`);
MCPLogger.info(`Executing tool: ${name} for model: ${tool.modelName}`);
// Execute the OneUptime operation
const result = await OneUptimeApiService.executeOperation(
@@ -118,7 +121,7 @@ class OneUptimeMCPServer {
],
};
} catch (error) {
logger.error(`Error executing tool ${name}: ${error}`);
MCPLogger.error(`Error executing tool ${name}: ${error}`);
if (error instanceof McpError) {
throw error;
@@ -184,12 +187,12 @@ class OneUptimeMCPServer {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info("OneUptime MCP Server is running!");
logger.info(`Available tools: ${this.tools.length} total`);
MCPLogger.info("OneUptime MCP Server is running!");
MCPLogger.info(`Available tools: ${this.tools.length} total`);
// Log some example tools
const exampleTools = this.tools.slice(0, 5).map(t => t.name);
logger.info(`Example tools: ${exampleTools.join(', ')}`);
MCPLogger.info(`Example tools: ${exampleTools.join(', ')}`);
}
}
@@ -199,24 +202,24 @@ async function main(): Promise<void> {
const mcpServer = new OneUptimeMCPServer();
await mcpServer.run();
} catch (error) {
logger.error(`Failed to start MCP server: ${error}`);
MCPLogger.error(`Failed to start MCP server: ${error}`);
process.exit(1);
}
}
// Handle graceful shutdown
process.on("SIGINT", () => {
logger.info("Received SIGINT, shutting down gracefully...");
MCPLogger.info("Received SIGINT, shutting down gracefully...");
process.exit(0);
});
process.on("SIGTERM", () => {
logger.info("Received SIGTERM, shutting down gracefully...");
MCPLogger.info("Received SIGTERM, shutting down gracefully...");
process.exit(0);
});
// Start the server
main().catch((error) => {
logger.error(`Unhandled error: ${error}`);
MCPLogger.error(`Unhandled error: ${error}`);
process.exit(1);
});

View File

@@ -134,6 +134,19 @@ npm start
}
```
## Troubleshooting
### MCP Logging Compatibility
This server uses a custom `MCPLogger` class that ensures all log messages are directed to `stderr` instead of `stdout`. This is critical for MCP protocol compliance, as `stdout` is reserved for JSON-RPC messages.
**Fixed Issues:**
- `Failed to parse message` warnings caused by log messages going to stdout
- Dotenv initialization messages interfering with MCP protocol
- All informational logging now properly directed to stderr
If you encounter parsing warnings, ensure no code is writing to stdout directly - use `MCPLogger` instead of console methods.
## Architecture
- **DynamicToolGenerator**: Automatically discovers and generates tools for all OneUptime models

View File

@@ -1,7 +1,7 @@
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
import { OneUptimeToolCallArgs } from "../Types/McpTypes";
import Logger from "Common/Server/Utils/Logger";
import MCPLogger from "../Utils/MCPLogger";
import API from "Common/Utils/API";
import URL from "Common/Types/API/URL";
import Route from "Common/Types/API/Route";
@@ -35,7 +35,7 @@ export default class OneUptimeApiService {
this.api = new API(protocol, hostname, baseRoute);
Logger.info(`OneUptime API Service initialized with: ${config.url}`);
MCPLogger.info(`OneUptime API Service initialized with: ${config.url}`);
}
/**
@@ -58,7 +58,7 @@ export default class OneUptimeApiService {
const headers = this.getHeaders();
const data = this.getRequestData(operation, args);
Logger.info(`Executing ${operation} operation for ${modelName} at ${route.toString()}`);
MCPLogger.info(`Executing ${operation} operation for ${modelName} at ${route.toString()}`);
try {
let response: HTTPResponse<any> | HTTPErrorResponse;
@@ -84,10 +84,10 @@ export default class OneUptimeApiService {
throw new Error(`API request failed: ${response.statusCode} - ${response.message}`);
}
Logger.info(`Successfully executed ${operation} operation for ${modelName}`);
MCPLogger.info(`Successfully executed ${operation} operation for ${modelName}`);
return response.data;
} catch (error) {
Logger.error(`Error executing ${operation} operation for ${modelName}: ${error}`);
MCPLogger.error(`Error executing ${operation} operation for ${modelName}: ${error}`);
throw error;
}
}

View File

@@ -5,7 +5,7 @@ import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
import { McpToolInfo, ModelToolsResult } from "../Types/McpTypes";
import Logger from "Common/Server/Utils/Logger";
import MCPLogger from "./MCPLogger";
import { ModelSchema, ModelSchemaType } from "Common/Utils/Schema/ModelSchema";
import { AnalyticsModelSchema, AnalyticsModelSchemaType } from "Common/Utils/Schema/AnalyticsModelSchema";
@@ -78,7 +78,7 @@ export default class DynamicToolGenerator {
additionalProperties: false
};
} catch (error) {
Logger.warn(`Failed to convert Zod schema to JSON Schema: ${error}`);
MCPLogger.warn(`Failed to convert Zod schema to JSON Schema: ${error}`);
return {
type: "object",
properties: {},
@@ -100,7 +100,7 @@ export default class DynamicToolGenerator {
const tools = this.generateToolsForDatabaseModel(model, ModelClass);
allTools.push(...tools.tools);
} catch (error) {
Logger.error(`Error generating tools for database model ${ModelClass.name}: ${error}`);
MCPLogger.error(`Error generating tools for database model ${ModelClass.name}: ${error}`);
}
}
@@ -111,11 +111,11 @@ export default class DynamicToolGenerator {
const tools = this.generateToolsForAnalyticsModel(model, ModelClass);
allTools.push(...tools.tools);
} catch (error) {
Logger.error(`Error generating tools for analytics model ${ModelClass.name}: ${error}`);
MCPLogger.error(`Error generating tools for analytics model ${ModelClass.name}: ${error}`);
}
}
Logger.info(`Generated ${allTools.length} MCP tools for OneUptime models`);
MCPLogger.info(`Generated ${allTools.length} MCP tools for OneUptime models`);
return allTools;
}

84
MCP/Utils/MCPLogger.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* MCP Logger - A logger specifically designed for MCP servers
* All logs are directed to stderr to avoid interfering with the JSON-RPC protocol on stdout
*/
import { LogLevel } from "Common/Server/EnvironmentConfig";
import ConfigLogLevel from "Common/Server/Types/ConfigLogLevel";
import { JSONObject } from "Common/Types/JSON";
import Exception from "Common/Types/Exception/Exception";
export type LogBody = string | JSONObject | Exception | Error | unknown;
export default class MCPLogger {
public static getLogLevel(): ConfigLogLevel {
if (!LogLevel) {
return ConfigLogLevel.INFO;
}
return LogLevel;
}
public static serializeLogBody(body: LogBody): string {
if (typeof body === "string") {
return body;
} else if (body instanceof Exception || body instanceof Error) {
return body.message;
}
return JSON.stringify(body);
}
public static info(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG || logLevel === ConfigLogLevel.INFO) {
// Use stderr instead of stdout for MCP compatibility
process.stderr.write(`[INFO] ${this.serializeLogBody(message)}\n`);
}
}
public static error(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (
logLevel === ConfigLogLevel.DEBUG ||
logLevel === ConfigLogLevel.INFO ||
logLevel === ConfigLogLevel.WARN ||
logLevel === ConfigLogLevel.ERROR
) {
// Use stderr for error messages
process.stderr.write(`[ERROR] ${this.serializeLogBody(message)}\n`);
}
}
public static warn(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (
logLevel === ConfigLogLevel.DEBUG ||
logLevel === ConfigLogLevel.INFO ||
logLevel === ConfigLogLevel.WARN
) {
// Use stderr for warning messages
process.stderr.write(`[WARN] ${this.serializeLogBody(message)}\n`);
}
}
public static debug(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG) {
// Use stderr for debug messages
process.stderr.write(`[DEBUG] ${this.serializeLogBody(message)}\n`);
}
}
public static trace(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG) {
// Use stderr for trace messages
process.stderr.write(`[TRACE] ${this.serializeLogBody(message)}\n`);
}
}
}

14
MCP/test-logger.ts Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env npx ts-node
import MCPLogger from "./Utils/MCPLogger";
// Test the MCP Logger to verify it outputs to stderr
MCPLogger.info("Test info message - should go to stderr");
MCPLogger.warn("Test warning message - should go to stderr");
MCPLogger.error("Test error message - should go to stderr");
// This should go to stdout and would cause MCP parsing issues
console.log("This would cause MCP parsing issues");
// Show that stdout is clear for JSON-RPC
process.stdout.write('{"jsonrpc":"2.0","id":1,"result":"test"}\n');