GBIF Biodiversity MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@GBIF Biodiversity MCP Serverfind occurrences of Danaus plexippus in Mexico"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://gbif-biodiversity.caseyjhand.com/mcp
Overview
GBIF species taxonomy, occurrence records, datasets, and publishers. Match names to the backbone taxonomy, search 3.9B+ occurrence records with Darwin Core filters, and browse dataset and publisher metadata from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
Tool | Description |
| Match a species name against the GBIF backbone taxonomy — returns taxonKey, confidence score, and full classification |
| Match up to 50 scientific names to backbone taxon keys in one call — results in input order, per-name NONE/ERROR isolation |
| Fetch a single backbone taxon by key — full classification, authorship, synonymy, vernacular name, descendant count |
| Search or browse the GBIF backbone taxonomy by name fragment, rank, or a kingdom, family, or genus name resolved to its backbone key |
| Return the root-to-parent classification chain for a taxon — root-first ordered array from kingdom to the queried taxon's immediate parent (the taxon itself is not included) |
| List direct children of a backbone taxon — genera within a family, species within a genus |
| Search 3.9B+ GBIF occurrence records with Darwin Core filters — country, publishing country, state/province, bounding box, WKT geometry, year, month, basis of record, presence/absence, IUCN Red List category |
| Count occurrences matching a filter without fetching records — fast single-number response, filtered to sightings by default |
| Fetch a single occurrence record by key — full Darwin Core record with GADM geography, presence/absence status, conservation status, media, and quality flags |
| Aggregate occurrence counts by a dimension — country, year, basis of record, dataset, kingdom, presence/absence, IUCN Red List category |
| Search GBIF datasets by keyword, type, country, publishing organization, or hosting organization |
| Fetch full dataset metadata by UUID — title, description, citation, contacts, license, DOI, coverage |
| Search GBIF-registered publishing organizations by name fragment or country |
Resources
Resource | Description |
| Taxon record from the GBIF backbone — classification, authorship, synonymy status, vernacular name |
| Dataset metadata — title, description, citation, license, contacts, coverage |
Both resources are also reachable via gbif_get_species and gbif_get_dataset; many MCP clients are tool-only and never surface resources.
Related MCP server: MCP Registry Search
Capability reference
gbif_match_species tool
Fuzzy matching handles minor typos and vernacular names; set
strict: truefor exact-only matchingReturns
taxonKey— the backbone key required bygbif_search_occurrences,gbif_count_occurrences, andgbif_occurrence_facetsConfidence score 0–100; below 80 warrants review
Full classification hierarchy with keys at each rank: kingdom, phylum, class, order, family, genus, species
matchType NONEindicates no usable match — try removing strict mode or broadening the nameResolves synonyms: always returns the accepted backbone key regardless of which name form was queried;
matchedTaxonKeycarries the synonym's own key when the two differkingdomdisambiguates a name that appears in more than one kingdom, and is rejected when supplied blank: GBIF drops a blank one and matches against the whole backbone —Parus majorresolves to taxon 9705453 withkingdom=exactly as it does with nokingdomat all, wherePlantaeresolves to 9711704 — so the undisambiguated answer would come back looking like a disambiguated one. Omit the field to match against the whole backbone
gbif_bulk_match_species tool
The batch counterpart to
gbif_match_species— built for checklist, inventory, and species-list workflows that would otherwise need one round trip per nameReturns one result per input name, in input order; each carries
taxonKey,matchType, and confidencePer-name isolation: an unmatched name yields
matchType NONEand a per-name lookup failure yieldsmatchType ERRORwith the message and, when the failure was classified, areason— neither sinks the rest of the batchSame synonym resolution as
gbif_match_species:taxonKeyis the accepted taxon,matchedTaxonKeythe synonym it was queried understrict: truerequires an exact match for every name; common names are not supported (usegbif_search_species)
gbif_get_species tool
Full classification, authorship string, and vernacular (English) name when available
taxonomicStatus: ACCEPTED, SYNONYM, DOUBTFUL — when SYNONYM,acceptedKeyandacceptedidentify the current namenumDescendantsandnumOccurrencesfor scope at a glanceextinctfield present only when explicitly flagged — not false on unlabeled taxapublishedIncarries the original description citation when available
gbif_search_species tool
Accepts name fragments matching scientific and vernacular names
Filter by rank, kingdom, family, or genus to scope browsing
kingdom,family, andgenusare given as names and resolved to a backbone key before the search runs, since/species/searchscopes by key alone. The narrowest one supplied is what scopes — the three nest, and GBIF combines two keys with OR rather than AND.kingdomsupplied besidefamilyorgenusdisambiguates that name instead of scoping on its own:Prunellanames both a bird genus and a plant genus and resolves to neither without itNames are matched exactly and capitalized as GBIF writes them, so
paridaeandParidaeefail asunresolved_taxon_scoperather than being ignored. An alternative family name lands on the taxon it is a synonym of —Compositaescopes to Asteraceae. Afamilyandgenusin different lineages fail asconflicting_taxon_scope. The scope actually applied comes back in the enrichmentisExtinctfilter for extinct vs. extant taxaScope to a specific checklist dataset with
datasetKey— omit the field for the GBIF backbone. GBIF reads a higher-taxon key inside the checklist that key belongs to, so pairingdatasetKeywith a kingdom, family, or genus matches nothing unless the checklist is the backbone; the empty-result notice says soq,kingdom,family,genus, anddatasetKeyare rejected when supplied blank rather than dropped: a blankdatasetKeyreturns the unfiltered backbone result, andq=returns the whole 46,623,754-name index whereq=with a space returns nothing. Omit a filter to leave it offPaginated — limit up to 1000, use offset to walk through large groups
gbif_get_species_classification tool
Root-first from kingdom down to the immediate parent of the queried taxon (kingdom → phylum → class → … → parent)
The queried taxon itself is not included — use
gbif_get_speciesfor its own recordEach entry: rank, canonical name, scientific name, taxon key
Useful for building taxonomic trees or placing an unfamiliar taxon in context without manual backbone navigation
gbif_get_species_children tool
Genera within a family, species within a genus, subspecies within a species
Each child: key, name, rank, taxonomic status, common name, occurrence count, descendant count
Paginated — limit up to 1000, iterate with offset for large groups like Coleoptera
gbif_search_occurrences tool
Use
taxonKeyfromgbif_match_speciesfor reliable results — resolves synonyms automatically;scientificNamefilter does notGeographic filters:
country(ISO 3166-1 alpha-2, uppercase),stateProvince, bounding box (decimalLatitude/decimalLongituderanges as "min,max"), or WKT polygon (geometry)publishingCountry(ISO 3166-1 alpha-2, uppercase) is the country of the publishing organization, not of the observation — a different question fromcountry, and the two disagree on most records: of 60,290,950 records observed in GB, 1,548,928 were published by US organizationsBoth country filters take the uppercase two-letter form only. GBIF parses a lowercase or alpha-3 code and then matches the verbatim string, so
gbandUSAreturn zero records instead of an error; the schema pattern turns that silent wrong answer into a validation error naming the accepted form. A two-letter code GBIF does not assign (XX) is rejected upstream by name and surfaces asinvalid_filterstateProvinceis matched verbatim — exact and case-sensitive, with no controlled vocabulary behind it. Take a value from aSTATE_PROVINCEfacet rather than guessing; an unmatched value returns zero records instead of an error, and the enrichment notice says so when it happensEvery filter is checked whenever it is supplied, not only when it holds a value. GBIF ignores a parameter it is given with no value and answers 200 with the unfiltered scope, so a blank filter used to widen the query silently:
stateProvince: ""returns all 60,290,950 records of ataxonKey=212+country=GBscope whereEnglandreturns 47,672,439 — the same total, and the same first record, as a call that sends nostateProvinceat all. A blank or whitespace-only value now fails asinvalid_filterinstead. Omit a field to leave that filter offTemporal filters:
yearas single year or range,month(1–12) for seasonal queriesbasisOfRecordenum:HUMAN_OBSERVATION,PRESERVED_SPECIMEN,MACHINE_OBSERVATION, and morehasCoordinateto require or exclude georeferenced recordsoccurrenceStatus—PRESENT(default),ABSENT, orANY. GBIF indexes absence records (a survey that looked for the taxon and did not find it) alongside sightings; the default excludes them and the enrichment says so on every calliucnRedListCategory—CR,EN,VU,NT,LC,DD,EX,EW,CDOutput per record adds
taxonomicStatus,eventTime(with UTC offset),occurrenceStatus, andiucnRedListCategoryPagination capped at offset+limit = 100,001, the deepest page GBIF serves. The API has no cursor or scroll, so a larger result set is covered by partitioning it — facet by
DATASET_KEYwithgbif_occurrence_facets, then search eachdatasetKeyon its own. Retrieving a set in one piece is not something this server can do: that needs GBIF's Download API with a GBIF.org account, or GBIF's monthly snapshot on AWS Open Data
gbif_count_occurrences tool
Backed by
/occurrence/searchatlimit=0— no record payload, and the same endpointgbif_search_occurrencesqueries, so the two agree on the same question. GBIF's dedicated/occurrence/countendpoint takes a closed parameter set that rejectsoccurrenceStatusandiucnRedListCategoryoutrightSupported filters:
taxonKey,country,publishingCountry,stateProvince,isGeoreferenced,datasetKey,year,occurrenceStatus,iucnRedListCategory.countryandpublishingCountrytake the uppercase alpha-2 form only, and a blank filter is rejected rather than dropped, both for the reasons given undergbif_search_occurrencesCounts sightings only by default, matching
gbif_search_occurrences. For absence-heavy taxa the unfiltered figure is a different question entirely — Radicipes gracilis has 2,351,582 indexed records of which 79 are presencesUse to assess result set size before deciding whether to paginate a full search. A count above 100,001 means paging cannot reach the end of it, and the enrichment notice says so and names the partition route
gbif_get_occurrence tool
Complete Darwin Core record — all coordinate fields, administrative geography (continent, country, state/province, locality), dates
occurrenceID, full classification (class/classKey), GADM administrative units (levels 0–3, each with a stable GID and name), and sourceidentifiersoccurrenceStatus— check it before reading the record as a sighting;ABSENTmeans a survey looked and found nothing, and the record still carries coordinates, a date, and a recordertaxonomicStatus,eventTime(with UTC offset), andiucnRedListCategoryCollections metadata: institution code, collection code, catalog number
Collector and identifier names, individual count, sex, life stage
Associated media (images, audio, video) with URLs and license
GBIF data quality issue flags for provenance assessment
gbif_occurrence_facets tool
Facets:
COUNTRY,STATE_PROVINCE,YEAR,BASIS_OF_RECORD,DATASET_KEY,KINGDOM_KEY,PHYLUM_KEY,CLASS_KEY,ORDER_KEY,FAMILY_KEY,GENUS_KEY,SPECIES_KEY,PUBLISHING_COUNTRY,MONTH,OCCURRENCE_STATUS,IUCN_RED_LIST_CATEGORYScope with
taxonKey,country,publishingCountry,stateProvince,year,geometry,basisOfRecord,datasetKey,occurrenceStatus, oriucnRedListCategoryfilters — so aCOUNTRY,PUBLISHING_COUNTRY, orSTATE_PROVINCEbucket can be passed straight back to drill into it.countryandpublishingCountrytake the uppercase alpha-2 form only, and a blank filter is rejected rather than dropped, both for the reasons given undergbif_search_occurrencesAggregates sightings only by default, matching the search and count tools. To measure the presence/absence split itself, pass
facet: OCCURRENCE_STATUSwithoccurrenceStatus: ANYReturns one page of values ranked by count descending — up to
facetLimit(max 100), the top ones only whilefacetOffsetis 0 — with no record payloadsDATASET_KEYis the basis for splitting a result set too large forgbif_search_occurrencesto page: every occurrence carries exactly onedatasetKey, so its buckets sum to the scope's full total, and it has the cardinality to cut a large scope into pageable pieces.BASIS_OF_RECORDandPUBLISHING_COUNTRYare gap-free too and both have a matching filter on the occurrence tools, so either can drive a further split of a bucket still over the cap — but on that scope they return 9 and 41 buckets againstDATASET_KEY's 550, too coarse for the first cut. A dimension a record can lack drops that record: a 60,290,950-record scope faceted byYEARsums to 59,407,400, leaving 883,550 undated records out, andMONTH,STATE_PROVINCE, andSPECIES_KEYbehave the same way —STATE_PROVINCEincluded, even though the occurrence tools can filter on itThe scope filters here are narrower than
gbif_search_occurrencesaccepts — noscientificName,month, bounding box,hasCoordinate,isInCluster, orcoordinateUncertaintyInMeters. Buckets sum to a search's total only when both calls carry the same filters; re-apply the rest on each per-datasetKeysearchPage past the first
facetLimitwithfacetOffset(advance byfacetLimitper page) to walk high-cardinality facets likeDATASET_KEY; enrichment echoes the appliedfacetOffsetand setsmoreValuesLikelywhen a full page suggests more values remainCore tool for distribution analysis ("which countries have the most records?") and trend queries ("how has observation volume changed since 2010?")
gbif_search_datasets tool
Filters: free-text query, dataset type (
OCCURRENCE,CHECKLIST,METADATA,SAMPLING_EVENT),publishingCountry(ISO 3166-1 alpha-2, uppercase), publishing and hosting organization UUIDs (lowercase)publishingCountrytakes the uppercase two-letter form only, for the same reason the occurrence filters do:/dataset/searchmatches the verbatim stored code, sogbandGBRreturn zero datasets whereGBreturns 2,416. A two-letter code GBIF does not assign (XX) is rejected upstream by name and surfaces asinvalid_filterReturns title, type, description, license, DOI, and record count.
recordCountspans everyoccurrenceStatus, absences included —gbif_count_occurrenceswith the same key counts sightings only by default, so the two figures differ by designThe
descriptionis a 300-character preview —descriptionTruncatedflags when it was shortened, andgbif_get_datasetreturns the full textAn organization key from
gbif_search_publisherschains in two ways, and they answer different questions.publishingOrgmatches the organization whose data it is;hostingOrgmatches the organization whose installation serves it.publishingOrgis almost always the one meant — Butterfly Conservation (0d72dd7f-6f05-46af-85c2-8b6e77ce5534) publishes 3 datasets and hosts none, while the National Biodiversity Network (07f617d0-c688-11d8-bf62-b8a03c50a862) hosts 984, those 3 among them, and publishes 1Supplying both organization filters intersects them rather than combining them, so the same key in both fields returns only what that organization published and serves. An empty result under both is called out by name in the response notice
Both take the lowercase 8-4-4-4-12 hex UUID form and are checked before the request goes out, because GBIF answers the two silent ways of getting them wrong with a wrong result rather than an error: an empty organization value returns the whole 123,527-dataset index, and these two filters are matched case-sensitively, so an upper-cased key returns 0 where the same key lowercased matches. Both fail locally as
invalid_filter, as a malformed value doesThe free-text
qis rejected blank on the same ground, and the route makes the case plainly:q=returns all 123,527 datasets whileq=with a single space returns none. One character between the whole index and an empty page is not a distinction to leave to chance — omit the field to browse without a termPaginated — limit up to 1000
gbif_get_dataset tool
Full description, citation text (for academic reference), license, DOI
Contacts with role, name, organization, and email — capped by
contactLimit(default 10, max 100);contactsTotal/contactsReturnedreport the full countTemporal and geographic coverage ranges when the publisher declares them
recordCount— the indexed occurrence total, matching whatgbif_search_datasetsreports, for every dataset type (aCHECKLISTreports 0). It spans everyoccurrenceStatus, absences included;gbif_count_occurrenceswith the same key counts sightings only by default, so the two figures differ by designnumConstituentsfor aggregate datasets (e.g. iNaturalist, eBird)Use after
gbif_search_datasetsor when an occurrence record'sdatasetKeyneeds provenance detail
gbif_search_publishers tool
Filter by name fragment or country
countryhere is unconstrained on purpose: the registry endpoint matches the parsed country rather than the verbatim string, sogb,GBR, andGBall return the same 223 organizations. The uppercase-only rule the occurrence tools andgbif_search_datasetscarry exists to close a silent zero that does not occur on this route. One value is rejected — an emptycountry, which the registry answers with all 3,561 registered organizations rather than an error; omit the field to search every country. Whitespace alone is left to the registry, which answers it with400 Cannot parse … into a known Country, an error naming the valueqis rejected blank in both forms, because the registry drops either one and answers with all 3,561 organizations against 460 formuseum; omit the field to browse without a name termReturns organization key, title, and country. The key chains into
gbif_search_datasetsaspublishingOrgfor the datasets the organization published, or ashostingOrgfor the ones its own installation serves.publishingOrgis the usual chain: of the first 25 GB organizations the registry lists, all 25 host no datasets while 13 publish one or twoPaginated — limit up to 1000
gbif://species/{taxonKey} resource
Taxon record as
application/json— classification, authorship, synonymy status, vernacular name, descendant counttaxonKeycomes fromgbif_match_speciesorgbif_search_speciesextinctpresent only when explicitly flagged, absent otherwise, matchinggbif_get_speciesA key that resolves to no backbone taxon fails as
not_found; a non-numeric key segment is rejected locally as a validation error, and a numeric one GBIF cannot parse comes back asinvalid_filter
gbif://dataset/{datasetKey} resource
Dataset record as
application/json— title, description, citation, license, DOI, temporal and geographic coverageContacts fixed at 10 (no configurable limit, unlike
gbif_get_dataset'scontactLimit);contactsTotal/contactsReturnedreport the full countrecordCountspans everyoccurrenceStatus, absences includeddatasetKeycomes fromgbif_search_datasetsor an occurrence record'sdatasetKeyfield; a key that resolves to no dataset fails asnot_found
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
GBIF-specific:
Full GBIF REST API v1 coverage: species taxonomy, occurrences, datasets, and publishers
gbif_match_speciesas the entry point — resolves synonyms to backbone taxon keys used throughoutOccurrence pagination guarded at GBIF's own offset+limit = 100,001 boundary — an over-cap request fails locally with a recovery hint naming the
DATASET_KEYpartition technique, instead of spending the retry budget on a deterministic upstream rejection, and a match larger than the cap says so on the first page rather than after hundreds of themWKT polygon geometry support for geographic occurrence queries
Darwin Core field mapping with explicit provenance on sparse upstream fields
Agent-friendly output:
gbif_match_speciesis the mandatory first step — all downstream tools document which key they expectGraceful sparse-field handling — optional fields absent from the API response are omitted rather than null-filled
Discriminated error contracts with typed reasons, structured recovery hints, and
whendocumentation per tool
Getting started
Public Hosted Instance
A public instance is available at https://gbif-biodiversity.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"gbif-biodiversity-mcp-server": {
"type": "streamable-http",
"url": "https://gbif-biodiversity.caseyjhand.com/mcp"
}
}
}Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"gbif-biodiversity-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/gbif-biodiversity-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"gbif-biodiversity-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/gbif-biodiversity-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with Docker:
{
"mcpServers": {
"gbif-biodiversity-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/gbif-biodiversity-mcp-server:latest"]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
Bun v1.4.0 or higher.
No credentials — the GBIF endpoints this server calls are public.
Installation
Clone the repository:
git clone https://github.com/cyanheads/gbif-biodiversity-mcp-server.gitNavigate into the directory:
cd gbif-biodiversity-mcp-serverInstall dependencies:
bun installConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
Variable | Description | Default |
| Transport: |
|
| HTTP server port |
|
| HTTP endpoint path where the MCP server is mounted |
|
| Public origin override for TLS-terminating reverse-proxy deployments | none |
| HTTP session mode: |
|
| Authentication: |
|
| Log level ( |
|
| Opt-in Bun-only forced-GC pressure loop (ms). Try |
|
| Directory for log files (Node.js only) |
|
| Storage backend: |
|
| GBIF API base URL override |
|
| HTTP request timeout in milliseconds |
|
|
| server name, version, and repository URL |
| Enable OpenTelemetry |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdioRun checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Docker
docker build -t gbif-biodiversity-mcp-server .
docker run --rm -p 3010:3010 gbif-biodiversity-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/gbif-biodiversity-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Directory | Purpose |
| Tool definitions ( |
| Resource definitions. Species and dataset stable-URI resources. |
| GBIF REST API service layer — client, request handling, type definitions. |
| Server-specific environment variable parsing and validation with Zod. |
| Unit and integration tests, mirroring the |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor logging,ctx.statefor storageRegister new tools and resources in the
createApp()arrays
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Search ReliefWeb humanitarian reports, disasters, jobs, training, and country profiles via MCP.
GBIF MCP — wraps the Global Biodiversity Information Facility API v1 (free, no auth)
Query U.S. Census Bureau data, variables, and geography via MCP.
Search events, conference weeks, cities, venues and artist schedules via remote MCP.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables searching Figshare research articles, datasets, papers, and posters through the MCP protocol.3 npmMIT
- AlicenseNot gradedqualityDmaintenanceEnables semantic and full-text search over the official Model Context Protocol registry to discover MCP servers for integration.9Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables searching OpenAleph entities and documents using natural language queries through the MCP protocol.182 npm1MIT
- AlicenseNot gradedqualityAmaintenanceGeocode, reverse geocode, and run Overpass spatial queries on OpenStreetMap data via MCP.600 npm5Apache 2.0