Security
This page is the prose version of SECURITY.md.
Trust model
A Mealie API token acts as exactly one Mealie user and inherits that user's group, household and permission flags. Whoever holds it can read and rewrite that user's whole recipe collection, meal plans, shopping lists and cookbooks, and — through create_share_token — publish any recipe at a URL that needs no login.
Give the server a dedicated, non-admin user. Mealie's admin-only surface (backups, restore, user management, group settings, AI provider configuration) is not exposed by any tool here, but a token minted from an admin account still carries those rights if anything else ever reaches the API with it. MEALIE_READ_ONLY=true narrows the server further: the write and import tools are not registered at all.
Treat every environment variable this server reads as a secret. The MCP client process, and therefore the model driving it, sees every tool result — do not point this server at an instance whose data you would not put in a model's context.
Not exposed, on purpose
Everything under /api/admin (backups, restore, maintenance, user, group and household management, email, AI provider settings), /api/users/api-tokens (a tool that mints API credentials is privilege-escalation surface), the authentication routes, user CRUD and passwords, webhooks, event notifications and recipe actions (all three trigger outbound HTTP from the instance), meal plan rules, migrations, seeders, invitations, bulk export and ZIP download, and asset and image uploads.
PUT /api/recipes/{slug} is not exposed either: it replaces the entire 33-field recipe object, so a partial update through it silently drops ingredients, steps and tags. update_recipe uses PATCH.
Confirmation tokens
Deleting a recipe, organizer, cookbook, shopping list or comment, merging foods or units, and creating a public share link all require a server-generated token: the first call returns the token together with a description of what is about to happen, the second call — same tool, same arguments, plus the token — performs it.
The tokens are single-use and bound to the specific target, so a model cannot satisfy the gate on its own and a token issued for one target cannot be replayed against another. For operations on a set of ids the token is bound to a fingerprint of the whole sorted set — a confirmation for three shopping-list items cannot delete a fourth appended between the two calls — and for a merge to the direction as well, because swapping the arguments would destroy the wrong record.
create_share_token is guarded even though it destroys nothing: it is the one tool that widens who can see the data, and unlike a deletion the effect is invisible until someone uses the link. delete_share_token needs no confirmation — it narrows access.
Confirmation prompts quote no upstream text — ids, counts and flags only — so a hostile recipe name cannot ride along into the prompt the user approves.
Untrusted content
Recipes are attacker-controlled text. A recipe imported from a website carries whatever that site wrote, and it stays in the database afterwards, so the content comes back through get_recipe and search_recipes long after the import. Comments come from other users of the instance. Every tool result that can contain instance content is therefore prefixed with an explicit untrusted-content marker telling the model to treat it as data, not as instructions.
Mealie performs the fetch, not this server. import_recipe_from_url, preview_recipe_url and the image import hand a URL or a payload to Mealie, which retrieves it from inside its own network. URLs are therefore restricted to http and https — zod's .url() alone would accept file:, javascript: and data: — and loopback, private-range, link-local and .lan/.internal/.local hosts are refused, since no recipe lives there. This is not a complete SSRF defence and cannot be: the name is resolved by Mealie, and a public name can point at a private address. The real boundary is Mealie's own network egress.
Bounded responses
Oversized results drop whole items rather than cutting the JSON mid-string, and a response body is never read past 8 MB. Redirects are refused so the token cannot be resent to another host.
Reporting a vulnerability
Use private vulnerability reporting. Do not open a public issue for an unpatched vulnerability, and do not include real credentials, tokens, hostnames or private configuration in a report. You can expect an initial response within a week; fixed vulnerabilities are published as a new release with a note in the changelog. Only the latest release and the current main branch receive security fixes.