Search iNaturalist sightings, identification threads, phenology, and look-alike species.
One address for any MCP client. No sign-in needed on this server.
10 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
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
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. Keyless and read-only, it runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 |
| 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.
inaturalist_list_reference tooltopic picks one table: controlled_terms (fetched live), quality_grades, licenses, ranks, iconic_taxa, or conservation_status_codes; taxon_id, valid only with controlled_terms, adds observed_usage, the annotation pairs recorded for that taxon with countssource is upstream or static; annotation attributes and their values carry numeric ids to pass as term_id and term_value_id, since labels repeat across attributesinaturalist_resolve_name toolq plus type: taxon (default; name-prefix autocomplete, so "monarch" hits where "monarch butterfly" misses, with an optional rank) or place / project / user / any (scored cross-kind search); limit 1–30, default 10kind and id, plus login on users; a miss returns found: false with guidance instead of an errorinaturalist_find_places toolq (place-name prefix) or all four of nelat, nelng, swlat, swlng, otherwise invalid_geography; per_page 1–30 (default 10) applies to the box arm only, per listq returns places[], a box returns standard[] and community[]; each place carries id, bbox, ancestor_place_ids, place_type, admin_level, and location, with boundary polygons strippedinaturalist_search_observations tooltaxon_id, d1/d2, quality_grade, captive, term_id+term_value_id, iconic_taxa, hrank/lrank, csi, threatened/native/introduced/endemic, user_id or user_login, project_id, and q+search_on; per_page 1–25, default 10; include expands photos, annotations, sounds (identification threads live on inaturalist_get_observation)page walks the first 10,000 results; past that, order by id descending and pass each page's next_cursor as cursor. Returns total_results (a live estimate) and has_more, and echoes applied_filterslicense / photo_license take the seven CC codes, OR-joined; photo_license matches any photo under the code, independent of the record's own license_codeinaturalist_get_observation toolobservation_ids in one upstream request; include defaults to ["identifications"], with comments, photos, annotations, and sounds availableobservations in request order, misses in unresolved; fails as not_found only when none resolve. Adds community_taxon, description, and filled observation_fieldsmax(4, floor(40 / records returned)) entries per array and reports *_total beside *_showninaturalist_get_species_counts toolper_page 1–50, default 25, with page offsetposition (absolute, counted from page 1), taxon_id, and observation_count; truncationCeiling is the last count shown, which nothing off the page exceedsinaturalist_get_histogram toolinterval: month_of_year (default) or week_of_year for a seasonal curve, or year / month / week / day / hour for absolute dates; date_field observed (default) or created; taxon_id optional, and an annotation pair (Life Stage 1 = Larva 6) or iconic_taxa narrows the curvetotal across all of them; capped at 800 buckets from the start of the range, with truncated / shown / capinaturalist_get_leaderboard toolkind: observers or identifiers, with one area form, taxon_id, d1/d2, and quality_grade; per_page 1–250, default 25page × per_page past 500 fails as leaderboard_window_exceeded; entries carry rank, login, and count (measured per count_metric), plus species_count for observersinaturalist_get_similar_species tooltaxon_id at genus or finer, otherwise taxon_rank_too_coarse; an optional area, d1/d2, quality_grade, and captive scope the set to a region; limit 1–50, default 20misidentification_count, with truncationCeiling when the limit cuts the setinaturalist_get_taxon tooltaxon_id; sections selects from summary, taxonomy, children, conservation, photos, encyclopedia, and an unknown name fails as unknown_sectionkind: "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 severalinaturalist://taxa/{taxon_id} resourceapplication/json; a resource read can't name sections, so use inaturalist_get_taxon for the outline pathtaxon_id comes from inaturalist_resolve_name; cached for six hoursinaturalist://observations/{observation_id} resourceapplication/json with its first 40 identifications and first 40 filled observation fields, each with its _total beside its _shownobservation_id comes from inaturalist_search_observations; cached for fifteen minutesBuilt 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:
fields= parameter, so every response is projected in-process (a common taxon record arrives at 95 KB)place_id, lat+lng+radius in kilometres (up to 500), or a four-corner box. Partial or mixed forms fail as invalid_geography, and d1 after d2 as inverted_date_rangeterm_value_id, an impossible date, an unknown licence coderate_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 notlicense_code means all rights reserved, photo attribution must travel with the image, and photos are linked, never proxied. An obscured coordinate is a locality, and observers are identified by login onlyAgent-friendly output:
applied_filters (research-grade and wild-only unless widened, plus any ordering a cursor forced)inaturalist_list_reference for vocabularies, since an unrecognised value returns nothing); a page past the end names the last page with resultstruncated, shown, and cap, plus truncationCeiling on descending rankingsA 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"
}
}
}
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
User-Agent with a contact URL is sent by default; keep one in any INATURALIST_USER_AGENT override.git clone https://github.com/cyanheads/inaturalist-mcp-server.git
cd inaturalist-mcp-server
bun install
cp .env.example .env
# edit .env to override defaults — no required vars
| 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 |
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.
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 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.
| 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. |
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.