@sebamomann/plants-mcp 2.11.0 → 2.13.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/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
3
3
  Notable changes to `@sebamomann/plants-mcp`. Versioning is semver against the **tool surface** —
4
4
  see the table in `AGENTS.md` for what counts as major, minor, and patch.
5
5
 
6
+ ## 2.13.0 — 2026-09-30
7
+
8
+ **New tool: `merge_soils`.** Tool count 78 → 79 (28 read, 33 write, 18 admin). A **minor** bump —
9
+ one new write tool, nothing renamed or removed:
10
+
11
+ - `POST /api/v1/soils/merge` with `sourceId`/`targetId` (both required) moves every plant and
12
+ potting-event record from `sourceId` onto `targetId`, then deletes `sourceId` — for duplicate
13
+ soil mixes (idea 89; prod had 23 soils for 96 plants, including a literal duplicate pair
14
+ differing only by a space).
15
+ - Unlike deleting a soil directly, it never refuses for living plants still using the source —
16
+ moving them onto the target first is the point, so nothing is left dangling. `sourceId`'s own
17
+ mix recipe (its components/parts) is dropped; `targetId` keeps its own unchanged.
18
+ - Irreversible, like `admin_merge_plant_types` — there is no unmerge tool for soils. Shares its
19
+ ownership-checked, transactional core (`app/_lib/soils/mergeSoils.ts`) with the app's own
20
+ "Zusammenführen mit …" action on the soil detail page, so an MCP-triggered merge and a
21
+ UI-triggered one behave identically.
22
+
23
+ ## 2.12.0 — 2026-09-30
24
+
25
+ **`record_fertilization` now also logs the watering a feeding is given with.** Tool count
26
+ unchanged (78). A **minor** bump — one new response field, nothing renamed or removed, but a
27
+ behaviour change worth reading before the next call:
28
+
29
+ - Fertilizer goes into the can, so the app treats a fed plant as a watered plant. Every fertilized
30
+ plant now also gets that day's watering-type event — routed exactly like `record_watering` (a
31
+ `WateringEvent` for a scheduled plant, a `RefillEvent` for reservoir, a `HydroEvent` top-up for
32
+ hydro) — unless one already exists for that day. The response gains `alsoWatered: number[]`
33
+ listing the plants that got one.
34
+ - A plant skipped because it was already fertilized that day is left entirely alone (no watering
35
+ either), as before.
36
+ - **Callers that used to pair `record_fertilization` with `record_watering` for the same plants
37
+ should drop the second call** — it would now be skipped as a same-day duplicate anyway, but the
38
+ extra round trip is wasted.
39
+ - This matches the app: its fertilize card's primary now reads "Düngen + gießen" and writes both,
40
+ and the old "also water" secondary is gone. Solid fertilizers (sticks, granules) are not modelled
41
+ yet — see `docs/feature-ideas.md` #79 — so the rule is unconditional for now.
42
+
6
43
  ## 2.11.0 — 2026-09-29
7
44
 
8
45
  **`dismiss_care_recommendation` gains a new dismissible type, `possibleDecline`.** Tool count
package/README.md CHANGED
@@ -60,7 +60,7 @@ needs a running Sprig instance and an API key from it. Start at the
60
60
 
61
61
  ## What an assistant can do
62
62
 
63
- **78 tools** over your collection: 28 that read it, 32 that write to it. **18 admin tools** are also
63
+ **79 tools** over your collection: 28 that read it, 33 that write to it. **18 admin tools** are also
64
64
  available — but only to a key created by an admin — for finding and understanding duplicates in the
65
65
  shared global plant type catalog, restructuring its genus/species/cultivar tree, reading, directly
