From 10e13b5bc79b27f88ad64ec12a3760a0d442c38b Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 16 Jul 2025 14:27:36 -0600 Subject: [PATCH] Fix resource implementation to follow FastMCP best practices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- pyproject.toml | 2 +- src/mcp_mailu/server.py | 86 ++++++++++++++++++++--------------------- uv.lock | 2 +- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25c9974..879e205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-mailu" -version = "0.4.0" +version = "0.4.1" description = "FastMCP server for Mailu email server API integration" authors = [ {name = "Ryan Malloy", email = "ryan@supported.systems"} diff --git a/src/mcp_mailu/server.py b/src/mcp_mailu/server.py index b41e8bf..952b3fb 100644 --- a/src/mcp_mailu/server.py +++ b/src/mcp_mailu/server.py @@ -3,7 +3,7 @@ import json import logging import os -from typing import Optional +from typing import Optional, Union import httpx from dotenv import load_dotenv @@ -62,159 +62,159 @@ def create_mcp_server() -> FastMCP: # List resources - no parameters @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get("/user") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error listing users: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get("/domain") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error listing domains: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alias") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error listing aliases: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alternative") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error listing alternative domains: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get("/relay") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error listing relays: {e}" + return [{"error": str(e)}] # Detail resources with parameters @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/user/{email}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting user {email}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting domain {domain}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/{alias}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting alias {alias}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alternative/{alt}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting alternative domain {alt}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/relay/{name}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting relay {name}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/users") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting users for domain {domain}: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/manager") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting managers for domain {domain}: {e}" + return [{"error": str(e)}] @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.""" try: async with get_mailu_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) + return response.json() except Exception as e: - return f"Error getting manager {email} for domain {domain}: {e}" + return {"error": str(e)} @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.""" try: async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/destination/{domain}") response.raise_for_status() - return json.dumps(response.json(), indent=2) + return response.json() except Exception as e: - return f"Error getting aliases for destination domain {domain}: {e}" + return [{"error": str(e)}] # ===== TOOLS (ACTIONS) ===== diff --git a/uv.lock b/uv.lock index 77e3dfe..84dedf6 100644 --- a/uv.lock +++ b/uv.lock @@ -613,7 +613,7 @@ wheels = [ [[package]] name = "mcp-mailu" -version = "0.3.2" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "fastmcp" },