@sebamomann/plants-mcp 1.2.0 → 1.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,49 @@
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
+ ## 1.7.0 — 2026-08-08
7
+
8
+ **New plant status: `EXTERNAL` (reference plant).** A plant somebody else owns, recorded only as
9
+ an anchor for propagation lineage — never cared for, no care settings, not part of the collection.
10
+ `list_plants` accepts it (and `SPLIT`, which the enum had been missing) as a `status` filter, and
11
+ `collection_stats` now excludes `EXTERNAL` plants from every count and total, matching the app's
12
+ `/stats` page. No tool added or removed.
13
+
14
+ ## 1.6.0 — 2026-08-04
15
+
16
+ **Plants can now carry freeform tags.** `update_plant` gained a `tags` argument (replaces the
17
+ full list; `null`/`[]` clears it) and `list_plants` gained a `tag` filter (exact match). Tags are
18
+ a plain string list on the plant, not a separate catalog — no new id-resolution tool needed.
19
+ `get_plant`/`list_plants` responses also now include `tags` (no doc change required for that half,
20
+ per the "added response fields are safe" rule — the argument additions are what make this minor).
21
+
22
+ ## 1.5.0 — 2026-08-04
23
+
24
+ **`list_care_recommendations` gained a new recommendation type: `repotDue`.** Fires when a living,
25
+ non-propagating plant hasn't been repotted in over 2 years — anchored to the latest potting event,
26
+ or to `acquiredAt`/`createdAt` if it's never been repotted at all. Dismissible, same as the other
27
+ info-severity recommendations. No tool added or removed, so this is a minor bump for the new
28
+ response value, not a new tool. The stale-photo recommendation's threshold also dropped from 90 to
29
+ 60 days — same field shape, just a tighter default.
30
+
31
+ ## 1.4.0 — 2026-08-04
32
+
33
+ **New tool: `sitter_briefing`.** Printable care sheet for the caller's current or next
34
+ `sitter`-mode vacation: per-plant sitter instructions, watering notes, cycle and next-due state,
35
+ plus the day-by-day watering schedule for the trip. Optional `locationId` narrows the plant list
36
+ to one location. Returns `{ hasVacation: false }` when there is no upcoming or running sitter
37
+ vacation — read-only, never creates or modifies one. Backed by a new
38
+ `GET /api/v1/vacation/sitter-briefing` endpoint, reusing the same data-loading logic as the public
39
+ `/p/:token` sitter page.
40
+
41
+ ## 1.3.0 — 2026-08-04
42
+
43
+ **New tool: `collection_stats`.** Counts and totals across the whole collection — plants by
44
+ status, by location, and by plant type; total spend (`price`) and total revenue (`soldPrice` on
45
+ sold plants); acquisitions bucketed by month. Backed by a new `GET /api/v1/collection/stats`
46
+ endpoint. Answers questions like "how many plants do I have" or "how much have I spent" without
47
+ listing every plant and counting client-side.
48
+
6
49
  ## 1.2.0 — 2026-08-03
7
50
 
8
51
  **`update_plant_care` gained two new optional arguments: `wateringMode` and `fertilizingMode`.**
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
- **27 tools** over your collection: 17 that read it, 10 that write to it.
63
+ **29 tools** over your collection: 19 that read it, 10 that write to it.
64
64
 
65
65
  Reads cover plants, watering and fertilization history, the full care timeline, photo metadata,
66
66
  health entries, the derived care schedule, collection-wide activity, and the lookup catalogs.
@@ -97,7 +97,7 @@ healthy server, not a hang — press Ctrl-C.
97
97
 
98
98
  The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
99
99
 
100
- - **`read`** — on every key. Gates all 17 read tools.
100
+ - **`read`** — on every key. Gates all 19 read tools.
101
101
  - **`write`** — opt-in when you create the key. Gates the 10 write tools.
102
102
 
103
103
  | Situation | HTTP | What the assistant sees |
@@ -132,11 +132,13 @@ Not oversights — deliberate limits:
132
132
  | Tool | Endpoint | Arguments |
133
133
  |---|---|---|
134
134
  | `whoami` | `GET /api/v1/me` | none — who the key belongs to |
