@sebamomann/plants-mcp 2.5.1 → 2.7.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,34 @@
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.7.0 — 2026-09-24
7
+
8
+ **Nudges.** 77 tools registered overall (28 read, 31 write, 18 admin). A **minor** bump — one new
9
+ tool, nothing renamed or removed:
10
+
11
+ - `list_nudges` — the caller's whole ranked "gaps worth mentioning" queue: stale or missing photos,
12
+ plants without a type or location, long-unresolved health entries, and app features never tried.
13
+ Each entry is `{ kind, variant, count, href }`; the app itself only ever shows the first one, but
14
+ an assistant sees the whole queue. Read-only — snoozing a nudge stays an in-app action, deliberately;
15
+ see `docs/nudges.md`'s "MCP" section and `docs/mcp-server.md`'s roadmap for why `snooze_nudge` isn't
16
+ part of this.
17
+
18
+ ## 2.6.0 — 2026-09-24
19
+
20
+ **Snooze until a date.** Tool count unchanged (76 overall). A **minor** bump — one new optional
21
+ argument, nothing renamed or removed: `snooze_care` takes `snoozedUntil` (`YYYY-MM-DD`, after today,
22
+ within a year) as an alternative to `durationDays`, which is now optional. Exactly one of the two is
23
+ required; passing both or neither is a 400. The app's own snooze picker gained the same date field.
24
+ Behaviour change without a shape change: a `durationDays` preset now counts from today once
25
+ `originalDueAt` has passed (it used to count from `originalDueAt` regardless, so "3 days" on a
26
+ week-overdue plant landed four days in the past and deferred nothing).
27
+
28
+ ## 2.5.2 — 2026-09-24
29
+
30
+ **Dependency refresh.** Tool count unchanged (76 overall). A **patch** bump — nothing renamed or
31
+ removed: `@modelcontextprotocol/sdk` 1.29 → 1.30 and `zod` 4.4 → 4.6; the workspace's tests run
32
+ on Vitest 5.
33
+
6
34
  ## 2.5.1 — 2026-09-20
7
35
 
8
36
  **Richer admin dashboard status.** Tool count unchanged (76 overall). A **patch** bump — the
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
- **76 tools** over your collection: 27 that read it, 31 that write to it. **18 admin tools** are also
63
+ **77 tools** over your collection: 28 that read it, 31 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,
@@ -68,8 +68,9 @@ editing, merging, and reviewing proposals and duplicate candidates in that catal
68
68
 
69
69
  Reads cover plants, the shared global plant type catalog, watering and fertilization history, the
70
70
  full care timeline, photo metadata, health entries, the derived care schedule, collection-wide
71
- activity, the wishlist, watched types, notifications, vacation status, and the lookup catalogs
72
- (including pots). Writes cover recording waterings, fertilizings, repottings, reservoir refills and
71
+ activity, nudges (gaps in the collection worth mentioning), the wishlist, watched types,
72
+ notifications, vacation status, and the lookup catalogs (including pots). Writes cover recording
73
+ waterings, fertilizings, repottings, reservoir refills and
73
74
  hydro events, snoozing a due date, health notes, dismissing care recommendations, editing a plant's
74
75
  identity/care schedule/settings/lifecycle status, adding a plant (identified or not), propagating
75
76
  one, merging plants together and reversing that merge, deleting a watering or fertilization event,
@@ -109,7 +110,7 @@ healthy server, not a hang — press Ctrl-C.
109
110
 
110
111
  The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
111
112
 
112
- - **`read`** — on every key. Gates all 27 read tools.
113
+ - **`read`** — on every key. Gates all 28 read tools.
113
114
  - **`write`** — opt-in when you create the key. Gates the 31 write tools.
114
115
  - **`admin`** — only offered when the key's creator is themselves an admin, and only while they
115
116
  still are one (the app re-checks this on every admin-tool call, not just at key creation). Gates
@@ -247,6 +248,20 @@ vacation in **either** mode (`sitter` or `shift`), with just `{ id, mode, starts
247
248
  isUpcoming, plantCount }` — no per-plant detail or schedule. Use `sitter_briefing` for the full
