@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 +37 -0
- package/README.md +34 -5
- package/dist/index.js +5 -1
- package/package.json +2 -2
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
|
-
**
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "MCP server for the Sprig plant app:
|
|
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>",
|