garden-mcp-server

Day-in-the-Country-LLC/garden-mcp-server

3.1

If you are the rightful owner of garden-mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.

MCP server with tools for gardening questions and planning.

Tools
3
Resources
0
Prompts
0

garden-mcp-server

MCP server providing tools for gardening questions and planning.

Current toolset (fastMCP):

  • Weather Forecast: Get daily forecasts for a location (Open‑Meteo).
  • Weather Archive: Historical daily data via Open‑Meteo ERA5 archive.
  • Meteostat History: Station observations (daily) with common fields (tavg/tmin/tmax/prcp).
  • Identify Plant: Identify plants from an image using Plant.id (top candidates with details).
  • Enrich Plants: Fetch extra info for candidate scientific names (iNaturalist + Wikipedia).
  • Fetch URL Text: Fetch a web page and extract readable text (for client‑side RAG).
  • Garden Search: Search the web for gardening topics (DuckDuckGo).
  • Bed Layout Planner: Calculate plant counts and simple grid layouts.

Quick start (using uv)

  • Requirements: Python 3.11+ and uv installed (macOS: brew install uv).
  • Create a virtual environment and install project (with dev tools):
    • uv venv --python 3.11
    • uv pip install -e .[dev]
  • Run the MCP server over stdio:
    • uv run garden-mcp-server

Testing and linting (with uv)

  • Run tests: uv run pytest
  • Lint: uv run ruff check .

Configuration

  • Weather forecast uses Open‑Meteo’s free APIs (no key required).
  • Weather archive uses Open‑Meteo ERA5 archive (no key required).
  • Meteostat tools use the free Meteostat client (no key required).
  • Garden search uses DuckDuckGo via duckduckgo-search.

Integration

This server uses the fastMCP framework and speaks MCP over stdio. Point your MCP-capable client (e.g., an IDE or agent runtime) to the garden-mcp-server executable (or invoke with uv run garden-mcp-server). See the client’s documentation for configuring MCP servers.

Authentication and secrets

  • Do not commit API keys or secrets to this repository. It is public by design.
  • All tools that require authentication must read credentials from environment variables. Suggested naming: GARDEN_<SERVICE>_API_KEY (e.g., GARDEN_WEATHER_API_KEY).
  • The server automatically loads a local .env file at startup for convenience. .env is already in .gitignore.
  • Example usage:
    • One‑off: GARDEN_WEATHER_API_KEY=sk_live_... uv run garden-mcp-server
    • With .env:
      • Create .env in the project root (not committed):
        • GARDEN_WEATHER_API_KEY=sk_live_...
      • Run: uv run garden-mcp-server
  • Our default weather tool uses Open‑Meteo (no key). If you switch to a key‑based provider, follow the env‑var pattern above.
  • Plant identification uses Plant.id and requires an API key. BYOT/K policy applies:
    • Public: callers pass plant_id_api_key per request.
    • Private/internal: set GARDEN_PLANT_ID_API_KEY in env; authenticated callers with scope GARDEN_INTERNAL_SCOPE (default internal) may omit plant_id_api_key.
    • Do not log or store user‑supplied keys.

Client examples

  • Python (stdio via uv):

    • pip install fastmcp
    • Example:
      • import asyncio
        from fastmcp.client import Client
        from fastmcp.client.transports import UvStdioTransport
        
        async def main():
            # Launch the server in a subprocess via uv run
            transport = UvStdioTransport(
                command="garden-mcp-server",
                project_directory=".",  # repo root with pyproject.toml
            )
            client = Client(transport)
            async with client:
                # List available tools
                tools = await client.list_tools()
                print([t.name for t in tools])
        
                # Call forecast tool
                res = await client.call_tool(
                    "tool_get_weather_forecast",
                    {"location": "Portland, OR", "days": 3},
                )
                print(res)
        
                # Call Open-Meteo archive tool
                res = await client.call_tool(
                    "tool_open_meteo_archive",
                    {
                        "location": "Portland, OR",
                        "start_date": "2024-01-01",
                        "end_date": "2024-01-07",
                        "daily_vars": [
                            "temperature_2m_max",
                            "temperature_2m_min",
                            "precipitation_sum",
                        ],
                    },
                )
                print(res)
        
                # Call Meteostat history tool
                res = await client.call_tool(
                    "tool_meteostat_history",
                    {
                        "location": "Portland, OR",
                        "start_date": "2024-01-01",
                        "end_date": "2024-01-07",
                        "fields": ["tavg", "tmin", "tmax", "prcp"],
                    },
                )
                print(res)
        
                # Identify a plant by image URL (BYOK or env-based)
                res = await client.call_tool(
                    "tool_identify_plant",
                    {
                        "image_url": "https://example.com/photo.jpg",
                        "top_n": 3,
                        # Public deployment (BYOK): pass your Plant.id key here
                        # "plant_id_api_key": "pk_live_...",
                    },
                )
                print(res)
        
                # Enrich the first two scientific names via iNaturalist/Wikipedia
                names = [c["scientific_name"] for c in res["candidates"][:2]]
                enriched = await client.call_tool(
                    "tool_enrich_plants",
                    {"scientific_names": names, "max_results": 2},
                )
                print(enriched)
        
                # Fetch page text for RAG (LLM summarization stays client-side)
                page = await client.call_tool(
                    "tool_fetch_url_text",
                    {"url": "https://example.com/garden-guide", "max_chars": 12000},
                )
                print(page["title"], len(page["text"]))
        
        asyncio.run(main())
        
  • Error handling and validation:

    • Tools validate ISO date formats (YYYY-MM-DD) and enforce start_date <= end_date.
    • Open‑Meteo archive checks variable names for simple identifier format.
    • Meteostat history validates requested fields against known columns (e.g., tavg, tmin, tmax, prcp, snow, wdir, wspd, wpgt, pres, tsun).