66
66
  editing, merging, and reviewing proposals and duplicate candidates in that catalog, and (7 read-only,
@@ -75,7 +75,8 @@ hydro events, snoozing a due date, health notes, dismissing care recommendations
75
75
  identity/care schedule/settings/lifecycle status, adding a plant (identified or not), propagating
76
76
  one, merging plants together and reversing that merge, deleting a watering or fertilization event,
77
77
  managing the wishlist (add, note, remove), watching or unwatching a plant type, marking notifications
78
- read, creating a location/soil/fertilizer/pot, and contributing a new species to the shared catalog.
78
+ read, creating a location/soil/fertilizer/pot, merging a duplicate soil into another, and
79
+ contributing a new species to the shared catalog.
79
80
 
80
81
  **Plant identity moved to a shared, community-maintained catalog in 2.0.0** — see **Breaking changes
81
82
  in 2.0.0** below before upgrading a client that stored a `plantTypeId`.
@@ -111,7 +112,7 @@ healthy server, not a hang — press Ctrl-C.
111
112
  The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
112
113
 
113
114
  - **`read`** — on every key. Gates all 28 read tools.
114
- - **`write`** — opt-in when you create the key. Gates the 32 write tools.
115
+ - **`write`** — opt-in when you create the key. Gates the 33 write tools.
115
116
  - **`admin`** — only offered when the key's creator is themselves an admin, and only while they
116
117
  still are one (the app re-checks this on every admin-tool call, not just at key creation). Gates
117
118
  the 18 admin catalog tools — 7 read-only, 11 that write — see **Admin tools** below. `admin` alone
@@ -139,7 +140,10 @@ Not oversights — deliberate limits:
139
140
  alternative, so those two are destructive and **cannot be undone**. `remove_from_wishlist` and
140
141
  `unwatch_type` are two more deletes, but not in the same sense — a wish or a watch is a personal
141
142
  to-do item / subscription with no history to lose, trivially re-added with `add_to_wishlist` /
142
- `watch_type` if removed by mistake.
143
+ `watch_type` if removed by mistake. `merge_soils` also ends with a deleted catalog entry, but as
144
+ a side effect of consolidating a duplicate, not a standalone delete: everything the source soil
145
+ referenced moves to the target first, so no plant or potting-event history is lost — only the
146
+ now-redundant soil row and its own mix recipe.
143
147
  - **No sales or trades.** Those records name a second person who never consented to your API key.
144
148
  - **No share-link creation.** Minting a public URL for your collection is a decision for the UI.
145
149
  - **No cross-user access.** Ownership comes from the key; a tool has no way to even express
@@ -201,12 +205,22 @@ overrides the Apr–Sep default), `locationId`.
201
205
  | `get_care_calendar` | `GET /api/v1/care/calendar` | `days` (1–60, default 14) | Day-by-day projection plus an overdue group |
202
206
  | `list_care_recommendations` | `GET /api/v1/care/recommendations` | none | Detected problems: cycle mismatch, chronic lateness, missed seasonal fertilizing, fertilizer pause after repotting, repotting overdue, stale photos |
203
207
 
204
- Four things to know when reading schedule results:
208
+ Five things to know when reading schedule results:
205
209
 
206
210
  - **Days, not timestamps.** `nextDue` and `date` are local `YYYY-MM-DD` strings — a UTC timestamp
207
211
  would render as the wrong day east of Greenwich.
208
212
  - **`neverLogged` is its own bucket.** A plant with a cycle but no event yet has no anchor, so no
209
213
  due date can be computed. Those are returned separately rather than reported as overdue.
214
+ - **`reservoirChecks` is where reservoir and hydro plants show up.** They have no watering cycle, so
215
+ they never appear in `tasks`, `days[]` or `neverLogged` at all. Instead, `list_due_care` and
216
+ `get_care_calendar` each carry a `reservoirChecks` array of `{ plantId, nextCheckAt,
217
+ daysUntilCheck, confidence }` — a *predicted* next check, the median gap between that plant's own
218
+ last few refills (reservoir) or hydro entries, not a schedule the user configured. `nextCheckAt` is
219
+ a `YYYY-MM-DD` day like `nextDue`; `daysUntilCheck` is negative once it has passed; `confidence` is
220
+ `"rough"` below three observed gaps and `"established"` from there on. The array ignores
221
+ `windowDays`/`days` — every plant with a prediction is always in it — and a plant with fewer than
222
+ two entries on distinct days has no entry at all. `list_overdue_care` carries the same array
223
+ narrowed to the checks whose day is today or already past (`daysUntilCheck <= 0`).
210
224
  - **Fertilizing follows the app's calendar, including where it's grouped.** A fertilizing entry
211
225
  carries `wateringAlignedOn` (a `YYYY-MM-DD` day, `null` when absent) — set when the app's
212
226
  water-align rule has attached that feeding to a watering: unconditionally when it's due before
