Skip to main content
Glama
matthiashuebner

Xentral MCP HTTP Server

mcp_server.py16.9 kB
#!/usr/bin/env python3 """ Xentral MCP HTTP Server A Model Context Protocol (MCP) HTTP server for Xentral ERP integration. Features: - Real MCP HTTP Server: Full JSON-RPC 2.0 compatible implementation - 100+ Tools: Automatically discovered from xentral/ directory - Runtime Configuration: Update API credentials dynamically - Comprehensive Logging: All requests and responses logged - CORS Support: Compatible with web-based MCP clients """ import os import sys import logging import json import importlib import inspect from pathlib import Path from datetime import datetime # Third-party imports from flask import Flask, request, jsonify, Response, g from flask_cors import CORS # Local imports from config import config from mcp_protocol import MCPProtocol, MCPTool, MCPToolParameter # Setup logging def setup_logging(): """Configure logging for the MCP server.""" log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' # Create handlers list handlers = [logging.StreamHandler(sys.stderr)] # Only add file handler if we can write to the directory try: handlers.append(logging.FileHandler('mcp_server.log')) except (OSError, PermissionError): # Skip file logging if we can't write (e.g., read-only filesystem) pass logging.basicConfig( level=getattr(logging, config.log_level), format=log_format, handlers=handlers ) # Set specific loggers logging.getLogger('werkzeug').setLevel(logging.WARNING) return logging.getLogger(__name__) logger = setup_logging() # Initialize MCP protocol handler mcp_protocol = MCPProtocol( server_name=config.server_name, server_version=config.server_version ) # ============================================================================= # TOOL DISCOVERY AND INITIALIZATION # ============================================================================= def _class_name_to_tool_name(class_name: str) -> str: """Convert CamelCase class name to snake_case tool name.""" result = "" for i, char in enumerate(class_name): if char.isupper() and i > 0: result += "_" result += char.lower() return result def initialize_tools(): """Initialize MCP tools by scanning the xentral directory for implementations.""" try: logger.info("Initializing MCP tools by scanning xentral directory...") # Step 1: Discover implemented tools in xentral directory xentral_dir = Path("xentral") implemented_tools = {} if not xentral_dir.exists(): logger.warning(f"Xentral directory {xentral_dir} does not exist") return False # Add current directory to Python path if not already there current_path = str(Path.cwd()) if current_path not in sys.path: sys.path.insert(0, current_path) # Scan for Python files in xentral directory for py_file in xentral_dir.glob("*.py"): if py_file.name.startswith("__") or py_file.name == "base.py": continue module_name = py_file.stem try: # Import the module full_module_name = f"xentral.{module_name}" module = importlib.import_module(full_module_name) # Find classes that have an execute method (tool implementations) for name, obj in inspect.getmembers(module, inspect.isclass): if (hasattr(obj, 'execute') and callable(getattr(obj, 'execute')) and name != 'XentralAPIBase'): # Convert class name to tool name tool_name = _class_name_to_tool_name(name) implemented_tools[tool_name] = obj logger.info(f"✅ Found implemented tool: {tool_name} ({name})") except Exception as e: logger.error(f"Error importing {full_module_name}: {e}") continue # Step 2: Create MCP tools for implemented tools all_tools = [] for tool_name, tool_class in implemented_tools.items(): # Infer parameters and description parameters = _infer_tool_parameters(tool_name) description = _infer_tool_description(tool_name) mcp_tool = MCPTool( name=tool_name, description=description, parameters=parameters ) # Store reference to implementation class mcp_tool._implementation_class = tool_class mcp_tool._is_implemented = True all_tools.append(mcp_tool) logger.info(f"✅ Created MCP tool: {tool_name}") if not all_tools: logger.warning("No implemented tools were found") return False # Register all tools with the MCP protocol mcp_protocol.register_tools(all_tools) logger.info(f"✅ Successfully initialized {len(all_tools)} MCP tools") return True except Exception as e: logger.error(f"Failed to initialize tools: {e}") return False def _infer_tool_parameters(tool_name: str) -> list: """Infer parameters for a tool based on its name.""" if "search" in tool_name: if "customer" in tool_name: return [ MCPToolParameter("customer_id", "integer", "Customer ID", required=False), MCPToolParameter("customer_number", "string", "Customer Number", required=False), MCPToolParameter("name", "string", "Customer Name", required=False), MCPToolParameter("email", "string", "Email Address", required=False), MCPToolParameter("phone", "string", "Phone Number", required=False), MCPToolParameter("city", "string", "City", required=False), MCPToolParameter("page", "integer", "Page Number", required=False), MCPToolParameter("limit", "integer", "Results Limit", required=False), MCPToolParameter("raw", "boolean", "Show raw API response", required=False) ] # Default parameters for unknown tools return [ MCPToolParameter("id", "integer", "Record ID", required=False), MCPToolParameter("page", "integer", "Page Number", required=False), MCPToolParameter("limit", "integer", "Results Limit", required=False), MCPToolParameter("raw", "boolean", "Show raw API response", required=False) ] def _infer_tool_description(tool_name: str) -> str: """Infer description for a tool based on its name.""" descriptions = { "search_customers": "Search and find customers by various criteria", } return descriptions.get(tool_name, f"Execute {tool_name.replace('_', ' ')} operation") # ============================================================================= # FLASK APPLICATION SETUP # ============================================================================= def create_app(): """Create and configure Flask application.""" app = Flask(__name__) CORS(app) # Enable CORS for MCP clients # ============================================================================= # REQUEST LOGGING MIDDLEWARE # ============================================================================= @app.before_request def log_request(): """Log incoming MCP requests.""" if config.log_requests and (request.path == '/mcp' or request.path.startswith('/mcp/')): timestamp = datetime.now().isoformat() logger.info(f"[{timestamp}] MCP Request: {request.method} {request.path}") if request.is_json and request.json: request_data = request.json logger.info(f"[{timestamp}] Request Data: {request_data}") if 'method' in request_data: mcp_method = request_data['method'] logger.info(f"[{timestamp}] MCP Method: {mcp_method}") if mcp_method == 'tools/call' and 'params' in request_data: params = request_data['params'] tool_name = params.get('name', 'unknown') tool_args = params.get('arguments', {}) logger.info(f"[{timestamp}] Tool Call: {tool_name}") logger.info(f"[{timestamp}] Tool Arguments: {tool_args}") # ============================================================================= # MCP HTTP ENDPOINTS # ============================================================================= @app.route('/mcp', methods=['POST']) def handle_mcp_request(): """Handle MCP JSON-RPC requests.""" try: if not request.is_json: return jsonify({ "jsonrpc": "2.0", "error": { "code": -32700, "message": "Request must be JSON" } }), 400 # Process the MCP request request_data = request.get_data(as_text=True) response_json = mcp_protocol.handle_request(request_data) return Response( response_json, mimetype='application/json', headers={'Access-Control-Allow-Origin': '*'} ) except Exception as e: logger.exception(f"Error handling MCP request: {e}") return jsonify({ "jsonrpc": "2.0", "error": { "code": -32603, "message": "Internal server error" } }), 500 # Alternative endpoints for different MCP client implementations @app.route('/mcp/list_tools', methods=['POST']) def list_tools(): """Alternative endpoint for listing tools.""" return handle_mcp_request() @app.route('/mcp/call_tool', methods=['POST']) def call_tool(): """Alternative endpoint for calling tools.""" return handle_mcp_request() @app.route('/mcp/initialize', methods=['POST']) def initialize(): """Alternative endpoint for initialization.""" return handle_mcp_request() # ============================================================================= # HEALTH & INFO ENDPOINTS # ============================================================================= @app.route('/health', methods=['GET']) def health_check(): """Health check endpoint.""" return jsonify({ "status": "healthy", "server": config.server_name, "version": config.server_version, "initialized": mcp_protocol.initialized, "tools_count": len(mcp_protocol.tools) }), 200 @app.route('/info', methods=['GET']) def server_info(): """Server information endpoint.""" return jsonify({ "server": mcp_protocol.get_server_info(), "config": { "api_url": config.api_url, "api_key": "***" if config.api_key else "not_configured", "debug": config.debug_mode } }), 200 @app.route('/tools', methods=['GET']) def list_all_tools(): """List all available tools.""" tools_list = [] for tool in mcp_protocol.tools.values(): tools_list.append({ "name": tool.name, "description": tool.description, "parameters": [ { "name": p.name, "type": p.type, "required": p.required } for p in tool.parameters ] }) return jsonify({ "total": len(tools_list), "tools": tools_list }), 200 @app.route('/config/credentials', methods=['POST']) def update_credentials(): """Update API credentials at runtime.""" try: data = request.get_json() if not data: return jsonify({"error": "Request body is required"}), 400 api_url = data.get('api_url') api_key = data.get('api_key') if not api_url or not api_key: return jsonify({"error": "Both api_url and api_key are required"}), 400 # Update configuration config.update_credentials(api_url, api_key) logger.info("✅ API credentials updated successfully") return jsonify({ "status": "success", "message": "API credentials updated", "api_url": config.api_url }), 200 except Exception as e: logger.error(f"Error updating credentials: {e}") return jsonify({"error": str(e)}), 500 # ============================================================================= # ERROR HANDLERS # ============================================================================= @app.errorhandler(404) def not_found(error): """Handle 404 errors.""" return jsonify({ "error": "Endpoint not found", "available_endpoints": [ "/mcp (POST) - Main MCP JSON-RPC endpoint", "/health (GET) - Health check", "/info (GET) - Server information", "/tools (GET) - List all tools", "/config/credentials (POST) - Update API credentials" ] }), 404 @app.errorhandler(500) def internal_error(error): """Handle 500 errors.""" logger.exception(f"Internal server error: {error}") return jsonify({ "error": "Internal server error", "message": "Check server logs for details" }), 500 return app # ============================================================================= # MAIN EXECUTION # ============================================================================= def validate_configuration(): """Validate server configuration before starting.""" errors = config.validate_config() if errors: print("⚠️ Configuration Issues:") for error in errors: print(f" - {error}") print("\n💡 You can update credentials later via POST /config/credentials") return False return True def main(): """Main entry point for the MCP server.""" try: app = create_app() except ImportError as e: print(f"❌ Import error: {e}") return 1 print("=" * 60) print("🚀 Xentral MCP HTTP Server") print("=" * 60) # Display configuration print(f"Server: {config.server_name} v{config.server_version}") print(f"API URL: {config.api_url}") print(f"API Key: {'✓ Configured' if config.api_key else '✗ Not configured'}") print(f"Host: {config.server_host}") print(f"Port: {config.server_port}") print(f"Debug: {config.debug_mode}") print("-" * 60) # Validate configuration if not validate_configuration(): print("\n💡 Update credentials via POST /config/credentials endpoint") print("-" * 60) # Initialize tools if not initialize_tools(): print("❌ Failed to initialize tools. Server will start but may not function properly.") return 1 print(f"✅ Loaded {len(mcp_protocol.tools)} MCP tools") print("-" * 60) # Start server print(f"🌐 Starting MCP server on http://{config.server_host}:{config.server_port}") print("📋 Available MCP endpoints:") print(" - POST /mcp - Main MCP JSON-RPC endpoint") print(" - GET /health - Health check") print(" - GET /info - Server information") print(" - GET /tools - List all tools") print(" - POST /config/credentials - Update API credentials") print() print("=" * 60) try: app.run( host=config.server_host, port=config.server_port, debug=config.debug_mode, threaded=True ) except KeyboardInterrupt: print("\n👋 Server stopped by user") except Exception as e: logger.error(f"Failed to start server: {e}") return 1 return 0 if __name__ == '__main__': sys.exit(main())

Latest Blog Posts

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/matthiashuebner/xentral-mcp'

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