135
- | `list_plants` | `GET /api/v1/plants` | all optional: `status`, `lifecycle`, `locationId`, `plantTypeId`, `soilId`, `fertilizerId`, `search` (type name / notes), `limit` (1–200), `offset`, `sort` |
135
+ | `list_plants` | `GET /api/v1/plants` | all optional: `status`, `lifecycle`, `locationId`, `plantTypeId`, `soilId`, `fertilizerId`, `tag` (exact match), `search` (type name / notes), `limit` (1–200), `offset`, `sort` |
136
136
  | `get_plant` | `GET /api/v1/plants/:id` | `id` **(required)** — full detail: catalogs, care config, lineage, recent events |
137
137
 
138
- `status` is one of `LIVING`, `DEAD`, `GIFTED`, `LOST`, `SOLD`, `TRADED`, `MERGED`; `lifecycle` is
139
- `PROPAGATING` or `ESTABLISHED`.
138
+ `status` is one of `LIVING`, `DEAD`, `GIFTED`, `LOST`, `SOLD`, `TRADED`, `MERGED`, `SPLIT`,
139
+ `EXTERNAL`; `lifecycle` is `PROPAGATING` or `ESTABLISHED`. `EXTERNAL` is a **reference plant** —
140
+ somebody else's plant, recorded only as an anchor for propagation lineage. It is never cared for,
141
+ never appears in due/overdue care, and is excluded from `collection_stats`.
140
142
 
141
143
  ### Per-plant history
142
144
 
@@ -164,7 +166,7 @@ overrides the Apr–Sep default), `locationId`.
164
166
  | `list_due_care` | `GET /api/v1/care/due` | `windowDays` (0–60, default 0 = that day only), `includeOverdue` (default true) | "What should I do today?" |
165
167
  | `list_overdue_care` | `GET /api/v1/care/overdue` | — | "What have I fallen behind on?" — most overdue first, with days late |
166
168
  | `get_care_calendar` | `GET /api/v1/care/calendar` | `days` (1–60, default 14) | Day-by-day projection plus an overdue group |