@@ -369,6 +383,10 @@ fertilization logged.
369
383
  `record_fertilization` defaults `fertilizerId` and `fertilizerPercent` to **each plant's own
370
384
  settings** when omitted, so a bulk call across differently-configured plants still does the right
371
385
  thing per plant. `record_refill` and `record_hydro_event` default the same two fields the same way.
386
+ Since 2.12.0 it also mirrors the app's rule that fertilizer goes into the can: every fertilized
387
+ plant gets that day's watering logged as well (routed like `record_watering` — a refill for
388
+ reservoir plants, a top-up for hydro) unless one already exists, and the response lists those
389
+ plants under `alsoWatered`. Do not follow it with `record_watering` for the same plants.
372
390
 
373
391
  `record_repotting` re-derives the plant's current soil from its latest potting event, and syncs
374
392
  watering/fertilizing mode when the new pot's self-watering flag changes it — the same the app's own
@@ -534,6 +552,10 @@ which of those it is, and `values.otherSignalCount` says how many more also appl
534
552
  `propagationCheckDue`, dismissing it is temporary (about a month), since a still-true situation
535
553
  should surface again rather than hide forever.
536
554
 
555
+ `noRecentPhoto` and `repotDue` dismiss the same way — temporarily, not forever — since their
556
+ fingerprints are anchored to the last photo/potting event and nothing short of a new one moves them:
557
+ `noRecentPhoto` re-fires after about a month, `repotDue` after about a season.
558
+
537
559
  ### Wishlist
538
560
 
539
561
  | Tool | Endpoint | Arguments |
@@ -573,6 +595,7 @@ mirror (opening the page already marks everything read).
573
595
  |---|---|---|
574
596
  | `create_location` | `POST /api/v1/locations` | `name` **(required)**, `parentId` (an owned location, to nest under) |
575
597
  | `create_soil` | `POST /api/v1/soils` | `name` **(required)**, `notes`, `items` (`{ componentName, parts }[]`, each component upserted by name) |
598
+ | `merge_soils` | `POST /api/v1/soils/merge` | `sourceId` **(required)**, `targetId` **(required)** |
576
599
  | `create_fertilizer` | `POST /api/v1/fertilizers` | `name` **(required)**, `npk`, `baseDosePerLiterMl` (0–100) |
577
600
  | `create_pot` | `POST /api/v1/pots` | `name` **(required)**, `material`, `isSelfWatering`, `notes`, `links` (`{ url, label? }[]`) |
578
601
  | `create_plant_type` | `POST /api/v1/plant-types` | at least one of `genus`/`commonName`; `species`, `cultivar`, `values` (`sunRequirement`/`waterRequirement` required within it), `note`, `confirmDespiteDuplicates` |
@@ -582,6 +605,12 @@ inline" picker on the plant form — same validation, same duplicate-name handli
582
605
  used by one of your own rows in that catalog is rejected). Pot *sizes* have no create tool yet; add
583
606
  one from the app after creating the pot.
584
607
 
608
+ `merge_soils` moves every plant and potting-event record from `sourceId` onto `targetId`, then
609
+ deletes `sourceId` — for duplicate soil mixes (e.g. names differing only by a stray space). Unlike
610
+ deleting a soil directly, it never refuses for living plants still using it — moving them onto the
611
+ target is the point. `sourceId`'s own mix recipe is dropped; `targetId` keeps its own. There is no
612
+ unmerge — it is the one irreversible write tool in this catalog section.
613
+
585
614
  `create_plant_type` adds a species/cultivar the shared catalog is missing — the same capability as
586
615
  the plant-type picker's "Neue Art anlegen" dialog, always landing `UNVERIFIED`. Unlike that dialog
587
616
  (which warns about likely duplicates client-side before you even submit), this checks
package/dist/index.js CHANGED
@@ -139,7 +139,7 @@ server.tool("record_watering", "WRITE: log a watering for one or more plants. Re
139
139
  plantIds: z.array(z.number().int().positive()).min(1).max(200).describe("Plant ids to water."),
140
140
  wateredAt: optionalPlantDate(""),
141
141
  }, async ({ plantIds, wateredAt }) => apiSend("POST", "/api/v1/care/watering", { plantIds, wateredAt }));
