Version License Docker MCP SDK npm TypeScript Bun
Install in Claude Desktop Install in Cursor Install in VS Code
Public Hosted Server: https://gbif-biodiversity.caseyjhand.com/mcp
Overview
Species taxonomy, occurrence records, datasets, and publishers from the Global Biodiversity Information Facility (GBIF). Match names to the backbone taxonomy, search and aggregate 3.9B+ occurrence records with Darwin Core filters, and trace records back to their datasets and publishers.
Tools
| Tool | Description |
|---|---|
gbif_match_species | Match a scientific name to the GBIF backbone, returning its taxon key, confidence, and classification |
gbif_bulk_match_species | Match up to 50 scientific names in one call, one result per name in input order |
gbif_get_species | Fetch a backbone taxon by key: classification, authorship, synonymy, vernacular name, descendant count |
gbif_search_species | Search or browse the backbone by name fragment, rank, or a kingdom, family, or genus name |
gbif_get_species_classification | Return a taxon's parent chain, root-first from kingdom to its immediate parent |
gbif_get_species_children | List a taxon's direct children, such as the genera in a family or the species in a genus |
gbif_search_occurrences | Search 3.9B+ occurrence records by taxon, place, date, basis of record, presence/absence, and IUCN category |
gbif_count_occurrences | Count occurrences matching a filter without fetching records |
gbif_get_occurrence | Fetch one full Darwin Core occurrence record by key |
gbif_occurrence_facets | Aggregate occurrence counts by country, year, dataset, taxon, status, and other dimensions, optionally naming taxon and dataset keys |
gbif_search_datasets | Search datasets by keyword, type, publishing country, or publishing or hosting organization |
gbif_get_dataset | Fetch full dataset metadata by UUID: citation, contacts, license, DOI, publishing organization key, coverage |
gbif_search_publishers | Search GBIF-registered publishing organizations by name or country |
Resources
| Resource | Description |
|---|---|
gbif://species/{taxonKey} | Taxon record from the GBIF backbone: classification, authorship, synonymy status, vernacular name |
gbif://dataset/{datasetKey} | Dataset metadata: title, description, citation, license, contacts, coverage, publishing organization key |
Both resources are also reachable via gbif_get_species and gbif_get_dataset; many MCP clients are tool-only and never surface resources.
Skills
| Skill | Description |
|---|---|
gbif-biodiversity-guide | Agent guide: where each key comes from, which region filter selects which records, and the checks before quoting a count, a trend, or an absence. Installed with the Claude Code and Codex plugins and the .mcpb, and served over MCP at skill://gbif-biodiversity-guide/SKILL.md |
Capability reference
gbif_match_species tool
- One scientific
name(common names go togbif_search_species), with optionalstrict, akingdomto disambiguate, and an expectedrank - Returns
taxonKey, the accepted taxon to pass to the occurrence tools, plusmatchedTaxonKeywhen a synonym was resolved,matchType(EXACT,FUZZY, orHIGHERRANK: a match at a higher rank, such as a misspelled species falling back to its genus or kingdom, wheretaxonKeycovers that whole taxon, or its accepted taxon when the taxon GBIF fell back to is a synonym, and anoticenames it),confidence0–100 (review below 80; aHIGHERRANKmatch can score above 90), and the classification with a key at each rank matchTypeNONEfails asno_match, carrying GBIF's note when it gives one (for several equal matches, setkingdom); a blankkingdomfails asinvalid_filter
gbif_bulk_match_species tool
- Scientific
nameswithstrict; the first 50 are processed in input order. With read-limit clamping enabled (default), extra names are returned inunprocessedNames, preserving order and duplicates for the next call - Each result carries
nameandmatchType, and on a matchtaxonKey,matchedTaxonKey(synonyms), andconfidence; aHIGHERRANKrow carries anotenaming the higher taxon GBIF matched and what itstaxonKeycovers: that taxon, or its accepted taxon when it is a synonym. An unmatched name isNONE, with GBIF's explanation innotewhen it gives one (such as several equal matches for a name shared across kingdoms); a failed lookup isERRORwitherrorand, when classified,reason, and neither fails the batch
gbif_get_species tool
- One backbone
taxonKey, a non-negative integer (0is the backbone kingdom incertae sedis); a key not in the backbone fails asnot_found, one past the 32-bit range asinvalid_filter, and a fraction or negative key fails at the schema - Returns the classification with rank keys,
authorship,vernacularName,numDescendants,publishedIn, andparentKey; ataxonomicStatusofSYNONYMcomes withacceptedKeyandaccepted. GBIF's taxon record carries no occurrence count: usegbif_count_occurrences
gbif_search_species tool
qname fragment (scientific and vernacular),rank,isExtinct, and a checklistdatasetKey(omit for the backbone); up to 1,000 per page with integerlimitandoffset- Taxa carry the checklist usage
key, optional sourcedatasetKeyand mapped backbonenubKey,rank,taxonomicStatus, classification names,vernacularName(GBIF's own, else the first English vernacular name containingqwhenqis not part of the row's own scientific name, else the first English one), andnumDescendants.numOccurrencesappears only when GBIF supplies a positive value; its species search sends a0placeholder on every row, so count withgbif_count_occurrencesonnubKeyor aSPECIES_KEYfacet. Enrichment reportstotalCount,endOfRecords, and thetaxonScopeapplied. Occurrence queries usenubKeywhen supplied; otherwise resolve the name withgbif_match_species. OmitteddatasetKeyselects GBIF Backbone Taxonomyd7dddbf4-2cf0-4f39-9b2a-bb099caae36c kingdom,family, andgenustake names, matched exactly as GBIF capitalizes them; the narrowest one scopes, andkingdombeside a narrower one disambiguates it. An unknown name fails asunresolved_taxon_scope, a genus outside the named family asconflicting_taxon_scope, and a scope paired with a non-backbonedatasetKeymatches nothing. A blank filter or a malformeddatasetKeyfails asinvalid_filter
gbif_get_species_classification tool
- One
taxonKey; returnsclassificationroot-first from kingdom to the immediate parent, each entry withkey,rank,name, andscientificName - The queried taxon is not included (use
gbif_get_species); a root taxon returns an empty chain with a notice, an unknown key fails asnot_found, one past the 32-bit range asinvalid_filter, and a fraction or negative key fails at the schema
gbif_get_species_children tool
- One
taxonKey(a non-negative integer;0is the backbone's incertae sedis kingdom), up to 1,000 children per page with integerlimitandoffset; no filters. A fraction or negative value fails at the schema; an unknown key fails asnot_found, one past GBIF's 32-bit range asinvalid_filter - Each child carries
key,canonicalName,rank,taxonomicStatus,vernacularNamewhen GBIF has one, andnumDescendants; GBIF sends no occurrence count, so usegbif_count_occurrenceswith the child'skey. GBIF reports no total, soendOfRecords(withtruncatedwhile more remain) is the continuation signal - An empty later page gives offset-reset guidance; only an empty first page for an existing taxon means no direct children
gbif_search_occurrences tool
- Filters:
taxonKey(preferred;scientificNamedoes not match synonyms),country,publishingCountry,stateProvince,gadmGid, adecimalLatitude/decimalLongitudebox or WKTgeometry,year,month,basisOfRecord,hasCoordinate,isInCluster,coordinateUncertaintyInMeters,datasetKey,catalogNumber,institutionCode,recordedBy,mediaType,occurrenceStatus,iucnRedListCategory; up to 300 per page gadmGidis an uppercase GADM GID (ESP.1_1) GBIF derives from each record's coordinates, so records without coordinates match none; take one from aGADM_LEVEL_0_GID–GADM_LEVEL_3_GIDfacet orgbif_get_occurrence'sgadmfield.catalogNumberandinstitutionCodematch the whole value case-insensitively,recordedByone collector exactly and case-sensitively (take a spelling from aRECORDED_BYfacet), andmediaTypeisStillImage,MovingImage,Sound, orInteractiveResource.taxonKey,month,limit, andoffsetare integers- All supplied spatial filters intersect: combining
geometrywith latitude or longitude ranges restricts results to their overlap - Records carry
key,taxonKey,taxonomicStatus, coordinates,eventDateandeventTime,basisOfRecord,occurrenceStatus,iucnRedListCategory,datasetKey,institutionCode,catalogNumber, andissues; enrichment reportstotalCount,endOfRecords, and theoccurrenceStatusapplied - Search, count and facets echo the same
scope: effective supplied biological filters, withoccurrenceStatusalways included and pagination controls excluded.ANYis echoed but omitted upstream.taxonKeyandscientificNamecombine with OR. All 11 GBIF basis-of-record values are accepted, includingFOSSIL_SPECIMEN,OBSERVATION, andUNKNOWN - Latitude, longitude and coordinate uncertainty are independent optional facts; text renders each supplied value, including zero, in decimal degrees or meters
offset + limitcaps at 100,001 and GBIF has no cursor; a deeper request fails aspagination_cap_exceeded, and a larger match is reached by partitioning on aDATASET_KEYfacet fromgbif_occurrence_facets. A rejected filter value fails asinvalid_filter
gbif_count_occurrences tool
- Accepts every biological filter listed for
gbif_search_occurrences, with the same constraints and spatial intersection.isGeoreferencedremains an alias forhasCoordinate; either alone or both equal works, conflicting values fail asinvalid_filter - Returns
count, with theoccurrenceStatusapplied in the enrichment; a count above 100,001 carries a notice naming the partition route, and a rejected filter value fails asinvalid_filter
gbif_get_occurrence tool
- One
occurrenceKeyfrom a search result, a positive integer (a non-positive or fractional key is rejected at the schema); an unknown key fails asnot_found - The full Darwin Core record:
occurrenceStatus(ABSENTis a survey that found nothing, not a sighting), coordinates, GADM levels 0–3 undergadm, dates witheventTime,taxonomicStatus,iucnRedListCategory, institution, collection, and catalog codes,recordedByandidentifiedBy,media,identifiers, andissues
gbif_occurrence_facets tool
facetis one ofCOUNTRY,STATE_PROVINCE,YEAR,MONTH,BASIS_OF_RECORD,DATASET_KEY,PUBLISHING_COUNTRY,KINGDOM_KEY,PHYLUM_KEY,CLASS_KEY,ORDER_KEY,FAMILY_KEY,GENUS_KEY,SPECIES_KEY,OCCURRENCE_STATUS,IUCN_RED_LIST_CATEGORY,GADM_LEVEL_0_GID–GADM_LEVEL_3_GID,INSTITUTION_CODE,MEDIA_TYPE,RECORDED_BY; up to 100 values per page (facetLimit, default 10), paged withfacetOffset- Accepts every biological filter listed for
gbif_search_occurrences, with the same constraints and spatial intersection. Repeat them on the facet call and each partition search to preserve the population. ReturnstotalOccurrencesandcountsranked by count; a rejected scope value fails asinvalid_filter - Each facet request fetches one extra bucket to prove continuation.
truncated,shown, andcapappear only when that bucket exists; advancefacetOffsetby the exposed page size. An exactly full final page carries no truncation flag. Empty later pages give offset-reset guidance; an empty first page distinguishes zero matching occurrences from a dimension with no data DATASET_KEYbuckets sum to the scope's full total, so it is the facet for partitioning a search too large to page;YEAR,MONTH,STATE_PROVINCE,SPECIES_KEY,INSTITUTION_CODE,MEDIA_TYPE, andRECORDED_BYdrop records that lack the field, and the GADM levels drop records without coordinates or outside every GADM unit at that level.MEDIA_TYPEandRECORDED_BYbuckets overlap (a record with two media types or two collectors counts in each), so their sums can exceedtotalOccurrences.INSTITUTION_CODEbuckets come back lowercased andinstitutionCodetakes them unchanged. Passfacet: OCCURRENCE_STATUSwithoccurrenceStatus: ANYfor the presence/absence splitincludeLabels: truenames each bucket on aKINGDOM_KEY–SPECIES_KEYorDATASET_KEYpage:counts[].labelis the taxon'scanonicalNameor the datasettitle. It costs one GBIF request per exposed bucket (never the lookahead), at most 8 in flight server-wide, no retries, and one 10 s deadline per page; a bucket whose lookup fails or misses it has nolabeland is counted innotice. Other dimensions ignore it, and without it a facet call is one request
gbif_search_datasets tool
- Filters:
q,type(OCCURRENCE,CHECKLIST,METADATA,SAMPLING_EVENT),publishingCountry,publishingOrg,hostingOrg; up to 1,000 per page - Returns
key,title,type,license,doi,recordCount,publishingOrganizationKeyandpublishingOrganizationTitle, and a 300-characterdescriptionpreview, withdescriptionTruncatedflagging a cut (gbif_get_datasethas the full text).publishingOrganizationKeygoes back in aspublishingOrg.limitandoffsetare integers publishingOrgmatches the organization whose data it is and is the usual chain fromgbif_search_publishers;hostingOrgmatches the organization whose installation serves it, and the two together intersect. Both take the lowercase UUID form; anything else, like any rejected filter value, fails asinvalid_filter
gbif_get_dataset tool
- One
datasetKeyUUID andcontactLimit(default 10, max 100;0drops contact detail but keepscontactsTotal); a malformed key fails asinvalid_filter, an unknown one asnot_found - Returns
description,citationText,license,doi,publishingOrganizationKey(goes intogbif_search_datasetsaspublishingOrg; the detail record carries no organization name),recordCount,numConstituents,contactswithcontactsTotalandcontactsReturned,temporalCoverages, andgeographicCoverages
gbif_search_publishers tool
qname fragment andcountry(alpha-2 or alpha-3, any case); up to 1,000 per page with integerlimitandoffset- Returns organization
key,title,country, andcity; the key chains intogbif_search_datasetsaspublishingOrg - A blank
q, an emptycountry, or a country GBIF cannot parse fails asinvalid_filter
gbif://species/{taxonKey} resource
- Taxon record as
application/json: classification names,authorship,vernacularName,taxonomicStatuswithacceptedKeyandaccepted,numDescendants taxonKeyis a whole signed decimal integer within the signed 32-bit range;0is the backbone kingdom incertae sedis. The segment is URI-decoded once and surrounding whitespace is trimmed; leading+, zero padding and their percent-encoded equivalents are accepted. An unknown key fails asnot_found; malformed escapes, suffixes, fractions, exponents, hexadecimal forms, embedded whitespace and out-of-range values fail asinvalid_filterbefore a request. Use a backbone key fromgbif_match_speciesor a search result'snubKey, resolving its name when that mapping is absent
gbif://dataset/{datasetKey} resource
- The
gbif_get_datasetrecord asapplication/json, includingpublishingOrganizationKey, with contacts fixed at 10 datasetKeycomes fromgbif_search_datasetsor an occurrence record; an unknown key fails asnot_found, a malformed one asinvalid_filter
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:
- GBIF API v1 species, occurrence, dataset, and organization endpoints, keyless, with an identifying
User-Agenton every request - Synonym-safe keys:
gbif_match_speciesresolves a synonym to its accepted taxon, and the occurrence tools filter on thattaxonKey - Sightings by default: the search, count, and facet tools default
occurrenceStatustoPRESENT, excluding absence records. DatasetrecordCountspans every status, so it can exceed agbif_count_occurrencestotal for the same key - Strict country codes:
country(where a record was observed) andpublishingCountry(the publisher's country) on the occurrence tools, andpublishingCountryongbif_search_datasets, accept only uppercase ISO 3166-1 alpha-2, because other forms silently match nothing there.stateProvincematches verbatim and case-sensitively, so take values from aSTATE_PROVINCEfacet - Blank filters rejected: GBIF reads an empty parameter as no filter, so an optional filter sent blank or whitespace-only fails as
invalid_filterinstead of widening the query. The exception is a blankcountryorpublishingCountryon the occurrence tools, or a blankpublishingCountryongbif_search_datasets, which fails earlier at the uppercase pattern as-32602 invalid_arguments. Omit a field to leave it off
Agent-friendly output:
- Scope echo: enrichment reports
totalCount,endOfRecords, theoccurrenceStatusapplied, and the resolvedtaxonScope, with notices on empty results, over-cap totals, and astateProvince,gadmGid, orrecordedBythat matched nothing - Graceful partial failure:
gbif_bulk_match_speciesreturns aNONEorERRORrow per name instead of failing the batch - Typed errors:
invalid_filter,not_found,no_match,unresolved_taxon_scope,conflicting_taxon_scope, andpagination_cap_exceeded, each with a recovery hint - Sparse fields omitted rather than null-filled:
extinctappears only on taxa GBIF explicitly flags - Dataset metadata preserves temporal ranges, single dates, and verbatim periods such as
1975-current; unsupported coverage entries are disclosed. An unavailable supplementary record count carriesrecordCountNoticewhile retaining metadata - Publisher descriptions retain paragraph breaks and decode numeric character references once; Markdown output renders publisher text literally
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/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No credentials: the GBIF endpoints this server calls are public.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/gbif-biodiversity-mcp-server.git
- Navigate into the directory:
cd gbif-biodiversity-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optionally set GBIF_USER_AGENT to a contact URL or email
Configuration
| Variable | Description | Default |
|---|---|---|
GBIF_BASE_URL | GBIF API base URL. | https://api.gbif.org/v1 |
GBIF_CLAMP_READ_LIMITS | Clamp oversized whole-number read limits to the documented caps and disclose requested/applied values. Bulk matching processes the first 50 names and returns the remainder in unprocessedNames. Set false to advertise and enforce rejecting maxima instead. Strict boolean parsing rejects malformed values. | true |
GBIF_REQUEST_TIMEOUT_MS | Per-request HTTP timeout, in ms. | 10000 |
GBIF_USER_AGENT | User-Agent sent on every GBIF request. GBIF asks integrators to identify themselves with a contact URL or email. | server name, version, and repository URL |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. src/index.ts declares stateless; set this to override it. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
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:stdio -
Run 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-server
The 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 |
|---|---|
src/mcp-server/tools | Tool definitions (*.tool.ts) and shared tool utilities. Thirteen tools across species taxonomy, occurrences, datasets, and publishers. |
src/mcp-server/resources | Resource definitions. Species and dataset resources. |
src/services/gbif | GBIF API service layer: HTTP client with retry, timeouts, and error mapping, plus raw response types. |
src/config | Server-specific environment variable parsing and validation with Zod. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools and resources in the
createApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.