Skip to main content
Glama
README.md
# flora-mcp

An MCP server that identifies plants from a photo and tells you whether that
species is **native**, **introduced** or **invasive** in a given country.

The answer depends on the place, not just on the species: *Pontederia crassipes*
is native in the Amazon basin and an aggressive invasive in Guatemala. That is
exactly the kind of question a language model gets wrong from memory and a tool
call gets right.

Built for the course CC3067 - Redes (Universidad del Valle de Guatemala). It
speaks stdio and Streamable HTTP, so any MCP host can use it.

## Tools

| Tool | Purpose | Needs an API key |
| --- | --- | --- |
| `identify_plant_from_photo` | Identify a species from a local photo via Pl@ntNet, with confidence scores. | Pl@ntNet |
| `classify_species_in_country` | Native / introduced / invasive verdict for a species in a country, with its evidence. | No |
| `get_management_recommendations` | Curated control measures for a known invasive species. | No |
| `list_country_alien_species` | Plants in a country's GRIIS register of introduced species. | No |

## How the status is decided

GBIF publishes no single "is this invasive here?" field, so the verdict combines
three public sources and always returns the evidence alongside it:

| Source | What it contributes |
| --- | --- |
| `species/{key}/distributions` | A national checklist's `establishmentMeans` for that country: NATIVE, INTRODUCED, NATURALISED, MANAGED, INVASIVE. |
| GRIIS (per country) | The country's Global Register of Introduced and Invasive Species. Membership means the species is alien there. |
| GISD | The Global Invasive Species Database: species documented as invasive somewhere in the world. |

Precedence:

1. A distribution row for that country wins — it is a national checklist stating
   the establishment means directly. `INTRODUCED` plus a GISD listing is reported
   as **invasora**; `INTRODUCED` alone as **introducida**; `NATIVE` as **nativa**.
2. Otherwise, presence in the country's GRIIS register means alien there;
   combined with GISD, invasive.
3. Otherwise, occurrence records only prove the species has been *seen* there,
   which is reported as **"presente, estatus no documentado"** rather than
   guessed at.

Coverage is genuinely uneven. *Pinus oocarpa* is native to Guatemala but no
checklist says so on GBIF, so the tool reports "present, status not documented"
instead of inventing an answer. A host's system prompt should tell the model to
pass that honesty through.

One taxonomic trap is worth knowing about: Pl@ntNet returns *Eichhornia
crassipes* while GBIF's backbone has moved to *Pontederia crassipes*, and each
checklist is indexed under whichever name its compiler used. Every lookup tries
both the matched key and the accepted key for this reason.

## Requirements

