cookbook-co-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,6 +125,7 @@ unsupported-protocol-version error that names the supported version.
125
125
  | `COOKBOOK_TOKEN` | Required. The login token copied from the web app. |
126
126
  | `COOKBOOK_READ_ONLY` | Set to `1` to register only the read tools. |
127
127
  | `COOKBOOK_PORT` | Port for `serve` (or pass it as `serve <port>`). |
128
+ | `COOKBOOK_DEFAULT_SHOPPING_LIST` | Shopping list used when a tool is given none. Default `Shopping List`. |
128
129
  | `COOKBOOK_BACKUP_DIR` | Where recipe backups go. Default `~/.local/state/cookbook-mcp/backups`. |
129
130
  | `COOKBOOK_BACKUP_KEEP` | Edit backups kept per recipe. Default 20. |
130
131
  | `COOKBOOK_EXPORT_DIR` | Where exports go when no path is given. Default `~/Downloads`, else `~/.local/state/cookbook-mcp/exports`. |
@@ -127978,14 +127978,14 @@ var RecipesApi = class {
127978
127978
  }
127979
127979
  };
127980
127980
  function recipePatch(recipe, tags, fave = recipe.fave) {
127981
- const { category: _category, instructions, created: _c, changed: _ch, ...rest } = recipe;
127981
+ const { category: _category, instructions: instructions2, created: _c, changed: _ch, ...rest } = recipe;
127982
127982
  return {
127983
127983
  ...rest,
127984
127984
  fave,
127985
127985
  category: tags,
127986
127986
  categoryNames: [],
127987
127987
  // the app writes `directions` with zero-based id/item and a one-based itemId
127988
- directions: instructions.map((d, i2) => ({
127988
+ directions: instructions2.map((d, i2) => ({
127989
127989
  id: i2,
127990
127990
  item: i2,
127991
127991
  itemId: i2 + 1,
@@ -131900,7 +131900,7 @@ var updateRecipe = defineTool({
131900
131900
  apply: external_exports.boolean().default(false).describe("false previews only; true saves"),
131901
131901
  show_full: external_exports.boolean().default(false).describe("Preview only: also return the full recipe before and after, not just the changes")
131902
131902
  },
131903
- run: async ({ id, changed, ingredients, instructions, notes, servings, apply: apply2, show_full, ...fields }, { client, backup }) => {
131903
+ run: async ({ id, changed, ingredients, instructions: instructions2, notes, servings, apply: apply2, show_full, ...fields }, { client, backup }) => {
131904
131904
  const recipe = await freshRecipe(client, id);
131905
131905
  if (!recipe) return fail2(`No recipe with id ${id}`);
131906
131906
  const stale = staleError(recipe, changed);
@@ -131908,11 +131908,11 @@ var updateRecipe = defineTool({
131908
131908
  const edits = {
131909
131909
  ...pickBy({ ...fields, serving: servings }, (value) => value !== void 0),
131910
131910
  ...ingredients && { ingredients: toIngredients(ingredients) },
131911
- ...instructions && { instructions: toInstructions(instructions, recipe.instructions) },
131911
+ ...instructions2 && { instructions: toInstructions(instructions2, recipe.instructions) },
131912
131912
  ...notes !== void 0 && { notes }
131913
131913
  };
131914
131914
  if (isEmpty2(edits)) return fail2("Nothing to change: pass at least one field to edit");
131915
- const lostPhotos = instructions ? recipe.instructions.filter((s) => s.photo && !instructions.includes(s.text)).length : 0;
131915
+ const lostPhotos = instructions2 ? recipe.instructions.filter((s) => s.photo && !instructions2.includes(s.text)).length : 0;
131916
131916
  const warnings = lostPhotos > 0 ? [`${lostPhotos} step photo(s) will be dropped because their step text changed`] : [];
131917
131917
  if (ingredients && !isDrink(recipe, await client.tags.list())) {
131918
131918
  const unclear = ambiguousOunces(edits.ingredients ?? []);
@@ -132145,12 +132145,19 @@ var aisleOverrides = external_exports.array(
132145
132145
  function overrideMap(entries3) {
132146
132146
  return new Map(entries3.map((e) => [normalizeName(e.item), aisleId(e.aisle)]));
132147
132147
  }
132148
- var list = external_exports.string().optional().describe('List name or id; defaults to "Shopping List"');
132148
+ var list = external_exports.string().optional().describe("List name or id; defaults to the default list (marked by list_shopping_lists)");
132149
132149
  var on_hand = external_exports.array(external_exports.string()).default([]).describe("Extra ingredient names assumed on hand and skipped");
132150
132150
  var include_staples = external_exports.boolean().default(false).describe("Also add staples (water, salt, pepper, cooking oil and spray) normally skipped");
132151
132151
  var apply = external_exports.boolean().default(false).describe("false previews only; true makes the change");
132152
- var DEFAULT_LIST = "shopping list";
132153
- async function resolveList(client, ref) {
132152
+ var DEFAULT_LIST = "Shopping List";
132153
+ function findDefaultList(lists, configured) {
132154
+ const key = (configured ?? DEFAULT_LIST).toLowerCase();
132155
+ const match = lists.find((l) => l.id === configured || l.name.toLowerCase() === key);
132156
+ if (match || configured) return match;
132157
+ const others = lists.filter((l) => l.id !== "common");
132158
+ return others.length === 1 ? others[0] : void 0;
132159
+ }
132160
+ async function resolveList(client, ref, configured) {
132154
132161
  const lists = await client.shopping.lists();
132155
132162
  const names = lists.map((l) => l.name).join(", ");
132156
132163
  if (ref) {
@@ -132163,11 +132170,9 @@ async function resolveList(client, ref) {
132163
132170
  error: partial2.length === 0 ? `No list "${ref}". Lists: ${names}` : `"${ref}" matches several lists (${partial2.map((l) => l.name).join(", ")}); use the full name`
132164
132171
  };
132165
132172
  }
132166
- const byName = lists.find((l) => l.name.toLowerCase() === DEFAULT_LIST);
132167
- if (byName) return byName;
132168
- const others = lists.filter((l) => l.id !== "common");
132169
- if (others.length === 1) return others[0];
132170
- return { error: `No "Shopping List" found; pass \`list\`. Lists: ${names}` };
132173
+ const found = findDefaultList(lists, configured);
132174
+ if (found) return found;
132175
+ return { error: `No "${configured ?? DEFAULT_LIST}" found; pass \`list\`. Lists: ${names}` };
132171
132176
  }
132172
132177
  async function applyPlan(client, listId, plan) {
132173
132178
  await client.shopping.addItems(listId, plan.adds);
@@ -132218,8 +132223,8 @@ var addPlannedMealsToShoppingList = defineTool({
132218
132223
  aisles: aisleOverrides,
132219
132224
  apply
132220
132225
  },
132221
- run: async ({ from, to, list: ref, on_hand: extra, include_staples: staples, aisles, apply: go }, { client }) => {
132222
- const found = await resolveList(client, ref);
132226
+ run: async ({ from, to, list: ref, on_hand: extra, include_staples: staples, aisles, apply: go }, { client, config: config2 }) => {
132227
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132223
132228
  if ("error" in found) return fail2(found.error);
132224
132229
  const [planned, recipes] = await Promise.all([
132225
132230
  client.mealPlan.list(from, to, true),
@@ -132275,8 +132280,8 @@ var addRecipeToShoppingList = defineTool({
132275
132280
  aisles: aisleOverrides,
132276
132281
  apply
132277
132282
  },
132278
- run: async ({ recipe_id, servings, list: ref, on_hand: extra, include_staples: staples, aisles, apply: go }, { client }) => {
132279
- const found = await resolveList(client, ref);
132283
+ run: async ({ recipe_id, servings, list: ref, on_hand: extra, include_staples: staples, aisles, apply: go }, { client, config: config2 }) => {
132284
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132280
132285
  if ("error" in found) return fail2(found.error);
132281
132286
  const recipe = (await client.recipes.list()).find((r) => r.id === recipe_id);
132282
132287
  if (!recipe) return fail2(`No recipe with id ${recipe_id}`);
@@ -132318,8 +132323,8 @@ var addToShoppingList = defineTool({
132318
132323
  ).min(1).describe("Items to add; the same name and unit as an open item adds to its quantity"),
132319
132324
  list
132320
132325
  },
132321
- run: async ({ items, list: ref }, { client }) => {
132322
- const found = await resolveList(client, ref);
132326
+ run: async ({ items, list: ref }, { client, config: config2 }) => {
132327
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132323
132328
  if ("error" in found) return fail2(found.error);
132324
132329
  const lines = items.map((i2) => ({
132325
132330
  name: i2.name.trim(),
@@ -132352,8 +132357,8 @@ function itemActionTool(options2) {
132352
132357
  ),
132353
132358
  list
132354
132359
  },
132355
- run: async ({ items: queries, list: ref }, { client }) => {
132356
- const found = await resolveList(client, ref);
132360
+ run: async ({ items: queries, list: ref }, { client, config: config2 }) => {
132361
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132357
132362
  if ("error" in found) return fail2(found.error);
132358
132363
  const all = await client.shopping.items(found.id);
132359
132364
  const picked = [];
@@ -132393,8 +132398,8 @@ var clearCompleted = defineTool({
132393
132398
  description: "Delete every checked-off item from a shopping list. The open items stay. This cannot be undone.",
132394
132399
  hints: ["destructive", "idempotent"],
132395
132400
  inputSchema: { list },
132396
- run: async ({ list: ref }, { client }) => {
132397
- const found = await resolveList(client, ref);
132401
+ run: async ({ list: ref }, { client, config: config2 }) => {
132402
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132398
132403
  if ("error" in found) return fail2(found.error);
132399
132404
  const done = (await client.shopping.items(found.id)).filter((i2) => i2.complete);
132400
132405
  await client.shopping.deleteItems(
@@ -132430,8 +132435,8 @@ var deleteShoppingList = defineTool({
132430
132435
  name: external_exports.string().describe("The list's current name, to confirm"),
132431
132436
  apply
132432
132437
  },
132433
- run: async ({ list: ref, name: name5, apply: go }, { client }) => {
132434
- const found = await resolveList(client, ref);
132438
+ run: async ({ list: ref, name: name5, apply: go }, { client, config: config2 }) => {
132439
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132435
132440
  if ("error" in found) return fail2(found.error);
132436
132441
  if (found.id === "common") return fail2('The built-in "Regular items" list cannot be deleted');
132437
132442
  if (found.name.toLowerCase() !== name5.toLowerCase()) {
@@ -132465,8 +132470,8 @@ var getShoppingList = defineTool({
132465
132470
  ids: external_exports.boolean().default(false).describe("Return each item as {id, text} instead of text"),
132466
132471
  show_done: external_exports.boolean().default(false).describe("List the checked-off items, not just a count")
132467
132472
  },
132468
- run: async ({ list: ref, ids, show_done }, { client }) => {
132469
- const found = await resolveList(client, ref);
132473
+ run: async ({ list: ref, ids, show_done }, { client, config: config2 }) => {
132474
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132470
132475
  if ("error" in found) return fail2(found.error);
132471
132476
  const items = await client.shopping.items(found.id);
132472
132477
  const order = Object.values(AISLES);
@@ -132506,17 +132511,19 @@ var listLearnedAisles = defineTool({
132506
132511
  // src/tools/shopping/list-shopping-lists.ts
132507
132512
  var listShoppingLists = defineTool({
132508
132513
  name: "list_shopping_lists",
132509
- description: `List the shopping lists by name with open and done item counts. "Shopping List" is the one normally used; "Regular items" is the app's built-in list of regular groceries.`,
132514
+ description: `List the shopping lists by name with open and done item counts. The default list, used when a tool is given no list, is marked default: true; "Regular items" is the app's built-in list of regular groceries.`,
132510
132515
  hints: ["readOnly"],
132511
132516
  inputSchema: {},
132512
- run: async (_args, { client }) => {
132517
+ run: async (_args, { client, config: config2 }) => {
132513
132518
  const lists = await client.shopping.lists();
132519
+ const defaultList = findDefaultList(lists, config2.defaultShoppingList);
132514
132520
  return json2(
132515
132521
  await Promise.all(
132516
132522
  lists.map(async (l) => {
132517
132523
  const items = await client.shopping.items(l.id);
132518
132524
  return {
132519
132525
  name: l.name,
132526
+ default: l === defaultList,
132520
132527
  open: items.filter((i2) => !i2.complete).length,
132521
132528
  done: items.filter((i2) => i2.complete).length
132522
132529
  };
@@ -132546,8 +132553,8 @@ var setItemAisle = defineTool({
132546
132553
  list,
132547
132554
  remember: external_exports.boolean().default(true).describe("Also remember this aisle for future adds")
132548
132555
  },
132549
- run: async ({ items: queries, aisle, list: ref, remember }, { client }) => {
132550
- const found = await resolveList(client, ref);
132556
+ run: async ({ items: queries, aisle, list: ref, remember }, { client, config: config2 }) => {
132557
+ const found = await resolveList(client, ref, config2.defaultShoppingList);
132551
132558
  if ("error" in found) return fail2(found.error);
132552
132559
  const all = await client.shopping.items(found.id);
132553
132560
  const picked = [];
@@ -132759,6 +132766,7 @@ function loadConfig(env = process.env, home = homedir()) {
132759
132766
  exportDirs: env.COOKBOOK_EXPORT_DIR ? [env.COOKBOOK_EXPORT_DIR] : [join3(home, "Downloads"), join3(state, "exports")],
132760
132767
  backupDir: env.COOKBOOK_BACKUP_DIR ?? join3(state, "backups"),
132761
132768
  backupKeep: Math.max(1, Number(env.COOKBOOK_BACKUP_KEEP ?? 20) || 20),
132769
+ defaultShoppingList: env.COOKBOOK_DEFAULT_SHOPPING_LIST?.trim() || void 0,
132762
132770
  cache: loadCacheSettings(env)
132763
132771
  };
132764
132772
  }
@@ -132799,16 +132807,17 @@ function parseWindow(variable, raw, canBeOff) {
132799
132807
  }
132800
132808
 
132801
132809
  // src/server/instructions.ts
132802
- function serverInstructions(readOnly) {
132803
- return readOnly ? `${INSTRUCTIONS}
132804
- ${READ_ONLY_NOTE}` : INSTRUCTIONS;
132810
+ function serverInstructions(config2) {
132811
+ const text2 = instructions(config2.defaultShoppingList ?? DEFAULT_LIST);
132812
+ return config2.readOnly ? `${text2}
132813
+ ${READ_ONLY_NOTE}` : text2;
132805
132814
  }
132806
132815
  var READ_ONLY_NOTE = "READ-ONLY MODE: only tools that read are available; nothing can be created, changed or deleted.";
132807
- var INSTRUCTIONS = `CookBook recipe manager: the user's personal recipe library, tags and meal planner.
132816
+ var instructions = (defaultList) => `CookBook recipe manager: the user's personal recipe library, tags and meal planner.
132808
132817
  Find recipes with search_recipes or get_recent_recipes, then read one with get_recipe.
132809
132818
  scale_recipe shows scaled ingredient amounts without saving anything.
132810
132819
  set_recipe_photo replaces a recipe's main photo from a file path or URL.
132811
- Shopping lists: "Shopping List" is the default list; recipe and planned-meal adds skip staples (water, salt, pepper, cooking oils) and report them, and preview before writing.
132820
+ Shopping lists: "${defaultList}" is the default list; recipe and planned-meal adds skip staples (water, salt, pepper, cooking oils) and report them, and preview before writing.
132812
132821
  Aisles for new items come from the app's remembered products, else a crude keyword guess: pass the aisle yourself whenever you know it, and fix a wrong one with set_item_aisle or teach_aisle.
132813
132822
  Cooking and rating are separate: mark_cooked logs a cook (adds to the count, sets the last-cooked date, optional rating and date); rate_recipe only sets the stars.
132814
132823
  Edit tools (set_tags, set_favourite, rate_recipe, rename_recipe, set_recipe_photo, update_recipe) need that recipe's \`changed\` value from get_recipe and refuse if it is stale.
@@ -132830,7 +132839,7 @@ function createServerFactory(log) {
132830
132839
  const create2 = () => {
132831
132840
  const server = new McpServer(
132832
132841
  { name: "cookbook", version: "0.1.0" },
132833
- { instructions: serverInstructions(config2.readOnly) }
132842
+ { instructions: serverInstructions(config2) }
132834
132843
  );
132835
132844
  registerTools(server, context);
132836
132845
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cookbook-co-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Unofficial MCP server for CookBook Co.: recipes, tags, meal planner and shopping lists",
5
5
  "keywords": [
6
6
  "mcp",