added all mcp

This commit is contained in:
liph22
2025-12-19 23:59:54 +01:00
parent edfffd76e1
commit 14bc398915
6240 changed files with 2421602 additions and 0 deletions

View 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

View 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

View 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

View 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)

View 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")

View 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")

View 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})

View 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")