The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Plan To Eat listing page.
Stop typing recipes. Start telling them. Hand your favorite LLM the keys to your Plan to Eat recipe book, planner, and shopping list — over the Model Context Protocol.
A drop-in MCP server, a CLI, and a standalone Node client that give Claude, ChatGPT, any MCP-aware assistant, or your own shell full read/write access to your Plan to Eat account.
…or, when you'd rather not talk to anything:
That's it. No recipe re-typing. No copy-pasting URLs into a phone app. Just talk.
If you're not already using it, Plan to Eat is genuinely the best meal-planning app I've ever used:
👉 Sign up with my referral link — you get the trial, and I get a tiny thank-you. Win/win.
plan-to-eat
subcommand, with tables for humans and --json for scripts. One registry
feeds both surfaces, so they can't drift.freeze_recipe_portions; check what's stashed with list_frozen_recipes;
consume entries when you eat them (soft-delete, history preserved).fetch, the MCP SDK, and Zod. Boots in under a second.Recipe, Ingredient, PlannerEvent
shapes plus a generic _json<T> so your tools never have to guess what
comes back.core/client.ts is a clean, plain-Node API client you
can drop into any script..claude/skills/plan-to-eat/SKILL.md
teaches any agent the common workflows and gotchas (the supper-vs-dinner
alias, the description-vs-title mismatch, etc.) so it doesn't have to
rediscover them.Reverse-engineered from the live web app. There's no public Plan to Eat API, but the desktop site uses these same endpoints internally. Use at your own risk — they could change anything at any time.
Requires Node 18+ (for built-in fetch) and a
Plan to Eat account.
The package ships two bins: plan-to-eat-mcp (the MCP server) and
plan-to-eat (the CLI). To run the CLI through npx without installing, note
that you have to select it explicitly, since the default bin is the server:
The build emits CommonJS to dist/.
| Var | Required | Default | Description |
|---|---|---|---|
PLAN_TO_EAT_USERNAME | yes | — | Plan to Eat login email |
PLAN_TO_EAT_PASSWORD | yes | — | Plan to Eat password |
PLAN_TO_EAT_SESSION_FILE | no | ~/.plan-to-eat-session.json | Where the cookie session is cached. Set to "" to disable caching. |
The CLI also reads a .env in the working directory (shell variables win). The
MCP server does not — MCP hosts pass env explicitly, as shown below.
Any host that can launch a local stdio MCP server works. The server needs one command, two env vars, and nothing else — no ports, no OAuth, no daemon.
The repo ships as a Claude Code plugin — MCP server and both skills in one step:
That gives you:
| Component | What it is |
|---|---|
MCP server plan-to-eat | all 36 tools |
Skill plan-to-eat | how to use the MCP tools well — workflows and gotchas |
Skill plan-to-eat-cli | the same, for agents driving the CLI instead |
Confirm with claude plugin details plan-to-eat@plan-to-eat and claude mcp list.
The server reads PLAN_TO_EAT_USERNAME / PLAN_TO_EAT_PASSWORD from the
environment Claude Code was launched with, so export them in your shell profile
rather than committing them anywhere.
Add --scope user to make it available in every project instead of just this
one. Check it connected with claude mcp list, or /mcp inside a session.
Edit claude_desktop_config.json — Settings → Developer → Edit Config, or:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonRestart Claude Desktop, and you're cooking.
Cursor, Windsurf, Zed, Cline, Continue, OpenClaw, VS Code's MCP support, and custom SDK clients all take the same three things. Point them at:
| Field | Value |
|---|---|
| Transport | stdio |
| Command | npx |
| Args | ["-y", "plan-to-eat-mcp"] |
| Env | PLAN_TO_EAT_USERNAME, PLAN_TO_EAT_PASSWORD |
Most of them use the same mcpServers JSON block as Claude Desktop above —
often in .cursor/mcp.json, .vscode/mcp.json, or the host's settings UI.
If you installed globally (npm i -g plan-to-eat-mcp), use the
plan-to-eat-mcp bin as the command and drop the args entirely. If you built
from a clone, the command is node with
["/absolute/path/to/plan-to-eat-mcp/dist/mcp/server.js"].
Absolute paths matter for the clone route. MCP hosts don't launch servers from your project directory, so a relative path will fail to resolve. The
npxcommand above sidesteps this entirely.
You should see [plan-to-eat] mcp server ready on stdio (36 tools) on stderr.
That's the server waiting for a client — Ctrl-C out. If instead you get a
credentials error, fix that before wiring up a host, where the failure is
harder to see.
Two skills ship in .claude/skills/, covering the common flows and the sharp
edges (the supper-vs-dinner alias, the description-vs-title mismatch on
note entries, checking for duplicates before scheduling):
plan-to-eat — for agents calling the MCP tools.plan-to-eat-cli — for agents that have a shell but no MCP server. Same
30 capabilities, driven through subcommands, with --json for parsing.The plugin install above registers both. Agents on other hosts can read them as plain context — point them at the files, or paste one into your system prompt.
Or install just the skills, into any of 18+ agents, with the skills.sh CLI:
That copies both SKILL.md files into .agents/skills/ and symlinks them for
Claude Code, Cursor, Codex, Copilot, Gemini CLI and the rest. Add
--skill plan-to-eat-cli to take only one. Note that this installs the skills
and not the server — each skill's setup section walks the agent through building
the MCP server or CLI if it isn't already there.
For OpenClaw agents, both skills are on ClawHub:
Their frontmatter declares the credentials and binaries each one needs under
metadata.openclaw, so ClawHub can check your environment at install time.
Every MCP tool is also a subcommand. Underscores become dashes; both spellings work.
Or without installing — note the -p, since the package's default bin is the
MCP server, not the CLI:
get-recipe 123 and get-recipe --id 123 are the
same. <command> --help lists which arguments are positional.--json prints the raw payload instead of a table, for piping into jq.--event_ids 11 --event_ids 22) or take JSON
(--event_ids '[11,22]'). Object arguments take JSON:
--ingredients '{"title":"bread","amount":"2"}'.Each is an MCP tool and a CLI subcommand — add_planner_recipe the tool is
plan-to-eat add-planner-recipe in the shell. Full reference with input
schemas and return shapes: docs/TOOLS.md.
Recipes
| Tool | What it does |
|---|---|
list_recipes | Your whole recipe book (caps at ~500 entries). |
get_recipe | One recipe with directions, ingredients, tags, prep_notes, comments. |
create_recipe | Create. Only title is required. |
update_recipe | Patch any subset of fields. |
delete_recipe | Delete by id. |
Planner — read
| Tool | What it does |
|---|---|
list_planner_events | All planner entries, no date filter. |
get_planner_week | Events in a date range, with recipe_title pre-joined. end_date defaults to start_date + 6 days. |
Planner — write
| Tool | What it does |
|---|---|
add_planner_recipe | Schedule a recipe on a date + section. |
add_planner_ingredient | Attach a freeform ingredient ("2 lbs ground beef") to a meal slot. |
add_planner_note | Attach a freeform note ("Defrost chicken") to a meal slot. |
add_leftover_meal | Schedule a leftover from a previously planned recipe event (duplicate with plan_leftover + optional move). |
move_planner_event | Reschedule any planner event to a new date/section. |
reorder_planner_events | Reorder events within a section (pass ids in desired order). |
update_planner_entry_text | Edit the text of a note or ingredient entry. |
set_planner_servings | Change servings on a recipe event. |
duplicate_planner_event | Duplicate any event. Optional plan_leftover. |
delete_planner_event | Delete by id. |
find_planned_dates | Find planner events for a recipe in a date range. Useful for duplicate checks. |
Freezer
| Tool | What it does |
|---|---|
list_frozen_recipes | What's currently in the freezer. {include_consumed: true} to also see history. |
freeze_recipe_portions | Mark N portions of a cooked recipe as frozen, tied to the planner event they came from. |
delete_frozen_recipe | Mark a frozen entry as consumed (soft-delete: API zeroes count, row persists). |
Shopping list
A line is addressed by its item_ids array, not a scalar id — Plan to Eat
merges duplicate ingredients into one line that keeps every underlying row id.
| Tool | What it does |
|---|---|
get_shopping_list | The list, each line with the store (store_title) and aisle it's filed under, and which recipes pulled it in. |
add_shopping_list_items | Add items. Only title required; the aisle is guessed and the store defaults to the one last used for that item. |
update_shopping_list_items | Retitle / re-quantify a line, or move any number of lines to a different store or aisle. |
remove_shopping_list_items | Take lines off the list (soft delete). |
restore_shopping_list_items | Put removed lines back. |
Lookup tables & extras
| Tool | What it does |
|---|---|
list_courses / list_cuisines / list_main_ingredients / list_tags | Lookup tables. |
list_stores / list_grocery_categories | Your stores and grocery aisles — the ids store_id and category_id want. |
list_menus | Saved menus. |
list_friends | Friends list. |
get_counts | { friends, queued, frozen } from the recipe-book widget. |
update_planner_options | Set planner display preferences (timezone, start day, nutrition columns). Rarely needed. |
The client stashes credentials internally on first login so it can transparently re-authenticate if the cached cookies expire mid-session.
The tool registry is exported too, if you want to build your own adapter over the same 36 capabilities:
All paths under https://app.plantoeat.com.
/api/v1/*)| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/recipes | Whole recipe book. Caps at 500 entries; pagination params don't seem to work. |
| GET | /api/v1/recipes/:id | Single recipe with directions, ingredients, tags, prep_notes, comments. |
| POST | /api/v1/recipes | Create. Body: { "recipe": {...} }. Returns 201 + full recipe. |
| PUT | /api/v1/recipes/:id | Update. Same body shape. |
| DELETE | /api/v1/recipes/:id | Delete. Returns the deleted recipe. |
| GET | /api/v1/courses, /cuisines, /main_ingredients, /tags | Lookup tables. |
| GET | /api/v1/events | Planner entries (calendar). Returns the entire calendar — filter by date client-side. |
| GET | /api/v1/menus | Saved menus. |
| GET | /api/v1/shopping_list | Sync metadata only (updated_items, last_sync_time) — not the list. |
| GET | /api/v1/shopping_list/items | The shopping list itself, one entry per merged line. |
| GET | /api/v1/stores | Grocery stores. |
| GET | /api/v1/grocery_categories | Grocery aisles. |
| GET | /api/v1/friends | Friends. |
| GET | /api/v1/frozen_recipes | Freezer: [{id, recipe_id, count, servings, frozen_on}]. |
| DELETE | /api/v1/frozen_recipes/:id | Soft-delete (sets count: 0; row persists). Returns the updated row. |
| GET | /recipes/counts/ | { friends, queued, frozen }. (Note: not under /api/v1.) |
/planner/*)A different style: form-encoded bodies, text/javascript (empty) responses. The server mutates state and the UI re-fetches separately. Required headers: X-CSRF-Token, X-Requested-With: XMLHttpRequest, Accept: text/javascript. Because the response body is empty, the client recovers any new event ID by diffing /api/v1/events before and after.
| Method | Path | Body |
|---|---|---|
| POST | /planner/create | rid=<recipeId>&date=YYYY-MM-DD§ion=... (recipe), or date=...§ion=...&eventType=note|ingredient&title=<text> |
| POST | /planner/create/ | rid=<id>&frozen_id=&date=...§ion=... (frozen-recipe variant — note the trailing slash) |
| POST | /planner/update | eventid=<id>&date=...§ion=...&readonly=false (move/reschedule) |
| POST | /planner/update/<id> | description=<text> (edit note/ingredient text) |
| POST | /planner/update_serving | event=<id>&serving=<n> |
| POST | /planner/duplicate | id=<id>&plan_leftover=true|false&readonly=false |
| POST | /planner/update_order | ids=e<id1>,e<id2> — reorder events in a section |
| POST | /planner/destroy | id=<id>&readonly=false |
| POST | /planner/update_planner_options | Nested Rails keys: user[time_zone]=...&calendar_settings[show_calories]=1&... |
| POST | /frozen_recipes | id=<recipe_id>&eid=<event_id>&count=<n>&servings=<per-portion> — freeze N portions |
| GET | /planner/search_dates | Returns rendered HTML, not JSON — not used by the client. We filter /api/v1/events instead. |
section values are breakfast, lunch, dinner, snacks. Note that the server may normalize dinner → supper based on the user's per-account preference; reads will reflect the canonical name.
Cookie-based. The client supports two flows:
POST /login with login[email] and login[password] plus the
authenticity_token from the meta tag on GET /login. Server returns a 302
and sets cookies.ptermid2 (user id) and
ptermxt2 (long-lived token) is sufficient on its own — stash those and
skip re-login until they're invalidated.Write requests need an X-CSRF-Token header. The client grabs it from the
<meta name="csrf-token"> tag on the /recipes page on demand.
HTTP Basic auth on /api/v1/... is not supported (returns 401).
The single-recipe response has 65 fields. Writable on POST/PUT include:
title, description, source, url, servings, yield, scaling,
prep_time, cook_time, total_time (minutes),
course_id, cuisine_id, main_ingredient_id, tag_titles (comma list),
directions (free text), private, draft, rating,
nutrition strings (calories, sodium, etc.), and ingredients.
Ingredients use Rails nested-attributes — the wire field is
recipe_ingredients_attributes, not ingredients. The client and the
create_recipe / update_recipe tools accept the friendlier name
ingredients and translate. Each entry:
To delete an existing ingredient on update, include its id plus
"_destroy": true. The server back-fills amount_float, metric_amount,
metric_unit, and similar_titles.
One package, three layers. The logic lives in core/; the MCP server and the
CLI are thin adapters over the same registry.
Adding a tool means adding one entry to src/core/tools.ts. It appears in
the MCP server and the CLI at once, with the same name, schema, and
description — they can't drift apart.
Other files worth knowing:
docs/TOOLS.md — per-tool reference with input/output shapes and gotchas.
Hand-written; npm run check:docs fails if it and the registry disagree
about which tools exist..claude/skills/plan-to-eat/SKILL.md — how to use the MCP tools well..claude/skills/plan-to-eat-cli/SKILL.md — the same, for the CLI..claude-plugin/ — Claude Code plugin and marketplace manifests..mcp.json — the plugin's MCP server declaration.dist/ — emitted by npm run build.Upgrading from ≤0.4? The server moved from
dist/server.jstodist/mcp/server.js. The old path still works — it's a shim that loads the new one — so existing host configs and deployments keep running. New configs should use the new path or theplan-to-eat-mcpbin.
Playwright was used during reverse engineering and is kept as a devDependency
for any future API-discovery work; the runtime depends only on fetch, the
MCP SDK, and Zod.
| Command | What it does |
|---|---|
npm run build | Compile src/**/*.ts to dist/. |
npm run watch | Same, in watch mode. |
npm start | Run the compiled MCP server (dist/mcp/server.js). |
npm run cli -- <command> | Run the CLI without linking it globally. |
npm run verify | Smoke-test the client end-to-end against your account (recipes). |
npm run verify:planner | Smoke-test the planner write endpoints (creates + cleans up test events on a date 6 months out). |
npm test | Smoke-test the MCP server end-to-end (spawns it and calls tools). |
npm run check:docs | Check docs/TOOLS.md covers exactly the registered tools. |
npm run clean | Remove dist/. |
The verify* and test scripts hit your real account. They create and delete
their own test data, but they are not a dry run — see
CONTRIBUTING.md.
Bug reports, new endpoints, and upstream-breakage fixes are all welcome — see CONTRIBUTING.md. The one thing to know up front: the test scripts run against a real Plan to Eat account and create real data (then clean it up). Use your own.
This whole project exists because Plan to Eat is great. If you find this useful, the best thing you can do is give Plan to Eat a try with my referral link. Free 14-day trial, no card needed.
MIT — see LICENSE. Do whatever you want with it; just don't blame me if Plan to Eat ships a breaking change.