.codex-plugin — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited .codex-plugin (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<div align="center"> <h1>@cyanheads/gbif-biodiversity-mcp-server</h1> <p><b>Search GBIF species taxonomy, occurrence records, datasets, and publishers via MCP. STDIO or Streamable HTTP.</b> <div>13 Tools • 2 Resources</div> </p> </div>
<div align="center">
</div>
<div align="center">
</div>
<div align="center">
Public Hosted Server: https://gbif-biodiversity.caseyjhand.com/mcp
</div>
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.
| 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 <sub>tool</sub>name (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 <sub>tool</sub>names with strict; the first 50 are processed in input order. With read-limit clamping enabled (default), extra names are returned in unprocessedNames, preserving order and duplicates for the next callname 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 <sub>tool</sub>taxonKey; 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 <sub>tool</sub>q name fragment (scientific and vernacular), rank, isExtinct, and a checklist datasetKey (omit for the backbone); up to 1,000 per page with offsetkey, optional source datasetKey and mapped backbone nubKey, rank, taxonomicStatus, classification names, numOccurrences, and numDescendants; enrichment reports totalCount, endOfRecords, and the taxonScope applied. Occurrence queries use nubKey when supplied; otherwise resolve the name with gbif_match_species. Omitted datasetKey selects GBIF Backbone Taxonomy d7dddbf4-2cf0-4f39-9b2a-bb099caae36ckingdom, 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 <sub>tool</sub>taxonKey; 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 <sub>tool</sub>taxonKey, 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 <sub>tool</sub>taxonKey (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 pagegeometry with latitude or longitude ranges restricts results to their overlapkey, taxonKey, taxonomicStatus, coordinates, eventDate and eventTime, basisOfRecord, occurrenceStatus, iucnRedListCategory, datasetKey, and issues; enrichment reports totalCount, endOfRecords, and the occurrenceStatus appliedscope: effective supplied biological filters, with occurrenceStatus always included and pagination controls excluded. ANY is echoed but omitted upstream. taxonKey and scientificName combine with OR. All 11 GBIF basis-of-record values are accepted, including FOSSIL_SPECIMEN, OBSERVATION, and UNKNOWNoffset + 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 <sub>tool</sub>gbif_search_occurrences, with the same constraints and spatial intersection. isGeoreferenced remains an alias for hasCoordinate; either alone or both equal works, conflicting values fail as invalid_filtercount, 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 <sub>tool</sub>occurrenceKey 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 <sub>tool</sub>facet 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 facetOffsetgbif_search_occurrences, with the same constraints and spatial intersection. Repeat them on the facet call and each partition search to preserve the population. Returns totalOccurrences and counts ranked by count; a rejected scope value fails as invalid_filtertruncated, shown, and cap appear only when that bucket exists; advance facetOffset by 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 dataDATASET_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 <sub>tool</sub>q, 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 <sub>tool</sub>datasetKey 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 <sub>tool</sub>q 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} <sub>resource</sub>application/json: classification names, authorship, vernacularName, taxonomicStatus with acceptedKey and accepted, numDescendantstaxonKey is a whole signed decimal integer within the signed 32-bit range. 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 as not_found; malformed escapes, suffixes, fractions, exponents, hexadecimal forms, embedded whitespace and out-of-range values fail as invalid_filter before a request. Use a backbone key from gbif_match_species or a search result's nubKey, resolving its name when that mapping is absentgbif://dataset/{datasetKey} <sub>resource</sub>gbif_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 flags1975-current; unsupported coverage entries are disclosed. An unavailable supplementary record count carries recordCountNotice while retaining metadataA 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/mcpgit clone https://github.com/cyanheads/gbif-biodiversity-mcp-server.gitcd gbif-biodiversity-mcp-serverbun installcp .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_CLAMP_READ_LIMITS | Clamp oversized 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.
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suitedocker 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.
| 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 testThis project is licensed under the Apache 2.0 License. See the LICENSE file for details.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.