跳到正文
GitHub·· 7 天前AI 评分28

rootwise:基于 plant-intel-mcp 的 AI 园艺顾问,数据缺失时明确拒答

wyattlindsey/rootwise

AI 导读

rootwise 是一款基于 MCP 服务器 plant-intel-mcp 的 AI 园艺顾问,用真实园艺数据回答自由提问,并在数据不存在时明确说明而非编造。

正文

An AI gardening advisor that answers free-form questions using real horticultural data — and tells you when the data doesn't exist.

Live demo →

rootwise answering a companion-planting question, showing its tool calls, sources, and caveats

Why this exists

Ask any chatbot when to plant tomatoes and you'll get a confident answer. Some of it is true. None of it is traceable, and the parts it invents look exactly like the parts it knows.

rootwise is built on plant-intel-mcp, an MCP server designed so that gaps in the data stay visible. This app's job is to stop the model papering over them, and to show the reader it didn't:

  • Every tool call is on screen — which tool ran, what it was asked, and the raw JSON it returned.
  • Every source is credited, with its licence.
  • It refuses to invent. No source behind these tools publishes plant spacing, days-to-maturity, or frost-hardiness class. Asked for one, it says so and points you at a seed packet.
  • Caveats survive. When the frost model returns "these are 9 km gridded reanalysis values, not station data", that reaches the answer instead of being smoothed away.

In the screenshot above, verdict: "bad" and mechanism: "shared-family" are real output from the MCP server. So is the four-item caveat list. Only the prose is the model.

How it works

Browser ──SSE──> /api/chat ──> tool loop ──> Claude (Opus 5)
                                   │
                                   └──> MCP client ──in-memory──> plant-intel-mcp server
                                                                        │
                                                        Perenual · Open-Meteo · Permapeople

The app hosts the MCP server in its own process and talks to it as a real MCP client over an in-memory transport — full handshake, tools/list, tools/call. It's a genuine protocol client that simply skips the pipe, which means no subprocess, no open port, and no credential leaving the process.

MCP advertises tools as JSON Schema and the Messages API accepts JSON Schema, so the bridge needs no translation layer.

A few decisions worth explaining:

A hand-written tool loop, not the SDK's tool runner. Three reasons the runner doesn't cover: tool calls and results must be emitted in strict order relative to streamed text because the UI renders that order; the loop needs a seam between iterations to stop on an abort or a spent budget; and stacking the beta runner on beta fallbacks is one beta dependency more than this needs.

The model client is injected. Exactly as the MCP server injects fetch and its cache. That's what makes the entire UI and integration suite deterministic, instant, and free — a scripted fake replaces Claude while the real MCP server still runs underneath, so the tool output under test is genuine.

Refusals stream as SSE, not JSON. The status code still says what happened, but the client keeps one way to read a response, so a budget refusal renders through the same path as any other error.

Running it

npm install
npm run dev:demo     # scripted model + fixture data: no keys, no network

The MCP server it hosts is a published package — plant-intel-mcp — so it installs like any other dependency, and can be used on its own with any MCP client.

For live answers you need two keys:

ANTHROPIC_API_KEY=…      # https://console.anthropic.com
PERENUAL_API_KEY=…       # https://perenual.com/docs/api (free: 100 requests/day)
npm run dev
Variable Required Purpose
ANTHROPIC_API_KEY live mode The model.
PERENUAL_API_KEY live mode Species data. Free tier is 100 requests/day, species 1–3000, non-commercial.
PERMAPEOPLE_KEY_ID / _SECRET no Adds documented companion listings (CC BY-SA 4.0).
UPSTASH_REDIS_REST_URL / _TOKEN no Shared spend cap across instances. Without it the cap only holds per-instance.
PLANT_INTEL_CACHE_DIR no Set to /tmp/plant-intel on serverless, where the filesystem is otherwise read-only.
ROOTWISE_FAKE_MODEL no 1 for demo mode. Self-contained — needs no other variable.

Guarding a public AI endpoint

A demo anyone can hit spends real money and is trivially abusable, so the budget layer is load-bearing rather than decorative:

  • A per-IP hourly limit, so no one visitor monopolises the shared key.
  • A daily token ceiling across all visitors that holds however traffic arrives.
  • On exhaustion the UI offers to use the visitor's own key, so the demo degrades instead of going dark. That key is used for one request and is never logged, stored, or counted against the shared budget.

Counter keys carry their own window stamp, so a new hour is simply a new key. That removes the classic hazard: an expiry that silently fails leaves a counter with no TTL, and a counter that never resets locks a visitor out permanently.

The ceiling is denominated in money, not tokens. A turn that calls two tools sends their JSON results back as input on the next request, so input is usually the larger share of the bill — roughly 20 cents of input against 2 of output on a heavy turn. A cap that metered output alone would have left most of the spend unmetered. ROOTWISE_DAILY_BUDGET_CENTS sets it; the default of 100 allows on the order of fifteen questions a day.

The stable prefix — tool schemas plus the system prompt, about 3.5k tokens — is marked for prompt caching, since it is otherwise resent on every iteration of the tool loop and again on every follow-up question.

Testing

npm test          # 115 unit, integration, and component tests
npm run e2e       # 6 Playwright browser tests
npm run typecheck
npm run eval      # opt-in; spends real API credit
Layer Covers
Unit Tool bridging, SSE codec, budget arithmetic, prompt assembly, state folding
Integration /api/chat end to end, scripted model against the real in-process MCP server
Component Streaming text, tool chips, sources, caveats, error and cap states
Browser The full flow in Chromium, fully offline
Eval Real Claude: asserts tool selection and refusal-to-invent

The browser suite runs against demo mode — scripted model, fixture upstreams — so it is deterministic and free. The MCP server underneath is real and does its real mapping work, so what's asserted is genuine output. Without that, one e2e run would spend the entire daily Perenual budget.

What the evals check

The eval suite inverts that: real Claude, fixture upstreams. The variable under test is what the model does, so holding the data constant makes a failure mean something — and it spends none of the Perenual budget.

It asserts behaviour rather than prose quality:

  • A companion question reaches for companion_check, and the answer names the family behind the verdict.
  • Asked point-blank for a spacing figure, the answer contains no measurement at all — any digit followed by a length unit fails the case — and says the figure isn't published.
  • A timing question with no location asks where the garden is instead of calling planting_window on a guess.
  • With a location, it calls planting_window and passes through the ERA5 caveat when asked how reliable the date is.
  • A toxicity question is looked up, not recalled.

Assertions are deterministic rather than model-judged, because the behaviours worth pinning are the checkable ones: a tool was called or it wasn't; a spacing figure appeared or it didn't. A judge would add cost and a second source of flakiness for no extra signal.

The assertions themselves have unit tests. The spacing check began as "no measurement anywhere in the answer" and failed a response that correctly refused to give spacing and then cited a published mature height of 152 cm — the exact behaviour the contract asks for. It is now sentence-scoped, and that real transcript is kept as a regression fixture. An eval that fails correct behaviour is worse than no eval, because it argues for breaking something that works.

Licence

MIT. Data retrieved through the MCP server remains under its own sources' terms — see plant-intel-mcp.

来源:GitHub · github.com