167
- | `list_care_recommendations` | `GET /api/v1/care/recommendations` | none | Detected problems: cycle mismatch, chronic lateness, missed seasonal fertilizing, recent repotting, stale photos |
169
+ | `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 |
168
170
 
169
171
  Three things to know when reading schedule results:
170
172
 
@@ -185,6 +187,28 @@ Use this instead of looping the per-plant tools when the question is about a **t
185
187
  ("what did I water last week?") rather than one plant. Newest first, each entry tagged with a
186
188
  `kind` and naming its plant. Does **not** include acquisitions, gifts, sales, or trades.
187
189
 
190
+ ### Collection-wide aggregates
191
+
192
+ | Tool | Endpoint | Arguments |
193
+ |---|---|---|
194
+ | `collection_stats` | `GET /api/v1/collection/stats` | none |
195
+
196
+ Counts by status, location, and plant type; total spend (`price`) and total revenue (`soldPrice`
197
+ on sold plants); acquisitions bucketed by month. Use this instead of listing every plant when the
198
+ question is a count or a total, not which specific plants. Every status is included except
199
+ `EXTERNAL` (reference plants aren't yours) — no `MERGED` filter — matching the app's own `/stats`
200
+ page.
201
+
202
+ | Tool | Endpoint | Arguments |
203
+ |---|---|---|
204
+ | `sitter_briefing` | `GET /api/v1/vacation/sitter-briefing` | `locationId` (optional) |
205
+
206
+ Printable care sheet for the caller's current or next `sitter`-mode vacation: per-plant sitter
207
+ instructions, watering notes, cycle and next-due state, and the day-by-day watering schedule for
208
+ the trip. Returns `{ hasVacation: false }` when there's no upcoming or running sitter vacation —
209
+ this tool never creates or modifies one. `locationId` narrows the plant list to one location; the
210
+ schedule always covers the whole vacation.
211
+
188
212
  ### Catalogs
189
213
 
190
214
  For resolving the ids the filters take. No arguments.
@@ -223,7 +247,7 @@ thing per plant.
223
247
  | Tool | Endpoint | Arguments |
224
248
  |---|---|---|
225
249
  | `update_plant_care` | `PATCH /api/v1/plants/:id/care` | `plantId` **(required)**, plus any of `wateringMode` (`scheduled`\|`reservoir`\|`hydro`), `fertilizingMode` (`scheduled`\|`with_watering`), `sunRequirement` (1–4), `waterRequirement` (1–3), `wateringFrequencySummer`/`wateringFrequencyWinter` (labels), `wateringFrequencySummerDays`/`wateringFrequencyWinterDays` (1–365), `wateringNotes`, `fertilizingCycleSummerWeeks`/`fertilizingCycleWinterWeeks` (0–52), `fertilizerPercent` (0–1000), `fertilizingNotes` |
226
- | `update_plant` | `PATCH /api/v1/plants/:id` | `plantId` **(required)**, plus any of `locationId`, `soilId`, `fertilizerId`, `quantity` (0–9999), `notes`, `sitterInstructions` |
250
+ | `update_plant` | `PATCH /api/v1/plants/:id` | `plantId` **(required)**, plus any of `locationId`, `soilId`, `fertilizerId`, `quantity` (0–9999), `notes`, `tags` (replaces the full list), `sitterInstructions` |
227
251
 
228
252
  Both take a `plantId` and edit **only the fields you pass** — an omitted field keeps its current
229
253
  value, `null` clears it (unassigns a catalog id, or blanks a text field).
package/dist/index.js CHANGED
@@ -121,14 +121,15 @@ server.tool("whoami", "Return the authenticated user for the configured API key.
121
121
  // --- Plants -----------------------------------------------------------------
122
122
  server.tool("list_plants", "List the user's plants, with optional filters. Returns plant summaries.", {
123
123
  status: z
124
- .enum(["LIVING", "DEAD", "GIFTED", "LOST", "SOLD", "TRADED", "MERGED"])
124
+ .enum(["LIVING", "DEAD", "GIFTED", "LOST", "SOLD", "TRADED", "MERGED", "SPLIT", "EXTERNAL"])
125
125
  .optional()
126
- .describe("Filter by plant status."),
126
+ .describe("Filter by plant status. EXTERNAL is a reference plant: somebody else's plant, recorded only as an anchor for propagation lineage — never cared for and not part of the collection's totals."),
127
127
  lifecycle: z.enum(["PROPAGATING", "ESTABLISHED"]).optional().describe("Filter by lifecycle stage."),
128
128
  locationId: z.number().int().optional().describe("Filter by location id."),
129
129
  plantTypeId: z.number().int().optional().describe("Filter by plant type (species) id."),
130
130
  soilId: z.number().int().optional().describe("Filter by soil id."),
131
131
  fertilizerId: z.number().int().optional().describe("Filter by fertilizer id."),
132
+ tag: z.string().optional().describe("Filter by exact tag (case-sensitive)."),
132
133
  search: z.string().optional().describe("Free-text search over plant type name / notes."),
133
134
  limit: z.number().int().min(1).max(200).optional().describe("Max results (pagination)."),
134
135
  offset: z.number().int().min(0).optional().describe("Result offset (pagination)."),
@@ -183,7 +184,7 @@ server.tool("get_care_calendar", "Day-by-day care schedule over a date range, pl
183
184
  ...careArgs,
184
185
  days: z.number().int().min(1).max(60).optional().describe("Number of days to project (default 14, max 60)."),
185
186
  }, async (args) => apiGet("/api/v1/care/calendar", args));
186
- server.tool("list_care_recommendations", "Care problems detected across the collection: watering-cycle mismatches, chronic lateness, missed seasonal fertilizing, recent repotting, stale photos. Returns i18n message keys plus their values, not rendered text. Dismissed recommendations are excluded.", async () => apiGet("/api/v1/care/recommendations"));
187
+ server.tool("list_care_recommendations", "Care problems detected across the collection: watering-cycle mismatches, chronic lateness, missed seasonal fertilizing, a fertilizer pause right after repotting, repotting overdue (2+ years, or never), stale photos. Returns i18n message keys plus their values, not rendered text. Dismissed recommendations are excluded.", async () => apiGet("/api/v1/care/recommendations"));
187
188
  // --- Collection-wide activity ----------------------------------------------
188
189
  server.tool("list_recent_activity", "What was actually done across the whole collection, newest first: waterings, fertilizations, refills, hydro events, repottings, health notes, photos. Each entry is tagged with a kind and names its plant. Use this instead of calling the per-plant event tools in a loop when the question is about a time period ('what did I water last week?') rather than one plant. Does not include acquisitions, gifts, sales, or trades.", {
189
190
  since: z.string().optional().describe("Only events at or after this ISO date/timestamp, e.g. '2026-07-01'."),
@@ -195,6 +196,11 @@ server.tool("list_recent_activity", "What was actually done across the whole col
195
196
  limit: z.number().int().min(1).max(500).optional().describe("Max results (pagination)."),
196
197
  offset: z.number().int().min(0).optional().describe("Result offset (pagination)."),
197
198
  }, async ({ kinds, ...rest }) => apiGet("/api/v1/activity", { ...rest, kinds: kinds?.join(",") }));
199
+ // --- Collection-wide aggregates ---------------------------------------------
200
+ server.tool("collection_stats", "Counts and totals across the whole collection: how many plants by status (living, dead, gifted, sold, traded), by location, and by plant type; total spend (sum of price) and total revenue (sum of soldPrice for sold plants); acquisitions bucketed by month. Use this instead of listing every plant when the question is a count or a total, not which specific plants.", async () => apiGet("/api/v1/collection/stats"));
201
+ server.tool("sitter_briefing", "Printable care sheet for the caller's current or next sitter-covered vacation: per-plant sitter instructions, watering notes, watering cycle and next-due state, plus the day-by-day watering schedule for the trip. Returns { hasVacation: false } when there is no upcoming or running vacation in 'sitter' mode. Read-only — does not create or modify a vacation.", {
202
+ locationId: z.number().int().optional().describe("Only include plants in this location (id from list_locations). Omit for every plant on the vacation."),
203
+ }, async (args) => apiGet("/api/v1/vacation/sitter-briefing", args));
198
204
  // --- Care logging (writes) --------------------------------------------------
199
205
  /**
200
206
  * Mutating tools. These require an API key with the `write` scope; a read-only
@@ -239,13 +245,14 @@ server.tool("update_plant_care", "WRITE: edit a plant's care schedule — wateri
239
245
  fertilizerPercent: z.number().int().min(0).max(1000).optional().describe("Strength as a percentage of the base dose."),
240
246
  fertilizingNotes: z.string().max(500).nullable().optional().describe("Free-text fertilizing notes."),
241
247
  }, async ({ plantId, ...body }) => apiSend("PATCH", `/api/v1/plants/${plantId}/care`, body));
242
- server.tool("update_plant", "WRITE: edit a plant's location, soil, fertilizer, quantity, notes, or sitter instructions. Omitted fields keep their current value; null unassigns a catalog entry or clears notes/instructions. Requires a write-scoped API key.", {
248
+ server.tool("update_plant", "WRITE: edit a plant's location, soil, fertilizer, quantity, notes, tags, or sitter instructions. Omitted fields keep their current value; null unassigns a catalog entry, clears notes/instructions, or empties the tag list. Requires a write-scoped API key.", {
243
249
  plantId: z.number().int().positive().describe("Plant id."),
244
250
  locationId: z.number().int().positive().nullable().optional().describe("New location id, from list_locations. null unassigns it."),
245
251
  soilId: z.number().int().positive().nullable().optional().describe("New soil id, from list_soils. null unassigns it."),
246
252
  fertilizerId: z.number().int().positive().nullable().optional().describe("New fertilizer id, from list_fertilizers. null unassigns it."),
247
253
  quantity: z.number().int().min(0).max(9999).nullable().optional().describe("How many plants this row represents."),
248
254
  notes: z.string().max(5000).nullable().optional().describe("Free-text notes."),
255
+ tags: z.array(z.string().max(40)).nullable().optional().describe("Replaces the plant's full tag list (freeform labels, not a catalog). null or [] clears all tags."),
249
256
  sitterInstructions: z.string().max(500).nullable().optional().describe("Care instructions shown on a sitter link."),
250
257
  }, async ({ plantId, ...body }) => apiSend("PATCH", `/api/v1/plants/${plantId}`, body));
251
258
  server.tool("add_health_entry", "WRITE: add a manual health note to one plant — an observation (e.g. 'new leaf unfurling') or an issue (e.g. 'spider mites on undersides'). Requires a write-scoped API key.", {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sebamomann/plants-mcp",
3
- "version": "1.2.0",
4
- "description": "MCP server for the Sprig plant app: 27 tools to read a plant collection, log care, and edit a plant. Read-only by default; writes need a write-scoped API key. Two tools can delete a watering/fertilization event; nothing else is destructive.",
3
+ "version": "1.7.0",
4
+ "description": "MCP server for the Sprig plant app: 29 tools to read a plant collection, log care, and edit a plant. Read-only by default; writes need a write-scoped API key. Two tools can delete a watering/fertilization event; nothing else is destructive.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "sebamomann <github@sebamomann.de>",