Cloud Run deployment

  • Build container locally:
    • docker build -t gcr.io/PROJECT_ID/garden-mcp-server:latest .
    • docker push gcr.io/PROJECT_ID/garden-mcp-server:latest
  • Or using gcloud (recommended):
    • gcloud builds submit --tag gcr.io/PROJECT_ID/garden-mcp-server:latest
  • Deploy to Cloud Run:
    • gcloud run deploy garden-mcp-server --image gcr.io/PROJECT_ID/garden-mcp-server:latest --region REGION --platform managed --allow-unauthenticated
    • Set env vars as needed:
      • FASTMCP_HOST=0.0.0.0 (default)
      • GARDEN_MCP_HTTP_TRANSPORT=streamable-http or sse (default: streamable-http)
      • Optional paths: FASTMCP_STREAMABLE_HTTP_PATH=/mcp, FASTMCP_SSE_PATH=/sse, FASTMCP_MESSAGE_PATH=/messages/
  • Connecting from a client:
    • Streamable HTTP: Client("https://SERVICE.run.app/mcp")
    • SSE: Client("https://SERVICE.run.app/sse")
    • If you enable auth (JWT or OAuth), include appropriate auth in the client (e.g., Client(url, auth="<bearer-token>")).

Mobile uploads (GCS pre‑signed URLs)

  • The HTTP server exposes a helper route to upload images from mobile apps without exposing credentials:
    • POST /upload-url with JSON: { "file_name": "uploads/uuid.jpg", "content_type": "image/jpeg" }
    • Response: signed upload_url (PUT) and a signed get_url (GET) valid for a short time.
    • Requires environment: GARDEN_GCS_BUCKET and Cloud Run service account with storage.objects.create permission.
  • Flow:
    1. Mobile requests /upload-url to obtain URLs.
    2. Mobile PUT the image bytes to upload_url with header Content-Type set.
    3. Client calls tool_identify_plant with image_url = get_url (temporary access).
    4. Optionally, delete or rotate objects later per your retention policy.

Upload URL policy (use your own storage by default)

  • Control access to /upload-url via GARDEN_UPLOAD_URL_POLICY:
    • disabled: Do not serve upload URLs (public BYOS only).
    • internal (default): Only allow callers with the GARDEN_INTERNAL_SCOPE (default internal).
    • public: Allow anyone (not recommended for shared buckets).
  • BYOS (Bring Your Own Storage): Clients can skip /upload-url entirely by:
    • Sending image_base64 (small/medium images), or
    • Passing an image_url that points to their own signed URL in their storage (GCS/S3/etc.).
    • This ensures third parties never use your bucket.

Recommended auth (JWT) for Cloud Run

  • Symmetric JWT (HS256) with a shared secret (simple and secure if managed via Secret Manager):
    • Create a secret: echo -n 'a-strong-shared-secret' | gcloud secrets create garden-jwt-secret --data-file=-
    • Deploy with auth enabled:
      • gcloud run deploy garden-mcp-server \ --image gcr.io/PROJECT_ID/garden-mcp-server:latest \ --region REGION --platform managed --allow-unauthenticated \ --set-env-vars=FASTMCP_HOST=0.0.0.0,GARDEN_MCP_HTTP_TRANSPORT=streamable-http,FASTMCP_SERVER_AUTH=JWT,FASTMCP_SERVER_AUTH_JWT_ALGORITHM=HS256 \ --update-secrets=FASTMCP_SERVER_AUTH_JWT_PUBLIC_KEY=garden-jwt-secret:latest
    • Client usage: pass a Bearer token signed with HS256 using the shared secret.

Defaults and endpoints

  • Default transport for Cloud Run is streamable-http (robust, works well with Cloud Run request model). Use sse if your client explicitly requires SSE and your service concurrency/timeouts are tuned for long-lived connections.
  • Health endpoints are provided when running in HTTP mode:
    • GET /healthz → JSON {"status":"ok"}
    • GET /readyzok

Public MCP + BYOK (Bring Your Own Key)

  • To keep the MCP public while ensuring others pay for their own Plant.id usage:
    • Public instance: do NOT set GARDEN_PLANT_ID_API_KEY so callers must pass plant_id_api_key per request (BYOK).
    • Private/internal instance: set GARDEN_PLANT_ID_API_KEY for your own app.
    • Single instance with policy: set GARDEN_PLANT_ID_API_KEY and require an auth scope (default internal) to use it. Others must pass plant_id_api_key.
      • Configure the required scope via GARDEN_INTERNAL_SCOPE (default: internal).
  • Applies to all paid provider tools in this repo. When users pass a key (e.g., plant_id_api_key), the server uses it only for the upstream API call and never logs/stores it.