forked from rsp2k/mcp-mailu
🔄 Refactor API endpoints to use MCP resources vs tools (v0.3.0)
ARCHITECTURAL IMPROVEMENT: Proper separation of concerns in MCP design 📖 RESOURCES (Read-only data retrieval): - mailu://users - All users - mailu://domains - All domains - mailu://aliases - All aliases - mailu://alternative-domains - All alternative domains - mailu://relays - All relays - mailu://user/{email} - Specific user details - mailu://domain/{domain} - Specific domain details - mailu://alias/{alias} - Specific alias details - mailu://alternative-domain/{alt} - Specific alternative domain - mailu://relay/{name} - Specific relay details - mailu://domain/{domain}/users - Users in specific domain - mailu://domain/{domain}/managers - Managers for specific domain - mailu://domain/{domain}/manager/{email} - Specific manager details - mailu://alias/destination/{domain} - Aliases by destination domain ⚡ TOOLS (Actions and modifications): - create_user, update_user, delete_user - create_domain, update_domain, delete_domain, generate_dkim_keys - create_domain_manager, delete_domain_manager - create_alias, update_alias, delete_alias - create_alternative_domain, delete_alternative_domain - create_relay, update_relay, delete_relay ✨ BENEFITS: - Better UX: Resources are discoverable and browsable - Cleaner separation: Read vs Write operations - Structured data: Resources return properly formatted JSON - URI-based access: RESTful resource addressing - Improved performance: Resources can be cached by clients 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4f17a4e3bc
commit
3fe5bb02ad
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcp-mailu"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "FastMCP server for Mailu email server API integration"
|
||||
authors = [
|
||||
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||
|
@ -9,7 +9,7 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp import FastMCP, Context
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
@ -1059,21 +1059,172 @@ def create_mcp_server() -> FastMCP:
|
||||
logger.error(f"Failed to create OpenAPI-based server: {openapi_error}")
|
||||
logger.info("Falling back to basic MCP server with manual tools")
|
||||
|
||||
# Create a comprehensive MCP server with manual tools (with error handling)
|
||||
# Create a comprehensive MCP server with resources and tools
|
||||
mcp = FastMCP("Mailu MCP Server")
|
||||
|
||||
# ===== USER ENDPOINTS =====
|
||||
@mcp.tool()
|
||||
async def list_users() -> str:
|
||||
# ===== RESOURCES (READ-ONLY DATA) =====
|
||||
|
||||
# List resources - no parameters
|
||||
@mcp.resource("mailu://users")
|
||||
async def users_resource(ctx: Context) -> 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()}"
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing users: {e}"
|
||||
|
||||
@mcp.resource("mailu://domains")
|
||||
async def domains_resource(ctx: Context) -> 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 json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing domains: {e}"
|
||||
|
||||
@mcp.resource("mailu://aliases")
|
||||
async def aliases_resource(ctx: Context) -> str:
|
||||
"""List all aliases in the Mailu instance."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/alias")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing aliases: {e}"
|
||||
|
||||
@mcp.resource("mailu://alternative-domains")
|
||||
async def alternative_domains_resource(ctx: Context) -> str:
|
||||
"""List all alternative domains in the Mailu instance."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/alternative")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing alternative domains: {e}"
|
||||
|
||||
@mcp.resource("mailu://relays")
|
||||
async def relays_resource(ctx: Context) -> str:
|
||||
"""List all relays in the Mailu instance."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/relay")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing relays: {e}"
|
||||
|
||||
# Individual item resources - with parameters
|
||||
@mcp.resource("mailu://user/{email}")
|
||||
async def user_resource(email: str, ctx: Context) -> str:
|
||||
"""Get details of a specific user."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/user/{email}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting user {email}: {e}"
|
||||
|
||||
@mcp.resource("mailu://domain/{domain}")
|
||||
async def domain_resource(domain: str, ctx: Context) -> str:
|
||||
"""Get details of a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting domain {domain}: {e}"
|
||||
|
||||
@mcp.resource("mailu://alias/{alias}")
|
||||
async def alias_resource(alias: str, ctx: Context) -> str:
|
||||
"""Get details of a specific alias."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alias/{alias}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting alias {alias}: {e}"
|
||||
|
||||
@mcp.resource("mailu://alternative-domain/{alt}")
|
||||
async def alternative_domain_resource(alt: str, ctx: Context) -> str:
|
||||
"""Get details of a specific alternative domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alternative/{alt}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting alternative domain {alt}: {e}"
|
||||
|
||||
@mcp.resource("mailu://relay/{name}")
|
||||
async def relay_resource(name: str, ctx: Context) -> str:
|
||||
"""Get details of a specific relay."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/relay/{name}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting relay {name}: {e}"
|
||||
|
||||
# Domain-specific resources - with parameters
|
||||
@mcp.resource("mailu://domain/{domain}/users")
|
||||
async def domain_users_resource(domain: str, ctx: Context) -> str:
|
||||
"""List all users in a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/users")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing users for domain {domain}: {e}"
|
||||
|
||||
@mcp.resource("mailu://domain/{domain}/managers")
|
||||
async def domain_managers_resource(domain: str, ctx: Context) -> str:
|
||||
"""List all managers for a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/manager")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing managers for domain {domain}: {e}"
|
||||
|
||||
@mcp.resource("mailu://domain/{domain}/manager/{email}")
|
||||
async def domain_manager_resource(domain: str, email: str, ctx: Context) -> str:
|
||||
"""Get details of a specific domain manager."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/manager/{email}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting manager {email} for domain {domain}: {e}"
|
||||
|
||||
@mcp.resource("mailu://alias/destination/{domain}")
|
||||
async def aliases_by_domain_resource(domain: str, ctx: Context) -> str:
|
||||
"""Find aliases by destination domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alias/destination/{domain}")
|
||||
response.raise_for_status()
|
||||
return json.dumps(response.json(), indent=2)
|
||||
except Exception as e:
|
||||
return f"Error finding aliases for domain {domain}: {e}"
|
||||
|
||||
# ===== TOOLS (ACTIONS/MODIFICATIONS) =====
|
||||
|
||||
# USER TOOLS
|
||||
|
||||
@mcp.tool()
|
||||
async def create_user(email: str, raw_password: str, comment: str = "", quota_bytes: int = 0,
|
||||
global_admin: bool = False, enabled: bool = True, enable_imap: bool = True,
|
||||
@ -1111,16 +1262,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating user: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_user(email: str) -> str:
|
||||
"""Get details of a specific user."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/user/{email}")
|
||||
response.raise_for_status()
|
||||
return f"User details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting user: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def update_user(email: str, raw_password: str = "", comment: str = "", quota_bytes: int = None,
|
||||
@ -1169,17 +1310,7 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error deleting user: {e}"
|
||||
|
||||
# ===== DOMAIN ENDPOINTS =====
|
||||
@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}"
|
||||
# ===== DOMAIN TOOLS =====
|
||||
|
||||
@mcp.tool()
|
||||
async def create_domain(name: str, comment: str = "", max_users: int = -1, max_aliases: int = -1,
|
||||
@ -1204,16 +1335,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating domain: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_domain(domain: str) -> str:
|
||||
"""Get details of a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}")
|
||||
response.raise_for_status()
|
||||
return f"Domain details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting domain: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def update_domain(domain: str, comment: str = "", max_users: int = None, max_aliases: int = None,
|
||||
@ -1257,28 +1378,8 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error generating DKIM keys: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def list_domain_users(domain: str) -> str:
|
||||
"""List all users in a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/users")
|
||||
response.raise_for_status()
|
||||
return f"Domain users: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error listing domain users: {e}"
|
||||
|
||||
# ===== DOMAIN MANAGER ENDPOINTS =====
|
||||
@mcp.tool()
|
||||
async def list_domain_managers(domain: str) -> str:
|
||||
"""List all managers for a specific domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/manager")
|
||||
response.raise_for_status()
|
||||
return f"Domain managers: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error listing domain managers: {e}"
|
||||
# ===== DOMAIN MANAGER TOOLS =====
|
||||
|
||||
@mcp.tool()
|
||||
async def create_domain_manager(domain: str, user_email: str) -> str:
|
||||
@ -1292,16 +1393,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating domain manager: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_domain_manager(domain: str, email: str) -> str:
|
||||
"""Get details of a specific domain manager."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/domain/{domain}/manager/{email}")
|
||||
response.raise_for_status()
|
||||
return f"Domain manager details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting domain manager: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_domain_manager(domain: str, email: str) -> str:
|
||||
@ -1314,17 +1405,7 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error deleting domain manager: {e}"
|
||||
|
||||
# ===== ALIAS ENDPOINTS =====
|
||||
@mcp.tool()
|
||||
async def list_aliases() -> str:
|
||||
"""List all aliases in the Mailu instance."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/alias")
|
||||
response.raise_for_status()
|
||||
return f"Aliases: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error listing aliases: {e}"
|
||||
# ===== ALIAS TOOLS =====
|
||||
|
||||
@mcp.tool()
|
||||
async def create_alias(email: str, destination: str = "", comment: str = "", wildcard: bool = False) -> str:
|
||||
@ -1343,16 +1424,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating alias: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_alias(alias: str) -> str:
|
||||
"""Get details of a specific alias."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alias/{alias}")
|
||||
response.raise_for_status()
|
||||
return f"Alias details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting alias: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def update_alias(alias: str, destination: str = "", comment: str = "", wildcard: bool = None) -> str:
|
||||
@ -1381,28 +1452,8 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error deleting alias: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def find_aliases_by_domain(domain: str) -> str:
|
||||
"""Find aliases by destination domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alias/destination/{domain}")
|
||||
response.raise_for_status()
|
||||
return f"Aliases for domain: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error finding aliases by domain: {e}"
|
||||
|
||||
# ===== ALTERNATIVE DOMAIN ENDPOINTS =====
|
||||
@mcp.tool()
|
||||
async def list_alternative_domains() -> str:
|
||||
"""List all alternative domains."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/alternative")
|
||||
response.raise_for_status()
|
||||
return f"Alternative domains: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error listing alternative domains: {e}"
|
||||
# ===== ALTERNATIVE DOMAIN TOOLS =====
|
||||
|
||||
@mcp.tool()
|
||||
async def create_alternative_domain(name: str, domain: str) -> str:
|
||||
@ -1416,16 +1467,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating alternative domain: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_alternative_domain(alt: str) -> str:
|
||||
"""Get details of a specific alternative domain."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/alternative/{alt}")
|
||||
response.raise_for_status()
|
||||
return f"Alternative domain details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting alternative domain: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_alternative_domain(alt: str) -> str:
|
||||
@ -1438,17 +1479,7 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error deleting alternative domain: {e}"
|
||||
|
||||
# ===== RELAY ENDPOINTS =====
|
||||
@mcp.tool()
|
||||
async def list_relays() -> str:
|
||||
"""List all relays."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get("/relay")
|
||||
response.raise_for_status()
|
||||
return f"Relays: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error listing relays: {e}"
|
||||
# ===== RELAY TOOLS =====
|
||||
|
||||
@mcp.tool()
|
||||
async def create_relay(name: str, smtp: str = "", comment: str = "") -> str:
|
||||
@ -1462,16 +1493,6 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error creating relay: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_relay(name: str) -> str:
|
||||
"""Get details of a specific relay."""
|
||||
try:
|
||||
async with client as mailu_client:
|
||||
response = await mailu_client.get(f"/relay/{name}")
|
||||
response.raise_for_status()
|
||||
return f"Relay details: {response.json()}"
|
||||
except Exception as e:
|
||||
return f"Error getting relay: {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def update_relay(name: str, smtp: str = "", comment: str = "") -> str:
|
||||
@ -1499,7 +1520,7 @@ def create_mcp_server() -> FastMCP:
|
||||
except Exception as e:
|
||||
return f"Error deleting relay: {e}"
|
||||
|
||||
logger.info("Created comprehensive MCP server with all manual tools and error handling")
|
||||
logger.info("Created comprehensive MCP server with resources (read-only) and tools (actions) and error handling")
|
||||
return mcp
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user