added all mcp
This commit is contained in:
16
mealie-mcp-bundle/server/mealie/__init__.py
Normal file
16
mealie-mcp-bundle/server/mealie/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .client import MealieClient
|
||||
from .food import FoodMixin
|
||||
from .group import GroupMixin
|
||||
from .mealplan import MealplanMixin
|
||||
from .recipe import RecipeMixin
|
||||
from .user import UserMixin
|
||||
|
||||
class MealieFetcher(
|
||||
RecipeMixin,
|
||||
UserMixin,
|
||||
GroupMixin,
|
||||
MealplanMixin,
|
||||
FoodMixin,
|
||||
MealieClient,
|
||||
):
|
||||
pass
|
||||
15
mealie-mcp-bundle/server/mealie/__init___1.py
Normal file
15
mealie-mcp-bundle/server/mealie/__init___1.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from .client import MealieClient
|
||||
from .group import GroupMixin
|
||||
from .mealplan import MealplanMixin
|
||||
from .recipe import RecipeMixin
|
||||
from .user import UserMixin
|
||||
|
||||
|
||||
class MealieFetcher(
|
||||
RecipeMixin,
|
||||
UserMixin,
|
||||
GroupMixin,
|
||||
MealplanMixin,
|
||||
MealieClient,
|
||||
):
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
BIN
mealie-mcp-bundle/server/mealie/__pycache__/food.cpython-313.pyc
Normal file
BIN
mealie-mcp-bundle/server/mealie/__pycache__/food.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mealie-mcp-bundle/server/mealie/__pycache__/user.cpython-313.pyc
Normal file
BIN
mealie-mcp-bundle/server/mealie/__pycache__/user.cpython-313.pyc
Normal file
Binary file not shown.
146
mealie-mcp-bundle/server/mealie/client.py
Normal file
146
mealie-mcp-bundle/server/mealie/client.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Any, Dict
|
||||
|
||||
import httpx
|
||||
from httpx import ConnectError, HTTPStatusError, ReadTimeout
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class MealieApiError(Exception):
|
||||
"""Custom exception for Mealie API errors with status code and response details."""
|
||||
|
||||
def __init__(self, status_code: int, message: str, response_text: str = None):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
self.response_text = response_text
|
||||
super().__init__(f"{message} (Status Code: {status_code})")
|
||||
|
||||
|
||||
class MealieClient:
|
||||
|
||||
def __init__(self, base_url: str, api_key: str):
|
||||
if not base_url:
|
||||
raise ValueError("Base URL cannot be empty")
|
||||
if not api_key:
|
||||
raise ValueError("API key cannot be empty")
|
||||
|
||||
logger.debug({"message": "Initializing MealieClient", "base_url": base_url})
|
||||
try:
|
||||
self._client = httpx.Client(
|
||||
base_url=base_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=30.0, # Set a reasonable timeout for requests
|
||||
)
|
||||
# Test connection
|
||||
logger.debug({"message": "Testing connection to Mealie API"})
|
||||
self._client.get("/api/app/about")
|
||||
logger.info({"message": "Successfully connected to Mealie API"})
|
||||
except ConnectError as e:
|
||||
error_msg = f"Failed to connect to Mealie API at {base_url}: {str(e)}"
|
||||
logger.error({"message": error_msg})
|
||||
logger.debug(
|
||||
{"message": "Error traceback", "traceback": traceback.format_exc()}
|
||||
)
|
||||
raise ConnectionError(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"Error initializing Mealie client: {str(e)}"
|
||||
logger.error({"message": error_msg})
|
||||
logger.debug(
|
||||
{"message": "Error traceback", "traceback": traceback.format_exc()}
|
||||
)
|
||||
raise
|
||||
|
||||
def _handle_request(self, method: str, url: str, **kwargs) -> Dict[str, Any] | str:
|
||||
"""Common request handler with error handling for all API calls."""
|
||||
try:
|
||||
logger.debug(
|
||||
{
|
||||
"message": "Making API request",
|
||||
"method": method,
|
||||
"url": url,
|
||||
"body": kwargs.get("json"),
|
||||
}
|
||||
)
|
||||
|
||||
if "params" in kwargs:
|
||||
logger.debug(
|
||||
{"message": "Request parameters", "params": kwargs["params"]}
|
||||
)
|
||||
if "json" in kwargs:
|
||||
logger.debug({"message": "Request payload", "payload": kwargs["json"]})
|
||||
|
||||
response = self._client.request(method, url, **kwargs)
|
||||
response.raise_for_status() # Raise an exception for 4XX/5XX responses
|
||||
|
||||
logger.debug(
|
||||
{"message": "Request successful", "status_code": response.status_code}
|
||||
)
|
||||
# Log the response content at debug level
|
||||
try:
|
||||
response_data = response.json()
|
||||
logger.debug({"message": "Response content", "data": response_data})
|
||||
return response_data
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(
|
||||
{"message": "Response content (non-JSON)", "content": response.text}
|
||||
)
|
||||
return response.text
|
||||
|
||||
except HTTPStatusError as e:
|
||||
status_code = e.response.status_code
|
||||
error_detail = f"HTTP Error {status_code}"
|
||||
|
||||
# Try to parse error details from response
|
||||
try:
|
||||
error_detail = e.response.json()
|
||||
except Exception:
|
||||
error_detail = e.response.text
|
||||
|
||||
error_msg = f"API error for {method} {url}: {error_detail}"
|
||||
logger.error(
|
||||
{
|
||||
"message": "API request failed",
|
||||
"method": method,
|
||||
"url": url,
|
||||
"status_code": status_code,
|
||||
"error_detail": error_detail,
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
{"message": "Failed Request body", "content": e.request.content}
|
||||
)
|
||||
raise MealieApiError(status_code, error_msg, e.response.text) from e
|
||||
|
||||
except ReadTimeout:
|
||||
error_msg = f"Request timeout for {method} {url}"
|
||||
logger.error({"message": error_msg, "method": method, "url": url})
|
||||
logger.debug(
|
||||
{"message": "Error traceback", "traceback": traceback.format_exc()}
|
||||
)
|
||||
raise TimeoutError(error_msg)
|
||||
|
||||
except ConnectError as e:
|
||||
error_msg = f"Connection error for {method} {url}: {str(e)}"
|
||||
logger.error(
|
||||
{"message": error_msg, "method": method, "url": url, "error": str(e)}
|
||||
)
|
||||
logger.debug(
|
||||
{"message": "Error traceback", "traceback": traceback.format_exc()}
|
||||
)
|
||||
raise ConnectionError(error_msg) from e
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error for {method} {url}: {str(e)}"
|
||||
logger.error(
|
||||
{"message": error_msg, "method": method, "url": url, "error": str(e)}
|
||||
)
|
||||
logger.debug(
|
||||
{"message": "Error traceback", "traceback": traceback.format_exc()}
|
||||
)
|
||||
raise
|
||||
49
mealie-mcp-bundle/server/mealie/food.py
Normal file
49
mealie-mcp-bundle/server/mealie/food.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class FoodMixin:
|
||||
"""Mixin for Mealie food and unit API operations."""
|
||||
|
||||
def get_foods(
|
||||
self,
|
||||
search: Optional[str] = None,
|
||||
page: Optional[int] = None,
|
||||
per_page: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get list of foods."""
|
||||
params = {}
|
||||
if search:
|
||||
params["search"] = search
|
||||
if page:
|
||||
params["page"] = page
|
||||
if per_page:
|
||||
params["perPage"] = per_page
|
||||
|
||||
logger.info({"message": "Retrieving foods", "parameters": params})
|
||||
return self._handle_request("GET", "/api/foods", params=params)
|
||||
|
||||
def get_units(self) -> Dict[str, Any]:
|
||||
"""Get list of all units."""
|
||||
logger.info({"message": "Retrieving units"})
|
||||
return self._handle_request("GET", "/api/units")
|
||||
|
||||
def create_food(self, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Create a new food item."""
|
||||
data = {"name": name}
|
||||
if description:
|
||||
data["description"] = description
|
||||
|
||||
logger.info({"message": "Creating food", "name": name})
|
||||
return self._handle_request("POST", "/api/foods", json=data)
|
||||
|
||||
def create_unit(self, name: str, abbreviation: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Create a new unit."""
|
||||
data = {"name": name}
|
||||
if abbreviation:
|
||||
data["abbreviation"] = abbreviation
|
||||
|
||||
logger.info({"message": "Creating unit", "name": name})
|
||||
return self._handle_request("POST", "/api/units", json=data)
|
||||
17
mealie-mcp-bundle/server/mealie/group.py
Normal file
17
mealie-mcp-bundle/server/mealie/group.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class GroupMixin:
|
||||
"""Mixin class for group-related API endpoints"""
|
||||
|
||||
def get_current_group(self) -> Dict[str, Any]:
|
||||
"""Get information about the current user's group.
|
||||
|
||||
Returns:
|
||||
Dictionary containing group details such as id, name, slug, and other group information.
|
||||
"""
|
||||
logger.info({"message": "Retrieving current group information"})
|
||||
return self._handle_request("GET", "/api/groups/self")
|
||||
105
mealie-mcp-bundle/server/mealie/mealplan.py
Normal file
105
mealie-mcp-bundle/server/mealie/mealplan.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from utils import format_api_params
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class MealplanMixin:
|
||||
"""Mixin class for mealplan-related API endpoints"""
|
||||
|
||||
def get_mealplans(
|
||||
self,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
page: Optional[int] = None,
|
||||
per_page: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get all mealplans for the current household with pagination.
|
||||
|
||||
Args:
|
||||
start_date: Start date for filtering meal plans (ISO format YYYY-MM-DD)
|
||||
end_date: End date for filtering meal plans (ISO format YYYY-MM-DD)
|
||||
page: Page number to retrieve
|
||||
per_page: Number of items per page
|
||||
|
||||
Returns:
|
||||
JSON response containing mealplan items and pagination information
|
||||
|
||||
Raises:
|
||||
MealieApiError: If the API request fails
|
||||
"""
|
||||
param_dict = {
|
||||
"startDate": start_date,
|
||||
"endDate": end_date,
|
||||
"page": page,
|
||||
"perPage": per_page,
|
||||
}
|
||||
|
||||
params = format_api_params(param_dict)
|
||||
|
||||
logger.info({"message": "Retrieving mealplans", "parameters": params})
|
||||
response = self._handle_request(
|
||||
"GET", "/api/households/mealplans", params=params
|
||||
)
|
||||
return response
|
||||
|
||||
def create_mealplan(
|
||||
self,
|
||||
date: str,
|
||||
recipe_id: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
entry_type: str = "breakfast",
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new mealplan entry.
|
||||
|
||||
Args:
|
||||
date: Date for the mealplan in ISO format (YYYY-MM-DD)
|
||||
recipe_id: UUID of the recipe to add to the mealplan (optional)
|
||||
title: Title for the mealplan entry if not using a recipe (optional)
|
||||
entry_type: Type of mealplan entry (breakfast, lunch, dinner, etc.)
|
||||
|
||||
Returns:
|
||||
JSON response containing the created mealplan entry
|
||||
|
||||
Raises:
|
||||
ValueError: If neither recipe_id nor title is provided
|
||||
MealieApiError: If the API request fails
|
||||
"""
|
||||
if not recipe_id and not title:
|
||||
raise ValueError("Either recipe_id or title must be provided")
|
||||
if not date:
|
||||
raise ValueError("Date cannot be empty")
|
||||
|
||||
# Build the request payload
|
||||
payload = {
|
||||
"date": date,
|
||||
"entryType": entry_type,
|
||||
}
|
||||
|
||||
if recipe_id:
|
||||
payload["recipeId"] = recipe_id
|
||||
if title:
|
||||
payload["title"] = title
|
||||
|
||||
logger.info(
|
||||
{
|
||||
"message": "Creating mealplan entry",
|
||||
"date": date,
|
||||
"entry_type": entry_type,
|
||||
}
|
||||
)
|
||||
return self._handle_request("POST", "/api/households/mealplans", json=payload)
|
||||
|
||||
def get_todays_mealplan(self) -> List[Dict[str, Any]]:
|
||||
"""Get the mealplan entries for today.
|
||||
|
||||
Returns:
|
||||
List of today's mealplan entries
|
||||
|
||||
Raises:
|
||||
MealieApiError: If the API request fails
|
||||
"""
|
||||
logger.info({"message": "Retrieving today's mealplan"})
|
||||
return self._handle_request("GET", "/api/households/mealplans/today")
|
||||
107
mealie-mcp-bundle/server/mealie/recipe.py
Normal file
107
mealie-mcp-bundle/server/mealie/recipe.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from utils import format_api_params
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class RecipeMixin:
|
||||
"""Mixin class for recipe-related API endpoints"""
|
||||
|
||||
def get_recipes(
|
||||
self,
|
||||
search: Optional[str] = None,
|
||||
order_by: Optional[str] = None,
|
||||
order_by_null_position: Optional[str] = None,
|
||||
order_direction: Optional[str] = "desc",
|
||||
query_filter: Optional[str] = None,
|
||||
pagination_seed: Optional[str] = None,
|
||||
page: Optional[int] = None,
|
||||
per_page: Optional[int] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
tools: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Provides paginated list of recipes
|
||||
|
||||
Args:
|
||||
search: Search term to filter recipes by name, description, etc.
|
||||
order_by: Field to order results by
|
||||
order_by_null_position: How to handle nulls in ordering ('first' or 'last')
|
||||
order_direction: Direction to order results ('asc' or 'desc')
|
||||
query_filter: Advanced query filter
|
||||
pagination_seed: Seed for consistent pagination
|
||||
page: Page number to retrieve
|
||||
per_page: Number of items per page
|
||||
categories: List of category slugs to filter by
|
||||
tags: List of tag slugs to filter by
|
||||
tools: List of tool slugs to filter by
|
||||
|
||||
Returns:
|
||||
JSON response containing recipe items and pagination information
|
||||
"""
|
||||
|
||||
param_dict = {
|
||||
"search": search,
|
||||
"orderBy": order_by,
|
||||
"orderByNullPosition": order_by_null_position,
|
||||
"orderDirection": order_direction,
|
||||
"queryFilter": query_filter,
|
||||
"paginationSeed": pagination_seed,
|
||||
"page": page,
|
||||
"perPage": per_page,
|
||||
"categories": categories,
|
||||
"tags": tags,
|
||||
"tools": tools,
|
||||
}
|
||||
|
||||
params = format_api_params(param_dict)
|
||||
|
||||
logger.info({"message": "Retrieving recipes", "parameters": params})
|
||||
return self._handle_request("GET", "/api/recipes", params=params)
|
||||
|
||||
def get_recipe(self, slug: str) -> Dict[str, Any]:
|
||||
"""Retrieve a specific recipe by its slug
|
||||
|
||||
Args:
|
||||
slug: The slug identifier of the recipe to retrieve
|
||||
|
||||
Returns:
|
||||
JSON response containing all recipe details
|
||||
"""
|
||||
if not slug:
|
||||
raise ValueError("Recipe slug cannot be empty")
|
||||
|
||||
logger.info({"message": "Retrieving recipe", "slug": slug})
|
||||
return self._handle_request("GET", f"/api/recipes/{slug}")
|
||||
|
||||
def update_recipe(self, slug: str, recipe_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update a specific recipe by its slug
|
||||
|
||||
Args:
|
||||
slug: The slug identifier of the recipe to update
|
||||
recipe_data: Dictionary containing the recipe properties to update
|
||||
|
||||
Returns:
|
||||
JSON response containing the updated recipe details
|
||||
"""
|
||||
if not slug:
|
||||
raise ValueError("Recipe slug cannot be empty")
|
||||
if not recipe_data:
|
||||
raise ValueError("Recipe data cannot be empty")
|
||||
|
||||
logger.info({"message": "Updating recipe", "slug": slug})
|
||||
return self._handle_request("PUT", f"/api/recipes/{slug}", json=recipe_data)
|
||||
|
||||
def create_recipe(self, name: str) -> str:
|
||||
"""Create a new recipe
|
||||
|
||||
Args:
|
||||
name: The name of the new recipe
|
||||
|
||||
Returns:
|
||||
Slug of the newly created recipe
|
||||
"""
|
||||
logger.info({"message": "Creating new recipe", "name": name})
|
||||
return self._handle_request("POST", "/api/recipes", json={"name": name})
|
||||
17
mealie-mcp-bundle/server/mealie/user.py
Normal file
17
mealie-mcp-bundle/server/mealie/user.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
logger = logging.getLogger("mealie-mcp")
|
||||
|
||||
|
||||
class UserMixin:
|
||||
"""Mixin class for user-related API endpoints"""
|
||||
|
||||
def get_current_user(self) -> Dict[str, Any]:
|
||||
"""Get information about the currently logged in user.
|
||||
|
||||
Returns:
|
||||
Dictionary containing user details such as id, username, email, and other profile information.
|
||||
"""
|
||||
logger.info({"message": "Retrieving current user information"})
|
||||
return self._handle_request("GET", "/api/users/self")
|
||||
Reference in New Issue
Block a user