@sebamomann/plants-mcp 0.1.0 → 1.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 +55 -0
- package/README.md +351 -70
- package/dist/index.js +12 -3
- package/package.json +17 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to `@sebamomann/plants-mcp`. Versioning is semver against the **tool surface** —
|
|
4
|
+
see the table in `AGENTS.md` for what counts as major, minor, and patch.
|
|
5
|
+
|
|
6
|
+
## 1.0.0 — 2026-07-26
|
|
7
|
+
|
|
8
|
+
**The tool surface is now stable.** This is the point of the major bump: the 23 tools, their
|
|
9
|
+
arguments, and their descriptions are declared a public contract, and from here a removal or
|
|
10
|
+
rename requires another major. **Nothing broke in this release** — every tool from 0.1.0 works
|
|
11
|
+
identically, and upgrading needs no changes to your client config.
|
|
12
|
+
|
|
13
|
+
- Moved into the app repository as the `mcp` npm workspace, so an `app/api/v1/*` endpoint and the
|
|
14
|
+
tool exposing it now change in one commit. Published package name, `bin`, and tarball contents
|
|
15
|
+
are unchanged; `npx @sebamomann/plants-mcp` is unaffected.
|
|
16
|
+
- Bumped `@modelcontextprotocol/sdk` to ^1.29.0 and `zod` to ^4.4.3. The SDK already resolved zod
|
|
17
|
+
4 internally, and the split with our zod 3 made `tsc` run out of memory; aligning both on zod 4
|
|
18
|
+
fixes it. The generated JSON Schema is unchanged except that integer arguments now also carry an
|
|
19
|
+
explicit safe-integer `maximum`.
|
|
20
|
+
- Toolchain aligned with the app repo: TypeScript 6, `@types/node` 26. `tsx` now comes from the
|
|
21
|
+
workspace root.
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
First published release. 23 tools: 17 read, 6 write.
|
|
26
|
+
|
|
27
|
+
### Reads
|
|
28
|
+
|
|
29
|
+
- **Identity** — `whoami`.
|
|
30
|
+
- **Plants** — `list_plants` (filter by status, lifecycle, location, type, soil, fertilizer, free
|
|
31
|
+
text; sorting and pagination) and `get_plant`.
|
|
32
|
+
- **Per-plant history** — `list_watering_events`, `list_fertilization_events`, `list_care_events`
|
|
33
|
+
(combined timeline, tagged by kind), `list_photos` (metadata only), `list_health_entries`.
|
|
34
|
+
- **Care schedule** — `list_due_care` (overdue plus the given day, optional look-ahead),
|
|
35
|
+
`list_overdue_care` (past-due only, with days late), `get_care_calendar` (day-by-day projection),
|
|
36
|
+
`list_care_recommendations` (detected care problems, each with a dismissal fingerprint). Season,
|
|
37
|
+
care mode, and snoozes are applied server-side; `LIVING` plants only.
|
|
38
|
+
- **Activity** — `list_recent_activity`, collection-wide care activity over a date range, so a
|
|
39
|
+
question about a time period doesn't need a loop over per-plant tools.
|
|
40
|
+
- **Catalogs** — `list_locations`, `list_plant_types`, `list_soils`, `list_fertilizers`.
|
|
41
|
+
|
|
42
|
+
### Writes (require a `write`-scoped API key)
|
|
43
|
+
|
|
44
|
+
- **Care logging** — `record_watering` and `record_fertilization`, both bulk (up to 200 plant ids).
|
|
45
|
+
Reservoir and hydro plants route to a refill/top-up; plants that fertilize with watering also get
|
|
46
|
+
a fertilization logged. Fertilizer and strength default to each plant's own settings.
|
|
47
|
+
- **Health entries** — `add_health_entry`, `update_health_entry`, `resolve_health_entry`
|
|
48
|
+
(reversible in both directions), addressed by entry id.
|
|
49
|
+
- **Recommendations** — `dismiss_care_recommendation`, per occurrence via `fingerprint`.
|
|
50
|
+
|
|
51
|
+
Writes are idempotent per day: a plant already watered or fertilized on the target day is returned
|
|
52
|
+
in a `skipped` list rather than logged twice, so retries never shift the care schedule. Unknown ids
|
|
53
|
+
come back in an `invalid` list instead of failing the whole call.
|
|
54
|
+
|
|
55
|
+
There are no destructive tools. Sales and trades are not exposed, by design.
|
package/README.md
CHANGED
|
@@ -1,36 +1,117 @@
|
|
|
1
|
-
# plants-mcp
|
|
1
|
+
# @sebamomann/plants-mcp
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
fertilizings, and health notes. It talks to the app's `/api/v1/*` REST API, authenticated with a
|
|
7
|
-
**per-user API key**.
|
|
3
|
+
**Let an AI assistant look after your houseplants.** This is an
|
|
4
|
+
[MCP](https://modelcontextprotocol.io) server that connects Claude — or any MCP client — to your
|
|
5
|
+
plant collection in **Sprig**, so you can just ask:
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
> *"What needs watering today?"*
|
|
8
|
+
> *"Which plants am I behind on?"*
|
|
9
|
+
> *"Log that I watered everything on the windowsill."*
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
// Add to your MCP client config, restart it, and you're done.
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"plants": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "@sebamomann/plants-mcp"],
|
|
18
|
+
"env": {
|
|
19
|
+
"PLANT_API_URL": "https://your-sprig-instance",
|
|
20
|
+
"PLANT_API_KEY": "sprig_live_..."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Full walkthrough in [Setup](#setup). The app also has this guide built in, with your key and
|
|
28
|
+
server address already filled in: **Account → API keys → How to connect an assistant**.
|
|
29
|
+
|
|
30
|
+
## What is Sprig?
|
|
31
|
+
|
|
32
|
+
Sprig is a web app for keeping a plant collection alive and on schedule. You add your plants with
|
|
33
|
+
their type, location, soil and pot; it tracks watering and fertilizing cycles and tells you what is
|
|
34
|
+
due today, this week, and what you have fallen behind on. Every plant keeps a full history —
|
|
35
|
+
acquisition, propagation, repotting, photos, health notes — and the app derives care
|
|
36
|
+
recommendations from it (chronic lateness, a missed fertilizing season, a plant repotted too
|
|
37
|
+
recently to feed). It also covers soil mixes, fertilizers and pots as your own libraries, plus
|
|
38
|
+
sharing and trading with other growers.
|
|
39
|
+
|
|
40
|
+
This package exposes the care-relevant part of that to an assistant. It is **not** a standalone
|
|
41
|
+
tool: it needs a running Sprig instance and an API key from it. If you don't have one, start at
|
|
42
|
+
the [app repository](https://github.com/sebamomann/plants).
|
|
43
|
+
|
|
44
|
+
## What this server gives an assistant
|
|
45
|
+
|
|
46
|
+
**23 tools** over your collection: 17 that read it, 6 that log care.
|
|
47
|
+
|
|
48
|
+
Reads cover plants, watering and fertilization history, the full care timeline, photo metadata,
|
|
49
|
+
health entries, the derived care schedule, collection-wide activity, and the lookup catalogs.
|
|
50
|
+
Writes cover recording waterings and fertilizings, health notes, and dismissing care
|
|
51
|
+
recommendations.
|
|
52
|
+
|
|
53
|
+
**Reads need only the `read` scope, which every API key has. Writes need a key created with the
|
|
54
|
+
`write` scope.** There are no destructive tools — nothing here deletes a plant, an event, a photo,
|
|
55
|
+
or a catalog entry. See [Permissions](#permissions--scopes).
|
|
56
|
+
|
|
57
|
+
Every tool is a thin proxy over the app's `/api/v1/*` REST API, authenticated with a per-user API
|
|
58
|
+
key. All results are scoped to that key's owner, and the server holds no logic of its own — which
|
|
59
|
+
is why it stays correct as the app grows.
|
|
60
|
+
|
|
61
|
+
---
|
|
12
62
|
|
|
13
63
|
## Setup
|
|
14
64
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
### 1. Generate an API key
|
|
66
|
+
|
|
67
|
+
In the app: **Account → API keys → Generate**. Choose:
|
|
68
|
+
|
|
69
|
+
| Key type | Scopes | Can do |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| **Read-only** | `read` | Every read tool. Write tools return HTTP 403. |
|
|
72
|
+
| **Read & write** | `read`, `write` | Everything, including the 6 care-logging tools. |
|
|
73
|
+
|
|
74
|
+
The key is shown **once** — copy it then. Format: `sprig_live_…`. Pick read-only unless you
|
|
75
|
+
actually want the assistant logging care; you can change a key's access level later without
|
|
76
|
+
re-issuing it.
|
|
77
|
+
|
|
78
|
+
Right after generating, the app offers **How to connect an assistant** — that guide hands you the
|
|
79
|
+
config block below with your key and server address already filled in.
|
|
80
|
+
|
|
81
|
+
### 2. Install
|
|
82
|
+
|
|
83
|
+
Nothing to install by hand — `npx` fetches the package on demand the first time your client starts
|
|
84
|
+
the server. **Requires Node.js 20 or newer.** To check it runs at all:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
PLANT_API_KEY=sprig_live_... npx @sebamomann/plants-mcp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It should print `[plants-mcp] v… connected.` on stderr and then wait for a client on stdin. That is
|
|
91
|
+
a healthy server, not a hang — press Ctrl-C.
|
|
92
|
+
|
|
93
|
+
To work on it locally instead, clone the app repo — this server lives in it as the `mcp`
|
|
94
|
+
workspace — and build from the repo root:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
git clone https://github.com/sebamomann/plants.git
|
|
98
|
+
cd plants
|
|
99
|
+
npm install
|
|
100
|
+
npm run mcp:build # -> mcp/dist/index.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. Configure
|
|
104
|
+
|
|
105
|
+
Two environment variables (see `.env.example`):
|
|
106
|
+
|
|
107
|
+
| Variable | Required | Default | Description |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `PLANT_API_URL` | no | `http://localhost:3000` | Base URL of the running app. Trailing slashes are stripped. |
|
|
110
|
+
| `PLANT_API_KEY` | **yes** | — | The `sprig_live_…` key from step 1. The server exits 1 at boot if it's missing. |
|
|
30
111
|
|
|
31
112
|
## Register with Claude
|
|
32
113
|
|
|
33
|
-
Add to your
|
|
114
|
+
Add the server to your client's config:
|
|
34
115
|
|
|
35
116
|
```json
|
|
36
117
|
{
|
|
@@ -39,7 +120,7 @@ Add to your MCP client config (e.g. Claude Code / Claude Desktop). Example:
|
|
|
39
120
|
"command": "npx",
|
|
40
121
|
"args": ["-y", "@sebamomann/plants-mcp"],
|
|
41
122
|
"env": {
|
|
42
|
-
"PLANT_API_URL": "
|
|
123
|
+
"PLANT_API_URL": "https://your-sprig-instance",
|
|
43
124
|
"PLANT_API_KEY": "sprig_live_..."
|
|
44
125
|
}
|
|
45
126
|
}
|
|
@@ -47,62 +128,262 @@ Add to your MCP client config (e.g. Claude Code / Claude Desktop). Example:
|
|
|
47
128
|
}
|
|
48
129
|
```
|
|
49
130
|
|
|
131
|
+
Where that config lives:
|
|
132
|
+
|
|
133
|
+
| Client | Location |
|
|
134
|
+
|---|---|
|
|
135
|
+
| **Claude Desktop** | Settings → Developer → Edit Config (`claude_desktop_config.json`) |
|
|
136
|
+
| **Claude Code** | `.mcp.json` in your project folder, or run `claude mcp add` |
|
|
137
|
+
|
|
138
|
+
Then **restart the client**. The tool list is exchanged once at handshake, so a running client will
|
|
139
|
+
not see the server — or newly added tools after an upgrade — until it reconnects.
|
|
140
|
+
|
|
141
|
+
To verify, ask your assistant *"what needs watering today?"*. If the key is wrong you get a 401
|
|
142
|
+
back as readable text rather than a silent failure.
|
|
143
|
+
|
|
50
144
|
To run a local checkout instead, point at the build output —
|
|
51
|
-
`"command": "node", "args": ["/…/plants
|
|
52
|
-
`"command": "npx", "args": ["tsx", "/…/plants
|
|
145
|
+
`"command": "node", "args": ["/…/plants/mcp/dist/index.js"]` — or skip the build and use `tsx`:
|
|
146
|
+
`"command": "npx", "args": ["tsx", "/…/plants/mcp/src/index.ts"]`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Permissions & scopes
|
|
151
|
+
|
|
152
|
+
The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
|
|
153
|
+
|
|
154
|
+
- **`read`** — on every key. Gates all GET endpoints, i.e. all 17 read tools.
|
|
155
|
+
- **`write`** — opt-in when you create the key. Gates every mutating endpoint.
|
|
156
|
+
|
|
157
|
+
What the failure modes look like:
|
|
158
|
+
|
|
159
|
+
| Situation | HTTP | What the assistant sees |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| Missing, malformed, or revoked key | 401 | The error text, verbatim. Revoking takes effect immediately. |
|
|
162
|
+
| Valid read-only key calling a write tool | 403 | The error text, so it can tell you the key can't write. |
|
|
163
|
+
| Another user's plant id | 404 | Indistinguishable from a nonexistent id — by design. |
|
|
164
|
+
|
|
165
|
+
**Adding a write tool never retroactively widens an existing read-only key.** That is the whole
|
|
166
|
+
point of the scope split: keys created before a write tool existed cannot call it.
|
|
167
|
+
|
|
168
|
+
### What this server deliberately cannot do
|
|
169
|
+
|
|
170
|
+
Not oversights — deliberate limits:
|
|
171
|
+
|
|
172
|
+
- **No deletes.** No tool removes a plant, event, photo, or catalog entry. The blast radius of a
|
|
173
|
+
misunderstood delete is a multi-year history. Retiring a plant is a *status* change, and both
|
|
174
|
+
resolving a health entry and dismissing a recommendation are reversible.
|
|
175
|
+
- **No sales or trades.** Not readable, not writable, not planned. Those records name a second
|
|
176
|
+
person who never consented to your API key, and the actions are socially irreversible.
|
|
177
|
+
- **No share-link creation.** Minting a public URL for collection data is a decision for the UI.
|
|
178
|
+
- **No cross-user access.** Ownership is enforced per-request from the key; a tool has no way to
|
|
179
|
+
even express "someone else's plant".
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Read tools
|
|
53
184
|
|
|
54
|
-
|
|
185
|
+
### Identity
|
|
55
186
|
|
|
56
|
-
| Tool | Endpoint |
|
|
187
|
+
| Tool | Endpoint | Arguments |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `whoami` | `GET /api/v1/me` | none |
|
|
190
|
+
|
|
191
|
+
### Plants
|
|
192
|
+
|
|
193
|
+
| Tool | Endpoint | Arguments |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `list_plants` | `GET /api/v1/plants` | `status` (`LIVING` \| `DEAD` \| `GIFTED` \| `LOST` \| `SOLD` \| `TRADED` \| `MERGED`), `lifecycle` (`PROPAGATING` \| `ESTABLISHED`), `locationId`, `plantTypeId`, `soilId`, `fertilizerId`, `search` (free text over type name / notes), `limit` (1–200), `offset`, `sort` (e.g. `name`, `createdAt`, `updatedAt`) — all optional |
|
|
196
|
+
| `get_plant` | `GET /api/v1/plants/:id` | `id` **(required)** — full detail: catalogs, care config, lineage, recent event summaries |
|
|
197
|
+
|
|
198
|
+
### Per-plant history
|
|
199
|
+
|
|
200
|
+
All five take the same arguments: `id` **(required)**, `limit` (1–500), `offset`. Newest first.
|
|
201
|
+
|
|
202
|
+
| Tool | Endpoint | Returns |
|
|
57
203
|
|---|---|---|
|
|
58
|
-
| `whoami` | `GET /api/v1/me` | Identify the authenticated user |
|
|
59
|
-
| `list_plants` | `GET /api/v1/plants` | List plants with filters (status, lifecycle, location, type, soil, fertilizer, search, pagination, sort) |
|
|
60
|
-
| `get_plant` | `GET /api/v1/plants/:id` | Full detail for one plant |
|
|
61
204
|
| `list_watering_events` | `GET /api/v1/plants/:id/watering-events` | Watering history |
|
|
62
205
|
| `list_fertilization_events` | `GET /api/v1/plants/:id/fertilization-events` | Fertilization history |
|
|
63
|
-
| `list_care_events` | `GET /api/v1/plants/:id/events` | Combined
|
|
64
|
-
| `list_photos` | `GET /api/v1/plants/:id/photos` | Photo metadata |
|
|
65
|
-
| `list_health_entries` | `GET /api/v1/plants/:id/health` | Health
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
|
206
|
+
| `list_care_events` | `GET /api/v1/plants/:id/events` | Combined timeline — watering, fertilization, refill, hydro, potting, snoozes — each tagged with a `kind` |
|
|
207
|
+
| `list_photos` | `GET /api/v1/plants/:id/photos` | Photo **metadata** (urls, `takenAt`). Not the images themselves. |
|
|
208
|
+
| `list_health_entries` | `GET /api/v1/plants/:id/health` | Health and AI-analysis entries. Source of the `entryId` the health write tools take. |
|
|
209
|
+
|
|
210
|
+
### Care schedule
|
|
211
|
+
|
|
212
|
+
These return schedule state the **server** derives, using the same helpers the app's own calendar
|
|
213
|
+
uses — season, care mode, and snoozes are already applied. Prefer them over recomputing due dates
|
|
214
|
+
from raw event lists. They cover `LIVING` plants only.
|
|
215
|
+
|
|
216
|
+
Shared optional arguments: `date` (`YYYY-MM-DD`, defaults to today), `season` (`summer` \| `winter`,
|
|
217
|
+
overrides the Apr–Sep default), `locationId`.
|
|
218
|
+
|
|
219
|
+
| Tool | Endpoint | Extra arguments | Answers |
|
|
220
|
+
|---|---|---|---|
|
|
221
|
+
| `list_due_care` | `GET /api/v1/care/due` | `windowDays` (0–60, default 0 = that day only), `includeOverdue` (default true) | "What should I do today?" — overdue plus anything due on `date` |
|
|
222
|
+
| `list_overdue_care` | `GET /api/v1/care/overdue` | — | "What have I fallen behind on?" — past-due only, most overdue first, with days late |
|
|
223
|
+
| `get_care_calendar` | `GET /api/v1/care/calendar` | `days` (1–60, default 14) | Day-by-day projection plus an overdue group |
|
|
224
|
+
| `list_care_recommendations` | `GET /api/v1/care/recommendations` | none | Detected problems: watering-cycle mismatch, chronic lateness, missed seasonal fertilizing, recent repotting, stale photos |
|
|
225
|
+
|
|
226
|
+
Three things to know when reading schedule results:
|
|
227
|
+
|
|
228
|
+
- **Days, not timestamps.** `nextDue` and `date` are local `YYYY-MM-DD` strings. Care is
|
|
229
|
+
day-granular, and a UTC timestamp renders as the wrong day east of Greenwich.
|
|
230
|
+
- **`neverLogged` is its own bucket.** A plant with a cycle but no event yet has no anchor, so no
|
|
231
|
+
due date can be computed. Those are returned separately rather than reported as overdue.
|
|
232
|
+
- **Recommendations return i18n message keys plus values**, not rendered prose. Dismissed ones are
|
|
233
|
+
excluded.
|
|
234
|
+
|
|
235
|
+
### Collection-wide activity
|
|
236
|
+
|
|
237
|
+
| Tool | Endpoint | Arguments |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `list_recent_activity` | `GET /api/v1/activity` | `since`, `until` (ISO date or timestamp), `kinds` (any of `watering`, `fertilization`, `refill`, `hydro`, `potting`, `health`, `photo`; defaults to all), `limit` (1–500), `offset` |
|
|
240
|
+
|
|
241
|
+
Use this instead of looping the per-plant tools when the question is about a **time period**
|
|
242
|
+
("what did I water last week?") rather than one plant. Newest first, each entry tagged with a
|
|
243
|
+
`kind` and naming its plant. Does **not** include acquisitions, gifts, sales, or trades.
|
|
244
|
+
|
|
245
|
+
### Catalogs
|
|
246
|
+
|
|
247
|
+
For resolving the ids the filters take. No arguments.
|
|
248
|
+
|
|
249
|
+
| Tool | Endpoint |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `list_locations` | `GET /api/v1/locations` |
|
|
252
|
+
| `list_plant_types` | `GET /api/v1/plant-types` |
|
|
253
|
+
| `list_soils` | `GET /api/v1/soils` |
|
|
254
|
+
| `list_fertilizers` | `GET /api/v1/fertilizers` |
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Write tools
|
|
259
|
+
|
|
260
|
+
**All six require a `write`-scoped key**; a read-only key gets HTTP 403. Every description starts
|
|
261
|
+
with `WRITE:` so a model cannot mistake one for a read.
|
|
262
|
+
|
|
263
|
+
### Care logging
|
|
264
|
+
|
|
265
|
+
| Tool | Endpoint | Arguments |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `record_watering` | `POST /api/v1/care/watering` | `plantIds` **(required,** 1–200**)**, `wateredAt` (`YYYY-MM-DD`, defaults to now, cannot be in the future) |
|
|
268
|
+
| `record_fertilization` | `POST /api/v1/care/fertilization` | `plantIds` **(required,** 1–200**)**, `fertilizerId`, `fertilizerPercent` (0–1000), `fertilizedAt` |
|
|
269
|
+
|
|
270
|
+
`record_watering` mirrors what the app does on a one-click watering:
|
|
271
|
+
|
|
272
|
+
- **Reservoir and hydro plants** are recorded as a refill / top-up automatically, not as a
|
|
273
|
+
conventional watering.
|
|
274
|
+
- **Plants configured to fertilize with watering** also get a fertilization logged.
|
|
275
|
+
|
|
276
|
+
`record_fertilization` defaults `fertilizerId` and `fertilizerPercent` to **each plant's own
|
|
277
|
+
settings** when omitted, so a bulk call across differently-configured plants still does the right
|
|
278
|
+
thing per plant.
|
|
279
|
+
|
|
280
|
+
### Health entries
|
|
281
|
+
|
|
282
|
+
| Tool | Endpoint | Arguments |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| `add_health_entry` | `POST /api/v1/plants/:id/health` | `plantId` **(required)**, `kind` **(required,** `observation` \| `issue`**)**, `text` **(required,** 1–1000 chars**)**, `category` (≤80 chars, e.g. `pests`), `severity` (`low` \| `medium` \| `high`) |
|
|
285
|
+
| `update_health_entry` | `PATCH /api/v1/health-entries/:id` | `entryId` **(required)**, plus any of `kind`, `text`, `category`, `severity` — omitted fields keep their current value |
|
|
286
|
+
| `resolve_health_entry` | `PATCH /api/v1/health-entries/:id` | `entryId` **(required)**, `resolved` (default `true`; pass `false` to reopen) |
|
|
287
|
+
|
|
288
|
+
`kind` is the distinction that matters: `observation` for a neutral note ("new leaf unfurling"),
|
|
289
|
+
`issue` for a problem ("spider mites on undersides").
|
|
290
|
+
|
|
291
|
+
The two edit tools take an **`entryId`, not a `plantId`** — get it from `list_health_entries`. The
|
|
292
|
+
entry id is already unique and ownership-checked, so nesting it under a plant would only add a way
|
|
293
|
+
to get it wrong. Resolving is reversible in both directions.
|
|
294
|
+
|
|
295
|
+
### Recommendations
|
|
296
|
+
|
|
297
|
+
| Tool | Endpoint | Arguments |
|
|
79
298
|
|---|---|---|
|
|
80
|
-
| `
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
299
|
+
| `dismiss_care_recommendation` | `POST /api/v1/care/recommendations/dismiss` | `plantId` **(required)**, `type` **(required)**, `fingerprint` **(required)** |
|
|
300
|
+
|
|
301
|
+
Pass all three **exactly as returned by `list_care_recommendations`**. The `fingerprint` identifies
|
|
302
|
+
that specific occurrence, so dismissing one never suppresses a later, different recurrence of the
|
|
303
|
+
same problem.
|
|
304
|
+
|
|
305
|
+
Dismissible `type` values: `wateringOftenLate`, `fertilizingOftenLate`, `noFertilizerThisSeason`,
|
|
306
|
+
`recentlyRepottedAvoidFertilizer`, `noRecentPhoto`.
|
|
307
|
+
|
|
308
|
+
`wateringCycleMismatch` is **not** dismissible — it is a configuration contradiction, fixed by
|
|
309
|
+
editing the plant rather than hidden.
|
|
310
|
+
|
|
311
|
+
### Write semantics
|
|
312
|
+
|
|
313
|
+
Two guarantees that make retries and bulk calls safe:
|
|
314
|
+
|
|
315
|
+
**Idempotent per day.** A plant already watered or fertilized on the target day is **skipped** and
|
|
316
|
+
returned in a `skipped` list rather than logged twice. This matters more than it sounds: care
|
|
317
|
+
schedules are computed from the newest event, so a duplicate would silently shift every future due
|
|
318
|
+
date. A retried call after a timeout is safe.
|
|
86
319
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
320
|
+
**Bulk over loops.** `record_watering` and `record_fertilization` take up to 200 `plantIds` in one
|
|
321
|
+
call — prefer that over a loop. Unknown ids come back in an `invalid` list instead of failing the
|
|
322
|
+
whole call, so one bad id doesn't lose the other 199.
|
|
90
323
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
the UI on purpose.
|
|
324
|
+
So a successful bulk write is not all-or-nothing: read the `skipped` and `invalid` lists in the
|
|
325
|
+
response, not just the HTTP status.
|
|
94
326
|
|
|
95
|
-
|
|
96
|
-
involve a second user and are out of scope for an assistant acting on the owner's key.
|
|
327
|
+
---
|
|
97
328
|
|
|
98
|
-
|
|
99
|
-
|
|
329
|
+
## Responses and errors
|
|
330
|
+
|
|
331
|
+
Tools return the API's JSON **verbatim**, pretty-printed with 2-space indentation. Nothing is
|
|
332
|
+
reshaped, renamed, or summarised — which is why an added field on an API response needs no new
|
|
333
|
+
version of this package.
|
|
334
|
+
|
|
335
|
+
Failures come back as MCP errors (`isError: true`) with the detail in the text, so the assistant
|
|
336
|
+
can act on them or explain them to you:
|
|
337
|
+
|
|
338
|
+
| Failure | Text |
|
|
339
|
+
|---|---|
|
|
340
|
+
| Network / DNS / connection refused | `Network error calling <path>: <error>` |
|
|
341
|
+
| Non-2xx response | `Request to <path> failed (HTTP <status>): <body>` |
|
|
342
|
+
|
|
343
|
+
A non-JSON body is passed through as raw text rather than being swallowed. If `PLANT_API_KEY` is
|
|
344
|
+
unset the process exits 1 at boot with a message on stderr — the client will report the server as
|
|
345
|
+
failed to start.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Versioning
|
|
350
|
+
|
|
351
|
+
Semver against the **tool surface**, which is this package's public API:
|
|
352
|
+
|
|
353
|
+
| Change | Bump |
|
|
354
|
+
|---|---|
|
|
355
|
+
| New tool, new optional argument | **minor** |
|
|
356
|
+
| Bug fix, description wording, dependency bump | **patch** |
|
|
357
|
+
| Tool removed or renamed, argument removed or made required, env var renamed | **major** |
|
|
358
|
+
|
|
359
|
+
The version reported in the MCP handshake is read from `package.json` at boot, so it always matches
|
|
360
|
+
the installed package. See `CHANGELOG.md` for what changed, and `AGENTS.md` for the release process.
|
|
361
|
+
|
|
362
|
+
One caveat worth stating plainly: because tools pass the API's JSON straight through, a **response
|
|
363
|
+
shape change in the app can affect you without this package changing version**. Added fields are
|
|
364
|
+
safe; renames and removals are not.
|
|
365
|
+
|
|
366
|
+
## Development
|
|
367
|
+
|
|
368
|
+
From the repo root:
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
npm run mcp:dev # run from source via tsx, no build step
|
|
372
|
+
npm run mcp:build # tsc -> mcp/dist/
|
|
373
|
+
npm run mcp:typecheck # tsc --noEmit (also part of the root `npm run check`)
|
|
374
|
+
```
|
|
100
375
|
|
|
101
|
-
The
|
|
102
|
-
|
|
103
|
-
|
|
376
|
+
The whole server is `mcp/src/index.ts`. It needs the app running (default
|
|
377
|
+
`http://localhost:3000`) and a real API key to do anything useful. `mcp/AGENTS.md` documents the
|
|
378
|
+
architecture rules; the canonical spec for the tool surface is `docs/mcp-server.md` at the repo
|
|
379
|
+
root.
|
|
104
380
|
|
|
105
381
|
## Security
|
|
106
382
|
|
|
107
|
-
- The key is sent only to `PLANT_API_URL
|
|
108
|
-
|
|
383
|
+
- The key is sent only to `PLANT_API_URL`, as a bearer token. Keep it secret; treat it like a
|
|
384
|
+
password. It is passed via environment variable, so it never lands in the tool arguments a model
|
|
385
|
+
can see or echo.
|
|
386
|
+
- Prefer a **read-only key** unless you specifically want an assistant logging care.
|
|
387
|
+
- Revoke a key any time from **Account → API Keys**; requests with it start returning 401
|
|
388
|
+
immediately.
|
|
389
|
+
- All data is scoped to the key's owner. Another user's data returns 404.
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* MCP server for the Sprig plant app: reads the collection, and — with a
|
|
8
|
+
* write-scoped key — logs care. There are no destructive tools.
|
|
7
9
|
*
|
|
8
10
|
* Every tool is a thin proxy over the app's `/api/v1/*` REST endpoints,
|
|
9
11
|
* authenticated with a per-user API key sent as `Authorization: Bearer <key>`.
|
|
10
12
|
* Tools forward their arguments as query params and return the API's JSON
|
|
11
13
|
* verbatim, so they stay correct even if the API response shape evolves.
|
|
12
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Single source of truth for the advertised version: package.json, so a release
|
|
17
|
+
* bump cannot leave the MCP handshake reporting a stale number. `../package.json`
|
|
18
|
+
* resolves to the package root both from `dist/index.js` (published) and from
|
|
19
|
+
* `src/index.ts` (tsx dev).
|
|
20
|
+
*/
|
|
21
|
+
const { version: VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
22
|
const API_URL = (process.env.PLANT_API_URL ?? "http://localhost:3000").replace(/\/+$/, "");
|
|
14
23
|
const API_KEY = process.env.PLANT_API_KEY;
|
|
15
24
|
if (!API_KEY) {
|
|
@@ -105,7 +114,7 @@ async function apiSend(method, path, body) {
|
|
|
105
114
|
}
|
|
106
115
|
return { content: [{ type: "text", text: pretty }] };
|
|
107
116
|
}
|
|
108
|
-
const server = new McpServer({ name: "plants-mcp", version:
|
|
117
|
+
const server = new McpServer({ name: "plants-mcp", version: VERSION });
|
|
109
118
|
// --- Identity ---------------------------------------------------------------
|
|
110
119
|
server.tool("whoami", "Return the authenticated user for the configured API key.", async () => apiGet("/api/v1/me"));
|
|
111
120
|
// --- Plants -----------------------------------------------------------------
|
|
@@ -253,7 +262,7 @@ server.tool("list_fertilizers", "List the user's fertilizers.", async () => apiG
|
|
|
253
262
|
async function main() {
|
|
254
263
|
const transport = new StdioServerTransport();
|
|
255
264
|
await server.connect(transport);
|
|
256
|
-
console.error(`[plants-mcp] connected. API base: ${API_URL}`);
|
|
265
|
+
console.error(`[plants-mcp] v${VERSION} connected. API base: ${API_URL}`);
|
|
257
266
|
}
|
|
258
267
|
main().catch((err) => {
|
|
259
268
|
console.error("[plants-mcp] fatal:", err);
|
package/package.json
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebamomann/plants-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Sprig plant app: 23 tools to read a plant collection and log care. Read-only by default; waterings, fertilizings, and health notes need a write-scoped API key. No destructive tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "sebamomann <github@sebamomann.de>",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/sebamomann/plants
|
|
10
|
+
"url": "git+https://github.com/sebamomann/plants.git",
|
|
11
|
+
"directory": "mcp"
|
|
11
12
|
},
|
|
12
13
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/sebamomann/plants
|
|
14
|
+
"url": "https://github.com/sebamomann/plants/issues"
|
|
14
15
|
},
|
|
15
|
-
"homepage": "https://github.com/sebamomann/plants
|
|
16
|
+
"homepage": "https://github.com/sebamomann/plants/tree/main/mcp#readme",
|
|
16
17
|
"keywords": [
|
|
17
18
|
"mcp",
|
|
19
|
+
"mcp-server",
|
|
18
20
|
"model-context-protocol",
|
|
21
|
+
"claude",
|
|
19
22
|
"plants",
|
|
23
|
+
"houseplants",
|
|
24
|
+
"gardening",
|
|
25
|
+
"plant-care",
|
|
20
26
|
"sprig"
|
|
21
27
|
],
|
|
22
28
|
"engines": {
|
|
@@ -26,7 +32,8 @@
|
|
|
26
32
|
"plants-mcp": "dist/index.js"
|
|
27
33
|
},
|
|
28
34
|
"files": [
|
|
29
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"CHANGELOG.md"
|
|
30
37
|
],
|
|
31
38
|
"publishConfig": {
|
|
32
39
|
"access": "public"
|
|
@@ -39,12 +46,11 @@
|
|
|
39
46
|
"prepublishOnly": "npm run build"
|
|
40
47
|
},
|
|
41
48
|
"dependencies": {
|
|
42
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
43
|
-
"zod": "^
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
+
"zod": "^4.4.3"
|
|
44
51
|
},
|
|
45
52
|
"devDependencies": {
|
|
46
|
-
"@types/node": "^
|
|
47
|
-
"
|
|
48
|
-
"typescript": "^5.7.2"
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
49
55
|
}
|
|
50
56
|
}
|