v0.3.0
Added
-
Every tool declares an
outputSchemaand answers withstructuredContent
beside the text block. A client no longer has to parse prose to use a result —
which nine of them made unavoidable, since they answered with a sentence. The
sentence stays, in the text block.Most tools carry
untrusted: trueandsource: "mealie"as fields, not only
as a preamble in the text: recipes are routinely scraped from arbitrary
websites and comments come from other users of the instance. The ten without
the marker answer with an id this server was given, or — forget_about— a
version string and the permission flags of the account it authenticates as.Mealie's records are described as open objects with the top-level keys this
server builds. A self-hosted Mealie is any release, and a strict shape would
turn a field one adds into a tool that fails outright. -
Tools that need a confirmation now ask the user, on clients that can show
a prompt. The two-callconfirm_tokenremains for clients that cannot, so
nothing that works today stops working — but where a person can be asked, one
is, instead of a token that only proves the same call was made twice. This
covers all guarded tools,create_share_tokenamong them. -
delete_share_tokennow asks too. Its description said in so many words
that it needed "no confirmation — this narrows access rather than widening it",
and the direction really is the safe one. What is not is that the link cannot be
reissued: a new share token is a different URL, so whoever was sent the old one
finds a dead link, and this server cannot tell whom that was. -
ELICITATIONswitches the dialog off —falsesends a client that could have
been asked down the two-call-token path instead. For a scheduled job or a test
harness, where a dialog is the wrong shape rather than an unwanted one.It does not remove the guard: there is no setting in which a guarded call
goes unannounced. Two deliberate rough edges come with it. The variable is
not prefixed, so oneexport ELICITATION=falsereaches every MCP server in
the environment — which is why a server started with it off prints a line
saying so, and why the fallback text names the server instead of blaming a
client that was working fine. And a value that is neithertruenorfalse
stops the server, where theMEALIE_*booleans beside it fail off on a
typo: this is the only variable here that defaults to on. It is read after
MEALIE_API_TOKENis wiped from the environment, so that exit cannot leave the
token behind. -
A
docs/guide/approval.mdpage.
Changed
-
The advertised schemas avoid spellings that are legal JSON Schema and still
get a tool refused, or its constraint silently dropped, by some MCP clients:
an open object now writes"additionalProperties": truerather than the
empty schema{}zod emits for it; and a nullable field is written as
anyOfbranches rather than"type": ["string", "null"], which several
clients read as a single type and then drop. What the tools accept and return
is unchanged; only the way the schema says so is. -
A result too large to shrink is now an error rather than an envelope carrying
the oversized document as a string. That envelope is valid JSON and no longer
a valid answer: the SDK checks a result against the schema its tool
declares. -
update_recipewith no fields answers{recipe, changed: false, note}rather
than the bare sentence. It is still not an error — a model that resolved every
field to its current value should not be punished for asking. -
The two-call
confirm_tokenprompt is an error result. What was asked for did
not happen, which is whatisErrorsays. The text is unchanged and still
carries the token. -
MEALIE_READ_ONLYnow accepts1,trueandyesin any case and ignores
surrounding whitespace, matching the rest of the family. It only ever takes
capability away, so an operator who wroteMEALIE_READ_ONLY=Truemeant the safe
thing and now gets it — where before that spelling silently left every write tool
registered.MEALIE_INSECURE_TLSstays exactlytrueon purpose: it weakens the
server, so only the one unambiguous spelling should do it. -
The shared libraries move to
mcp-approval0.7.1,mcp-tool-allowlist0.2.1,
mcp-internal-hosts0.2.1,mcp-integration-harness0.2.0 andsvg-asset-set
0.2.0. -
Runs on MCP SDK 2.0. Existing clients see the same protocol revision they
always did; the change is the package layout behind it, and it is what lets
the dialog above work on both protocol eras from one code path — including
behind a stateless gateway, where the older mechanism silently fell back to
the weaker token for every client. -
The linter is oxlint instead of eslint plus typescript-eslint, which
lifts the TypeScript ceiling: typescript-eslint pinstypescriptbelow 6.1,
so this repository was held on TypeScript 6 by its linter rather than by its
code. -
The tool filter, the confirmation store, the host classifier and the
documentation-asset generator now come frommcp-tool-allowlist,
mcp-approval,mcp-internal-hostsandsvg-asset-setrather
than from copies kept here — 802 fewer lines, and one place to fix each. None
of them has a runtime dependency of its own. -
A
confirm_tokenthat does not match its arguments is refused with the
reason instead of being answered with a fresh prompt, in the same words as
every other server in the family. The binding is unchanged: a confirmation
issued for one recipe still cannot delete another. A wrong token also no
longer voids the outstanding one — voiding it let anyone who could reach the
tool cancel a pending confirmation by sending rubbish, which protected
nothing. -
stdio is served through
serveStdio, so the connection's era is negotiated
on the opening exchange rather than assumed. A client that pins the
2026-07-28era is served it; until now itsserver/discoverprobe was
answered with "Method not found" and only2025-11-25was on offer. A client
that speaks the older era sees no change — it is still pinned to one instance
for the life of the connection, exactly as a hand-wired
StdioServerTransportserved it.
Fixed
-
search_recipesanswered a narrowed question with the whole collection.
The tool describedtags,categoriesandtoolsas taking "names, slugs or
UUIDs". Mealie takes no names:_uuids_for_itemslooks a non-UUID up as a slug,
returns an empty list when nothing matches, and_build_recipe_filterthen
testsif tags:— so the filter is not attached at all. Measured on v3.22.0
with three recipes, one tagged "Weeknight Dinner":
?tags=Weeknight%20Dinnerreturned all three, as did the mistyped slug
weeknight-dinnerrr, with nothing in the answer saying it had not been
filtered. Names and slugs are now resolved to ids before the search runs, and
an entry that resolves to nothing is an error. -
search_recipespromised AND and did not deliver it.cookbookand the
organizer filters are mutually exclusive in Mealie —_build_recipe_filter
returns the cookbook's own filter and returns early — so
{cookbook: "desserts", tags: ["vegan"]}silently ignored the tags. Confirmed
live. The combination is now refused. -
search_recipes({foods: […]})could only 500. Mealie resolves foods not at
all and puts the value straight intoRecipeIngredientModel.food_id == food, so
a name or slug reaches theGUIDtype decorator and comes back as HTTP 500.
foodsnow takes UUIDs only, assuggest_recipesalways has. -
order_by: "random"always failed. Mealie's pagination model validates
paginationSeed is required when orderBy is randomand answers HTTP 422; the
tool took no seed, so the option could not be used. It now generates one per
call. -
Mealie's organizer slug routes answer "no such slug" two different ways —
/categories/slug/nopeis a clean 404 while/tags/slug/nopeand
/tools/slug/nopeare HTTP 500. The lookup reads both as a miss and falls
through to the name search; a Mealie that is genuinely failing still reports
the failure, because that second request has to succeed for anything to
resolve. Found by the integration suite, not by reading. -
merge_foodsandmerge_unitsnamed the wrong tool. Both come out of one
factory, and the factory passedtoolName: 'create_unit'— a real, unrelated
tool of this server. That name is printed in two places a caller acts on: the
fallback instruction ("callcreate_unitagain with the token") and the
sentence after a decline. Callers were being pointed at something that creates
rather than merges. Introduced on 2026-09-01 with the move tomcp-approval. -
Four
update_*tools were annotateddestructiveHint: false:
update_recipe,update_organizer,update_mealplan_entryand
update_shopping_list_items. Mealie keeps no version history, so replacing an
instruction list leaves nowhere to read the old one back from — that is the
definition the whole family uses, and Wiki.js'supdate_pageis genuinely on
the other side of it because Wiki.js has page history. The difference is the
backend, not the verb. -
Confirmation tokens are compared with a constant-time comparison. The
copy in this repository used!==, which leaks through timing how much of a
guess was right. Reaching a token still requires having received it in a
previous tool result, so this closes a margin rather than a hole. -
An entry in
MEALIE_ALLOW_TOOLSthat is not tool-name-shaped is now
redacted in the error rather than quoted back.MEALIE_TOKENand
MEALIE_ALLOW_TOOLSare adjacent lines in every compose file, and a paste
into the wrong one used to print the credential into the client's log.
Security
-
The confirmation gate was drawn along the wrong line. It followed the tool
name — everything calleddelete_*ormerge_*asked — rather than "cannot
be undone", and four tools fell in the gap.update_recipewith
{ingredients: [], instructions: []}emptied a recipe in one call and answered
with the now-empty recipe, wheredelete_recipeon the same recipe cost two
calls and a token; Mealie keeps no version history, so both are equally final.
update_organizer,update_mealplan_entryandupdate_shopping_list_items
were the other three.The line is now the one
annotations.tsalways stated — content a person
wrote, replaced with no way back — applied per call rather than per tool.
update_recipeasks when it replaces name, description, ingredients,
instructions, tags, categories or notes, and goes straight through for times,
servings, yield and the source link.update_shopping_list_itemsasks for
noteand not for ticking off.update_mealplan_entryasks fortitleand
textand not for a move.update_organizeralways asks: a rename
regenerates the slug. Each approval is bound to a fingerprint of the replacing
values as well as to the target, so one shown for one new instruction list
cannot be spent on a call that clears the list instead.The lasting part is
test/gating.test.ts, which claims this over the whole
catalogue: every tool annotateddestructiveHint: truehas to accept a
confirm_token, has to write nothing on its first call, and has to appear in
that file's table. A per-tool test would not have found the gap — every
per-tool test that existed passed. -
create_cookbook(is_public: true)published without asking. The one other
tool that widens who can see something;create_share_tokenhas been guarded
since it existed. It exposes less than a share link — the recipes themselves
needsettings.publicof their own — but the name, description and saved
filter go out, and there is noupdate_cookbookto take it back with. A
private cookbook is still created without a prompt. -
import_recipe_from_html_or_jsonclaimedopenWorldHint: falsewhile
Mealie fetched an address out of the document it was handed. Verified against
v3.22.0: a pasted{"image": "http://…/latest/meta-data/"}putsImage URL: …
in Mealie's log and goes throughrecipe_data_service.scrape_image. Mealie's
own guard refuses onis_private, which is False for100.100.100.200
(Alibaba Cloud metadata) and for all of100.64.0.0/10. The tool now carries
openWorldHint: true, and the addresses this server can find in the document —
schema.orgimage/thumbnailUrl/contentUrlin all three shapes,<img src>,
og:image— go through the sameassertFetchableUrlas a URL argument. -
source_urlwas the only URL argument with no scheme check.httpUrl
exists because zod's.url()acceptsjavascript:,file:anddata:;
source_urlwas a plain string, Mealie does not validateorg_urleither, and
the value comes back to every reader throughrecipeDetail. It is now
httpUrl.