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