About inaturalist-mcp-server
inaturalist-mcp-server connects MCP clients to iNaturalist’s wildlife observation data. It supports searches, identification context, seasonal patterns, species rankings, and taxon profiles without requiring an API key.
Key features
- 01Search observations by place, date, taxon, and filters
- 02Retrieve observations with identification threads
- 03Rank species and contributor activity
- 04Chart taxon records by season or date
- 05Find commonly confused taxa and taxon profiles
Use cases
- 01Find wildlife records for a region and date range
- 02Review community identifications for an observation
- 03Compare seasonal sighting patterns for a taxon
- 04Explore common identification mix-ups
MCP support
Exposes 10 tools and two domain resources to MCP clients. Supports STDIO and Streamable HTTP transports.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://inaturalist.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-inaturalist https://inaturalist.caseyjhand.com/mcp{"mcpServers":{"cyanheads-inaturalist":{"type":"http","url":"https://inaturalist.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-inaturalist":{"type":"http","url":"https://inaturalist.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-inaturalist":{"type":"http","url":"https://inaturalist.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-inaturalist":{"serverUrl":"https://inaturalist.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
10| Parameter | Type | Required | Description |
|---|---|---|---|
| topic | enum | Yes | Which vocabulary to decode. controlled_terms is fetched live and cached; the rest are spec-derived static tables.controlled_terms | quality_grades | licenses | ranks | iconic_taxa | conservation_status_codes |
| taxon_id | integer | No | Add observed annotation usage for this taxon, ranked by how often each attribute/value pair has been recorded. Valid only with topic controlled_terms. Resolve a name to an id with inaturalist_resolve_name. |
Details
From the project README@cyanheads/inaturalist-mcp-server
Search wildlife sightings, read identification threads, rank species by area, chart phenology, and find look-alike taxa from iNaturalist via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://inaturalist.caseyjhand.com/mcp
Overview
iNaturalist's index of 387M+ georeferenced citizen-science observations of plants, animals, and fungi. Search sightings by area, date, taxon, and annotation; read the identification thread behind a record; chart when a taxon appears; rank the species of an area; and check what a look-alike is most often confused with. No API key required.
Tools
| Tool | Description |
|---|---|
inaturalist_list_reference | Decode the controlled vocabularies the other tools filter on: annotation attributes and values, quality grades, licences, ranks, iconic taxa, conservation-status codes |
inaturalist_resolve_name | Resolve a common or scientific name to a taxon id, or a place, project, or observer name to its id, as ranked candidates |
inaturalist_find_places | Resolve a place name to a place id, or list the places covering a map area, each with its bounding box and containment chain |
inaturalist_search_observations | Search georeferenced sightings by area, date, taxon, quality grade, annotation, conservation status, observer, project, and licence |
inaturalist_get_observation | Fetch up to 10 observations by id with their community identification thread and consensus taxon |
inaturalist_get_species_counts | Rank the distinct species recorded in an area and period, most-observed first, optionally for one observer or project |
inaturalist_get_histogram | Build a phenology histogram for a taxon in an area: which months, weeks, or years it is recorded in |
inaturalist_get_leaderboard | Rank the most active observers or identifiers for an area, period, and taxon |
inaturalist_get_similar_species | List the taxa a genus-or-finer taxon is most often misidentified as, ranked by correction count |
inaturalist_get_taxon | Fetch a taxon profile: taxonomic path, conservation listings by authority, encyclopedia summary, photos, and children |
Resources
| Resource | Description |
|---|---|
inaturalist://taxa/{taxon_id} | Taxon profile by numeric taxon id, as injectable context |
inaturalist://observations/{observation_id} | One observation with its identification thread expanded, as injectable context |
Both resources mirror data also reachable through inaturalist_get_taxon and inaturalist_get_observation, for clients that don't surface MCP resources.
The published inaturalist-guide covers identifier provenance, matched cohorts, annotations, and evidence coverage across calls. It ships with npm, plugin, MCPB, and Docker installs; clients supporting MCP skill discovery can retrieve it directly. The SDK also exposes skill resources alongside the two domain resources.
Capability reference
inaturalist_list_reference tool
topicpicks one table:controlled_terms(fetched live),quality_grades,licenses,ranks,iconic_taxa, orconservation_status_codes;taxon_id, valid only withcontrolled_terms, addsobserved_usage, the annotation pairs recorded for that taxon with countssourceisupstreamorstatic; annotation attributes and their values carry numeric ids to pass asterm_idandterm_value_id, since labels repeat across attributes
inaturalist_resolve_name tool
qplustype:taxon(default; name-prefix autocomplete, so "monarch" hits where "monarch butterfly" misses, with an optionalrank) orplace/project/user/any(scored cross-kind search); appliedlimitup to 30, default 10- Each candidate carries
kindandid, plusloginon users; a miss returnsfound: falsewithguidanceinstead of an error
inaturalist_find_places tool
- Exactly one of
q(place-name prefix) or all four ofnelat,nelng,swlat,swlng, otherwiseinvalid_geography; appliedper_pageup to 30 (default 10) applies to the box arm only, per list qreturnsplaces[], a box returnsstandard[]andcommunity[]; each place carriesid,bbox,ancestor_place_ids,place_type,admin_level, andlocation, with boundary polygons stripped
inaturalist_search_observations tool
- One area form plus
taxon_id,d1/d2,quality_grade,captive,term_id+term_value_id,iconic_taxa,hrank/lrank,csi,threatened/native/introduced/endemic,user_idoruser_login,project_id, andq+search_on; appliedper_pageup to 25, default 10; optionalsoundsfilters for audio-bearing records, whileincludeexpandsphotos,annotations,sounds(identification threads live oninaturalist_get_observation) pagewalks the first 10,000 results; past that, order byiddescending and pass each page'snext_cursorascursor. Returnstotal_results(a live estimate) andhas_more, and echoesapplied_filterslicense/photo_licensetake the seven CC codes, OR-joined;photo_licensematches any photo under the code, independent of the record's ownlicense_code- The complete serialized reply stays within 50,000 UTF-8 bytes by returning a prefix of whole records. Ordered
deferredobservation IDs identify the suffix; fetch them withinaturalist_get_observationand the sameincludebefore following the upstream page continuation
inaturalist_get_observation tool
- Submit 1–100
observation_ids: duplicates are removed in first-seen order, the first ten distinct IDs are attempted in one upstream request, and the rest return indeferred; with limit clamping disabled, at most ten submitted IDs are accepted.includedefaults to["identifications"], withcomments,photos,annotations, andsoundsavailable - Resolved records come back in
observationsin request order, attempted misses inunresolved; fails asnot_foundonly when none resolve and nothing is deferred. Addscommunity_taxon,description, and filledobservation_fields; strings remain complete, so detail replies have no universal byte ceiling - Identifications, comments, and observation fields share a 40-entry budget: each record keeps
max(4, floor(40 / records returned))entries per array and reports*_totalbeside*_shown
inaturalist_get_species_counts tool
- One area form plus the search filters for taxon, dates, quality grade, captive, annotations, iconic taxa, observer, and project; applied
per_pageup to 50, default 25, withpageoffset - Rows carry
position(absolute, counted from page 1),taxon_id, andobservation_count;truncationCeilingis the last count shown, which nothing off the page exceeds
inaturalist_get_histogram tool
interval:month_of_year(default) orweek_of_yearfor a seasonal curve, oryear/month/week/day/hourfor absolute dates;date_fieldobserved(default) orcreated;taxon_idoptional, and an annotation pair (Life Stage1= Larva6) oriconic_taxanarrows the curve- Returns every bucket in order, zeros included, plus a
totalacross all of them; capped at 800 buckets from the start of the range, withtruncated/shown/cap d1/d2bound observation dates;created_d1/created_d2separately bound upload dates.date_fieldselects which date is bucketed without changing either filter pair
inaturalist_get_leaderboard tool
kind:observersoridentifiers, with one area form,taxon_id,d1/d2, andquality_grade; appliedper_pageup to 250, default 25- Only the top 500 are addressable, so
page × per_pagepast 500 fails asleaderboard_window_exceeded; entries carryrank,login, andcount(measured percount_metric), plusspecies_countfor observers - A full final page reports
truncated: falsewhen the matching set is exhausted
inaturalist_get_similar_species tool
taxon_idat genus or finer, otherwisetaxon_rank_too_coarse; an optional area,d1/d2,quality_grade, andcaptivescope the set to a region; appliedlimitup to 50, default 20- Ranked by
misidentification_count, withtruncationCeilingwhen the limit cuts the set
inaturalist_get_taxon tool
- One
taxon_id;sectionsselects fromsummary,taxonomy,children,conservation,photos,encyclopedia, and an unknown name fails asunknown_section kind: "full"carries the profile;kind: "outline"lists each section with its byte size when the profile overflows. A named section always comes back whole, so sum the outline's sizes before asking for several
inaturalist://taxa/{taxon_id} resource
- The whole projected taxon document as
application/json; a resource read can't name sections, so useinaturalist_get_taxonfor the outline path taxon_idcomes frominaturalist_resolve_name; cached for six hours
inaturalist://observations/{observation_id} resource
- One observation as
application/jsonwith its first 40 identifications and first 40 filled observation fields, each with its_totalbeside its_shown observation_idcomes frominaturalist_search_observations; cached for fifteen minutes
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.
iNaturalist-specific:
- Keyless, read-only coverage of the iNaturalist v1 API. Upstream ignores its own
fields=parameter, so every response is projected in-process (a common taxon record arrives at 95 KB) - Area-scoped tools take one area form:
place_id,lat+lng+radiusin kilometres (up to 500), or a four-corner box. Partial or mixed forms fail asinvalid_geography, andd1afterd2asinverted_date_range - Per-endpoint parameter allowlist, plus in-process rejection of input upstream would silently widen or zero out: a lone
term_value_id, an impossible date, an unknown licence code - Self-paced traffic: spaced request starts, capped concurrency, and a per-UTC-day budget that fails as
rate_budget_exhausted. The counter is per process, so every caller of the hosted instance shares it. Taxa, places, controlled terms, histograms, and look-alike sets are cached; observation queries are not - Licences relayed verbatim: a null
license_codemeans all rights reserved, photoattributionmust travel with the image, and photos are linked, never proxied. Anobscuredcoordinate is a locality, and observers are identified byloginonly
Agent-friendly output:
- Applied defaults echoed: search, species counts, histograms, and leaderboards return
applied_filters(research-grade and wild-only by default, plus any ordering a cursor forced).captive: trueselects captive or cultivated records only;falseselects wild records only - Zero-hit notices name the filter most likely responsible and the tool that decodes it (
inaturalist_list_referencefor vocabularies, since an unrecognised value returns nothing); a page past the end names the last page with results - Truncation disclosed on every path:
truncated,shown, andcap, plustruncationCeilingon descending rankings - Empty filter arrays are ignored with a notice; nonempty annotation values still require a nonempty attribute array.
include: []retains its explicit expansion selection - An unknown
place_idreturns typedunknown_place_idguidance to resolve it withinaturalist_find_places - Upstream free text renders as literal quoted content, preserving line separators, HTML, entities, and complete strings after CommonMark parsing. NUL-containing strings use a labeled JSON string literal; URL fields render as links. Present empty expansions, zero counts, and null or empty descriptions remain visible
Getting started
Public Hosted Instance
A public instance is available at https://inaturalist.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"inaturalist-mcp-server": {
"type": "streamable-http",
"url": "https://inaturalist.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"inaturalist-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/inaturalist-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"inaturalist-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/inaturalist-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"inaturalist-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/inaturalist-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 API key: the iNaturalist v1 API is keyless. The published terms ask clients to identify themselves, so a
User-Agentwith a contact URL is sent by default; keep one in anyINATURALIST_USER_AGENToverride.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/inaturalist-mcp-server.git
- Navigate into the directory:
cd inaturalist-mcp-server
- Install dependencies:
bun install
- Configure environment (optional):
cp .env.example .env
# edit .env to override defaults — no required vars
Configuration
| Variable | Description | Default |
|---|---|---|
INATURALIST_USER_AGENT | User-Agent sent on every request to api.inaturalist.org. Keep a contact URL in any override. | inaturalist-mcp-server/<version> (+<repo url>) |
INATURALIST_MIN_REQUEST_INTERVAL_MS | Minimum gap between outbound request starts, in ms. The default holds the rate near 54 per minute. | 1100 |
INATURALIST_MAX_CONCURRENT_REQUESTS | Maximum outbound requests in flight. | 4 |
INATURALIST_DAILY_REQUEST_BUDGET | Outbound requests allowed per UTC day, counted in-process and reset on restart. | 9000 |
INATURALIST_CLAMP_LIMITS | Strict boolean. Clamp oversized list limits to 30/30/25/50/50/250 for name resolution, nearby places, search, species counts, similar species, and leaderboards, with a notice, log, and counter. false restores the prior advertised schema caps and rejects oversized input. | true |
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. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, notice, warning, error). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend for the response cache: 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:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t inaturalist-mcp-server .
docker run --rm -p 3010:3010 inaturalist-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/inaturalist-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: identity, server instructions, tool and resource registration, service init. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) plus the shared filter, observation-record, and taxon-document helpers. |
src/mcp-server/resources | Resource definitions (*.resource.ts): taxon and observation. |
src/services/inaturalist | iNaturalist service layer: allowlisted client, pacer, daily budget, cache, and response projections. |
tests/ | Unit, integration, and smoke 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.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.