From 67dac1605925aa1bdd72aab59911d5d8a77d33fc Mon Sep 17 00:00:00 2001 From: fredamn76 Date: Tue, 11 Aug 2026 22:32:21 +0200 Subject: [PATCH] Add an import-profile entrypoint without delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buzz's read-only Mealie agent cannot import recipes, and the full server exposes delete_recipe and the Home Assistant shopping-list write to any client that runs it. Neither profile fits an on-demand import agent. Add IMPORT_TOOL_NAMES — the read-only allowlist plus import_recipe_url, import_recipe_text, import_recipe_image, patch_recipe, parse_ingredients and set_cover_image — and expose it through main_import(). Adding a recipe is recoverable from the Mealie UI; removing one is not, so delete_recipe stays out of the profile even though it writes. Verified over stdio: the entrypoint lists exactly those 15 tools. Co-authored-by: fredamn76 Signed-off-by: fredamn76 Co-Authored-By: Claude Opus 5 --- mealie_mcp/server.py | 21 +++++++++++++++++++++ pyproject.toml | 1 + tests/test_server_contracts.py | 13 +++++++++++++ 3 files changed, 35 insertions(+) diff --git a/mealie_mcp/server.py b/mealie_mcp/server.py index 6ef44ce..6fbbfa5 100644 --- a/mealie_mcp/server.py +++ b/mealie_mcp/server.py @@ -53,6 +53,17 @@ READ_ONLY_TOOL_NAMES = frozenset( } ) +IMPORT_TOOL_NAMES = READ_ONLY_TOOL_NAMES | frozenset( + { + "import_recipe_url", + "import_recipe_text", + "import_recipe_image", + "patch_recipe", + "parse_ingredients", + "set_cover_image", + } +) + class MealieAuthError(RuntimeError): """The Mealie token is missing, expired, or wrong (HTTP 401).""" @@ -776,3 +787,13 @@ def main_read_only() -> None: """Run the same server without import, patch, delete, parse, image, or HA writes.""" _configure_read_only(mcp) mcp.run() + + +def main_import() -> None: + """Run the server with the import pipeline but without delete or HA writes. + + Adding a recipe is recoverable through the Mealie UI; removing one is not, + so `delete_recipe` stays out even though this profile writes. + """ + _configure_read_only(mcp, IMPORT_TOOL_NAMES) + mcp.run() diff --git a/pyproject.toml b/pyproject.toml index 3d26762..934b039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dev = ["pytest>=8.0"] [project.scripts] mealie-mcp = "mealie_mcp.server:main" mealie-mcp-read-only = "mealie_mcp.server:main_read_only" +mealie-mcp-import = "mealie_mcp.server:main_import" [build-system] requires = ["hatchling"] diff --git a/tests/test_server_contracts.py b/tests/test_server_contracts.py index 7ab5a80..b8fdec2 100644 --- a/tests/test_server_contracts.py +++ b/tests/test_server_contracts.py @@ -60,6 +60,19 @@ def test_read_only_tool_allowlist_is_explicit(): } +def test_import_profile_adds_the_pipeline_but_never_delete(): + assert server.IMPORT_TOOL_NAMES == server.READ_ONLY_TOOL_NAMES | { + "import_recipe_url", + "import_recipe_text", + "import_recipe_image", + "patch_recipe", + "parse_ingredients", + "set_cover_image", + } + assert "delete_recipe" not in server.IMPORT_TOOL_NAMES + assert "shopping_list_add" not in server.IMPORT_TOOL_NAMES + + class TestUrlImportResponse: def test_slug_only_import_response_is_resolved_before_reporting(self, monkeypatch): recipe = {"slug": "lax-med-citron", "name": "Lax med citron", "recipeIngredient": []}