- Python >= 3.11
- A free Pl@ntNet API key ([my.plantnet.org](https://my.plantnet.org/)) — only
  for photo identification; 500 identifications/day on the free plan

GBIF needs no key and no account.

## Installation

```bash
git clone https://github.com/USUARIO/flora-mcp.git
cd flora-mcp
uv venv .venv
.venv\Scripts\activate      # Windows; source .venv/bin/activate elsewhere
uv pip install -e .
```

Copy `.env.example` to `.env` and add your Pl@ntNet key:

```powershell
Copy-Item .env.example .env
```

## Environment variables

| Variable | Required | Purpose |
| --- | --- | --- |
| `PLANTNET_API_KEY` | For `identify_plant_from_photo` only | Pl@ntNet API key. |
| `FLORA_IMAGES_DIR` | No | Where relative image names are resolved. Defaults to `./workspace`. A host whose images live elsewhere should set this to an absolute path. |

## Running

```bash
flora-mcp                                  # stdio (default)
flora-mcp --http --port 8100               # Streamable HTTP, this machine only
flora-mcp --http --host 0.0.0.0 --port 8100  # reachable from the network
```

Over stdio the process stays silent and waits for JSON-RPC on stdin — that is
normal for that transport, not a hang. Nothing is ever written to stdout, since
that channel carries the protocol.

The server has no authentication, so only bind `0.0.0.0` on a network you trust.

## Tool reference

### `identify_plant_from_photo`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `image_path` | string | yes | Absolute path, or a file name inside `FLORA_IMAGES_DIR`. |
| `organ` | string | no | What the photo shows: `auto` (default), `leaf`, `flower`, `fruit`, `bark`, `habit`, `other`. |

```json
{ "image_path": "planta2.jpg" }
```

```json
{
  "found": true,
  "image": "planta2.jpg",
  "organ": "auto",
  "candidates": [
    {
      "scientific_name": "Spathoglottis plicata",
      "common_names": ["Orquídea de tierra"],
      "family": "Orchidaceae",
      "confidence": 27.3
    }
  ]
}
```

Always report the confidence. Anything below roughly 30% is a guess, and the
tool returns several candidates precisely so the caller can say so.

### `classify_species_in_country`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `scientific_name` | string | yes | Scientific name; a synonym is accepted and resolved. |
| `place` | string | yes | Country name or ISO code — `Guatemala`, `GT`, `GTM`. |

```json
{ "scientific_name": "Eichhornia crassipes", "place": "Guatemala" }
```

```json
{
  "resolved": true,
  "species": "Eichhornia crassipes (Mart.) Solms",
  "country": "Guatemala",
  "country_code": "GT",
  "status": "invasora",
  "basis": "introducida segun checklist nacional (INTRODUCED) y listada en GISD",
  "evidence": {
    "distribution_rows": [
      {
        "establishment_means": "INTRODUCED",
        "occurrence_status": null,
        "source": "Global Dataset of Freshwater Invasive and Alien Species"
      }
    ],
    "in_country_griis": false,
    "in_gisd": true,
    "occurrence_records": 98,
    "native_range_sample": ["BR"]
  }
}
```

`status` is one of `nativa`, `introducida`, `invasora`,
`presente, estatus no documentado`, or `sin registros`. `basis` says which rule
produced it, and `evidence` carries what it was based on.

### `get_management_recommendations`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `scientific_name` | string | yes | Scientific name; synonyms in the table are resolved. |

```json
{ "scientific_name": "Eichhornia crassipes" }
```

```json
{
  "found": true,
  "scientific_name": "pontederia crassipes",
  "common_name": "Jacinto de agua / lirio acuatico",
  "urgency": "alta",
  "why": "Forma tapetes flotantes que agotan el oxigeno del agua...",
  "methods": [
    "Control mecanico: extraccion manual o con cosechadora antes de la floracion...",
    "Control biologico: gorgojos Neochetina bruchi y N. eichhorniae..."
  ]
}
```

When a species is not in the table the reply is `{"found": false, ...}` with a
`covered_species` list, so the caller can say what the server does and does not
know instead of improvising control measures.

### `list_country_alien_species`

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `place` | string | yes | Country name or ISO code. |
| `limit` | integer | no | Maximum species to return. Default 25. |

```json
{ "place": "Guatemala", "limit": 5 }
```

```json
{
  "resolved": true,
  "country": "Guatemala",
  "country_code": "GT",
  "species": [
    { "scientific_name": "Acanthophora spicifera (M.Vahl) Børgesen", "canonical_name": "Acanthophora spicifera", "family": "Rhodomelaceae" },
    { "scientific_name": "Pinus caribaea Morelet", "canonical_name": "Pinus caribaea", "family": "Pinaceae" }
  ]
}
```

GRIIS registers *introduced* species, only some of which are invasive. Present
these as introduced, and use `classify_species_in_country` to find out which is
which. Not every country publishes a GRIIS checklist.

## Configuring it in an MCP host

Generic stdio configuration:

```json
{
  "servers": {
    "flora": {
      "transport": "stdio",
      "command": "C:/ruta/al/repo/flora-mcp/.venv/Scripts/python.exe",
      "args": ["-m", "flora_mcp.server"],
      "cwd": "C:/ruta/al/repo/flora-mcp",
      "env": { "PLANTNET_API_KEY": "..." }
    }
  }
}
```

On Linux or macOS use `.venv/bin/python`. With `uv` installed, a host can skip
the explicit path entirely:

```
uv run --directory ../flora-mcp flora-mcp
```

If the host keeps its images in its own folder rather than this repo's
`workspace/`, pass `FLORA_IMAGES_DIR` as an absolute path pointing there —
otherwise relative file names resolve against this repo and photo lookups fail.

## Testing with MCP Inspector

```bash
npx -y @modelcontextprotocol/inspector .venv\Scripts\python.exe -m flora_mcp.server
```

Suggested sequence:

1. `list_country_alien_species` with `place = Guatemala`
2. `classify_species_in_country` with one of the species it returns
3. `get_management_recommendations` for the same species
4. `identify_plant_from_photo` with a photo dropped in `workspace/`

## Tests

```bash
.venv\Scripts\python.exe -m pytest -m "not network"
```

That covers the classification heuristic against stubbed GBIF responses and
launches the server as a real subprocess to exercise it over JSON-RPC, on both
stdio and HTTP. Drop the `-m` filter to also run the tests that hit the live
GBIF API.

## Extending the knowledge base

`src/flora_mcp/knowledge_base.py` is the hand-written half of the server: GBIF
says *what* a species is, not what to do about it. Add entries keyed by
lowercase scientific name, and register any synonym Pl@ntNet might return in
`SYNONYMS` so both names reach the same advice.

## Limitations

- GBIF's per-country coverage is uneven; absence of a status is not a status.
- The management table is curated by hand and currently covers seven species
  relevant to Mesoamerica.
- Pl@ntNet's confidence is often low on partial or unusual photos. The server
  reports it rather than hiding it.
- Both Pl@ntNet and GBIF require internet access.
- No authentication: anything that can reach the process can call the tools.

## Technologies

Python, MCP Python SDK, JSON-RPC, Pl@ntNet API, GBIF API.

## Academic use

Developed for CC3067 Redes, Universidad del Valle de Guatemala. Where
third-party code or documentation is reused, the corresponding attribution is
kept in the comments.

TDQS

A4.2/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct stage in the plant management workflow: photo identification, status classification, management advice, and country-level alien listing. There is no overlap in purpose, and the boundary between identifying a plant and classifying its status is clear.

Naming Consistency5/5

All tool names follow a consistent snake_case verb_noun pattern: identify_plant_from_photo, classify_species_in_country, get_management_recommendations, list_country_alien_species. The naming is uniform and predictable.

Tool Count5/5

Four tools is a compact but well-scoped set for a flora-management assistant. Each tool is substantive and maps to one distinct part of the domain, avoiding both bloat and thinness.

Completeness4/5

The tools cover the main workflow: identify a plant, classify its status, view management recommendations, and list alien species for a country. A minor gap is the lack of a direct species lookup by name without a photo, but the overall lifecycle is well covered.

Maintenance

ActivityMaintained
ResponsivenessNo issues