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, and latest sensor snapshot. No writes. No AI. 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 keeps its `source` and `quality` labels verbatim and adds a response-time `freshness` field (`fresh`, `stale`, or `invalid`) plus `current_live`. `quality` is one of ok/degraded/stale/invalid. Canonical `source` labels are exactly live/manual/csv/demo/stale/invalid, where `live` means fresh validated connected telemetry; legacy rows may carry other ingest labels such as sim or vendor bridge names. 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 sim, stale, invalid, or unknown labels 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.
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 three 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 }.
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.