248
249
  sitter sheet; use this one just to know whether a vacation (of either mode) exists at all.
249
250
 
251
+ ### Nudges
252
+
253
+ | Tool | Endpoint | Arguments |
254
+ |---|---|---|
255
+ | `list_nudges` | `GET /api/v1/nudges` | none |
256
+
257
+ Gaps in the collection worth mentioning, distinct from due/overdue care: stale or missing photos,
258
+ plants without a type or location, long-unresolved health entries, and app features the caller has
259
+ never tried. Returns the caller's whole ranked queue as `{ kind, variant, count, href }[]` — the app
260
+ itself only ever shows the first entry, but an assistant benefits from seeing everything pending.
261
+ `count` is an aggregate number for that kind (`0` for a feature-discovery entry); `href` is the
262
+ in-app page to send the user to. An empty list means nothing is currently worth nudging about.
263
+ Read-only — snoozing a nudge stays an in-app action by design.
264
+
250
265
  ### Wishlist
251
266
 
252
267
  | Tool | Endpoint | Arguments |
@@ -336,7 +351,7 @@ description starts with `WRITE:` so a model cannot mistake one for a read.
336
351
  | `record_repotting` | `POST /api/v1/plants/:id/repotting` | `plantId`, `potId` **(both required)**, plus `potSizeId`, `soilId`, `pottedAt`, `hasDrainage`, `hasClimbingAid`, `notes` |
337
352
  | `record_refill` | `POST /api/v1/plants/:id/refill` | `plantId` **(required)**, plus `fertilizerId`, `fertilizerPercent`, `refilledAt` |
338
353
  | `record_hydro_event` | `POST /api/v1/plants/:id/hydro` | `plantId` **(required)**, plus `kind` (`topup`\|`change`), `waterLevel` (0–100), `fertilizerId`, `fertilizerPercent`, `recordedAt`, `notes` |
339
- | `snooze_care` | `POST /api/v1/care/snooze` | `plantId`, `careType` (`water`\|`fert`), `durationDays` (1, 3, 7, or 14), `originalDueAt` **(all required)**, `note` |
354
+ | `snooze_care` | `POST /api/v1/care/snooze` | `plantId`, `careType` (`water`\|`fert`), `originalDueAt` **(all required)**, exactly one of `durationDays` (1, 3, 7, or 14) or `snoozedUntil` (`YYYY-MM-DD`), `note` |
340
355
 
341
356
  `record_watering` mirrors a one-click watering in the app: reservoir and hydro plants are recorded
342
357
  as a refill / top-up automatically, and plants configured to fertilize with watering also get a
@@ -360,7 +375,10 @@ hydro-cultured plant.
360
375
 
361
376
  `snooze_care` postpones a plant's next watering or fertilizing due date — the same picker the care
362
377
  hub offers. `originalDueAt` is the due date being pushed out (`nextDue` from `list_due_care`/
363
- `get_care_calendar` for that plant/careType), not today's date.
378
+ `get_care_calendar` for that plant/careType), not today's date. Pass exactly one of `durationDays`
379
+ (a preset counted from `originalDueAt`, or from today once that date has passed — so "3 days" on
380
+ an overdue plant means three days from now) or `snoozedUntil` (an explicit day: tomorrow at the
381
+ earliest, at most a year out).
364
382
 
365
383
  ### Plant edits
366
384
 
package/dist/index.js CHANGED
@@ -121,6 +121,8 @@ server.tool("sitter_briefing", "Printable care sheet for the caller's current or
121
121
  locationId: z.number().int().optional().describe("Only include plants in this location (id from list_locations). Omit for every plant on the vacation."),
122
122
  }, async (args) => apiGet("/api/v1/vacation/sitter-briefing", args));
123
123
  server.tool("get_vacation_status", "Whether the caller has a current or upcoming vacation, in either mode ('sitter' or 'shift') — unlike sitter_briefing, which only ever reports a 'sitter'-mode one. Returns { hasVacation: false } when none is running or scheduled, otherwise the vacation's id, mode, date range, whether it has started yet (isActive/isUpcoming), and how many plants it covers. Read-only status only — for a sitter-mode vacation's full per-plant detail and schedule, use sitter_briefing.", async () => apiGet("/api/v1/vacation/status"));
