1
0
forked from rsp2k/mcp-mailu

Fix resource implementation to follow FastMCP best practices

- Return dict/list directly instead of JSON strings for proper serialization
- FastMCP automatically serializes dicts/lists to JSON
- Update return types from str to dict/list/Union types
- Fix error handling to return structured data instead of strings
- Add Union type import for better type hints
- Version bump to 0.4.1 for resource fixes

Per FastMCP docs:
- Dictionaries/Lists are automatically serialized to JSON
- Resources should return native Python data structures
- FastMCP handles the serialization to proper MCP format

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ryan Malloy 2025-07-16 14:27:36 -06:00
parent cd40a3e899
commit 10e13b5bc7
3 changed files with 45 additions and 45 deletions

View File

@ -1,6 +1,6 @@
[project] [project]
name = "mcp-mailu" name = "mcp-mailu"
version = "0.4.0" version = "0.4.1"
description = "FastMCP server for Mailu email server API integration" description = "FastMCP server for Mailu email server API integration"
authors = [ authors = [
{name = "Ryan Malloy", email = "ryan@supported.systems"} {name = "Ryan Malloy", email = "ryan@supported.systems"}

View File

@ -3,7 +3,7 @@
import json import json
import logging import logging
import os import os
from typing import Optional from typing import Optional, Union
import httpx import httpx
from dotenv import load_dotenv from dotenv import load_dotenv
@ -62,159 +62,159 @@ def create_mcp_server() -> FastMCP:
# List resources - no parameters # List resources - no parameters
@mcp.resource("mailu://users") @mcp.resource("mailu://users")
async def users_resource(ctx: Context) -> str: async def users_resource(ctx: Context) -> list:
"""List all users in the Mailu instance.""" """List all users in the Mailu instance."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get("/user") response = await mailu_client.get("/user")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error listing users: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://domains") @mcp.resource("mailu://domains")
async def domains_resource(ctx: Context) -> str: async def domains_resource(ctx: Context) -> list:
"""List all domains in the Mailu instance.""" """List all domains in the Mailu instance."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get("/domain") response = await mailu_client.get("/domain")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error listing domains: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://aliases") @mcp.resource("mailu://aliases")
async def aliases_resource(ctx: Context) -> str: async def aliases_resource(ctx: Context) -> list:
"""List all aliases in the Mailu instance.""" """List all aliases in the Mailu instance."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get("/alias") response = await mailu_client.get("/alias")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error listing aliases: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://alternatives") @mcp.resource("mailu://alternatives")
async def alternatives_resource(ctx: Context) -> str: async def alternatives_resource(ctx: Context) -> list:
"""List all alternative domains in the Mailu instance.""" """List all alternative domains in the Mailu instance."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get("/alternative") response = await mailu_client.get("/alternative")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error listing alternative domains: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://relays") @mcp.resource("mailu://relays")
async def relays_resource(ctx: Context) -> str: async def relays_resource(ctx: Context) -> list:
"""List all relays in the Mailu instance.""" """List all relays in the Mailu instance."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get("/relay") response = await mailu_client.get("/relay")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error listing relays: {e}" return [{"error": str(e)}]
# Detail resources with parameters # Detail resources with parameters
@mcp.resource("mailu://user/{email}") @mcp.resource("mailu://user/{email}")
async def user_resource(ctx: Context, email: str) -> str: async def user_resource(ctx: Context, email: str) -> dict:
"""Get details of a specific user.""" """Get details of a specific user."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/user/{email}") response = await mailu_client.get(f"/user/{email}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting user {email}: {e}" return {"error": str(e)}
@mcp.resource("mailu://domain/{domain}") @mcp.resource("mailu://domain/{domain}")
async def domain_resource(ctx: Context, domain: str) -> str: async def domain_resource(ctx: Context, domain: str) -> dict:
"""Get details of a specific domain.""" """Get details of a specific domain."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/domain/{domain}") response = await mailu_client.get(f"/domain/{domain}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting domain {domain}: {e}" return {"error": str(e)}
@mcp.resource("mailu://alias/{alias}") @mcp.resource("mailu://alias/{alias}")
async def alias_resource(ctx: Context, alias: str) -> str: async def alias_resource(ctx: Context, alias: str) -> dict:
"""Get details of a specific alias.""" """Get details of a specific alias."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/alias/{alias}") response = await mailu_client.get(f"/alias/{alias}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting alias {alias}: {e}" return {"error": str(e)}
@mcp.resource("mailu://alternative/{alt}") @mcp.resource("mailu://alternative/{alt}")
async def alternative_resource(ctx: Context, alt: str) -> str: async def alternative_resource(ctx: Context, alt: str) -> dict:
"""Get details of a specific alternative domain.""" """Get details of a specific alternative domain."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/alternative/{alt}") response = await mailu_client.get(f"/alternative/{alt}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting alternative domain {alt}: {e}" return {"error": str(e)}
@mcp.resource("mailu://relay/{name}") @mcp.resource("mailu://relay/{name}")
async def relay_resource(ctx: Context, name: str) -> str: async def relay_resource(ctx: Context, name: str) -> dict:
"""Get details of a specific relay.""" """Get details of a specific relay."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/relay/{name}") response = await mailu_client.get(f"/relay/{name}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting relay {name}: {e}" return {"error": str(e)}
@mcp.resource("mailu://domain/{domain}/users") @mcp.resource("mailu://domain/{domain}/users")
async def domain_users_resource(ctx: Context, domain: str) -> str: async def domain_users_resource(ctx: Context, domain: str) -> list:
"""List all users in a specific domain.""" """List all users in a specific domain."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/domain/{domain}/users") response = await mailu_client.get(f"/domain/{domain}/users")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting users for domain {domain}: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://domain/{domain}/managers") @mcp.resource("mailu://domain/{domain}/managers")
async def domain_managers_resource(ctx: Context, domain: str) -> str: async def domain_managers_resource(ctx: Context, domain: str) -> list:
"""List all managers for a specific domain.""" """List all managers for a specific domain."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/domain/{domain}/manager") response = await mailu_client.get(f"/domain/{domain}/manager")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting managers for domain {domain}: {e}" return [{"error": str(e)}]
@mcp.resource("mailu://domain/{domain}/manager/{email}") @mcp.resource("mailu://domain/{domain}/manager/{email}")
async def domain_manager_resource(ctx: Context, domain: str, email: str) -> str: async def domain_manager_resource(ctx: Context, domain: str, email: str) -> dict:
"""Get details of a specific domain manager.""" """Get details of a specific domain manager."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/domain/{domain}/manager/{email}") response = await mailu_client.get(f"/domain/{domain}/manager/{email}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting manager {email} for domain {domain}: {e}" return {"error": str(e)}
@mcp.resource("mailu://aliases/destination/{domain}") @mcp.resource("mailu://aliases/destination/{domain}")
async def aliases_by_destination_resource(ctx: Context, domain: str) -> str: async def aliases_by_destination_resource(ctx: Context, domain: str) -> list:
"""Find aliases by destination domain.""" """Find aliases by destination domain."""
try: try:
async with get_mailu_client() as mailu_client: async with get_mailu_client() as mailu_client:
response = await mailu_client.get(f"/alias/destination/{domain}") response = await mailu_client.get(f"/alias/destination/{domain}")
response.raise_for_status() response.raise_for_status()
return json.dumps(response.json(), indent=2) return response.json()
except Exception as e: except Exception as e:
return f"Error getting aliases for destination domain {domain}: {e}" return [{"error": str(e)}]
# ===== TOOLS (ACTIONS) ===== # ===== TOOLS (ACTIONS) =====

2
uv.lock generated
View File

@ -613,7 +613,7 @@ wheels = [
[[package]] [[package]]
name = "mcp-mailu" name = "mcp-mailu"
version = "0.3.2" version = "0.4.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "fastmcp" }, { name = "fastmcp" },