@sebamomann/plants-mcp 2.13.0 → 3.1.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 +75 -0
- package/README.md +44 -12
- package/dist/app/_lib/mcp/apiClient.js +103 -0
- package/dist/app/_lib/mcp/tools.js +573 -0
- package/dist/app/_lib/mcp/version.js +7 -0
- package/dist/index.js +1 -594
- package/dist/mcp/package.json +57 -0
- package/dist/mcp/src/index.js +36 -0
- package/package.json +4 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,81 @@
|
|
|
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.1.0 — 2026-10-19
|
|
7
|
+
|
|
8
|
+
**New tool `mark_watering_due`.** Tool count 82 (was 81). A **minor** bump:
|
|
9
|
+
|
|
10
|
+
- `mark_watering_due` (`POST /api/v1/care/due-today`, `plantId`) is the "needs water" answer to an
|
|
11
|
+
autumn "Prüfen" check: it records no watering but marks the plant due today, so it appears in
|
|
12
|
+
`list_due_care` `tasks`. Cleared by the next watering; replaced by `dismiss_transition_check` or a
|
|
13
|
+
snooze; if not acted on the plant reads as overdue from the next day.
|
|
14
|
+
- Additive: care-snooze timeline entries carry `reason` (`dueToday` for these marks, `stillWet` for
|
|
15
|
+
"noch feucht" skips, otherwise `null`).
|
|
16
|
+
|
|
17
|
+
## 3.0.0 — 2026-10-04
|
|
18
|
+
|
|
19
|
+
**`noFertilizerThisSeason` is renamed `fertilizingNotStarted`.** Tool count unchanged (81). A **major**
|
|
20
|
+
bump — an accepted argument value was removed:
|
|
21
|
+
|
|
22
|
+
- `list_care_recommendations` no longer returns `noFertilizerThisSeason` ("no feeding since the
|
|
23
|
+
season started", which fired on day one of every season). The new type `fertilizingNotStarted`
|
|
24
|
+
fires only for a plant with a fertilizer and an active fertilizing cycle that has **never** been
|
|
25
|
+
fertilized, once a full cycle has passed since feeding could have started (acquisition, the end of
|
|
26
|
+
a repotting rest, or the end of a winter pause). Cuttings are excluded.
|
|
27
|
+
- `dismiss_care_recommendation`'s `type` enum: `noFertilizerThisSeason` → `fertilizingNotStarted`.
|
|
28
|
+
Old dismissals of the removed type no longer apply to anything.
|
|
29
|
+
- Additive: `wateringCycleMismatch.values` gains `season` (`"summer"`/`"winter"`), the column the
|
|
30
|
+
suggestion is expressed in. The mismatch is now judged against the cycle in effect when each
|
|
31
|
+
interval started (season, gradual season ramp, controlled climate), so a season change alone no
|
|
32
|
+
longer makes a plant look off-cycle.
|
|
33
|
+
- `list_notifications`: a `CHANGELOG_PUBLISHED` row may now carry `messageKey:
|
|
34
|
+
"changelogPublishedSummary"` with `values: { summary }` (that day's summary) instead of
|
|
35
|
+
`changelogPublished` with `{ count }`.
|
|
36
|
+
|
|
37
|
+
## 2.16.0 — 2026-10-04
|
|
38
|
+
|
|
39
|
+
**Autumn "Prüfen" checks: `list_due_care` gains `checks`, new `dismiss_transition_check`.** Tool count
|
|
40
|
+
81 (was 80). A **minor** bump — one new tool and one additive response field:
|
|
41
|
+
|
|
42
|
+
- `list_due_care` returns an additive `checks` array — `{ plantId, name, lastWateredAt, checkSince,
|
|
43
|
+
regularDueAt }` — for plants whose watering interval grew with the autumn season change while the
|
|
44
|
+
plant may dry out at its summer pace. A question, not a task: not in `tasks`/`counts`. Existing
|
|
45
|
+
fields are unchanged.
|
|
46
|
+
- New write tool `dismiss_transition_check` (`plantId`): answers a check with "noch feucht", hiding
|
|
47
|
+
it until its regular due date without moving that date. The other answer ("dry") is
|
|
48
|
+
`record_watering`.
|
|
49
|
+
|
|
50
|
+
## 2.15.0 — 2026-10-04
|
|
51
|
+
|
|
52
|
+
**`update_plant_care` gains `keepWinterCare`; `get_plant` reports the effective schedule.** Tool count
|
|
53
|
+
unchanged (80). A **minor** bump — one new optional argument and new response fields:
|
|
54
|
+
|
|
55
|
+
- `update_plant_care` accepts `keepWinterCare: boolean`. A plant in a controlled-climate location
|
|
56
|
+
follows its summer watering and fertilizing cycle all year; `keepWinterCare: true` brings the
|
|
57
|
+
winter cycle back for both. The stored winter values are never cleared by it.
|
|
58
|
+
- `get_plant` returns `keepWinterCare`, the location's `controlledClimate` (inside `location`) and a
|
|
59
|
+
new top-level `followsSummerYearRound` boolean.
|
|
60
|
+
- **Behaviour change in `get_plant`:** its six schedule fields now show what scheduling actually uses.
|
|
61
|
+
For a plant that follows its summer cycle all year, the winter fields
|
|
62
|
+
(`wateringFrequencyWinterDays`, `wateringFrequencyWinter`, `fertilizingCycleWinterWeeks`) are the
|
|
63
|
+
*effective* (summer) values, not the stored winter ones. Nothing changes for plants outside a
|
|
64
|
+
controlled-climate location.
|
|
65
|
+
- `update_plant_care`'s result includes `keepWinterCare`.
|
|
66
|
+
|
|
67
|
+
## 2.14.0 — 2026-10-04
|
|
68
|
+
|
|
69
|
+
**New tool: `update_location`; `create_location` gains `controlledClimate`.** Tool count 79 → 80
|
|
70
|
+
(28 read, 34 write, 18 admin). A **minor** bump — one new write tool and one new optional argument:
|
|
71
|
+
|
|
72
|
+
- `PATCH /api/v1/locations/:id` takes any of `name`, `parentId` (`null` = top level) and
|
|
73
|
+
`controlledClimate`; only the fields present change. Shares its core with the location detail
|
|
74
|
+
page's save action, so cycle checks on `parentId` behave identically.
|
|
75
|
+
- `controlledClimate` marks a place with constant light and warmth (e.g. a plant cabinet): plants
|
|
76
|
+
placed directly there follow their summer watering and fertilizing cycle all year, unless the
|
|
77
|
+
plant opts back into winter care. `create_location` accepts it too.
|
|
78
|
+
- `list_locations` now returns `controlledClimate` per location (a new response field — no
|
|
79
|
+
change needed by callers).
|
|
80
|
+
|
|
6
81
|
## 2.13.0 — 2026-09-30
|
|
7
82
|
|
|
8
83
|
**New tool: `merge_soils`.** Tool count 78 → 79 (28 read, 33 write, 18 admin). A **minor** bump —
|
package/README.md
CHANGED
|
@@ -8,7 +8,14 @@ just ask:
|
|
|
8
8
|
> *"Which plants am I behind on?"*
|
|
9
9
|
> *"Log that I watered everything on the windowsill."*
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **Using Claude (claude.ai, the mobile app, Claude Desktop or Claude Code)?** Connect to the
|
|
12
|
+
> remote endpoint instead and sign in, no key to copy: add a custom connector with the URL
|
|
13
|
+
> `https://plants.sebamomann.de/api/mcp`, leave the OAuth fields empty and sign in to Sprig
|
|
14
|
+
> (in Claude Code: `claude mcp add --transport http sprig https://plants.sebamomann.de/api/mcp`,
|
|
15
|
+
> then `/mcp`). This package stays for **local stdio setups** with an API key, and API keys also
|
|
16
|
+
> work as a header against the remote endpoint for scripts.
|
|
17
|
+
|
|
18
|
+
## Quick start (local, with an API key)
|
|
12
19
|
|
|
13
20
|
**1. Get an API key.** In Sprig: **Account → Settings → API keys → Generate**. It is shown once — copy it
|
|
14
21
|
then. Pick read-only unless you actually want the assistant logging care; you can change a key's
|
|
@@ -60,7 +67,7 @@ needs a running Sprig instance and an API key from it. Start at the
|
|
|
60
67
|
|
|
61
68
|
## What an assistant can do
|
|
62
69
|
|
|
63
|
-
**
|
|
70
|
+
**82 tools** over your collection: 28 that read it, 36 that write to it. **18 admin tools** are also
|
|
64
71
|
available — but only to a key created by an admin — for finding and understanding duplicates in the
|
|
65
72
|
shared global plant type catalog, restructuring its genus/species/cultivar tree, reading, directly
|
|
66
73
|
editing, merging, and reviewing proposals and duplicate candidates in that catalog, and (7 read-only,
|
|
@@ -112,7 +119,7 @@ healthy server, not a hang — press Ctrl-C.
|
|
|
112
119
|
The scope lives on the API key, is enforced by the app, and cannot be widened from this side.
|
|
113
120
|
|
|
114
121
|
- **`read`** — on every key. Gates all 28 read tools.
|
|
115
|
-
- **`write`** — opt-in when you create the key. Gates the
|
|
122
|
+
- **`write`** — opt-in when you create the key. Gates the 36 write tools.
|
|
116
123
|
- **`admin`** — only offered when the key's creator is themselves an admin, and only while they
|
|
117
124
|
still are one (the app re-checks this on every admin-tool call, not just at key creation). Gates
|
|
118
125
|
the 18 admin catalog tools — 7 read-only, 11 that write — see **Admin tools** below. `admin` alone
|
|
@@ -174,7 +181,13 @@ facts (light, water, feeding need, toxicity, climate, growth, substrate/propagat
|
|
|
174
181
|
region), keyed by the same field names `get_plant_type` uses — and its six schedule fields
|
|
175
182
|
(`wateringFrequencySummerDays`/`WinterDays`, `wateringFrequencySummer`/`Winter`,
|
|
176
183
|
`fertilizingCycleSummerWeeks`/`WinterWeeks`) as `{ value, source: "plant"|"type"|"none", typeValue }`
|
|
177
|
-
instead of a raw value — see **Breaking changes in 2.0.0** below.
|
|
184
|
+
instead of a raw value — see **Breaking changes in 2.0.0** below. They show what scheduling actually
|
|
185
|
+
uses: for a plant in a controlled-climate location without `keepWinterCare`, the winter fields equal the
|
|
186
|
+
summer values, and `followsSummerYearRound` is `true` (also returned: `keepWinterCare` and
|
|
187
|
+
`location.controlledClimate`). `inWinterRest` is `true` while the plant is in its winter rest (its type's
|
|
188
|
+
`winterRest` fact is set, it is winter — roughly late October to mid-March — and it does not follow its summer
|
|
189
|
+
cycle year-round): such a plant drops leaves and wants almost no water or fertilizer on purpose, so don't treat
|
|
190
|
+
it as declining.
|
|
178
191
|
|
|
179
192
|
### Per-plant history
|
|
180
193
|
|
|
@@ -200,10 +213,10 @@ overrides the Apr–Sep default), `locationId`.
|
|
|
200
213
|
|
|
201
214
|
| Tool | Endpoint | Extra arguments | Answers |
|
|
202
215
|
|---|---|---|---|
|
|
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?" |
|
|
216
|
+
| `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
217
|
| `list_overdue_care` | `GET /api/v1/care/overdue` | — | "What have I fallen behind on?" — most overdue first, with days late |
|
|
205
218
|
| `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,
|
|
219
|
+
| `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
220
|
|
|
208
221
|
Five things to know when reading schedule results:
|
|
209
222
|
|
|
@@ -215,7 +228,8 @@ Five things to know when reading schedule results:
|
|
|
215
228
|
they never appear in `tasks`, `days[]` or `neverLogged` at all. Instead, `list_due_care` and
|
|
216
229
|
`get_care_calendar` each carry a `reservoirChecks` array of `{ plantId, nextCheckAt,
|
|
217
230
|
daysUntilCheck, confidence }` — a *predicted* next check, the median gap between that plant's own
|
|
218
|
-
last few refills (reservoir) or hydro entries,
|
|
231
|
+
last few refills (reservoir) or hydro entries, checked ~15% of it early (at least a day), not a
|
|
232
|
+
schedule the user configured. `nextCheckAt` is
|
|
219
233
|
a `YYYY-MM-DD` day like `nextDue`; `daysUntilCheck` is negative once it has passed; `confidence` is
|
|
220
234
|
`"rough"` below three observed gaps and `"established"` from there on. The array ignores
|
|
221
235
|
`windowDays`/`days` — every plant with a prediction is always in it — and a plant with fewer than
|
|
@@ -362,7 +376,7 @@ tools** → **Catalogs** below) lets a write-scoped key add it.
|
|
|
362
376
|
|
|
363
377
|
## Write tools
|
|
364
378
|
|
|
365
|
-
**All
|
|
379
|
+
**All 32 ordinary write tools require a `write`-scoped key**; a read-only key gets HTTP 403. Every
|
|
366
380
|
description starts with `WRITE:` so a model cannot mistake one for a read.
|
|
367
381
|
|
|
368
382
|
### Care logging
|
|
@@ -419,7 +433,7 @@ it is — that backlog is not the watering's doing.
|
|
|
419
433
|
|
|
420
434
|
| Tool | Endpoint | Arguments |
|
|
421
435
|
|---|---|---|
|
|
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` |
|
|
436
|
+
| `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
437
|
| `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
438
|
|
|
425
439
|
Both take a `plantId` and edit **only the fields you pass** — an omitted field keeps its current
|
|
@@ -529,11 +543,28 @@ The two edit tools take an **`entryId`, not a `plantId`** — get it from `list_
|
|
|
529
543
|
| Tool | Endpoint | Arguments |
|
|
530
544
|
|---|---|---|
|
|
531
545
|
| `dismiss_care_recommendation` | `POST /api/v1/care/recommendations/dismiss` | `plantId`, `type`, `fingerprint` — **all required** |
|
|
546
|
+
| `dismiss_transition_check` | `POST /api/v1/care/transition-checks/dismiss` | `plantId` **(required)** — a plant listed in `checks` of `list_due_care` |
|
|
547
|
+
| `mark_watering_due` | `POST /api/v1/care/due-today` | `plantId` **(required)** — marks the plant's watering due today without recording one |
|
|
548
|
+
|
|
549
|
+
`mark_watering_due` is the "needs water" answer to an autumn "Prüfen" check (and works for any plant
|
|
550
|
+
that looks thirsty): it records **no** watering, it pulls the plant's due date forward to today, so
|
|
551
|
+
the plant shows up in `list_due_care` `tasks` (and in the app's care plan, counts and reminders)
|
|
552
|
+
with the usual answers. The next watering clears the mark; a later `dismiss_transition_check` or
|
|
553
|
+
snooze replaces it; if nobody acts, the plant reads as overdue from the next day. Idempotent while
|
|
554
|
+
the mark stands. Returns `{ plantId, dueOn }`.
|
|
555
|
+
|
|
556
|
+
`dismiss_transition_check` answers an autumn "Prüfen" check with "noch feucht": as the season changes
|
|
557
|
+
to winter the watering interval grows, so a plant that dries out at its summer pace would go
|
|
558
|
+
unreminded for longer. `list_due_care` lists such plants in `checks`
|
|
559
|
+
(`{ plantId, name, lastWateredAt, checkSince, regularDueAt }`) from the day the summer-pace watering
|
|
560
|
+
would have been due until the regular due date. The dismissal hides the check until then and never
|
|
561
|
+
moves the due date; a new watering reopens the question. The other answer, "dry", is a plain
|
|
562
|
+
`record_watering`.
|
|
532
563
|
|
|
533
564
|
Pass all three **exactly as returned by `list_care_recommendations`**. The `fingerprint` identifies
|
|
534
565
|
that specific occurrence, so dismissing one never suppresses a later, different recurrence.
|
|
535
566
|
|
|
536
|
-
Dismissible `type` values: `wateringOftenLate`, `fertilizingOftenLate`, `
|
|
567
|
+
Dismissible `type` values: `wateringOftenLate`, `fertilizingOftenLate`, `fertilizingNotStarted`,
|
|
537
568
|
`recentlyRepottedAvoidFertilizer`, `noRecentPhoto`, `propagationCheckDue`, `possibleDecline`.
|
|
538
569
|
`wateringCycleMismatch` is **not** dismissible — it is a configuration contradiction, fixed by
|
|
539
570
|
editing the plant rather than hidden.
|
|
@@ -593,7 +624,8 @@ mirror (opening the page already marks everything read).
|
|
|
593
624
|
|
|
594
625
|
| Tool | Endpoint | Arguments |
|
|
595
626
|
|---|---|---|
|
|
596
|
-
| `create_location` | `POST /api/v1/locations` | `name` **(required)**, `parentId` (an owned location, to nest under) |
|
|
627
|
+
| `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) |
|
|
628
|
+
| `update_location` | `PATCH /api/v1/locations/:id` | `locationId` **(required)**, `name`, `parentId` (`null` = top level), `controlledClimate` — only the fields passed change |
|
|
597
629
|
| `create_soil` | `POST /api/v1/soils` | `name` **(required)**, `notes`, `items` (`{ componentName, parts }[]`, each component upserted by name) |
|
|
598
630
|
| `merge_soils` | `POST /api/v1/soils/merge` | `sourceId` **(required)**, `targetId` **(required)** |
|
|
599
631
|
| `create_fertilizer` | `POST /api/v1/fertilizers` | `name` **(required)**, `npk`, `baseDosePerLiterMl` (0–100) |
|
|
@@ -846,7 +878,7 @@ npm run mcp:typecheck # also part of the root `npm run check`
|
|
|
846
878
|
To point a client at your checkout, use `"command": "node", "args": ["/…/plants/mcp/dist/index.js"]`
|
|
847
879
|
— or skip the build with `"command": "npx", "args": ["tsx", "/…/plants/mcp/src/index.ts"]`.
|
|
848
880
|
|
|
849
|
-
The
|
|
881
|
+
The tools live in `app/_lib/mcp/tools.ts`; `mcp/src/index.ts` is only the stdio wrapper. `mcp/AGENTS.md` has the architecture rules; the canonical
|
|
850
882
|
spec for the tool surface is `docs/mcp-server.md` at the repo root.
|
|
851
883
|
|
|
852
884
|
## Security
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared HTTP proxy every tool in `tools.ts` is built on: `apiGet` for reads,
|
|
3
|
+
* `apiSend` for writes. Kept in its own module so it can be unit-tested in isolation,
|
|
4
|
+
* away from the `McpServer` and transport wiring.
|
|
5
|
+
*
|
|
6
|
+
* Both functions take the API base URL and key as a config object rather than
|
|
7
|
+
* reading them from `process.env` themselves, so a test can point them at a mocked
|
|
8
|
+
* `fetch` with fixture values, and a transport can build one client per request.
|
|
9
|
+
*
|
|
10
|
+
* No `@/` alias and nothing Next-specific here: the stdio build compiles this file too.
|
|
11
|
+
*/
|
|
12
|
+
export function createApiClient({ apiUrl, apiKey }) {
|
|
13
|
+
async function apiGet(path, query = {}) {
|
|
14
|
+
const url = new URL(`${apiUrl}${path}`);
|
|
15
|
+
for (const [key, value] of Object.entries(query)) {
|
|
16
|
+
if (value !== undefined && value !== "")
|
|
17
|
+
url.searchParams.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
let res;
|
|
20
|
+
try {
|
|
21
|
+
res = await fetch(url, {
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${apiKey}`,
|
|
24
|
+
Accept: "application/json",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: `Network error calling ${url.pathname}: ${String(err)}` }],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const body = await res.text();
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Request to ${url.pathname} failed (HTTP ${res.status}): ${body || res.statusText}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Pretty-print JSON when possible, otherwise return raw text.
|
|
47
|
+
let text = body;
|
|
48
|
+
try {
|
|
49
|
+
text = JSON.stringify(JSON.parse(body), null, 2);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// leave as-is
|
|
53
|
+
}
|
|
54
|
+
return { content: [{ type: "text", text }] };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Sends a JSON body to a mutating endpoint. Requires an API key with the
|
|
58
|
+
* `write` scope — a read-only key gets a 403 surfaced back to the model verbatim
|
|
59
|
+
* so it can tell the user their key can't write. `DELETE` sends no body.
|
|
60
|
+
*/
|
|
61
|
+
async function apiSend(method, path, body) {
|
|
62
|
+
const url = new URL(`${apiUrl}${path}`);
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(url, {
|
|
66
|
+
method,
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${apiKey}`,
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
71
|
+
},
|
|
72
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: `Network error calling ${url.pathname}: ${String(err)}` }],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Request to ${url.pathname} failed (HTTP ${res.status}): ${text || res.statusText}`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let pretty = text;
|
|
94
|
+
try {
|
|
95
|
+
pretty = JSON.stringify(JSON.parse(text), null, 2);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// leave as-is
|
|
99
|
+
}
|
|
100
|
+
return { content: [{ type: "text", text: pretty }] };
|
|
101
|
+
}
|
|
102
|
+
return { apiGet, apiSend };
|
|
103
|
+
}
|