"""FastMCP server for Mailu integration using OpenAPI specification.""" import asyncio import json import logging import os from pathlib import Path from typing import Any, Dict, Optional import httpx from dotenv import load_dotenv from fastmcp import FastMCP # Load environment variables from .env file load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Mailu API OpenAPI specification MAILU_OPENAPI_SPEC = { "swagger": "2.0", "basePath": "/api/v1", "info": { "title": "Mailu API", "version": "1.0" }, "host": "mail.example.com", # This will be configurable "schemes": ["https"], "produces": ["application/json"], "consumes": ["application/json"], "securityDefinitions": { "Bearer": { "type": "apiKey", "in": "header", "name": "Authorization" } }, "security": [{"Bearer": []}], "paths": { "/alias": { "get": { "responses": { "200": { "description": "Success", "schema": { "type": "array", "items": {"$ref": "#/definitions/Alias"} } } }, "summary": "List aliases", "operationId": "list_alias", "security": [{"Bearer": []}], "tags": ["alias"] }, "post": { "responses": { "409": { "description": "Duplicate alias", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Create a new alias", "operationId": "create_alias", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/Alias"} }], "security": [{"Bearer": []}], "tags": ["alias"] } }, "/alias/{alias}": { "parameters": [{ "name": "alias", "in": "path", "required": True, "type": "string" }], "get": { "responses": { "404": { "description": "Alias not found", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Alias"} } }, "summary": "Find alias", "operationId": "find_alias", "security": [{"Bearer": []}], "tags": ["alias"] }, "patch": { "responses": { "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "404": { "description": "Alias not found", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Update alias", "operationId": "update_alias", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/AliasUpdate"} }], "security": [{"Bearer": []}], "tags": ["alias"] }, "delete": { "responses": { "404": { "description": "Alias not found", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Delete alias", "operationId": "delete_alias", "security": [{"Bearer": []}], "tags": ["alias"] } }, "/domain": { "get": { "responses": { "200": { "description": "Success", "schema": { "type": "array", "items": {"$ref": "#/definitions/DomainGet"} } } }, "summary": "List domains", "operationId": "list_domain", "security": [{"Bearer": []}], "tags": ["domain"] }, "post": { "responses": { "409": { "description": "Duplicate domain/alternative name", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Create a new domain", "operationId": "create_domain", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/Domain"} }], "security": [{"Bearer": []}], "tags": ["domain"] } }, "/domain/{domain}": { "parameters": [{ "name": "domain", "in": "path", "required": True, "type": "string" }], "get": { "responses": { "404": { "description": "Domain not found", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Domain"} } }, "summary": "Find domain by name", "operationId": "find_domain", "security": [{"Bearer": []}], "tags": ["domain"] }, "patch": { "responses": { "409": { "description": "Duplicate domain/alternative name", "schema": {"$ref": "#/definitions/Response"} }, "404": { "description": "Domain not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Update an existing domain", "operationId": "update_domain", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/DomainUpdate"} }], "security": [{"Bearer": []}], "tags": ["domain"] }, "delete": { "responses": { "404": { "description": "Domain not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Delete domain", "operationId": "delete_domain", "security": [{"Bearer": []}], "tags": ["domain"] } }, "/domain/{domain}/users": { "parameters": [{ "name": "domain", "in": "path", "required": True, "type": "string" }], "get": { "responses": { "404": { "description": "Domain not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": { "type": "array", "items": {"$ref": "#/definitions/UserGet"} } } }, "summary": "List users from domain", "operationId": "list_user_domain", "security": [{"Bearer": []}], "tags": ["domain"] } }, "/user": { "get": { "responses": { "200": { "description": "Success", "schema": { "type": "array", "items": {"$ref": "#/definitions/UserGet"} } } }, "summary": "List users", "operationId": "list_users", "security": [{"Bearer": []}], "tags": ["user"] }, "post": { "responses": { "409": { "description": "Duplicate user", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception" }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Create user", "operationId": "create_user", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/UserCreate"} }], "security": [{"Bearer": []}], "tags": ["user"] } }, "/user/{email}": { "parameters": [{ "name": "email", "in": "path", "required": True, "type": "string" }], "get": { "responses": { "404": { "description": "User not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Find user", "operationId": "find_user", "security": [{"Bearer": []}], "tags": ["user"] }, "patch": { "responses": { "409": { "description": "Duplicate user", "schema": {"$ref": "#/definitions/Response"} }, "404": { "description": "User not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Update user", "operationId": "update_user", "parameters": [{ "name": "payload", "required": True, "in": "body", "schema": {"$ref": "#/definitions/UserUpdate"} }], "security": [{"Bearer": []}], "tags": ["user"] }, "delete": { "responses": { "404": { "description": "User not found", "schema": {"$ref": "#/definitions/Response"} }, "400": { "description": "Input validation exception", "schema": {"$ref": "#/definitions/Response"} }, "200": { "description": "Success", "schema": {"$ref": "#/definitions/Response"} } }, "summary": "Delete user", "operationId": "delete_user", "security": [{"Bearer": []}], "tags": ["user"] } } }, "definitions": { "UserCreate": { "required": ["email", "raw_password"], "properties": { "email": { "type": "string", "description": "The email address of the user", "example": "John.Doe@example.com" }, "raw_password": { "type": "string", "description": "The raw (plain text) password of the user. Mailu will hash the password using BCRYPT-SHA256", "example": "secret" }, "comment": { "type": "string", "description": "A description for the user. This description is shown on the Users page", "example": "my comment" }, "quota_bytes": { "type": "integer", "description": "The maximum quota for the user's email box in bytes", "example": "1000000000" }, "global_admin": { "type": "boolean", "description": "Make the user a global administrator" }, "enabled": { "type": "boolean", "description": "Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail" }, "displayed_name": { "type": "string", "description": "The display name of the user within the Admin GUI", "example": "John Doe" }, "forward_enabled": { "type": "boolean", "description": "Enable auto forwarding" }, "forward_destination": { "type": "array", "example": "Other@example.com", "items": { "type": "string", "description": "Email address to forward emails to" } }, "reply_enabled": { "type": "boolean", "description": "Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies" }, "reply_subject": { "type": "string", "description": "Optional subject for the automatic reply", "example": "Out of office" }, "reply_body": { "type": "string", "description": "The body of the automatic reply email", "example": "Hello, I am out of office. I will respond when I am back." } }, "type": "object" }, "UserGet": { "properties": { "email": { "type": "string", "description": "The email address of the user", "example": "John.Doe@example.com" }, "comment": { "type": "string", "description": "A description for the user", "example": "my comment" }, "quota_bytes": { "type": "integer", "description": "The maximum quota for the user's email box in bytes", "example": "1000000000" }, "global_admin": { "type": "boolean", "description": "Whether the user is a global administrator" }, "enabled": { "type": "boolean", "description": "Whether the user is enabled" }, "displayed_name": { "type": "string", "description": "The display name of the user", "example": "John Doe" } }, "type": "object" }, "UserUpdate": { "properties": { "raw_password": { "type": "string", "description": "The raw (plain text) password of the user", "example": "secret" }, "comment": { "type": "string", "description": "A description for the user", "example": "my comment" }, "quota_bytes": { "type": "integer", "description": "The maximum quota for the user's email box in bytes", "example": "1000000000" }, "enabled": { "type": "boolean", "description": "Enable or disable the user" }, "displayed_name": { "type": "string", "description": "The display name of the user", "example": "John Doe" } }, "type": "object" }, "Domain": { "required": ["name"], "properties": { "name": { "type": "string", "description": "FQDN (e.g. example.com)", "example": "example.com" }, "comment": { "type": "string", "description": "a comment" }, "max_users": { "type": "integer", "description": "maximum number of users", "default": -1, "minimum": -1 }, "max_aliases": { "type": "integer", "description": "maximum number of aliases", "default": -1, "minimum": -1 }, "max_quota_bytes": { "type": "integer", "description": "maximum quota for mailbox", "minimum": 0 }, "signup_enabled": { "type": "boolean", "description": "allow signup" } }, "type": "object" }, "DomainGet": { "allOf": [ {"$ref": "#/definitions/Domain"}, { "properties": { "dns_mx": {"type": "string"}, "dns_spf": {"type": "string"}, "dns_dkim": {"type": "string"}, "dns_dmarc": {"type": "string"} } } ] }, "DomainUpdate": { "properties": { "comment": { "type": "string", "description": "a comment" }, "max_users": { "type": "integer", "description": "maximum number of users", "default": -1, "minimum": -1 }, "max_aliases": { "type": "integer", "description": "maximum number of aliases", "default": -1, "minimum": -1 }, "signup_enabled": { "type": "boolean", "description": "allow signup" } }, "type": "object" }, "Alias": { "required": ["email"], "properties": { "email": { "type": "string", "description": "the alias email address", "example": "user@example.com" }, "destination": { "type": "array", "items": { "type": "string", "description": "alias email address", "example": "user@example.com" } }, "comment": { "type": "string", "description": "a comment" }, "wildcard": { "type": "boolean", "description": "enable SQL Like wildcard syntax" } }, "type": "object" }, "AliasUpdate": { "properties": { "destination": { "type": "array", "items": { "type": "string", "description": "alias email address", "example": "user@example.com" } }, "comment": { "type": "string", "description": "a comment" }, "wildcard": { "type": "boolean", "description": "enable SQL Like wildcard syntax" } }, "type": "object" }, "Response": { "properties": { "code": {"type": "integer"}, "message": {"type": "string"} }, "type": "object" } } } def create_mailu_client(base_url: str, api_token: str) -> httpx.AsyncClient: """Create an authenticated HTTP client for Mailu API.""" headers = { "Authorization": f"Bearer {api_token}", "Content-Type": "application/json", "Accept": "application/json" } return httpx.AsyncClient( base_url=base_url, headers=headers, timeout=30.0 ) async def create_mcp_server() -> FastMCP: """Create the MCP server with Mailu API integration.""" # Get configuration from environment variables mailu_base_url = os.getenv("MAILU_BASE_URL", "https://mail.example.com") mailu_api_token = os.getenv("MAILU_API_TOKEN", "") if not mailu_api_token: logger.warning("MAILU_API_TOKEN environment variable not set. Server will not work without authentication.") # Create authenticated HTTP client client = create_mailu_client(mailu_base_url + "/api/v1", mailu_api_token) # Fetch the actual OpenAPI specification from Mailu spec_url = "https://mail.supported.systems/api/v1/swagger.json" logger.info(f"Fetching OpenAPI spec from: {spec_url}") try: async with httpx.AsyncClient() as fetch_client: response = await fetch_client.get(spec_url) response.raise_for_status() spec = response.json() # Update the spec with the actual base URL if mailu_base_url: from urllib.parse import urlparse parsed = urlparse(mailu_base_url) if "host" in spec: spec["host"] = parsed.netloc if "schemes" in spec: spec["schemes"] = [parsed.scheme] except Exception as e: logger.error(f"Failed to fetch OpenAPI spec from {spec_url}: {e}") logger.info("Falling back to basic MCP server without OpenAPI integration") # Create a basic MCP server without OpenAPI mcp = FastMCP("Mailu MCP Server") @mcp.tool() async def list_users() -> str: """List all users in the Mailu instance.""" async with client as mailu_client: response = await mailu_client.get("/user") return f"Users: {response.json()}" @mcp.tool() async def list_domains() -> str: """List all domains in the Mailu instance.""" async with client as mailu_client: response = await mailu_client.get("/domain") return f"Domains: {response.json()}" logger.info("Created basic MCP server with manual tools") return mcp # Create MCP server from OpenAPI spec try: mcp = FastMCP.from_openapi( client=client, openapi_spec=spec, name="Mailu MCP Server", version="1.0.0" ) logger.info(f"Created MCP server with OpenAPI integration for Mailu API at {mailu_base_url}") return mcp except Exception as openapi_error: logger.error(f"Failed to create OpenAPI-based server: {openapi_error}") logger.info("Falling back to basic MCP server with manual tools") # Create a basic MCP server without OpenAPI mcp = FastMCP("Mailu MCP Server") @mcp.tool() async def list_users() -> str: """List all users in the Mailu instance.""" try: async with client as mailu_client: response = await mailu_client.get("/user") response.raise_for_status() return f"Users: {response.json()}" except Exception as e: return f"Error listing users: {e}" @mcp.tool() async def list_domains() -> str: """List all domains in the Mailu instance.""" try: async with client as mailu_client: response = await mailu_client.get("/domain") response.raise_for_status() return f"Domains: {response.json()}" except Exception as e: return f"Error listing domains: {e}" logger.info("Created basic MCP server with manual tools") return mcp async def main(): """Main entry point for the MCP server.""" logger.info("Starting Mailu MCP Server") try: # Create and run the MCP server mcp = await create_mcp_server() await mcp.run( transport="stdio", capture_keyboard_interrupt=True ) except Exception as e: logger.error(f"Failed to start MCP server: {e}") raise if __name__ == "__main__": asyncio.run(main())