142
- server.tool("record_fertilization", "WRITE: log a fertilization for one or more plants. fertilizerId and fertilizerPercent default to each plant's own settings when omitted. Plants already fertilized that day are skipped. Requires a write-scoped API key.", {
142
+ server.tool("record_fertilization", "WRITE: log a fertilization for one or more plants. Fertilizer goes into the watering can, so each fertilized plant also gets that day's watering logged (a refill for reservoir plants, a top-up for hydro) unless one already exists — reported as alsoWatered; do not call record_watering for the same plants afterwards. fertilizerId and fertilizerPercent default to each plant's own settings when omitted. Plants already fertilized that day are skipped entirely. Requires a write-scoped API key.", {
143
143
  plantIds: z.array(z.number().int().positive()).min(1).max(200).describe("Plant ids to fertilize."),
144
144
  fertilizerId: z.number().int().positive().optional().describe("Override fertilizer; defaults to each plant's own."),
145
145
  fertilizerPercent: z
@@ -398,6 +398,10 @@ server.tool("create_soil", "WRITE: create a soil mix, optionally with components
398
398
  .optional()
399
399
  .describe("Mix components as whole-number parts, e.g. [{ componentName: 'Kokoserde', parts: 3 }, { componentName: 'Perlite', parts: 1 }]. Omit for a soil with no recorded mix."),
400
400
  }, async ({ name, notes, items }) => apiSend("POST", "/api/v1/soils", { name, notes, items }));
401
+ server.tool("merge_soils", "WRITE: merge one soil into another — for duplicate soil mixes (e.g. names differing only by a stray space). Every plant and potting-event record on sourceId moves to targetId, then sourceId is deleted. Unlike deleting a soil directly, this never refuses for living plants still using it — moving them is the point. sourceId's own mix recipe (components/parts) is dropped; targetId keeps its own. Irreversible — there is no unmerge. Requires a write-scoped API key.", {
402
+ sourceId: z.number().int().positive().describe("The soil to merge away and delete. From list_soils."),
403
+ targetId: z.number().int().positive().describe("The soil that survives and absorbs sourceId's plants and potting events. From list_soils."),
404
+ }, async ({ sourceId, targetId }) => apiSend("POST", "/api/v1/soils/merge", { sourceId, targetId }));
401
405
  server.tool("list_fertilizers", "List the user's fertilizers.", async () => apiGet("/api/v1/fertilizers"));
402
406
  server.tool("create_fertilizer", "WRITE: create a fertilizer, with an optional NPK label and base dose (ml per liter of water) used to default record_fertilization's fertilizerPercent. Requires a write-scoped API key.", {
403
407
  name: z.string().min(1).max(100).describe("Fertilizer name."),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sebamomann/plants-mcp",
3
- "version": "2.11.0",
4
- "description": "MCP server for the Sprig plant app: 78 tools to read a plant collection, browse the shared global plant type catalog, log care (watering, fertilizing, repotting, refills, hydro events), edit a plant's identity/care/lifecycle status, add, propagate or merge plants, snooze a due date, check for nudges worth mentioning, manage a wishlist and notifications, watch a plant type for changes, check vacation status, read past problem diagnoses, create a location/soil/fertilizer/pot, and contribute a new species to the catalog. Read-only by default; writes need a write-scoped API key. Two tools can delete a watering/fertilization event; nothing else deletes collection history. 18 more tools read, edit, merge and review the shared global plant type catalog and the instance's operational status (queues, catalog health, activity, growth, community, trade and content aggregates) for an admin-scoped key.",
3
+ "version": "2.13.0",
4
+ "description": "MCP server for the Sprig plant app: 79 tools to read a plant collection, browse the shared global plant type catalog, log care (watering, fertilizing, repotting, refills, hydro events), edit a plant's identity/care/lifecycle status, add, propagate or merge plants, snooze a due date, check for nudges worth mentioning, manage a wishlist and notifications, watch a plant type for changes, check vacation status, read past problem diagnoses, create a location/soil/fertilizer/pot, merge a duplicate soil into another, and contribute a new species to the catalog. Read-only by default; writes need a write-scoped API key. Two tools can delete a watering/fertilization event; nothing else deletes collection history. 18 more tools read, edit, merge and review the shared global plant type catalog and the instance's operational status (queues, catalog health, activity, growth, community, trade and content aggregates) for an admin-scoped key.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "sebamomann <github@sebamomann.de>",