124
+ // --- Nudges -------------------------------------------------------------
125
+ server.tool("list_nudges", "Gaps in the collection worth mentioning: stale or missing photos, plants without a type or location, long-unresolved health entries, and app features the caller has never tried. This is the low-pressure 'is there anything I should suggest?' tool — distinct from due/overdue care, which is deadline-driven. Returns the caller's whole ranked queue (not just the top one the app itself would show), each entry as { kind, variant, count, href }: count is an aggregate number for that kind (0 for a feature-discovery entry, which pitches a feature rather than a quantity), and href is the in-app page to send the user to. An empty list means nothing is currently worth nudging about.", async () => apiGet("/api/v1/nudges"));
124
126
  // --- Care logging (writes) --------------------------------------------------
125
127
  /**
126
128
  * Mutating tools. These require an API key with the `write` scope; a read-only
@@ -174,10 +176,11 @@ server.tool("record_hydro_event", "WRITE: log a hydro-culture event for a hydro-
174
176
  recordedAt: optionalPlantDate(""),
175
177
  notes: z.string().max(2000).optional().describe("Free-text notes."),
176
178
  }, async ({ plantId, ...body }) => apiSend("POST", `/api/v1/plants/${plantId}/hydro`, body));
177
- server.tool("snooze_care", "WRITE: postpone a plant's next watering or fertilizing due date by a fixed duration — the same snooze picker the care hub offers. originalDueAt is the due date being pushed out (the nextDue value from list_due_care/get_care_calendar for that plant/careType), not today's date. durationDays must be one of the app's presets: 1, 3, 7, or 14. Requires a write-scoped API key.", {
179
+ server.tool("snooze_care", "WRITE: postpone a plant's next watering or fertilizing due date — the same snooze picker the care hub offers. originalDueAt is the due date being pushed out (the nextDue value from list_due_care/get_care_calendar for that plant/careType), not today's date. Pass exactly one of durationDays (a preset: 1, 3, 7, or 14 days, counted from originalDueAt or from today when that date has already passed) or snoozedUntil (an explicit YYYY-MM-DD day, tomorrow at the earliest and at most a year out). Requires a write-scoped API key.", {
178
180
  plantId: z.number().int().positive().describe("Plant id."),
179
181
  careType: z.enum(["water", "fert"]).describe("Which schedule to postpone."),
180
- durationDays: z.union([z.literal(1), z.literal(3), z.literal(7), z.literal(14)]).describe("Snooze duration in days — one of the app's presets."),
182
+ durationDays: z.union([z.literal(1), z.literal(3), z.literal(7), z.literal(14)]).optional().describe("Snooze duration in days — one of the app's presets, counted from originalDueAt, or from today if that date has passed. Omit when passing snoozedUntil."),
183
+ snoozedUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("The day the task should come back, as YYYY-MM-DD — after today, within the next 365 days. Omit when passing durationDays."),
181
184
  originalDueAt: z.string().describe("The due date being pushed out, e.g. from list_due_care's nextDue for this plant/careType."),
182
185
  note: z.string().max(160).optional().describe("Free-text reason, e.g. 'away for the week'."),
183
186
  }, async (args) => apiSend("POST", "/api/v1/care/snooze", { ...args }));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sebamomann/plants-mcp",
3
- "version": "2.5.1",
4
- "description": "MCP server for the Sprig plant app: 76 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, 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.7.0",
4
+ "description": "MCP server for the Sprig plant app: 77 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.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "sebamomann <github@sebamomann.de>",
@@ -49,12 +49,12 @@
49
49
  "prepublishOnly": "npm run build"
50
50
  },
51
51
  "dependencies": {
52
- "@modelcontextprotocol/sdk": "^1.29.0",
53
- "zod": "^4.4.3"
52
+ "@modelcontextprotocol/sdk": "^1.30.1",
53
+ "zod": "^4.6.5"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/node": "^26.1.1",
56
+ "@types/node": "^26.6.2",
57
57
  "typescript": "^6.0.3",
58
- "vitest": "^4.1.10"
58
+ "vitest": "^5.0.1"
59
59
  }
60
60
  }