mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
refactor: Replace logger with MCPLogger for improved logging compliance and add MCPLogger class for stderr logging
This commit is contained in:
35
MCP/Index.ts
35
MCP/Index.ts
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
84
MCP/Utils/MCPLogger.ts
Normal 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
14
MCP/test-logger.ts
Normal 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');
|
||||
Reference in New Issue
Block a user