Skip to main content
Glama

erpnext-server

error-handling-and-logging.md15 kB
# Error Handling and Logging Improvements This document outlines recommendations for improving error handling and logging in the ERPNext MCP server. ## Current Status The current implementation has several limitations in its error handling and logging approach: 1. Basic error handling with inconsistent patterns 2. Console logging without structured format 3. No distinct log levels for different environments 4. No dedicated error mapping between ERPNext and MCP error codes 5. Limited contextual information in error messages ## Proposed Improvements ### 1. Structured Error Handling #### Create a dedicated Error Handling Module ```typescript // src/utils/error-handler.ts import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { AxiosError } from "axios"; import { Logger } from "./logger"; export enum ERPNextErrorType { Authentication = "authentication", Permission = "permission", NotFound = "not_found", Validation = "validation", Server = "server", Network = "network", Unknown = "unknown" } export class ERPNextError extends Error { constructor( public readonly type: ERPNextErrorType, message: string, public readonly originalError?: Error ) { super(message); this.name = "ERPNextError"; } } /** * Maps ERPNext errors to MCP error codes */ export function mapToMcpError(error: ERPNextError): McpError { switch (error.type) { case ERPNextErrorType.Authentication: return new McpError(ErrorCode.Unauthorized, error.message); case ERPNextErrorType.Permission: return new McpError(ErrorCode.Forbidden, error.message); case ERPNextErrorType.NotFound: return new McpError(ErrorCode.NotFound, error.message); case ERPNextErrorType.Validation: return new McpError(ErrorCode.InvalidParams, error.message); case ERPNextErrorType.Server: return new McpError(ErrorCode.InternalError, error.message); case ERPNextErrorType.Network: return new McpError(ErrorCode.InternalError, `Network error: ${error.message}`); default: return new McpError(ErrorCode.InternalError, `Unknown error: ${error.message}`); } } /** * Categorizes an error based on its properties and returns an ERPNextError */ export function categorizeError(error: any): ERPNextError { if (error instanceof ERPNextError) { return error; } // Handle Axios errors if (error?.isAxiosError) { const axiosError = error as AxiosError; const statusCode = axiosError.response?.status; const responseData = axiosError.response?.data as any; const message = responseData?.message || responseData?.error || axiosError.message; if (!statusCode) { return new ERPNextError(ERPNextErrorType.Network, `Network error connecting to ERPNext: ${message}`, error); } switch (statusCode) { case 401: return new ERPNextError(ERPNextErrorType.Authentication, `Authentication failed: ${message}`, error); case 403: return new ERPNextError(ERPNextErrorType.Permission, `Permission denied: ${message}`, error); case 404: return new ERPNextError(ERPNextErrorType.NotFound, `Resource not found: ${message}`, error); case 400: return new ERPNextError(ERPNextErrorType.Validation, `Validation error: ${message}`, error); case 500: case 502: case 503: case 504: return new ERPNextError(ERPNextErrorType.Server, `ERPNext server error: ${message}`, error); default: return new ERPNextError(ERPNextErrorType.Unknown, `Unknown error: ${message}`, error); } } // Handle generic errors return new ERPNextError( ERPNextErrorType.Unknown, error?.message || 'An unknown error occurred', error instanceof Error ? error : undefined ); } /** * Handles errors in tool handlers, returning appropriate response format */ export function handleToolError(error: any, logger: Logger) { const erpError = categorizeError(error); // Log the error with appropriate context if (erpError.originalError) { logger.error(`${erpError.message}`, { errorType: erpError.type, originalError: erpError.originalError.message, stack: erpError.originalError.stack }); } else { logger.error(`${erpError.message}`, { errorType: erpError.type, stack: erpError.stack }); } // Return a formatted error response return { content: [{ type: "text", text: erpError.message }], isError: true }; } /** * Creates a correlation ID for tracking requests through the system */ export function createCorrelationId(): string { return `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; } ``` ### 2. Enhanced Logging System #### Structured Logger Implementation ```typescript // src/utils/logger.ts export enum LogLevel { ERROR = 0, WARN = 1, INFO = 2, DEBUG = 3 } export interface LogMetadata { [key: string]: any; } export interface LogEntry { timestamp: string; level: string; message: string; correlationId?: string; metadata?: LogMetadata; } export class Logger { private level: LogLevel; private serviceName: string; constructor(level: LogLevel = LogLevel.INFO, serviceName: string = "erpnext-mcp") { this.level = level; this.serviceName = serviceName; } setLevel(level: LogLevel) { this.level = level; } private formatLog(level: string, message: string, metadata?: LogMetadata): LogEntry { return { timestamp: new Date().toISOString(), level, message, correlationId: metadata?.correlationId, metadata: metadata ? { ...metadata, service: this.serviceName } : { service: this.serviceName } }; } private outputLog(logEntry: LogEntry) { // In production, you might want to use a more sophisticated logging system // For now, we'll use console with JSON formatting const logJson = JSON.stringify(logEntry); switch (logEntry.level) { case 'ERROR': console.error(logJson); break; case 'WARN': console.warn(logJson); break; case 'INFO': console.info(logJson); break; case 'DEBUG': console.debug(logJson); break; default: console.log(logJson); } } error(message: string, metadata?: LogMetadata) { if (this.level >= LogLevel.ERROR) { this.outputLog(this.formatLog('ERROR', message, metadata)); } } warn(message: string, metadata?: LogMetadata) { if (this.level >= LogLevel.WARN) { this.outputLog(this.formatLog('WARN', message, metadata)); } } info(message: string, metadata?: LogMetadata) { if (this.level >= LogLevel.INFO) { this.outputLog(this.formatLog('INFO', message, metadata)); } } debug(message: string, metadata?: LogMetadata) { if (this.level >= LogLevel.DEBUG) { this.outputLog(this.formatLog('DEBUG', message, metadata)); } } } ``` ### 3. Request Context and Correlation To track requests through the system: ```typescript // src/middleware/context.ts import { createCorrelationId } from "../utils/error-handler"; export interface RequestContext { correlationId: string; startTime: number; [key: string]: any; } export class RequestContextManager { private static contextMap = new Map<string, RequestContext>(); static createContext(requestId: string): RequestContext { const context: RequestContext = { correlationId: createCorrelationId(), startTime: Date.now() }; this.contextMap.set(requestId, context); return context; } static getContext(requestId: string): RequestContext | undefined { return this.contextMap.get(requestId); } static removeContext(requestId: string): void { this.contextMap.delete(requestId); } } ``` ### 4. Integration with MCP Server Use the error handling and logging systems with the MCP server: ```typescript // src/index.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Config } from "./utils/config"; import { Logger, LogLevel } from "./utils/logger"; import { RequestContextManager } from "./middleware/context"; async function main() { // Initialize logger const logger = new Logger( process.env.DEBUG ? LogLevel.DEBUG : LogLevel.INFO ); try { logger.info("Initializing ERPNext MCP server"); // Create server const server = new Server( { name: "erpnext-server", version: "0.1.0" }, { capabilities: { resources: {}, tools: {} } } ); // Set up request interceptors for correlation and context server.onRequest = (request) => { const context = RequestContextManager.createContext(request.id); logger.debug(`Received request: ${request.method}`, { correlationId: context.correlationId, request: { id: request.id, method: request.method, params: JSON.stringify(request.params) } }); }; // Set up response interceptors for timing and cleanup server.onResponse = (response) => { const context = RequestContextManager.getContext(response.id); if (context) { const duration = Date.now() - context.startTime; logger.debug(`Sending response`, { correlationId: context.correlationId, response: { id: response.id, duration: `${duration}ms`, hasError: !!response.error } }); // Clean up context RequestContextManager.removeContext(response.id); } }; // Set up error handler for server errors server.onerror = (error) => { logger.error("Server error", { error: error?.message, stack: error?.stack }); }; // Connect server with stdio transport const transport = new StdioServerTransport(); await server.connect(transport); logger.info('ERPNext MCP server running on stdio'); } catch (error) { logger.error("Failed to start server", { error: error?.message, stack: error?.stack }); process.exit(1); } } main(); ``` ### 5. Integrating with Client Update the ERPNext client to use the enhanced error handling: ```typescript // src/client/erpnext-client.ts import axios, { AxiosInstance } from "axios"; import { Logger } from "../utils/logger"; import { Config } from "../utils/config"; import { categorizeError, ERPNextError, ERPNextErrorType } from "../utils/error-handler"; export class ERPNextClient { private baseUrl: string; private axiosInstance: AxiosInstance; private authenticated: boolean = false; private logger: Logger; constructor(config: Config, logger: Logger) { this.logger = logger; this.baseUrl = config.getERPNextUrl(); // Initialize axios instance this.axiosInstance = axios.create({ baseURL: this.baseUrl, withCredentials: true, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); // Add request interceptor for logging this.axiosInstance.interceptors.request.use( (config) => { this.logger.debug(`Sending ${config.method?.toUpperCase()} request to ${config.url}`, { api: { method: config.method, url: config.url, hasData: !!config.data, hasParams: !!config.params } }); return config; }, (error) => { this.logger.error(`Request error: ${error.message}`); return Promise.reject(error); } ); // Add response interceptor for error handling this.axiosInstance.interceptors.response.use( (response) => { return response; }, (error) => { const erpError = categorizeError(error); this.logger.error(`API error: ${erpError.message}`, { errorType: erpError.type, statusCode: error.response?.status, data: error.response?.data }); return Promise.reject(erpError); } ); // Configure authentication if credentials provided const apiKey = config.getERPNextApiKey(); const apiSecret = config.getERPNextApiSecret(); if (apiKey && apiSecret) { this.axiosInstance.defaults.headers.common['Authorization'] = `token ${apiKey}:${apiSecret}`; this.authenticated = true; this.logger.info("Initialized with API key authentication"); } } // Client methods would use the same error handling pattern... async login(username: string, password: string): Promise<void> { try { this.logger.info(`Attempting login for user ${username}`); const response = await this.axiosInstance.post('/api/method/login', { usr: username, pwd: password }); if (response.data.message === 'Logged In') { this.authenticated = true; this.logger.info(`Successfully authenticated user ${username}`); } else { throw new ERPNextError( ERPNextErrorType.Authentication, "Login response did not confirm successful authentication" ); } } catch (error) { // Let the interceptor handle the error categorization this.authenticated = false; throw error; } } // Other methods would follow the same pattern... } ``` ## Benefits of Improved Error Handling and Logging 1. **Better Diagnostics**: Structured logging with correlation IDs makes it easier to track requests through the system and diagnose issues. 2. **Consistent Error Responses**: Standardized error handling ensures that clients receive consistent and informative error messages. 3. **Categorized Errors**: Errors are properly categorized, making it easier to handle different types of failures appropriately. 4. **Contextual Information**: Additional metadata is included with logs, providing more context for troubleshooting. 5. **Performance Tracking**: Request timing information helps identify performance bottlenecks. 6. **Environment-specific Logging**: Different log levels can be used in development vs. production environments. ## Implementation Plan 1. Create the error handling utility module 2. Implement the structured logger 3. Update the client to use the enhanced error handling 4. Update resource and tool handlers to leverage the new error system 5. Add request context management 6. Update the main server to integrate all components These changes will significantly improve the reliability and maintainability of the ERPNext MCP server by making errors more traceable and logs more informative.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rakeshgangwar/erpnext-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server