1
0
forked from rsp2k/mcp-mailu
mcp-mailu/src/mcp_mailu/server.py
Ryan Malloy 66d1c0732a Initial commit: FastMCP server for Mailu email API integration
- Complete FastMCP server with OpenAPI integration and fallback tools
- Automatic tool generation from Mailu REST API endpoints
- Bearer token authentication support
- Comprehensive test suite and documentation
- PyPI-ready package configuration with proper metadata
- Environment-based configuration support
- Production-ready error handling and logging
- Examples and publishing scripts included

Features:
- User management (list, create, update, delete)
- Domain management (list, create, update, delete)
- Alias management and email forwarding
- DKIM key generation
- Manager assignment for domains
- Graceful fallback when OpenAPI validation fails

Ready for Claude Desktop integration and PyPI distribution.
2025-07-16 11:55:44 -06:00

812 lines
29 KiB
Python

"""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())