@sebamomann/plants-mcp 2.13.0 → 3.0.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 +64 -0
- package/README.md +22 -9
- package/dist/index.js +16 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
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
|
+
## 3.0.0 — 2026-10-04
|
|
7
|
+
|
|
8
|
+
**`noFertilizerThisSeason` is renamed `fertilizingNotStarted`.** Tool count unchanged (81). A **major**
|
|
9
|
+
bump — an accepted argument value was removed:
|
|
10
|
+
|
|
11
|
+
- `list_care_recommendations` no longer returns `noFertilizerThisSeason` ("no feeding since the
|
|
12
|
+
season started", which fired on day one of every season). The new type `fertilizingNotStarted`
|
|
13
|
+
fires only for a plant with a fertilizer and an active fertilizing cycle that has **never** been
|
|
14
|
+
fertilized, once a full cycle has passed since feeding could have started (acquisition, the end of
|
|
15
|
+
a repotting rest, or the end of a winter pause). Cuttings are excluded.
|
|
16
|
+
- `dismiss_care_recommendation`'s `type` enum: `noFertilizerThisSeason` → `fertilizingNotStarted`.
|
|
17
|
+
Old dismissals of the removed type no longer apply to anything.
|
|
18
|
+
- Additive: `wateringCycleMismatch.values` gains `season` (`"summer"`/`"winter"`), the column the
|
|
19
|
+
suggestion is expressed in. The mismatch is now judged against the cycle in effect when each
|
|
20
|
+
interval started (season, gradual season ramp, controlled climate), so a season change alone no
|
|
21
|
+
longer makes a plant look off-cycle.
|
|
22
|
+
- `list_notifications`: a `CHANGELOG_PUBLISHED` row may now carry `messageKey:
|
|
23
|
+
"changelogPublishedSummary"` with `values: { summary }` (that day's summary) instead of
|
|
24
|
+
`changelogPublished` with `{ count }`.
|
|
25
|
+
|
|
26
|
+
## 2.16.0 — 2026-10-04
|
|
27
|
+
|
|
28
|
+
**Autumn "Prüfen" checks: `list_due_care` gains `checks`, new `dismiss_transition_check`.** Tool count
|
|
29
|
+
81 (was 80). A **minor** bump — one new tool and one additive response field:
|
|
30
|
+
|
|
31
|
+
- `list_due_care` returns an additive `checks` array — `{ plantId, name, lastWateredAt, checkSince,
|
|
32
|
+
regularDueAt }` — for plants whose watering interval grew with the autumn season change while the
|
|
33
|
+
plant may dry out at its summer pace. A question, not a task: not in `tasks`/`counts`. Existing
|
|
34
|
+
fields are unchanged.
|
|
35
|
+
- New write tool `dismiss_transition_check` (`plantId`): answers a check with "noch feucht", hiding
|
|
36
|
+
it until its regular due date without moving that date. The other answer ("dry") is
|
|
37
|
+
`record_watering`.
|
|
38
|
+
|
|
39
|
+
## 2.15.0 — 2026-10-04
|
|
40
|
+
|
|
41
|
+
**`update_plant_care` gains `keepWinterCare`; `get_plant` reports the effective schedule.** Tool count
|
|
42
|
+
unchanged (80). A **minor** bump — one new optional argument and new response fields:
|
|
43
|
+
|
|
44
|
+
- `update_plant_care` accepts `keepWinterCare: boolean`. A plant in a controlled-climate location
|
|
45
|
+
follows its summer watering and fertilizing cycle all year; `keepWinterCare: true` brings the
|
|
46
|
+
winter cycle back for both. The stored winter values are never cleared by it.
|
|
47
|
+
- `get_plant` returns `keepWinterCare`, the location's `controlledClimate` (inside `location`) and a
|
|
48
|
+
new top-level `followsSummerYearRound` boolean.
|
|
49
|
+
- **Behaviour change in `get_plant`:** its six schedule fields now show what scheduling actually uses.
|
|
50
|
+
For a plant that follows its summer cycle all year, the winter fields
|
|
51
|
+
(`wateringFrequencyWinterDays`, `wateringFrequencyWinter`, `fertilizingCycleWinterWeeks`) are the
|
|
52
|
+
*effective* (summer) values, not the stored winter ones. Nothing changes for plants outside a
|
|
53
|
+
controlled-climate location.
|
|
54
|
+
- `update_plant_care`'s result includes `keepWinterCare`.
|
|
55
|
+
|
|
56
|
+
## 2.14.0 — 2026-10-04
|
|
57
|
+
|
|
58
|
+
**New tool: `update_location`; `create_location` gains `controlledClimate`.** Tool count 79 → 80
|
|
59
|
+
(28 read, 34 write, 18 admin). A **minor** bump — one new write tool and one new optional argument:
|
|
60
|
+
|
|
61
|
+
- `PATCH /api/v1/locations/:id` takes any of `name`, `parentId` (`null` = top level) and
|
|
62
|
+
`controlledClimate`; only the fields present change. Shares its core with the location detail
|
|
63
|
+
page's save action, so cycle checks on `parentId` behave identically.
|
|
64
|
+
- `controlledClimate` marks a place with constant light and warmth (e.g. a plant cabinet): plants
|
|
65
|
+
placed directly there follow their summer watering and fertilizing cycle all year, unless the
|
|
66
|
+
plant opts back into winter care. `create_location` accepts it too.
|
|
67
|
+
- `list_locations` now returns `controlledClimate` per location (a new response field — no
|
|
68
|
+
change needed by callers).
|
|
69
|
+
|
|
6
70
|
## 2.13.0 — 2026-09-30
|
|
7
71
|
|
|
8
72
|
**New tool: `merge_soils`.** Tool count 78 → 79 (28 read, 33 write, 18 admin). A **minor** bump —
|
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
|
+
**81 tools** over your collection: 28 that read it, 35 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,
|
|
@@ -112,7 +112,7 @@ healthy server, not a hang — press Ctrl-C.
|
|
|
112
112
|
The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
|
|
113
113
|
|
|
114
114
|
- **`read`** — on every key. Gates all 28 read tools.
|
|
115
|
-
- **`write`** — opt-in when you create the key. Gates the
|
|
115
|
+
- **`write`** — opt-in when you create the key. Gates the 35 write tools.
|
|
116
116
|
- **`admin`** — only offered when the key's creator is themselves an admin, and only while they
|
|
117
117
|
still are one (the app re-checks this on every admin-tool call, not just at key creation). Gates
|
|
118
118
|
the 18 admin catalog tools — 7 read-only, 11 that write — see **Admin tools** below. `admin` alone
|
|
@@ -174,7 +174,10 @@ facts (light, water, feeding need, toxicity, climate, growth, substrate/propagat
|
|
|
174
174
|
region), keyed by the same field names `get_plant_type` uses — and its six schedule fields
|
|
175
175
|
(`wateringFrequencySummerDays`/`WinterDays`, `wateringFrequencySummer`/`Winter`,
|
|
176
176
|
`fertilizingCycleSummerWeeks`/`WinterWeeks`) as `{ value, source: "plant"|"type"|"none", typeValue }`
|
|
177
|
-
instead of a raw value — see **Breaking changes in 2.0.0** below.
|
|
177
|
+
instead of a raw value — see **Breaking changes in 2.0.0** below. They show what scheduling actually
|
|
178
|
+
uses: for a plant in a controlled-climate location without `keepWinterCare`, the winter fields equal the
|
|
179
|
+
summer values, and `followsSummerYearRound` is `true` (also returned: `keepWinterCare` and
|
|
180
|
+
`location.controlledClimate`).
|
|
178
181
|
|
|
179
182
|
### Per-plant history
|
|
180
183
|
|
|
@@ -200,10 +203,10 @@ overrides the Apr–Sep default), `locationId`.
|
|
|
200
203
|
|
|
201
204
|
| Tool | Endpoint | Extra arguments | Answers |
|
|
202
205
|
|---|---|---|---|
|
|
203
|
-
| `list_due_care` | `GET /api/v1/care/due` | `windowDays` (0–60, default 0 = that day only), `includeOverdue` (default true) | "What should I do today?" |
|
|
206
|
+
| `list_due_care` | `GET /api/v1/care/due` | `windowDays` (0–60, default 0 = that day only), `includeOverdue` (default true) | "What should I do today?" Also carries the additive `checks` array (autumn "Prüfen" questions — see `dismiss_transition_check`). |
|
|
204
207
|
| `list_overdue_care` | `GET /api/v1/care/overdue` | — | "What have I fallen behind on?" — most overdue first, with days late |
|
|
205
208
|
| `get_care_calendar` | `GET /api/v1/care/calendar` | `days` (1–60, default 14) | Day-by-day projection plus an overdue group |
|
|
206
|
-
| `list_care_recommendations` | `GET /api/v1/care/recommendations` | none | Detected problems: cycle mismatch, chronic lateness,
|
|
209
|
+
| `list_care_recommendations` | `GET /api/v1/care/recommendations` | none | Detected problems: cycle mismatch, chronic lateness, fertilizing never started, fertilizer pause after repotting, repotting overdue, stale photos |
|
|
207
210
|
|
|
208
211
|
Five things to know when reading schedule results:
|
|
209
212
|
|
|
@@ -362,7 +365,7 @@ tools** → **Catalogs** below) lets a write-scoped key add it.
|
|
|
362
365
|
|
|
363
366
|
## Write tools
|
|
364
367
|
|
|
365
|
-
**All
|
|
368
|
+
**All 32 ordinary write tools require a `write`-scoped key**; a read-only key gets HTTP 403. Every
|
|
366
369
|
description starts with `WRITE:` so a model cannot mistake one for a read.
|
|
367
370
|
|
|
368
371
|
### Care logging
|
|
@@ -419,7 +422,7 @@ it is — that backlog is not the watering's doing.
|
|
|
419
422
|
|
|
420
423
|
| Tool | Endpoint | Arguments |
|
|
421
424
|
|---|---|---|
|
|
422
|
-
| `update_plant_care` | `PATCH /api/v1/plants/:id/care` | `plantId` **(required)**, plus any of `wateringMode` (`scheduled`\|`reservoir`\|`hydro`), `fertilizingMode` (`scheduled`\|`with_watering`), `wateringFrequencySummer`/`wateringFrequencyWinter` (labels), `wateringFrequencySummerDays`/`wateringFrequencyWinterDays` (1–365), `wateringNotes`, `fertilizingCycleSummerWeeks`/`fertilizingCycleWinterWeeks` (0–52), `fertilizerPercent` (0–1000), `fertilizingNotes` |
|
|
425
|
+
| `update_plant_care` | `PATCH /api/v1/plants/:id/care` | `plantId` **(required)**, plus any of `wateringMode` (`scheduled`\|`reservoir`\|`hydro`), `fertilizingMode` (`scheduled`\|`with_watering`), `wateringFrequencySummer`/`wateringFrequencyWinter` (labels), `wateringFrequencySummerDays`/`wateringFrequencyWinterDays` (1–365), `wateringNotes`, `fertilizingCycleSummerWeeks`/`fertilizingCycleWinterWeeks` (0–52), `fertilizerPercent` (0–1000), `fertilizingNotes`, `keepWinterCare` (boolean — winter cycle despite a controlled-climate location) |
|
|
423
426
|
| `update_plant` | `PATCH /api/v1/plants/:id` | `plantId` **(required)**, plus any of `plantTypeId`, `nickname`, `locationId`, `soilId`, `fertilizerId`, `quantity` (0–9999), `notes`, `tags` (replaces the full list), `sitterInstructions`, `parentPlantId` |
|
|
424
427
|
|
|
425
428
|
Both take a `plantId` and edit **only the fields you pass** — an omitted field keeps its current
|
|
@@ -529,11 +532,20 @@ The two edit tools take an **`entryId`, not a `plantId`** — get it from `list_
|
|
|
529
532
|
| Tool | Endpoint | Arguments |
|
|
530
533
|
|---|---|---|
|
|
531
534
|
| `dismiss_care_recommendation` | `POST /api/v1/care/recommendations/dismiss` | `plantId`, `type`, `fingerprint` — **all required** |
|
|
535
|
+
| `dismiss_transition_check` | `POST /api/v1/care/transition-checks/dismiss` | `plantId` **(required)** — a plant listed in `checks` of `list_due_care` |
|
|
536
|
+
|
|
537
|
+
`dismiss_transition_check` answers an autumn "Prüfen" check with "noch feucht": as the season changes
|
|
538
|
+
to winter the watering interval grows, so a plant that dries out at its summer pace would go
|
|
539
|
+
unreminded for longer. `list_due_care` lists such plants in `checks`
|
|
540
|
+
(`{ plantId, name, lastWateredAt, checkSince, regularDueAt }`) from the day the summer-pace watering
|
|
541
|
+
would have been due until the regular due date. The dismissal hides the check until then and never
|
|
542
|
+
moves the due date; a new watering reopens the question. The other answer, "dry", is a plain
|
|
543
|
+
`record_watering`.
|
|
532
544
|
|
|
533
545
|
Pass all three **exactly as returned by `list_care_recommendations`**. The `fingerprint` identifies
|
|
534
546
|
that specific occurrence, so dismissing one never suppresses a later, different recurrence.
|
|
535
547
|
|
|
536
|
-
Dismissible `type` values: `wateringOftenLate`, `fertilizingOftenLate`, `
|
|
548
|
+
Dismissible `type` values: `wateringOftenLate`, `fertilizingOftenLate`, `fertilizingNotStarted`,
|
|
537
549
|
`recentlyRepottedAvoidFertilizer`, `noRecentPhoto`, `propagationCheckDue`, `possibleDecline`.
|
|
538
550
|
`wateringCycleMismatch` is **not** dismissible — it is a configuration contradiction, fixed by
|
|
539
551
|
editing the plant rather than hidden.
|
|
@@ -593,7 +605,8 @@ mirror (opening the page already marks everything read).
|
|
|
593
605
|
|
|
594
606
|
| Tool | Endpoint | Arguments |
|
|
595
607
|
|---|---|---|
|
|
596
|
-
| `create_location` | `POST /api/v1/locations` | `name` **(required)**, `parentId` (an owned location, to nest under) |
|
|
608
|
+
| `create_location` | `POST /api/v1/locations` | `name` **(required)**, `parentId` (an owned location, to nest under), `controlledClimate` (plants there follow their summer watering/fertilizing cycle year-round unless they opt back into winter care) |
|
|
609
|
+
| `update_location` | `PATCH /api/v1/locations/:id` | `locationId` **(required)**, `name`, `parentId` (`null` = top level), `controlledClimate` — only the fields passed change |
|
|
597
610
|
| `create_soil` | `POST /api/v1/soils` | `name` **(required)**, `notes`, `items` (`{ componentName, parts }[]`, each component upserted by name) |
|
|
598
611
|
| `merge_soils` | `POST /api/v1/soils/merge` | `sourceId` **(required)**, `targetId` **(required)** |
|
|
599
612
|
| `create_fertilizer` | `POST /api/v1/fertilizers` | `name` **(required)**, `npk`, `baseDosePerLiterMl` (0–100) |
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ server.tool("list_plants", "List the user's plants, with optional filters. Retur
|
|
|
53
53
|
offset: z.number().int().min(0).optional().describe("Result offset (pagination)."),
|
|
54
54
|
sort: z.enum(["displayName", "createdAt", "updatedAt", "acquiredAt"]).optional().describe("Sort key. 'displayName' sorts by the plant's resolved name (nickname, or its type's name); the rest sort newest first."),
|
|
55
55
|
}, async (args) => apiGet("/api/v1/plants", args));
|
|
56
|
-
server.tool("get_plant", "Get one plant with full detail: identity (type, nickname, display name, botanical name, unidentified state — see 'type'), typeFacts (the type's effective light/water/feeding need, toxicity, climate, growth, substrate/propagation, family/native region — using the catalog field keys from get_plant_type), catalogs (location, soil, fertilizer), care config, and recent event summaries. The six schedule fields (wateringFrequencySummerDays/WinterDays, wateringFrequencySummer/Winter, fertilizingCycleSummerWeeks/WinterWeeks) are each { value, source: 'plant'|'type'|'none', typeValue } — value is what scheduling/display use, source says where it came from, typeValue is what the type would say even when the plant overrides it.", { id: z.number().int().describe("Plant id.") }, async ({ id }) => apiGet(`/api/v1/plants/${id}`));
|
|
56
|
+
server.tool("get_plant", "Get one plant with full detail: identity (type, nickname, display name, botanical name, unidentified state — see 'type'), typeFacts (the type's effective light/water/feeding need, toxicity, climate, growth, substrate/propagation, family/native region — using the catalog field keys from get_plant_type), catalogs (location, soil, fertilizer), care config, and recent event summaries. The six schedule fields (wateringFrequencySummerDays/WinterDays, wateringFrequencySummer/Winter, fertilizingCycleSummerWeeks/WinterWeeks) are each { value, source: 'plant'|'type'|'none', typeValue } — value is what scheduling/display use, source says where it came from, typeValue is what the type would say even when the plant overrides it. For a plant in a controlled-climate location (location.controlledClimate) that has not set keepWinterCare, followsSummerYearRound is true and the three winter schedule fields show the SUMMER values — the plant follows its summer cycle all year (the raw stored winter values are kept and return when keepWinterCare is set or the plant moves elsewhere).", { id: z.number().int().describe("Plant id.") }, async ({ id }) => apiGet(`/api/v1/plants/${id}`));
|
|
57
57
|
// --- Events connected to a plant -------------------------------------------
|
|
58
58
|
const eventArgs = {
|
|
59
59
|
id: z.number().int().describe("Plant id."),
|
|
@@ -84,7 +84,7 @@ const careArgs = {
|
|
|
84
84
|
.describe("Override the season (Apr–Sep is summer). Defaults to the season of `date`."),
|
|
85
85
|
locationId: z.number().int().optional().describe("Only consider plants in this location."),
|
|
86
86
|
};
|
|
87
|
-
server.tool("list_due_care", "What needs water or fertilizer now: overdue work plus anything due on the given day. Use `windowDays` to look ahead. This is the right tool for 'what should I do today?'. A fertilizing entry carries `wateringAlignedOn` (YYYY-MM-DD, null when absent) when the app's calendar has grouped that feeding with a watering; `windowDays`/`includeOverdue` are judged by that day, not the feeding's own `nextDue` — see `get_care_calendar`'s description for the full rule.", {
|
|
87
|
+
server.tool("list_due_care", "What needs water or fertilizer now: overdue work plus anything due on the given day. Use `windowDays` to look ahead. This is the right tool for 'what should I do today?'. A fertilizing entry carries `wateringAlignedOn` (YYYY-MM-DD, null when absent) when the app's calendar has grouped that feeding with a watering; `windowDays`/`includeOverdue` are judged by that day, not the feeding's own `nextDue` — see `get_care_calendar`'s description for the full rule. The additive `checks` array lists autumn 'Prüfen' questions — { plantId, name, lastWateredAt, checkSince, regularDueAt } — for plants that dry out at their summer pace while the season change has stretched their interval: the summer-pace date (`checkSince`) has passed but the regular due date (`regularDueAt`) has not. A check is a question ('is the soil dry?'), not a task — it is not in `tasks`/`counts`. Answer 'dry' with `record_watering`, 'still moist' with `dismiss_transition_check`.", {
|
|
88
88
|
...careArgs,
|
|
89
89
|
windowDays: z
|
|
90
90
|
.number()
|
|
@@ -188,7 +188,7 @@ server.tool("snooze_care", "WRITE: postpone a plant's next watering or fertilizi
|
|
|
188
188
|
originalDueAt: z.string().describe("The due date being pushed out, e.g. from list_due_care's nextDue for this plant/careType."),
|
|
189
189
|
note: z.string().max(160).optional().describe("Free-text reason, e.g. 'away for the week'."),
|
|
190
190
|
}, async (args) => apiSend("POST", "/api/v1/care/snooze", { ...args }));
|
|
191
|
-
server.tool("update_plant_care", "WRITE: edit a plant's care schedule — watering mode, days/labels, fertilizing mode, cycles and percent, and the watering/fertilizing notes. Omitted fields keep their current value. null on wateringFrequencySummerDays/WinterDays, wateringFrequencySummer/Winter, or fertilizingCycleSummerWeeks/WinterWeeks means 'follow the type' — the plant's effective schedule then falls back to its type's suggested value (see get_plant's typeFacts / the schedule fields' typeValue) instead of having no schedule at all; a value equal to the type's suggested value is stored the same way (it follows the type). null on wateringNotes/fertilizingNotes just clears the note. Watering fields are ignored while the plant's watering isn't on a schedule (reservoir/hydro), and the fertilizing-cycle fields are ignored while it fertilizes with every watering — there's no cycle to configure in either case. If wateringMode/fertilizingMode is part of this same call, that check runs against the NEW mode, not the plant's current one — e.g. switching to 'reservoir' while also passing a watering cycle ignores that cycle. sunRequirement/waterRequirement are no longer settable here — light and water need are type-only facts now (get_plant's typeFacts, or get_plant_type). Use this (not update_plant) to fix a 'wateringCycleMismatch' recommendation where the mode itself is wrong. Requires a write-scoped API key.", {
|
|
191
|
+
server.tool("update_plant_care", "WRITE: edit a plant's care schedule — watering mode, days/labels, fertilizing mode, cycles and percent, and the watering/fertilizing notes. Omitted fields keep their current value. null on wateringFrequencySummerDays/WinterDays, wateringFrequencySummer/Winter, or fertilizingCycleSummerWeeks/WinterWeeks means 'follow the type' — the plant's effective schedule then falls back to its type's suggested value (see get_plant's typeFacts / the schedule fields' typeValue) instead of having no schedule at all; a value equal to the type's suggested value is stored the same way (it follows the type). null on wateringNotes/fertilizingNotes just clears the note. Watering fields are ignored while the plant's watering isn't on a schedule (reservoir/hydro), and the fertilizing-cycle fields are ignored while it fertilizes with every watering — there's no cycle to configure in either case. If wateringMode/fertilizingMode is part of this same call, that check runs against the NEW mode, not the plant's current one — e.g. switching to 'reservoir' while also passing a watering cycle ignores that cycle. sunRequirement/waterRequirement are no longer settable here — light and water need are type-only facts now (get_plant's typeFacts, or get_plant_type). Use this (not update_plant) to fix a 'wateringCycleMismatch' recommendation where the mode itself is wrong. keepWinterCare brings the winter cycle back for a plant in a controlled-climate location (which otherwise follows its summer cycle all year, see get_plant's followsSummerYearRound) — it governs both watering and fertilizing and has no effect elsewhere; the stored winter values are never cleared by it. Requires a write-scoped API key.", {
|
|
192
192
|
plantId: z.number().int().positive().describe("Plant id."),
|
|
193
193
|
wateringMode: z.enum(["scheduled", "reservoir", "hydro"]).optional().describe("How this plant is watered: on a schedule, from a reservoir, or in water/hydro culture."),
|
|
194
194
|
fertilizingMode: z.enum(["scheduled", "with_watering"]).optional().describe("How this plant is fertilized: on its own cycle, or automatically with every watering."),
|
|
@@ -201,6 +201,7 @@ server.tool("update_plant_care", "WRITE: edit a plant's care schedule — wateri
|
|
|
201
201
|
fertilizingCycleWinterWeeks: z.number().int().min(0).max(52).nullable().optional().describe("Winter fertilizing interval in weeks. null follows the type's suggested weeks. Ignored while fertilizing is 'with every watering'."),
|
|
202
202
|
fertilizerPercent: z.number().int().min(0).max(1000).optional().describe("Strength as a percentage of the base dose."),
|
|
203
203
|
fertilizingNotes: z.string().max(500).nullable().optional().describe("Free-text fertilizing notes."),
|
|
204
|
+
keepWinterCare: z.boolean().optional().describe("true: use the winter cycle despite a controlled-climate location; false: follow the summer cycle all year there. Only matters while the plant stands in a location with controlledClimate."),
|
|
204
205
|
}, async ({ plantId, ...body }) => apiSend("PATCH", `/api/v1/plants/${plantId}/care`, body));
|
|
205
206
|
server.tool("update_plant", "WRITE: edit a plant's type, nickname, location, soil, fertilizer, quantity, notes, tags, sitter instructions, or parent plant. Omitted fields keep their current value; null unassigns a catalog entry, clears notes/instructions, empties the tag list, or detaches the plant from its parent — and for plantTypeId, makes the plant 'unidentified'. nickname is required whenever the plant's state after this edit has no type (whether because plantTypeId is set to null here, or was already null and nickname isn't being set to something non-empty) — see plantIdentity's validatePlantNickname. Requires a write-scoped API key.", {
|
|
206
207
|
plantId: z.number().int().positive().describe("Plant id."),
|
|
@@ -239,7 +240,7 @@ server.tool("dismiss_care_recommendation", "WRITE: hide one care recommendation,
|
|
|
239
240
|
.enum([
|
|
240
241
|
"wateringOftenLate",
|
|
241
242
|
"fertilizingOftenLate",
|
|
242
|
-
"
|
|
243
|
+
"fertilizingNotStarted",
|
|
243
244
|
"recentlyRepottedAvoidFertilizer",
|
|
244
245
|
"noRecentPhoto",
|
|
245
246
|
"propagationCheckDue",
|
|
@@ -248,6 +249,9 @@ server.tool("dismiss_care_recommendation", "WRITE: hide one care recommendation,
|
|
|
248
249
|
.describe("Recommendation type, from list_care_recommendations."),
|
|
249
250
|
fingerprint: z.string().min(1).describe("Occurrence fingerprint, from list_care_recommendations."),
|
|
250
251
|
}, async (args) => apiSend("POST", "/api/v1/care/recommendations/dismiss", { ...args }));
|
|
252
|
+
server.tool("dismiss_transition_check", "WRITE: answer an autumn 'Prüfen' check with 'noch feucht' (still moist) — hides that plant's check until its regular due date, without moving the due date. Pass the plantId of an entry in `checks` of list_due_care. The answer is tied to the plant's last watering, so a new watering reopens the question next autumn/interval. For the other answer — the soil is dry — just call `record_watering`: a normal watering makes the check disappear. Requires a write-scoped API key.", {
|
|
253
|
+
plantId: z.number().int().positive().describe("Plant id, from `checks` of list_due_care."),
|
|
254
|
+
}, async (args) => apiSend("POST", "/api/v1/care/transition-checks/dismiss", { ...args }));
|
|
251
255
|
// --- Deleting events ---------------------------------------------------------
|
|
252
256
|
/**
|
|
253
257
|
* The only destructive tools in this server. Everything else — plants,
|
|
@@ -388,7 +392,14 @@ server.tool("create_plant_type", "WRITE: add a species/cultivar the shared globa
|
|
|
388
392
|
server.tool("create_location", "WRITE: create a location — the same 'create inline' picker on the plant form. Never conflicts with an existing owned catalog entry: a duplicate name is rejected by the underlying unique constraint, surfaced as an error. Requires a write-scoped API key.", {
|
|
389
393
|
name: z.string().min(1).max(100).describe("Location name."),
|
|
390
394
|
parentId: z.number().int().positive().optional().describe("An owned location id to nest this one under (from list_locations). Omit for a top-level location."),
|
|
391
|
-
|
|
395
|
+
controlledClimate: z.boolean().optional().describe("True for a place with constant light and warmth all year (e.g. a plant cabinet with a grow light): plants placed directly here follow their summer watering and fertilizing cycle year-round, unless the plant opts back into winter care. Omit for false."),
|
|
396
|
+
}, async ({ name, parentId, controlledClimate }) => apiSend("POST", "/api/v1/locations", { name, parentId, controlledClimate }));
|
|
397
|
+
server.tool("update_location", "WRITE: rename, move or change the climate setting of an owned location. Only the fields you pass change. Requires a write-scoped API key.", {
|
|
398
|
+
locationId: z.number().int().positive().describe("The location id (from list_locations)."),
|
|
399
|
+
name: z.string().min(1).max(100).optional().describe("New name."),
|
|
400
|
+
parentId: z.number().int().positive().nullable().optional().describe("An owned location id to nest this one under; null moves it to the top level. Omit to keep the current parent. Cycles are rejected."),
|
|
401
|
+
controlledClimate: z.boolean().optional().describe("True: plants placed directly in this location (not its sublocations) follow their summer watering and fertilizing cycle year-round, unless the plant opts back into winter care. False: normal seasonal cycle again. Omit to keep the current setting."),
|
|
402
|
+
}, async ({ locationId, name, parentId, controlledClimate }) => apiSend("PATCH", `/api/v1/locations/${locationId}`, { name, parentId, controlledClimate }));
|
|
392
403
|
server.tool("list_soils", "List the user's soils.", async () => apiGet("/api/v1/soils"));
|
|
393
404
|
server.tool("create_soil", "WRITE: create a soil mix, optionally with components (name + parts) — mirrors the app's soil mix editor. Each component is matched or created by name under the caller's account. Requires a write-scoped API key.", {
|
|
394
405
|
name: z.string().min(1).max(100).describe("Soil name."),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebamomann/plants-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server for the Sprig plant app:
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "MCP server for the Sprig plant app: 81 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, answer an autumn watering check, check for nudges worth mentioning, manage a wishlist and notifications, watch a plant type for changes, check vacation status, read past problem diagnoses, create or update a location (incl. its controlled-climate setting), create a 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>",
|