io.github.cyanheads/inaturalist-mcp-server
Search iNaturalist sightings, identification threads, phenology, and look-alike species.
Que pouvez-vous faire avec io.github.cyanheads/inaturalist-mcp-server ?
- 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... - 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 - Applied defaults echoed: search, species counts, histograms, and leaderboards return
applied_filters(research-grade and wild-only unless widened, plus any ordering a cursor forced) - 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
Installation
Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).
claude mcp add --transport http inaturalist-mcp-server 'https://inaturalist.caseyjhand.com/mcp'
{
"mcpServers": {
"inaturalist-mcp-server": {
"type": "http",
"url": "https://inaturalist.caseyjhand.com/mcp"
}
}
}
@cyanheads/inaturalist-mcp-serverclaude mcp add inaturalist-mcp-server -e 'MCP_LOG_LEVEL=info' -e 'INATURALIST_CLAMP_LIMITS=true' -e 'INATURALIST_USER_AGENT=<INATURALIST_USER_AGENT>' -e 'INATURALIST_MIN_REQUEST_INTERVAL_MS=1100' -e 'INATURALIST_MAX_CONCURRENT_REQUESTS=4' -e 'INATURALIST_DAILY_REQUEST_BUDGET=9000' -- npx -y @cyanheads/inaturalist-mcp-server@0.3.2
{
"mcpServers": {
"inaturalist-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/inaturalist-mcp-server@0.3.2"
],
"env": {
"MCP_LOG_LEVEL": "info",
"INATURALIST_CLAMP_LIMITS": "true",
"INATURALIST_USER_AGENT": "<INATURALIST_USER_AGENT>",
"INATURALIST_MIN_REQUEST_INTERVAL_MS": "1100",
"INATURALIST_MAX_CONCURRENT_REQUESTS": "4",
"INATURALIST_DAILY_REQUEST_BUDGET": "9000"
}
}
}
}
Variables d'environnement
MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').INATURALIST_CLAMP_LIMITSClamp oversized read limits and radius (20,040 km), and defer oversized by-id batches. Set false to restore strict rejection, including radius above 500 km.INATURALIST_USER_AGENTUser-Agent sent on every request to api.inaturalist.org. Defaults to the package name, its version, and the repository URL. Keep a contact URL in any override — the published terms ask for identifiable clients.INATURALIST_MIN_REQUEST_INTERVAL_MSMinimum spacing between outbound request starts, in milliseconds. The default holds the sustained rate near 54 requests per minute, under the 60 the upstream asks for and well under its 100 ceiling.INATURALIST_MAX_CONCURRENT_REQUESTSMaximum outbound requests in flight. Separate from the start interval: spacing bounds the sustained rate, this bounds the burst so one slow call does not stall the lane.INATURALIST_DAILY_REQUEST_BUDGETOutbound requests allowed per UTC day, counted in-process. The default sits under the 10,000 per day the upstream asks for, and exhaustion fails loudly rather than degrading silently. The counter is per process and resets on restart.
@cyanheads/inaturalist-mcp-serverclaude mcp add inaturalist-mcp-server -e 'MCP_TRANSPORT_TYPE=<MCP_TRANSPORT_TYPE>' -e 'MCP_HTTP_HOST=127.0.0.1' -e 'MCP_HTTP_PORT=3010' -e 'MCP_HTTP_ENDPOINT_PATH=/mcp' -e 'MCP_AUTH_MODE=none' -e 'MCP_LOG_LEVEL=info' -e 'INATURALIST_CLAMP_LIMITS=true' -e 'INATURALIST_USER_AGENT=<INATURALIST_USER_AGENT>' -e 'INATURALIST_MIN_REQUEST_INTERVAL_MS=1100' -e 'INATURALIST_MAX_CONCURRENT_REQUESTS=4' -e 'INATURALIST_DAILY_REQUEST_BUDGET=9000' -- npx -y @cyanheads/inaturalist-mcp-server@0.3.2
{
"mcpServers": {
"inaturalist-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/inaturalist-mcp-server@0.3.2"
],
"env": {
"MCP_TRANSPORT_TYPE": "<MCP_TRANSPORT_TYPE>",
"MCP_HTTP_HOST": "127.0.0.1",
"MCP_HTTP_PORT": "3010",
"MCP_HTTP_ENDPOINT_PATH": "/mcp",
"MCP_AUTH_MODE": "none",
"MCP_LOG_LEVEL": "info",
"INATURALIST_CLAMP_LIMITS": "true",
"INATURALIST_USER_AGENT": "<INATURALIST_USER_AGENT>",
"INATURALIST_MIN_REQUEST_INTERVAL_MS": "1100",
"INATURALIST_MAX_CONCURRENT_REQUESTS": "4",
"INATURALIST_DAILY_REQUEST_BUDGET": "9000"
}
}
}
}
Variables d'environnement
MCP_TRANSPORT_TYPESelects the HTTP transport.MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_AUTH_MODEAuthentication mode to use: 'none', 'jwt', or 'oauth'.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').INATURALIST_CLAMP_LIMITSClamp oversized read limits and radius (20,040 km), and defer oversized by-id batches. Set false to restore strict rejection, including radius above 500 km.INATURALIST_USER_AGENTUser-Agent sent on every request to api.inaturalist.org. Defaults to the package name, its version, and the repository URL. Keep a contact URL in any override — the published terms ask for identifiable clients.INATURALIST_MIN_REQUEST_INTERVAL_MSMinimum spacing between outbound request starts, in milliseconds. The default holds the sustained rate near 54 requests per minute, under the 60 the upstream asks for and well under its 100 ceiling.INATURALIST_MAX_CONCURRENT_REQUESTSMaximum outbound requests in flight. Separate from the start interval: spacing bounds the sustained rate, this bounds the burst so one slow call does not stall the lane.INATURALIST_DAILY_REQUEST_BUDGETOutbound requests allowed per UTC day, counted in-process. The default sits under the 10,000 per day the upstream asks for, and exhaustion fails loudly rather than degrading silently. The counter is per process and resets on restart.
Documentation
Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.
Sommaire
@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.
10 Tools • 2 Resources
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. Keyless and read-only, it runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
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.
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);limit1–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;per_page1–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;per_page1–25, default 10;includeexpandsphotos,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
inaturalist_get_observation tool
- 1–10
observation_ids in one upstream request;includedefaults to["identifications"], withcomments,photos,annotations, andsoundsavailable - Resolved records come back in
observationsin request order, misses inunresolved; fails asnot_foundonly when none resolve. Addscommunity_taxon,description, and filledobservation_fields - 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;
per_page1–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
inaturalist_get_leaderboard tool
kind:observersoridentifiers, with one area form,taxon_id,d1/d2, andquality_grade;per_page1–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
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;limit1–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 unless widened, plus any ordering a cursor forced) - 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 - Upstream free text (encyclopedia summaries, identification and comment bodies, place guesses, photo attributions) renders inside a markdown blockquote, marking it as third-party content
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 |
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.
Questions fréquentes
Qu'est-ce que io.github.cyanheads/inaturalist-mcp-server ?
Search iNaturalist sightings, identification threads, phenology, and look-alike species.
Comment installer io.github.cyanheads/inaturalist-mcp-server ?
Il fonctionne comme serveur distant. Dans Claude Code : claude mcp add --transport http inaturalist-mcp-server 'https://inaturalist.caseyjhand.com/mcp'. Pour les autres clients, collez la configuration JSON de cette page dans leurs réglages MCP.
Faut-il une clé API pour io.github.cyanheads/inaturalist-mcp-server ?
Son entrée au registre n'indique aucune clé ni secret obligatoire. Certaines fonctions peuvent tout de même demander un compte auprès du service auquel il se connecte.
io.github.cyanheads/inaturalist-mcp-server est-il open source ?
Oui. Son code est sur GitHub sous licence Apache-2.0.
Qui maintient io.github.cyanheads/inaturalist-mcp-server ?
Il est publié par cyanheads. AI.tn le référence sans le maintenir ni le recommander.
Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.