flora-mcp
# 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
Scored across 4 tools
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.
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.
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.
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.