Verdant Grow Diary logo

    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.

    Not connected
    This explorer needs a browser OAuth session. Connect once from Settings → Agent integrations, then return here. Connect this browser

    list_grows

    List the signed-in Verdant grower's own grows (id, name, stage, grow_type, archived flag, timestamps). Read-only.

    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.

    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.

    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_entries verifies 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 wider diary_entries policy.
    • Sensor truth preserved. source and quality labels are returned verbatim. A reading is only current live telemetry when current_live: true (quality ok + source live + freshness fresh). Manual, csv, demo, sim, stale, and invalid rows keep their labels and are never re-labeled as live.
    • No raw provenance leakage. raw_payload is 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, then id 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 to false.
    • limit — integer 1–100, optional. Defaults to 25.

    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 to 10.

    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.