
Connects Claude to the Global Biodiversity Information Facility's 2.4 billion occurrence records and backbone taxonomy. Ships with 12 tools covering species name matching, taxonomic lookups, occurrence search with Darwin Core filters (country, bounding box, year, basis of record), dataset metadata, and publisher search. Match a species name to get a taxonKey, then query occurrences by geography or time. Fetch full classification chains, browse child taxa, or aggregate counts by facets like country or year. Useful for ecological research, biodiversity analysis, and any workflow that needs authoritative taxonomic resolution or specimen data. Available over stdio or streamable HTTP, with a public hosted endpoint at gbif.caseyjhand.com.
Search GBIF species taxonomy, occurrence records, datasets, and publishers via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://gbif-biodiversity.caseyjhand.com/mcp
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. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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, counts |
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 |
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, coverage |
gbif_search_publishers | Search GBIF-registered publishing organizations by name or country |
| 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 |
Both resources are also reachable via gbif_get_species and gbif_get_dataset; many MCP clients are tool-only and never surface resources.
gbif_match_species toolname (common names go to gbif_search_species), with optional strict, a kingdom to disambiguate, and an expected ranktaxonKey, the accepted taxon to pass to the occurrence tools, plus matchedTaxonKey when a synonym was resolved, matchType (EXACT, FUZZY, HIGHERRANK), confidence 0–100 (review below 80), and the classification with a key at each rankmatchType NONE fails as no_match; a blank kingdom fails as invalid_filtergbif_bulk_match_species toolnames per call, with strict; results come back in input ordername and matchType, and on a match taxonKey, matchedTaxonKey (synonyms), and confidence; an unmatched name is NONE, a failed lookup is ERROR with error and, when classified, reason, and neither fails the batchgbif_get_species tooltaxonKey; a key not in the backbone fails as not_found, one GBIF cannot parse (a fraction, or past the 32-bit range) as invalid_filterauthorship, vernacularName, numDescendants, numOccurrences, publishedIn, and parentKey; a taxonomicStatus of SYNONYM comes with acceptedKey and acceptedgbif_search_species toolq name fragment (scientific and vernacular), rank, isExtinct, and a checklist datasetKey (omit for the backbone); up to 1,000 per page with offsetkey, rank, taxonomicStatus, classification names, numOccurrences, and numDescendants; enrichment reports totalCount, endOfRecords, and the taxonScope appliedkingdom, family, and genus take names, matched exactly as GBIF capitalizes them; the narrowest one scopes, and kingdom beside a narrower one disambiguates it. An unknown name fails as unresolved_taxon_scope, a genus outside the named family as conflicting_taxon_scope, and a scope paired with a non-backbone datasetKey matches nothing. A blank filter or a malformed datasetKey fails as invalid_filtergbif_get_species_classification tooltaxonKey; returns classification root-first from kingdom to the immediate parent, each entry with key, rank, name, and scientificNamegbif_get_species); a root taxon returns an empty chain with a notice, an unknown key fails as not_found, and one GBIF cannot parse as invalid_filtergbif_get_species_children tooltaxonKey, up to 1,000 children per page with offset; no filters. An unknown key fails as not_found, one GBIF cannot parse as invalid_filterkey, canonicalName, rank, taxonomicStatus, vernacularName, numOccurrences, and numDescendants; GBIF reports no total, so endOfRecords (with truncated while more remain) is the continuation signalgbif_search_occurrences tooltaxonKey (preferred; scientificName does not match synonyms), country, publishingCountry, stateProvince, a decimalLatitude/decimalLongitude box or WKT geometry, year, month, basisOfRecord, hasCoordinate, isInCluster, coordinateUncertaintyInMeters, datasetKey, occurrenceStatus, iucnRedListCategory; up to 300 per pagekey, taxonKey, taxonomicStatus, coordinates, eventDate and eventTime, basisOfRecord, occurrenceStatus, iucnRedListCategory, datasetKey, and issues; enrichment reports totalCount, endOfRecords, and the occurrenceStatus appliedoffset + limit caps at 100,001 and GBIF has no cursor; a deeper request fails as pagination_cap_exceeded, and a larger match is reached by partitioning on a DATASET_KEY facet from gbif_occurrence_facets. A rejected filter value fails as invalid_filtergbif_count_occurrences tooltaxonKey, country, publishingCountry, stateProvince, isGeoreferenced, datasetKey, year, occurrenceStatus, iucnRedListCategorycount, with the occurrenceStatus applied in the enrichment; a count above 100,001 carries a notice naming the partition route, and a rejected filter value fails as invalid_filtergbif_get_occurrence tooloccurrenceKey from a search result; an unknown key fails as not_found, one GBIF cannot parse as invalid_filteroccurrenceStatus (ABSENT is a survey that found nothing, not a sighting), coordinates, GADM levels 0–3 under gadm, dates with eventTime, taxonomicStatus, iucnRedListCategory, institution, collection, and catalog codes, recordedBy and identifiedBy, media, identifiers, and issuesgbif_occurrence_facets toolfacet is one of COUNTRY, 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; up to 100 values per page (facetLimit, default 10), paged with facetOffsettaxonKey, country, publishingCountry, stateProvince, year, geometry, basisOfRecord, datasetKey, occurrenceStatus, iucnRedListCategory. Returns totalOccurrences and counts ranked by count; a full page sets truncated in the enrichment, and a rejected scope value fails as invalid_filterDATASET_KEY buckets sum to the scope's full total, so it is the facet for partitioning a search too large to page; YEAR, MONTH, STATE_PROVINCE, and SPECIES_KEY drop records that lack the field. Pass facet: OCCURRENCE_STATUS with occurrenceStatus: ANY for the presence/absence splitgbif_search_datasets toolq, type (OCCURRENCE, CHECKLIST, METADATA, SAMPLING_EVENT), publishingCountry, publishingOrg, hostingOrg; up to 1,000 per pagekey, title, type, license, doi, recordCount, and a 300-character description preview, with descriptionTruncated flagging a cut (gbif_get_dataset has the full text)publishingOrg matches the organization whose data it is and is the usual chain from gbif_search_publishers; hostingOrg matches 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 as invalid_filtergbif_get_dataset tooldatasetKey UUID and contactLimit (default 10, max 100; 0 drops contact detail but keeps contactsTotal); a malformed key fails as invalid_filter, an unknown one as not_founddescription, citationText, license, doi, recordCount, numConstituents, contacts with contactsTotal and contactsReturned, temporalCoverages, and geographicCoveragesgbif_search_publishers toolq name fragment and country (alpha-2 or alpha-3, any case); up to 1,000 per pagekey, title, country, and city; the key chains into gbif_search_datasets as publishingOrgq, an empty country, or a country GBIF cannot parse fails as invalid_filtergbif://species/{taxonKey} resourceapplication/json: classification names, authorship, vernacularName, taxonomicStatus with acceptedKey and accepted, numDescendantstaxonKey comes from gbif_match_species or gbif_search_species; an unknown key fails as not_found, a non-numeric or unparseable one as invalid_filtergbif://dataset/{datasetKey} resourcegbif_get_dataset record as application/json, with contacts fixed at 10datasetKey comes from gbif_search_datasets or an occurrence record; an unknown key fails as not_found, a malformed one as invalid_filterBuilt 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:
User-Agent on every requestgbif_match_species resolves a synonym to its accepted taxon, and the occurrence tools filter on that taxonKeyoccurrenceStatus to PRESENT, excluding absence records. Dataset recordCount spans every status, so it runs higher than a gbif_count_occurrences total for the same keygbif_search_datasets, country (where a record was observed) and publishingCountry (the publisher's country) accept only uppercase ISO 3166-1 alpha-2, because other forms silently match nothing there. stateProvince matches verbatim and case-sensitively, so take values from a STATE_PROVINCE facetinvalid_filter instead of widening the query. Omit a field to leave it offAgent-friendly output:
totalCount, endOfRecords, the occurrenceStatus applied, and the resolved taxonScope, with notices on empty results, over-cap totals, and a stateProvince that matched nothinggbif_bulk_match_species returns a NONE or ERROR row per name instead of failing the batchinvalid_filter, not_found, no_match, unresolved_taxon_scope, conflicting_taxon_scope, and pagination_cap_exceeded, each with a recovery hintextinct appears only on taxa GBIF explicitly flagsA 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"
}
}
}
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
git clone https://github.com/cyanheads/gbif-biodiversity-mcp-server.git
cd gbif-biodiversity-mcp-server
bun install
cp .env.example .env
# optionally set GBIF_USER_AGENT to a contact URL or email
| Variable | Description | Default |
|---|---|---|
GBIF_BASE_URL | GBIF API base URL. | https://api.gbif.org/v1 |
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.
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 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.
| 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. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagecreateApp() arrays in src/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
GBIF_API_KEYGBIF API key for higher rate limits. Sent as HTTP Basic Auth username.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.