API reference
Verdant Grow OS MCP
A read-only Model Context Protocol server that lets an OAuth-signed-in grower's assistant read their own grows, diary entries, latest sensor snapshot, and bounded Grow Walk target/context evidence, including existing AI Doctor metadata and Action Queue summaries. No writes. No AI invocation. No device control.
Interactive tool explorer
Call each tool live as your signed-in account. Connect this browser once from Settings → Agent integrations and the explorer will reuse that OAuth session.
Try the tools
Run each MCP tool from this page using the browser OAuth session you minted on Settings → Agent integrations. Calls are made as your signed-in account and are RLS-scoped to your own data.
list_grows
List the signed-in Verdant grower's own grows (id, name, stage, grow_type, archived flag, timestamps). Read-only.
Defaults to false.
Connect this browser to run the tool.
list_recent_diary_entries
List recent diary entries for one of the signed-in grower's own grows. The grow must belong to the caller. Read-only.
Tip: run list_grows above to copy an id from your own grows.
Connect this browser to run the tool.
get_latest_sensor_snapshot
Fetch the most recent sensor reading per metric (temperature_c, humidity_pct, vpd_kpa, co2_ppm, soil_moisture_pct, soil_temp_c, ph, ec, ppfd) for one of the signed-in grower's own tents, ordered by capture time (captured_at, falling back to ingest time). Every reading includes constitution `source` (exactly live/manual/csv/demo/stale/invalid — vendor/transport tokens such as ecowitt, mqtt, or sim, and any unrecognized label, are never returned as source), `quality`, derived `confidence` (0–1), response-time `freshness` (`fresh`, `stale`, or `invalid`), and `current_live`. `quality` is one of ok/degraded/stale/invalid. `live` means fresh validated connected telemetry. Treat a reading as current live telemetry ONLY when `current_live` is true: quality must be `ok`, source must be `live`, and freshness must be `fresh`. Every other source, quality, or freshness state keeps its label and is never live: manual stays manual, csv stays csv, demo stays demo, and stale or invalid are never current or healthy. Read-only.
Only current_live=true readings are current live telemetry — every other label stays as-is.
Connect this browser to run the tool.
list_grow_walk_targets
List the signed-in Verdant grower's own tents and plants within one owned grow, ordered by deterministic physical-inspection priority. Results preserve missing evidence and source limits; archived targets are labeled historical; exact sensor evidence is loaded only by get_grow_walk_context. Priority is scouting guidance, not a diagnosis. Read-only.
Run list_grows first to copy one of your own grow ids.
Defaults to false.
Connect this browser to run the tool.
get_grow_walk_context
Fetch bounded, source-labeled evidence for one tent or plant the signed-in grower owns. Photo rows are metadata only, sensor evidence keeps source/quality/freshness labels, and partial lanes are named explicitly. Archived targets are labeled as historical. The result supports physical inspection and does not diagnose, approve actions, or control equipment. Read-only.
Connect this browser to run the tool.
Endpoint & auth
Streamable HTTP transport (MCP spec 2025-06-18). Callers authenticate with an OAuth 2.1 bearer token issued by this app's authorization server — end users complete a consent screen at /.lovable/oauth/consent. Session JWTs pasted from other flows are rejected.
POST https://knkwiiywfkbqznbxwqfh.supabase.co/functions/v1/mcp
Authorization: Bearer <oauth_access_token>
Content-Type: application/json- Issuer:
https://knkwiiywfkbqznbxwqfh.supabase.co/auth/v1 - Accepted audience:
authenticated - Dynamic client registration is enabled — Claude, ChatGPT, Cursor, and Codex can self-register.
Set up by issuer status
The status page at Settings → Agent integrations shows one of three OAuth issuer states and links here to the matching steps.
Issuer configured
The app advertises an OAuth issuer that matches the expected pattern (this is a check of the app's own configuration, not a live probe of the issuer). Point your assistant at the endpoint above, complete the consent screen, then confirm the five read-only tools appear. In this browser you can also run the built-in probe from the status page.
Issuer not configured
No OAuth issuer is advertised, so no assistant can authorize. The project owner must enable the OAuth 2.1 authorization server on the connected Supabase project and re-publish; until then, connecting cannot complete authorization and there is nothing a client-side setting can fix.
Issuer could not be verified
The advertised issuer did not match the expected pattern, so the status page will not claim it is configured. Compare the issuer shown on the status page with the one in the Endpoint & auth section above; if they differ, treat the connection as untrusted and do not complete consent until the owner reconciles them.
Safety invariants
These properties hold for every tool below and are enforced in the server, not by convention:
- Read-only. Every tool is annotated
readOnlyHint: true,idempotentHint: true,openWorldHint: false. There is no write, no AI call, and no device command surface. - Own data only. All database reads go through the caller's OAuth token, so Supabase RLS runs as that user. The service-role key is never referenced in tool code.
- Ownership guard on nested reads.
list_recent_diary_entriesverifies the grow is visible to the caller before returning entries, so an operator-role account cannot use this server to read another grower's diary through the widerdiary_entriespolicy. - Sensor truth preserved.
sourceandqualitylabels are returned verbatim. A reading is only current live telemetry whencurrent_live: true(qualityok+ sourcelive+ freshnessfresh). Manual, csv, demo, sim, stale, and invalid rows keep their labels and are never re-labeled as live. - No raw provenance leakage.
raw_payloadis selected long enough to exclude diagnostic Windows testbench rows, then stripped before the response is assembled. - Deterministic snapshots. The latest-per-metric selection breaks ties by
ts DESC,created_at DESC, thenid DESC, so identical inputs never flip the snapshot between calls.
list_grows
List the signed-in grower's own grows.
Parameters
includeArchived— boolean, optional. Include archived grows. Defaults tofalse.limit— integer 1–100, optional. Defaults to25.
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_grows",
"arguments": { "limit": 5 }
}
}Response (structuredContent)
{
"grows": [
{
"id": "3f9a…",
"name": "Tent A — Winter Run",
"stage": "flower",
"grow_type": "photoperiod",
"is_archived": false,
"started_at": "2026-05-14T00:00:00Z",
"created_at": "2026-05-14T14:22:11Z",
"updated_at": "2026-07-18T09:03:44Z"
}
]
}list_recent_diary_entries
List recent diary entries for one of the caller's own grows.
Parameters
growId— uuid, required. Must belong to the caller.limit— integer 1–50, optional. Defaults to10.
Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_recent_diary_entries",
"arguments": {
"growId": "3f9a1f7c-…",
"limit": 5
}
}
}Response (structuredContent)
{
"entries": [
{
"id": "…",
"grow_id": "3f9a1f7c-…",
"plant_id": "…",
"tent_id": "…",
"stage": "flower",
"note": "Watered 1.2L, runoff EC 1.8, pH 6.2.",
"entry_at": "2026-07-18T08:44:00Z",
"created_at": "2026-07-18T08:44:12Z"
}
]
}Presenter-safe fields only. Raw payloads, private image URLs, and internal detail JSON are never returned.
get_latest_sensor_snapshot
Latest reading per metric for one of the caller's own tents. Metrics: temperature_c, humidity_pct, vpd_kpa, co2_ppm, soil_moisture_pct, soil_temp_c, ph, ec, ppfd.
Parameters
tentId— uuid, required. Must belong to the caller.
Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_latest_sensor_snapshot",
"arguments": { "tentId": "b7ce…" }
}
}Response (structuredContent)
{
"snapshot": {
"tentId": "b7ce…",
"readings": {
"temperature_c": {
"id": "…",
"tent_id": "b7ce…",
"metric": "temperature_c",
"value": 24.6,
"quality": "ok",
"source": "live",
"ts": "2026-07-19T09:12:00Z",
"captured_at": "2026-07-19T09:11:58Z",
"freshness": "fresh",
"current_live": true
},
"vpd_kpa": {
"metric": "vpd_kpa",
"value": 1.12,
"quality": "ok",
"source": "manual",
"ts": "2026-07-19T08:55:00Z",
"captured_at": null,
"freshness": "fresh",
"current_live": false
}
}
}
}Only readings with current_live: true should be treated as current live telemetry. Every other combination — including source: "manual" at fresh quality — keeps its label and must not be presented as live.
When a tent has no non-diagnostic readings, the response is { "snapshot": null }.
list_grow_walk_targets
List the caller's own tents and plants within one owned grow for an in-person Grow Walk. Priority preserves missing evidence and source limits; the list intentionally omits per-target sensor snapshots, so use get_grow_walk_context for exact source-labeled sensor evidence. It is not a diagnosis or treatment plan.
Parameters
growId— uuid, required. Must belong to the caller.includeInactivePlants— boolean, optional. Defaults tofalse. An archived selection stays explicitly labeled as historical in the Grow Walk context.limit— integer 1–100, optional. Defaults to50.
Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "list_grow_walk_targets",
"arguments": { "growId": "3f9a1f7c-…", "limit": 20 }
}
}Returns presenter-safe target summaries plus an explicit bounded-summary receipt. It never exposes photo URLs, raw payloads, hidden detail JSON, or writes anything.
get_grow_walk_context
Fetch bounded, source-labeled evidence for one tent or plant the caller owns. Results name partial or unavailable evidence lanes rather than inventing context; archived targets stay labeled as historical.
Parameters
targetType—tentorplant, required.targetId— uuid, required. Must belong to the caller.lookbackHours— integer 24–168, optional. Defaults to72.
Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "get_grow_walk_context",
"arguments": { "targetType": "plant", "targetId": "b7ce…", "lookbackHours": 72 }
}
}Photo evidence is metadata only; sensor evidence retains source, quality, and freshness labels. The bounded Action Queue lane reports current nonterminal items even when they predate the selected history window; inspect its truncation receipt before treating its returned count as complete. This tool does not diagnose, invoke AI, approve actions, or control equipment.
Errors
The server distinguishes two error surfaces. Transport-level failures (missing or invalid OAuth token) come back as a JSON-RPC error object with an HTTP 401. Tool-level failures (bad parameters, unknown grow, unknown tent) come back as a normal tools/call result with isError: true and a human-readable text message — the JSON-RPC envelope itself is a success.
401 Unauthorized
The bearer token is missing, expired, revoked, or was not issued by this app's OAuth server (for example, a copied Supabase session JWT). The response includes a WWW-Authenticate header pointing at the OAuth protected-resource metadata.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://knkwiiywfkbqznbxwqfh.supabase.co/functions/v1/mcp/.well-known/oauth-protected-resource"
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "Unauthorized"
}
}How to recover: do not retry with the same token. Run the OAuth 2.1 authorization-code + PKCE flow again against the issuer above, or — in this browser — click Disconnect then Connect this browser from Settings → Agent integrations. Never paste an app session token as a workaround; the server requires an oauth_client claim and will keep rejecting it.
Invalid parameters
The bearer token was accepted, but the tool's Zod input schema rejected the arguments — for example a missing growId, a malformed UUID, or a limit out of range. The JSON-RPC call succeeds; the tool result carries the failure.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "Invalid parameters for tool \"list_recent_diary_entries\": growId: Required; limit: Number must be less than or equal to 50"
}
]
}
}How to recover: read the field list in the message, correct the arguments against the parameter tables above, and retry the same JSON-RPC call. Do not fall back to a wider tool or invent an id — an unknown growId/tentId that parses as a UUID surfaces as the ownership errors below, not this one.
Not found for the signed-in grower
The id parses correctly but is either unknown or belongs to another grower. RLS returns the same "not found" either way so ownership is never leaked through the error.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"isError": true,
"content": [
{ "type": "text", "text": "Grow not found for the signed-in grower." }
]
}
}How to recover: call list_grows (or, for tents, look them up inside a known grow in the app) to re-discover a valid id owned by the current user, then retry. Retrying the same id will keep returning this error.
Connect a client
The signed-in grower can verify tool access from the browser at Settings → Agent integrations. Third-party clients (Claude, ChatGPT, Cursor, Codex) point at the endpoint above and complete the OAuth consent flow — no manual token pasting.