Add an import-profile entrypoint without delete

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 <fredrik.fallman@gmail.com>
Signed-off-by: fredamn76 <fredrik.fallman@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:32:21 +02:00
parent e46c327875
commit 67dac16059
3 changed files with 35 additions and 0 deletions
+21
View File
@@ -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()
+1
View File
@@ -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"]
+13
View File
@@ -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": []}