Skip to main content
Glama
server.py18.7 kB
""" Mealie MCP Server FastMCP-based Model Context Protocol server for Mealie integration. Provides tools and resources for recipe management, meal planning, and shopping lists. """ import os import sys from pathlib import Path from typing import Optional # Ensure the src directory is in the path for imports sys.path.insert(0, str(Path(__file__).parent)) from dotenv import load_dotenv from fastmcp import FastMCP # Load environment variables load_dotenv() # Import tools from tools.recipes import ( recipes_search, recipes_get, recipes_list, recipes_create, recipes_create_from_url, recipes_update, recipes_delete, ) from tools.mealplans import ( mealplans_list, mealplans_today, mealplans_get, mealplans_create, mealplans_update, mealplans_delete, mealplans_random, mealplans_get_by_date, ) from tools.shopping import ( shopping_lists_list, shopping_lists_get, shopping_lists_create, shopping_lists_delete, shopping_items_add, shopping_items_add_bulk, shopping_items_check, shopping_items_delete, shopping_items_add_recipe, shopping_generate_from_mealplan, shopping_lists_clear_checked, ) # Import resources from resources.recipes import get_recipes_list, get_recipe_detail from resources.mealplans import get_current_mealplan, get_today_meals from resources.shopping import get_shopping_lists, get_shopping_list_detail # Create MCP server mcp = FastMCP("mealie") # ============================================================================= # Tools - Actions the AI can perform # ============================================================================= # ----------------------------------------------------------------------------- # Utility Tools # ----------------------------------------------------------------------------- @mcp.tool() def ping() -> str: """Test connectivity to the MCP server and Mealie instance.""" from client import MealieClient, MealieAPIError try: with MealieClient() as client: if client.test_connection(): return "pong - Mealie MCP server is running and connected to Mealie" except MealieAPIError as e: return f"MCP server running but Mealie connection failed: {e}" except Exception as e: return f"MCP server running but error occurred: {e}" # ----------------------------------------------------------------------------- # Recipe Tools (Phase 1) # ----------------------------------------------------------------------------- @mcp.tool() def mealie_recipes_search( query: str = "", tags: list[str] | None = None, categories: list[str] | None = None, limit: int = 10 ) -> str: """Search for recipes in Mealie. Args: query: Search term to filter recipes by name/description tags: List of tag names to filter by categories: List of category names to filter by limit: Maximum number of results (default 10) Returns: JSON string with list of matching recipes (name, slug, description, tags) """ return recipes_search(query=query, tags=tags, categories=categories, limit=limit) @mcp.tool() def mealie_recipes_get(slug: str) -> str: """Get complete details for a specific recipe. Args: slug: The recipe's URL slug identifier Returns: JSON string with full recipe details including ingredients, instructions, nutrition """ return recipes_get(slug=slug) @mcp.tool() def mealie_recipes_list(page: int = 1, per_page: int = 20) -> str: """List all recipes with pagination. Args: page: Page number (1-indexed) per_page: Number of recipes per page (default 20) Returns: JSON string with paginated recipe list and metadata """ return recipes_list(page=page, per_page=per_page) # ----------------------------------------------------------------------------- # Recipe CRUD Tools (Phase 4) # ----------------------------------------------------------------------------- @mcp.tool() def mealie_recipes_create( name: str, description: str = "", recipe_yield: str = "", total_time: str = "", prep_time: str = "", cook_time: str = "", ingredients: list[str] | None = None, instructions: list[str] | None = None, tags: list[str] | None = None, categories: list[str] | None = None, ) -> str: """Create a new recipe in Mealie. Args: name: Recipe name (required) description: Recipe description recipe_yield: Yield/servings (e.g., "4 servings") total_time: Total time (e.g., "1 hour 30 minutes") prep_time: Prep time (e.g., "20 minutes") cook_time: Cook time (e.g., "1 hour") ingredients: List of ingredient strings (e.g., ["2 cups flour", "1 tsp salt"]) instructions: List of instruction strings (e.g., ["Preheat oven", "Mix ingredients"]) tags: List of tag names to apply categories: List of category names to apply Returns: JSON string with created recipe details """ return recipes_create( name=name, description=description, recipe_yield=recipe_yield, total_time=total_time, prep_time=prep_time, cook_time=cook_time, ingredients=ingredients, instructions=instructions, tags=tags, categories=categories, ) @mcp.tool() def mealie_recipes_create_from_url(url: str, include_tags: bool = False) -> str: """Import a recipe from a URL by scraping it. Args: url: URL of the recipe to import include_tags: Whether to include tags from the scraped recipe (default False) Returns: JSON string with imported recipe details """ return recipes_create_from_url(url=url, include_tags=include_tags) @mcp.tool() def mealie_recipes_update( slug: str, name: str | None = None, description: str | None = None, recipe_yield: str | None = None, total_time: str | None = None, prep_time: str | None = None, cook_time: str | None = None, ingredients: list[str] | None = None, instructions: list[str] | None = None, tags: list[str] | None = None, categories: list[str] | None = None, ) -> str: """Update an existing recipe in Mealie. Args: slug: The recipe's slug identifier (required) name: New recipe name description: New description recipe_yield: New yield/servings total_time: New total time prep_time: New prep time cook_time: New cook time ingredients: New list of ingredient strings (replaces existing) instructions: New list of instruction strings (replaces existing) tags: New list of tag names (replaces existing) categories: New list of category names (replaces existing) Returns: JSON string with updated recipe details """ return recipes_update( slug=slug, name=name, description=description, recipe_yield=recipe_yield, total_time=total_time, prep_time=prep_time, cook_time=cook_time, ingredients=ingredients, instructions=instructions, tags=tags, categories=categories, ) @mcp.tool() def mealie_recipes_delete(slug: str) -> str: """Delete a recipe from Mealie. Args: slug: The recipe's slug identifier Returns: JSON string confirming deletion """ return recipes_delete(slug=slug) # ----------------------------------------------------------------------------- # Meal Plan Tools (Phase 2) # ----------------------------------------------------------------------------- @mcp.tool() def mealie_mealplans_list( start_date: str | None = None, end_date: str | None = None ) -> str: """List meal plans for a date range. Args: start_date: Start date in YYYY-MM-DD format (defaults to today) end_date: End date in YYYY-MM-DD format (defaults to 7 days from start) Returns: JSON string with list of meal plan entries """ return mealplans_list(start_date=start_date, end_date=end_date) @mcp.tool() def mealie_mealplans_today() -> str: """Get today's meal plan entries. Returns: JSON string with today's meals organized by entry type (breakfast, lunch, dinner, etc.) """ return mealplans_today() @mcp.tool() def mealie_mealplans_get(mealplan_id: str) -> str: """Get a specific meal plan entry by ID. Args: mealplan_id: The meal plan entry ID Returns: JSON string with meal plan entry details """ return mealplans_get(mealplan_id=mealplan_id) @mcp.tool() def mealie_mealplans_get_date(meal_date: str) -> str: """Get all meal plan entries for a specific date. Args: meal_date: Date in YYYY-MM-DD format Returns: JSON string with all meals for that date organized by type """ return mealplans_get_by_date(meal_date=meal_date) @mcp.tool() def mealie_mealplans_create( meal_date: str, entry_type: str, recipe_id: str | None = None, title: str | None = None, text: str | None = None ) -> str: """Create a new meal plan entry. Args: meal_date: Date for the meal in YYYY-MM-DD format entry_type: Type of meal - breakfast, lunch, dinner, side, or snack recipe_id: Optional recipe ID to associate with this entry title: Optional title for the entry (used if no recipe) text: Optional note or description Returns: JSON string with created meal plan entry """ return mealplans_create( meal_date=meal_date, entry_type=entry_type, recipe_id=recipe_id, title=title, text=text ) @mcp.tool() def mealie_mealplans_update( mealplan_id: str, meal_date: str | None = None, entry_type: str | None = None, recipe_id: str | None = None, title: str | None = None, text: str | None = None ) -> str: """Update an existing meal plan entry. Args: mealplan_id: The meal plan entry ID to update meal_date: Optional new date in YYYY-MM-DD format entry_type: Optional new meal type - breakfast, lunch, dinner, side, or snack recipe_id: Optional new recipe ID title: Optional new title text: Optional new note or description Returns: JSON string with updated meal plan entry """ return mealplans_update( mealplan_id=mealplan_id, meal_date=meal_date, entry_type=entry_type, recipe_id=recipe_id, title=title, text=text ) @mcp.tool() def mealie_mealplans_delete(mealplan_id: str) -> str: """Delete a meal plan entry. Args: mealplan_id: The meal plan entry ID to delete Returns: JSON string confirming deletion """ return mealplans_delete(mealplan_id=mealplan_id) @mcp.tool() def mealie_mealplans_random() -> str: """Get a random meal suggestion from available recipes. Returns: JSON string with a suggested recipe for meal planning """ return mealplans_random() # ----------------------------------------------------------------------------- # Shopping List Tools (Phase 3) # ----------------------------------------------------------------------------- @mcp.tool() def mealie_shopping_lists_list() -> str: """List all shopping lists. Returns: JSON string with list of shopping lists and their metadata """ return shopping_lists_list() @mcp.tool() def mealie_shopping_lists_get(list_id: str) -> str: """Get a specific shopping list with all items. Args: list_id: The shopping list ID Returns: JSON string with shopping list details and items """ return shopping_lists_get(list_id=list_id) @mcp.tool() def mealie_shopping_lists_create(name: str) -> str: """Create a new shopping list. Args: name: Name for the new shopping list Returns: JSON string with created shopping list details """ return shopping_lists_create(name=name) @mcp.tool() def mealie_shopping_lists_delete(list_id: str) -> str: """Delete a shopping list. Args: list_id: The shopping list ID to delete Returns: JSON string confirming deletion """ return shopping_lists_delete(list_id=list_id) @mcp.tool() def mealie_shopping_items_add( list_id: str, note: str | None = None, quantity: float | None = None, unit_id: str | None = None, food_id: str | None = None, display: str | None = None ) -> str: """Add an item to a shopping list. Args: list_id: The shopping list ID to add the item to note: Text description of the item (simplest way to add items) quantity: Quantity of the item unit_id: ID of the unit (e.g., cups, pounds) food_id: ID of the food from Mealie's food database display: Display text for the item Returns: JSON string with the added item """ return shopping_items_add( list_id=list_id, note=note, quantity=quantity, unit_id=unit_id, food_id=food_id, display=display ) @mcp.tool() def mealie_shopping_items_add_bulk( list_id: str, items: list[str] ) -> str: """Add multiple items to a shopping list at once. Args: list_id: The shopping list ID to add items to items: List of item descriptions (text notes) Returns: JSON string with count of added items """ return shopping_items_add_bulk(list_id=list_id, items=items) @mcp.tool() def mealie_shopping_items_check( item_id: str, checked: bool = True ) -> str: """Mark a shopping list item as checked or unchecked. Args: item_id: The shopping list item ID checked: True to mark as checked/purchased, False to uncheck Returns: JSON string confirming the update """ return shopping_items_check(item_id=item_id, checked=checked) @mcp.tool() def mealie_shopping_items_delete(item_id: str) -> str: """Remove an item from a shopping list. Args: item_id: The shopping list item ID to remove Returns: JSON string confirming deletion """ return shopping_items_delete(item_id=item_id) @mcp.tool() def mealie_shopping_add_recipe( list_id: str, recipe_id: str, scale: float = 1.0 ) -> str: """Add all ingredients from a recipe to a shopping list. Args: list_id: The shopping list ID to add ingredients to recipe_id: The recipe ID to get ingredients from scale: Scale factor for ingredient quantities (default 1.0) Returns: JSON string with updated shopping list """ return shopping_items_add_recipe(list_id=list_id, recipe_id=recipe_id, scale=scale) @mcp.tool() def mealie_shopping_generate_from_mealplan( start_date: str | None = None, end_date: str | None = None, list_name: str | None = None ) -> str: """Generate a shopping list from meal plan entries. This is the highest-value tool for household workflow - it reads the meal plan for a date range and creates a shopping list with all required ingredients. Args: start_date: Start date in YYYY-MM-DD format (defaults to today) end_date: End date in YYYY-MM-DD format (defaults to 7 days from start) list_name: Optional name for the shopping list (defaults to "Meal Plan - {date range}") Returns: JSON string with created shopping list details """ return shopping_generate_from_mealplan( start_date=start_date, end_date=end_date, list_name=list_name ) @mcp.tool() def mealie_shopping_clear_checked(list_id: str) -> str: """Remove all checked items from a shopping list. Args: list_id: The shopping list ID Returns: JSON string with count of removed items """ return shopping_lists_clear_checked(list_id=list_id) # ============================================================================= # Resources - Context the AI can read # ============================================================================= @mcp.resource("recipes://list") def resource_recipes_list() -> str: """Browse all recipes in Mealie organized by category.""" return get_recipes_list() @mcp.resource("recipes://{slug}") def resource_recipe_detail(slug: str) -> str: """Get detailed information about a specific recipe.""" return get_recipe_detail(slug) @mcp.resource("mealplans://current") def resource_current_mealplan() -> str: """View the current week's meal plan (Monday-Sunday).""" return get_current_mealplan() @mcp.resource("mealplans://today") def resource_today_meals() -> str: """View today's planned meals.""" return get_today_meals() @mcp.resource("mealplans://{date}") def resource_mealplan_date(date: str) -> str: """View meals planned for a specific date (YYYY-MM-DD format).""" from resources.mealplans import get_current_mealplan # Reuse the mealplans_get_by_date tool but format as markdown result = mealplans_get_by_date(date) import json data = json.loads(result) if "error" in data: return f"Error: {data['error']}" output = [f"# Meals for {data['date']}", ""] if data["count"] == 0: output.append("*No meals planned for this date*") return "\n".join(output) meals = data.get("meals", {}) for meal_type in ["breakfast", "lunch", "dinner", "side", "snack"]: if meal_type in meals: output.append(f"## {meal_type.capitalize()}") output.append("") for meal in meals[meal_type]: name = meal.get("recipe_name") or meal.get("title") or "Untitled" slug = meal.get("recipe_slug") output.append(f"- **{name}**" + (f" (`{slug}`)" if slug else "")) if meal.get("text"): output.append(f" - *Note: {meal['text']}*") output.append("") return "\n".join(output) @mcp.resource("shopping://lists") def resource_shopping_lists() -> str: """View all shopping lists with item counts.""" return get_shopping_lists() @mcp.resource("shopping://{list_id}") def resource_shopping_list_detail(list_id: str) -> str: """View a specific shopping list with all items.""" return get_shopping_list_detail(list_id) # ============================================================================= # Server entry point # ============================================================================= if __name__ == "__main__": mcp.run()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mdlopresti/mealie-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server