{
  "markdown": "<div align=\"center\">\n  <h1>@cyanheads/gbif-biodiversity-mcp-server</h1>\n  <p><b>Search GBIF species taxonomy, occurrence records, datasets, and publishers via MCP. STDIO or Streamable HTTP.</b>\n  <div>13 Tools • 2 Resources</div>\n  </p>\n</div>\n\n<div align=\"center\">\n\n[![Version](https://img.shields.io/badge/Version-0.7.3-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/gbif-biodiversity-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/gbif-biodiversity-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/gbif-biodiversity-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)\n\n</div>\n\n<div align=\"center\">\n\n[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/gbif-biodiversity-mcp-server/releases/latest/download/gbif-biodiversity-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=gbif-biodiversity-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvZ2JpZi1iaW9kaXZlcnNpdHktbWNwLXNlcnZlciJdfQ==) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22gbif-biodiversity-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fgbif-biodiversity-mcp-server%22%5D%7D)\n\n[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)\n\n**Public Hosted Server:** [https://gbif-biodiversity.caseyjhand.com/mcp](https://gbif-biodiversity.caseyjhand.com/mcp)\n\n</div>\n\n---\n\n## Tools\n\n13 tools for working with GBIF species taxonomy, occurrence records, datasets, and publishers:\n\n| Tool | Description |\n|:---|:---|\n| `gbif_match_species` | Match a species name against the GBIF backbone taxonomy — returns taxonKey, confidence score, and full classification |\n| `gbif_bulk_match_species` | Match up to 50 scientific names to backbone taxon keys in one call — results in input order, per-name NONE/ERROR isolation |\n| `gbif_get_species` | Fetch a single backbone taxon by key — full classification, authorship, synonymy, vernacular name, descendant count |\n| `gbif_search_species` | Search or browse the GBIF backbone taxonomy by name fragment, rank, or a kingdom, family, or genus name resolved to its backbone key |\n| `gbif_get_species_classification` | 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) |\n| `gbif_get_species_children` | List direct children of a backbone taxon — genera within a family, species within a genus |\n| `gbif_search_occurrences` | 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 |\n| `gbif_count_occurrences` | Count occurrences matching a filter without fetching records — fast single-number response, filtered to sightings by default |\n| `gbif_get_occurrence` | Fetch a single occurrence record by key — full Darwin Core record with GADM geography, presence/absence status, conservation status, media, and quality flags |\n| `gbif_occurrence_facets` | Aggregate occurrence counts by a dimension — country, year, basis of record, dataset, kingdom, presence/absence, IUCN Red List category |\n| `gbif_search_datasets` | Search GBIF datasets by keyword, type, country, publishing organization, or hosting organization |\n| `gbif_get_dataset` | Fetch full dataset metadata by UUID — title, description, citation, contacts, license, DOI, coverage |\n| `gbif_search_publishers` | Search GBIF-registered publishing organizations by name fragment or country |\n\n### `gbif_match_species`\n\nMatch a scientific or common name against the GBIF backbone taxonomy.\n\n- Fuzzy matching handles minor typos and vernacular names; set `strict: true` for exact-only matching\n- Returns `taxonKey` — the backbone key required by `gbif_search_occurrences`, `gbif_count_occurrences`, and `gbif_occurrence_facets`\n- Confidence score 0–100; below 80 warrants review\n- Full classification hierarchy with keys at each rank: kingdom, phylum, class, order, family, genus, species\n- `matchType NONE` indicates no usable match — try removing strict mode or broadening the name\n- Resolves synonyms: always returns the accepted backbone key regardless of which name form was queried; `matchedTaxonKey` carries the synonym's own key when the two differ\n- `kingdom` disambiguates 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 major` resolves to taxon 9705453 with `kingdom=` exactly as it does with no `kingdom` at all, where `Plantae` resolves to 9711704 — so the undisambiguated answer would come back looking like a disambiguated one. Omit the field to match against the whole backbone\n\n---\n\n### `gbif_bulk_match_species`\n\nMatch up to 50 scientific names against the GBIF backbone taxonomy in a single call.\n\n- The batch counterpart to `gbif_match_species` — built for checklist, inventory, and species-list workflows that would otherwise need one round trip per name\n- Returns one result per input name, in input order; each carries `taxonKey`, `matchType`, and confidence\n- Per-name isolation: an unmatched name yields `matchType NONE` and a per-name lookup failure yields `matchType ERROR` with the message and, when the failure was classified, a `reason` — neither sinks the rest of the batch\n- Same synonym resolution as `gbif_match_species`: `taxonKey` is the accepted taxon, `matchedTaxonKey` the synonym it was queried under\n- `strict: true` requires an exact match for every name; common names are not supported (use `gbif_search_species`)\n\n---\n\n### `gbif_get_species`\n\nFetch a complete taxon record by GBIF backbone key.\n\n- Full classification, authorship string, and vernacular (English) name when available\n- `taxonomicStatus`: ACCEPTED, SYNONYM, DOUBTFUL — when SYNONYM, `acceptedKey` and `accepted` identify the current name\n- `numDescendants` and `numOccurrences` for scope at a glance\n- `extinct` field present only when explicitly flagged — not false on unlabeled taxa\n- `publishedIn` carries the original description citation when available\n\n---\n\n### `gbif_search_species`\n\nSearch or browse the GBIF backbone taxonomy.\n\n- Accepts name fragments matching scientific and vernacular names\n- Filter by rank, kingdom, family, or genus to scope browsing\n- `kingdom`, `family`, and `genus` are given as names and resolved to a backbone key before the search runs, since `/species/search` scopes by key alone. The narrowest one supplied is what scopes — the three nest, and GBIF combines two keys with OR rather than AND. `kingdom` supplied beside `family` or `genus` disambiguates that name instead of scoping on its own: `Prunella` names both a bird genus and a plant genus and resolves to neither without it\n- Names are matched exactly and capitalized as GBIF writes them, so `paridae` and `Paridaee` fail as `unresolved_taxon_scope` rather than being ignored. An alternative family name lands on the taxon it is a synonym of — `Compositae` scopes to Asteraceae. A `family` and `genus` in different lineages fail as `conflicting_taxon_scope`. The scope actually applied comes back in the enrichment\n- `isExtinct` filter for extinct vs. extant taxa\n- Scope 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 pairing `datasetKey` with a kingdom, family, or genus matches nothing unless the checklist is the backbone; the empty-result notice says so\n- `q`, `kingdom`, `family`, `genus`, and `datasetKey` are rejected when supplied blank rather than dropped: a blank `datasetKey` returns the unfiltered backbone result, and `q=` returns the whole 46,623,754-name index where `q=` with a space returns nothing. Omit a filter to leave it off — see the note under `gbif_search_occurrences`\n- Paginated — limit up to 1000, use offset to walk through large groups\n\n---\n\n### `gbif_get_species_classification`\n\nReturn the root-to-parent classification chain for a taxon as an ordered array.\n\n- Root-first from kingdom down to the immediate parent of the queried taxon (kingdom → phylum → class → … → parent)\n- The queried taxon itself is not included — use `gbif_get_species` for its own record\n- Each entry: rank, canonical name, scientific name, taxon key\n- Useful for building taxonomic trees or placing an unfamiliar taxon in context without manual backbone navigation\n\n---\n\n### `gbif_get_species_children`\n\nList direct children of a backbone taxon.\n\n- Genera within a family, species within a genus, subspecies within a species\n- Each child: key, name, rank, taxonomic status, common name, occurrence count, descendant count\n- Paginated — limit up to 1000, iterate with offset for large groups like Coleoptera\n\n---\n\n### `gbif_search_occurrences`\n\nSearch 3.9B+ GBIF occurrence records with full Darwin Core filtering.\n\n- Use `taxonKey` from `gbif_match_species` for reliable results — resolves synonyms automatically; `scientificName` filter does not\n- Geographic filters: `country` (ISO 3166-1 alpha-2, uppercase), `stateProvince`, bounding box (`decimalLatitude`/`decimalLongitude` ranges as \"min,max\"), or WKT polygon (`geometry`)\n- `publishingCountry` (ISO 3166-1 alpha-2, uppercase) is the country of the *publishing organization*, not of the observation — a different question from `country`, and the two disagree on most records: of 60,290,950 records observed in GB, 1,548,928 were published by US organizations\n- Both country filters take the uppercase two-letter form only. GBIF parses a lowercase or alpha-3 code and then matches the verbatim string, so `gb` and `USA` return 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 as `invalid_filter`\n- `stateProvince` is matched verbatim — exact and case-sensitive, with no controlled vocabulary behind it. Take a value from a `STATE_PROVINCE` facet rather than guessing; an unmatched value returns zero records instead of an error, and the enrichment notice says so when it happens\n- Every 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 a `taxonKey=212` + `country=GB` scope where `England` returns 47,672,439 — the same total, and the same first record, as a call that sends no `stateProvince` at all. A blank or whitespace-only value now fails as `invalid_filter` instead. Omit a field to leave that filter off\n- Temporal filters: `year` as single year or range, `month` (1–12) for seasonal queries\n- `basisOfRecord` enum: `HUMAN_OBSERVATION`, `PRESERVED_SPECIMEN`, `MACHINE_OBSERVATION`, and more\n- `hasCoordinate` to require or exclude georeferenced records\n- `occurrenceStatus` — `PRESENT` (default), `ABSENT`, or `ANY`. 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 call\n- `iucnRedListCategory` — `CR`, `EN`, `VU`, `NT`, `LC`, `DD`, `EX`, `EW`, `CD`\n- Output per record adds `taxonomicStatus`, `eventTime` (with UTC offset), `occurrenceStatus`, and `iucnRedListCategory`\n- Pagination 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_KEY` with `gbif_occurrence_facets`, then search each `datasetKey` on 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\n\n---\n\n### `gbif_count_occurrences`\n\nCount occurrences matching a filter without fetching any records.\n\n- Backed by `/occurrence/search` at `limit=0` — no record payload, and the same endpoint `gbif_search_occurrences` queries, so the two agree on the same question. GBIF's dedicated `/occurrence/count` endpoint takes a closed parameter set that rejects `occurrenceStatus` and `iucnRedListCategory` outright\n- Supported filters: `taxonKey`, `country`, `publishingCountry`, `stateProvince`, `isGeoreferenced`, `datasetKey`, `year`, `occurrenceStatus`, `iucnRedListCategory`. `country` and `publishingCountry` take the uppercase alpha-2 form only, and a blank filter is rejected rather than dropped, both for the reasons given under `gbif_search_occurrences`\n- Counts 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 presences\n- Use 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\n\n---\n\n### `gbif_get_occurrence`\n\nFetch a single occurrence record by GBIF occurrence key.\n\n- Complete Darwin Core record — all coordinate fields, administrative geography (continent, country, state/province, locality), dates\n- `occurrenceID`, full classification (`class`/`classKey`), GADM administrative units (levels 0–3, each with a stable GID and name), and source `identifiers`\n- `occurrenceStatus` — check it before reading the record as a sighting; `ABSENT` means a survey looked and found nothing, and the record still carries coordinates, a date, and a recorder\n- `taxonomicStatus`, `eventTime` (with UTC offset), and `iucnRedListCategory`\n- Collections metadata: institution code, collection code, catalog number\n- Collector and identifier names, individual count, sex, life stage\n- Associated media (images, audio, video) with URLs and license\n- GBIF data quality issue flags for provenance assessment\n\n---\n\n### `gbif_occurrence_facets`\n\nAggregate occurrence counts across a dimension.\n\n- 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_CATEGORY`\n- Scope with `taxonKey`, `country`, `publishingCountry`, `stateProvince`, `year`, `geometry`, `basisOfRecord`, `datasetKey`, `occurrenceStatus`, or `iucnRedListCategory` filters — so a `COUNTRY`, `PUBLISHING_COUNTRY`, or `STATE_PROVINCE` bucket can be passed straight back to drill into it. `country` and `publishingCountry` take the uppercase alpha-2 form only, and a blank filter is rejected rather than dropped, both for the reasons given under `gbif_search_occurrences`\n- Aggregates sightings only by default, matching the search and count tools. To measure the presence/absence split itself, pass `facet: OCCURRENCE_STATUS` with `occurrenceStatus: ANY`\n- Returns one page of values ranked by count descending — up to `facetLimit` (max 100), the top ones only while `facetOffset` is 0 — with no record payloads\n- `DATASET_KEY` is the basis for splitting a result set too large for `gbif_search_occurrences` to page: every occurrence carries exactly one `datasetKey`, 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_RECORD` and `PUBLISHING_COUNTRY` are 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 against `DATASET_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 by `YEAR` sums to 59,407,400, leaving 883,550 undated records out, and `MONTH`, `STATE_PROVINCE`, and `SPECIES_KEY` behave the same way — `STATE_PROVINCE` included, even though the occurrence tools can filter on it\n- The scope filters here are narrower than `gbif_search_occurrences` accepts — no `scientificName`, `month`, bounding box, `hasCoordinate`, `isInCluster`, or `coordinateUncertaintyInMeters`. Buckets sum to a search's total only when both calls carry the same filters; re-apply the rest on each per-`datasetKey` search\n- Page past the first `facetLimit` with `facetOffset` (advance by `facetLimit` per page) to walk high-cardinality facets like `DATASET_KEY`; enrichment echoes the applied `facetOffset` and sets `moreValuesLikely` when a full page suggests more values remain\n- Core tool for distribution analysis (\"which countries have the most records?\") and trend queries (\"how has observation volume changed since 2010?\")\n\n---\n\n### `gbif_search_datasets`\n\nSearch GBIF datasets by keyword, type, country, publishing organization, or hosting organization.\n\n- Filters: free-text query, dataset type (`OCCURRENCE`, `CHECKLIST`, `METADATA`, `SAMPLING_EVENT`), `publishingCountry` (ISO 3166-1 alpha-2, uppercase), publishing and hosting organization UUIDs (lowercase)\n- `publishingCountry` takes the uppercase two-letter form only, for the same reason the occurrence filters do: `/dataset/search` matches the verbatim stored code, so `gb` and `GBR` return zero datasets where `GB` returns 2,416. A two-letter code GBIF does not assign (`XX`) is rejected upstream by name and surfaces as `invalid_filter`\n- Returns title, type, description, license, DOI, and record count. `recordCount` spans every `occurrenceStatus`, absences included — `gbif_count_occurrences` with the same key counts sightings only by default, so the two figures differ by design\n- The `description` is a 300-character preview — `descriptionTruncated` flags when it was shortened, and `gbif_get_dataset` returns the full text\n- An organization key from `gbif_search_publishers` chains in two ways, and they answer different questions. `publishingOrg` matches the organization whose data it is; `hostingOrg` matches the organization whose installation serves it. `publishingOrg` is 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 1\n- Supplying 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\n- 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 does\n- The free-text `q` is rejected blank on the same ground, and the route makes the case plainly: `q=` returns all 123,527 datasets while `q=` 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 term\n- Paginated — limit up to 1000\n\n---\n\n### `gbif_get_dataset`\n\nFetch full dataset metadata by UUID.\n\n- Full description, citation text (for academic reference), license, DOI\n- Contacts with role, name, organization, and email\n- Temporal and geographic coverage ranges when the publisher declares them\n- `recordCount` — the indexed occurrence total, matching what `gbif_search_datasets` reports, for every dataset type (a `CHECKLIST` reports 0). It spans every `occurrenceStatus`, absences included; `gbif_count_occurrences` with the same key counts sightings only by default, so the two figures differ by design\n- `numConstituents` for aggregate datasets (e.g. iNaturalist, eBird)\n- Use after `gbif_search_datasets` or when an occurrence record's `datasetKey` needs provenance detail\n\n---\n\n### `gbif_search_publishers`\n\nSearch organizations registered with GBIF.\n\n- Filter by name fragment or country\n- `country` here is unconstrained on purpose: the registry endpoint matches the *parsed* country rather than the verbatim string, so `gb`, `GBR`, and `GB` all return the same 223 organizations. The uppercase-only rule the occurrence tools and `gbif_search_datasets` carry exists to close a silent zero that does not occur on this route. One value is rejected — an empty `country`, 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 with `400 Cannot parse … into a known Country`, an error naming the value\n- `q` is rejected blank in both forms, because the registry drops either one and answers with all 3,561 organizations against 460 for `museum`; omit the field to browse without a name term\n- Returns organization key, title, and country. The key chains into `gbif_search_datasets` as `publishingOrg` for the datasets the organization published, or as `hostingOrg` for the ones its own installation serves. `publishingOrg` is the usual chain: of the first 25 GB organizations the registry lists, all 25 host no datasets while 13 publish one or two\n- Paginated — limit up to 1000\n\n## Resources\n\n| Type | Name | Description |\n|:---|:---|:---|\n| Resource | `gbif://species/{taxonKey}` | Taxon record from the GBIF backbone — classification, authorship, synonymy status, vernacular name |\n| Resource | `gbif://dataset/{datasetKey}` | Dataset metadata — title, description, citation, license, contacts, coverage |\n\n## Features\n\nBuilt on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core):\n\n- Declarative tool definitions — single file per tool, framework handles registration and validation\n- Unified error handling across all tools\n- Pluggable auth (`none`, `jwt`, `oauth`)\n- Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`\n- Structured logging with optional OpenTelemetry tracing\n- Runs locally (stdio/HTTP) or on Cloudflare Workers from the same codebase\n\nGBIF-specific:\n\n- Full GBIF REST API v1 coverage: species taxonomy, occurrences, datasets, and publishers\n- `gbif_match_species` as the entry point — resolves synonyms to backbone taxon keys used throughout\n- Occurrence pagination guarded at GBIF's own offset+limit = 100,001 boundary — an over-cap request fails locally with a recovery hint naming the `DATASET_KEY` partition 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 them\n- WKT polygon geometry support for geographic occurrence queries\n- Darwin Core field mapping with explicit provenance on sparse upstream fields\n\nAgent-friendly output:\n\n- `gbif_match_species` is the mandatory first step — all downstream tools document which key they expect\n- Graceful sparse-field handling — optional fields absent from the API response are omitted rather than null-filled\n- Discriminated error contracts with typed reasons, structured recovery hints, and `when` documentation per tool\n\n## Getting started\n\n### Self-Hosted / Local\n\nAdd the following to your MCP client configuration file.\n\n```json\n{\n  \"mcpServers\": {\n    \"gbif-biodiversity-mcp-server\": {\n      \"type\": \"stdio\",\n      \"command\": \"bunx\",\n      \"args\": [\"@cyanheads/gbif-biodiversity-mcp-server@latest\"],\n      \"env\": {\n        \"MCP_TRANSPORT_TYPE\": \"stdio\",\n        \"MCP_LOG_LEVEL\": \"info\"\n      }\n    }\n  }\n}\n```\n\nOr with npx (no Bun required):\n\n```json\n{\n  \"mcpServers\": {\n    \"gbif-biodiversity-mcp-server\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@cyanheads/gbif-biodiversity-mcp-server@latest\"],\n      \"env\": {\n        \"MCP_TRANSPORT_TYPE\": \"stdio\",\n        \"MCP_LOG_LEVEL\": \"info\"\n      }\n    }\n  }\n}\n```\n\nOr with Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"gbif-biodiversity-mcp-server\": {\n      \"type\": \"stdio\",\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\", \"-e\", \"MCP_TRANSPORT_TYPE=stdio\", \"ghcr.io/cyanheads/gbif-biodiversity-mcp-server:latest\"]\n    }\n  }\n}\n```\n\nFor Streamable HTTP, set the transport and start the server:\n\n```sh\nMCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http\n# Server listens at http://localhost:3010/mcp\n```\n\n### Prerequisites\n\n- [Bun v1.3.0](https://bun.sh/) or higher.\n- No credentials — the GBIF endpoints this server calls are public.\n\n### Installation\n\n1. **Clone the repository:**\n\n```sh\ngit clone https://github.com/cyanheads/gbif-biodiversity-mcp-server.git\n```\n\n2. **Navigate into the directory:**\n\n```sh\ncd gbif-biodiversity-mcp-server\n```\n\n3. **Install dependencies:**\n\n```sh\nbun install\n```\n\n## Configuration\n\nAll configuration is validated at startup via Zod schemas in `src/config/server-config.ts`. Key environment variables:\n\n| Variable | Description | Default |\n|:---|:---|:---|\n| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |\n| `MCP_HTTP_PORT` | HTTP server port | `3010` |\n| `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path where the MCP server is mounted | `/mcp` |\n| `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments | none |\n| `MCP_SESSION_MODE` | HTTP session mode: `stateful`, `stateless`, or `auto`. `auto` resolves to `stateful`; the Docker image sets `stateless`. | `auto` |\n| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth` | `none` |\n| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.) | `info` |\n| `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in Bun-only forced-GC pressure loop (ms). Try `60000` if RSS grows under sustained HTTP load. | `0` (disabled) |\n| `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |\n| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |\n| `GBIF_BASE_URL` | GBIF API base URL override | `https://api.gbif.org/v1` |\n| `GBIF_REQUEST_TIMEOUT_MS` | HTTP request timeout in milliseconds | `10000` |\n| `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 |\n| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |\n\n## Running the server\n\n### Local development\n\n- **Build and run the production version**:\n\n  ```sh\n  # One-time build\n  bun run rebuild\n\n  # Run the built server\n  bun run start:http\n  # or\n  bun run start:stdio\n  ```\n\n- **Run checks and tests**:\n  ```sh\n  bun run devcheck  # Lints, formats, type-checks, and more\n  bun run test      # Runs the test suite\n  ```\n\n## Project structure\n\n| Directory | Purpose |\n|:---|:---|\n| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Thirteen tools across species taxonomy, occurrences, datasets, and publishers. |\n| `src/mcp-server/resources` | Resource definitions. Species and dataset stable-URI resources. |\n| `src/services/gbif` | GBIF REST API service layer — client, request handling, type definitions. |\n| `src/config` | Server-specific environment variable parsing and validation with Zod. |\n| `tests/` | Unit and integration tests, mirroring the `src/` structure. |\n\n## Development guide\n\nSee [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:\n\n- Handlers throw, framework catches — no `try/catch` in tool logic\n- Use `ctx.log` for logging, `ctx.state` for storage\n- Register new tools and resources in the `createApp()` arrays\n\n## Contributing\n\nIssues and pull requests are welcome. Run checks and tests before submitting:\n\n```sh\nbun run devcheck\nbun run test\n```\n\n## License\n\nThis project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.\n",
  "bytes": 29003,
  "sha": "46eece347a7680b642cd0a3b1568addb33c255eba9e93e6db0ec9a0655aaeb5b",
  "repo_slug": "cyanheads/gbif-mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_cyanheads_gbif_mcp_server_a6224696/readme"
}