WoRMS Marine Taxa MCP Server
by cyanheads
README.md
<div align="center">
<h1>@cyanheads/worms-marine-taxa-mcp-server</h1>
<p><b>Resolve WoRMS marine species names; fetch classifications, synonyms, distributions, traits, and IDs via MCP. STDIO or Streamable HTTP.</b>
<div>8 Tools</div>
</p>
</div>
<div align="center">
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/worms-marine-taxa-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/worms-marine-taxa-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
</div>
<div align="center">
[](https://github.com/cyanheads/worms-marine-taxa-mcp-server/releases/latest/download/worms-marine-taxa-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=worms-marine-taxa-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvd29ybXMtbWFyaW5lLXRheGEtbWNwLXNlcnZlciJdfQ==) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22worms-marine-taxa-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fworms-marine-taxa-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
</div>
<div align="center">
**Public Hosted Server:** [https://worms-marine-taxa.caseyjhand.com/mcp](https://worms-marine-taxa.caseyjhand.com/mcp)
</div>
---
## Overview
Marine taxonomy from the [World Register of Marine Species (WoRMS)](https://www.marinespecies.org/). Resolve scientific or common names to AphiaIDs, follow accepted names, and pull classifications, synonyms, distributions, traits, and external identifiers from the keyless WoRMS REST API.
Every AphiaID selector accepts a positive safe integer, a decimal string, or a canonical WoRMS LSID such as `urn:lsid:marinespecies.org:taxname:137205`; returned AphiaIDs remain numbers. For other LSIDs use `worms_crosswalk_id` with `externalId` and `type: "lsid"`, or resolve a scientific name with `worms_resolve_taxon`.
### Tools
| Tool | Description |
|:---|:---|
| `worms_resolve_taxon` | Resolve scientific names (exact or fuzzy) or one common name to AphiaIDs, with accepted-name links |
| `worms_get_taxon` | Fetch full Aphia records by AphiaID, optionally with common names or sources |
| `worms_get_classification` | Fetch the root-to-tip classification, optionally with direct children |
| `worms_get_synonyms` | List synonyms of the accepted taxon behind an AphiaID |
| `worms_get_distributions` | Fetch recorded distributions, filtered by status, establishment means, invasiveness, or occurrence |
| `worms_get_traits` | Fetch attribute trees (body size, functional group, conservation listings) with sources and inheritance |
| `worms_crosswalk_id` | Map an AphiaID to nine external identifier systems, or reverse an external ID |
| `worms_list_reference` | List identifier systems, statuses, environment flags, distribution filter values, ranks, and trait definitions |
## Capability reference
### `worms_resolve_taxon` <sub>tool</sub>
- A nonempty `names` list (no total-input ceiling, at most 50 per upstream request) or one `vernacular`, never both. `fuzzy: true` opts into TAXAMATCH matching; `marineOnly` and `extantOnly` default to `true` for names. Vernacular search matches a whole recorded common name, or with `vernacularMatch: "contains"` every recorded name containing the text (`grouper` finds no exact match but many by contains); it applies neither filter
- Each result keeps its `inputIndex` (duplicates included), every match with `status`, `validAphiaId`, and `validName`, and a `guidance` line on a miss; continue with `nextInputOffset` and `nextMatchOffset`
- Vernacular results page with the upstream `offset` (1-based, steps of 50) and the local `resultOffset`
---
### `worms_get_taxon` <sub>tool</sub>
- A nonempty `aphiaIds` list with no total-input ceiling, at most 50 fetched per request; continue with `nextInputOffset` using the unchanged list. Unknown IDs stay in position as `found: false`; every submitted element is validated before fetching
- `includeVernaculars: true` (one ID only) adds common names, paged with `vernacularOffset`; a failed lookup returns `vernacularError` beside the record
- `includeSources: true` (one ID only, not with `includeVernaculars`) adds the taxon's sources, such as its `original description` and `basis of record`, with `reference`, `page`, `url`, `link`, `fulltext`, and `doi` verbatim. Optional `sourceUse: "original description"` filters whole use values ignoring case before `sourceOffset` and the byte budget; blanks omit the filter, populated values stay untrimmed. Continue `nextSourceOffset` with unchanged options to repeat the full primary record and reconstruct every selected row in order
- Successful source results report `sourceTotalCount` for the complete selected bibliography and `unfilteredSourceCount` only with an effective filter. Notices distinguish no matching use, an empty bibliography, and a past-end offset. A failed lookup returns `sourcesError` beside the record without counts; `sourceUse` is inactive without `includeSources`
---
### `worms_get_classification` <sub>tool</sub>
- One `aphiaId`; `ranks` is the root-to-tip lineage, with `found: false` when WoRMS has none
- `includeChildren: true` adds one 50-row page of direct children (`offset`, `marineOnly`, `extantOnly`); a failed children lookup returns `childrenError` beside the lineage
- The submitted AphiaID keeps its own recorded lineage. A `notice` names its linked accepted AphiaID, or the preferred AphiaID for an accepted alternative representation; a failed status lookup returns `statusError` beside the lineage
---
### `worms_get_synonyms` <sub>tool</sub>
- One `aphiaId`, accepted or not: synonyms are listed for its accepted taxon (`validAphiaId`), 50 per upstream `offset`
- `found` and `guidance` separate a missing taxon from a name with no known accepted ID
---
### `worms_get_distributions` <sub>tool</sub>
- One `aphiaId`; optional `recordStatus` (`valid`, `doubtful`, `inaccurate`), `establishmentMeans`, `invasiveness`, and `occurrence` filters combine with AND. `recordStatus` ignores case and surrounding whitespace and echoes its lowercase code; the others match case-insensitive whole values without trimming populated strings, with observed values in `worms_list_reference` topic `distribution_terms`. Blank filters are omitted
- Reports `unfilteredCount` and `filteredCount`; rows carry the locality, its MarineRegions `locationId`, and status fields in source order
- WoRMS attaches records to individual name usages, so by default the tool reads that AphiaID's own records. `includeSynonyms: true` combines the accepted taxon's records with those of its synonyms, 10 synonyms per call, each row tagged `sourceAphiaId`; `synonymUnion` reports the window, the `nextSynonymOffset` to continue from, and any `failedSynonyms`
- A `notice` names the linked accepted AphiaID, distinguishing an accepted alternative representation from its preferred AphiaID; without `includeSynonyms`, a failed status lookup returns `statusError` beside the rows
---
### `worms_get_traits` <sub>tool</sub>
- One `aphiaId`; `includeInherited` (default `true`) adds attributes recorded on higher taxa. On an accepted taxon WoRMS also returns attributes recorded on its synonyms whatever `includeInherited` is set to; `inheritedFrom` names the source taxon in both cases
- A `notice` names the linked accepted AphiaID, distinguishing an accepted alternative representation from its preferred AphiaID; a failed status lookup returns `statusError` beside the attributes
- Every node keeps `nodeIndex`, `parentIndex`, and `depth` from the full tree, so units and qualifiers stay attached to their values across pages; values and references are verbatim
- `measurementType` (case-insensitive whole value, such as `Body size`) keeps each matching node with its subtree and ancestors, indices unchanged; `totalCount` counts that set and `unfilteredCount` the whole tree
---
### `worms_crosswalk_id` <sub>tool</sub>
- Exactly one of `aphiaId` (forward) or `externalId` with `type` (reverse). Forward reads one `type` or all nine: `algaebase`, `bold`, `dyntaxa`, `fishbase`, `iucn`, `lsid`, `ncbi`, `tsn`, `gisd`
- Forward returns `ids` per type, `typeTotals` (0 means none recorded), and `failedTypes` for systems that errored; reverse returns `found` and the full record. IDs stay opaque strings, leading zeros included
---
### `worms_list_reference` <sub>tool</sub>
- `topic`: `external_id_types`, `statuses`, `environment_flags`, and `distribution_terms` are served offline; `ranks` and `trait_types` are read live
- `statuses` covers the 28 statuses the WoRMS manual and REST API document, each with a one-line meaning and a `group` (`accepted`, `unaccepted`, `uncertain`, `technical`). A record's status can be the specific reason itself, such as `junior subjective synonym`, so test the group rather than `status === "unaccepted"`; the guide is non-exhaustive
- `distribution_terms` lists the `establishmentMeans`, `invasiveness`, and `occurrence` values observed in WoRMS distributions, for the whole-value filters of `worms_get_distributions`; the lists are non-exhaustive
- Pages with `resultOffset`; trait definitions keep their tree indices across pages
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/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.
WoRMS-specific:
- Exact and TAXAMATCH fuzzy name matching with marine and extant filters; every match and record carries its accepted-name links
- One shared request queue: one upstream request at a time, 500 ms between starts, a 45-second tool deadline, and an 8 MiB response ceiling
- Per-call retrieval with no cache or bulk harvesting, as the [WoRMS webservice guidance](https://www.marinespecies.org/aphia.php?p=webservice) asks
- Every response carries WoRMS attribution ([doi:10.14284/170](https://doi.org/10.14284/170)); credit the WoRMS Editorial Board and keep per-record citations when reusing results ([terms](https://www.marinespecies.org/about.php#terms))
Agent-friendly output:
- Byte-bounded pages: reads return whole items within a 24,000-byte response and name the next offset (`nextResultOffset`, `nextInputOffset`, upstream `page.nextOffset`); a full 50-row upstream page reports `hasMore: null` rather than guessing
- Unknown stays unknown: null and absent fields pass through, a null environment flag means not recorded, and empty evidence never claims biological absence
- Partial failure: optional lookups (`vernacularError`, `sourcesError`, `childrenError`, `statusError`), synonym-union `failedSynonyms`, and crosswalk `failedTypes` keep the data that did arrive, with typed reasons and recovery hints
## Getting started
Connect to the hosted instance, no install:
```sh
claude mcp add --transport http worms-marine-taxa-mcp-server https://worms-marine-taxa.caseyjhand.com/mcp
```
Or run it locally:
```sh
claude mcp add worms-marine-taxa-mcp-server -- npx -y @cyanheads/worms-marine-taxa-mcp-server
```
For other clients, add this to the MCP config file:
```json
{
"mcpServers": {
"worms-marine-taxa-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/worms-marine-taxa-mcp-server"]
}
}
}
```
No API key or account is needed: the WoRMS REST API is public. For Docker or a source build, see [Running the server](#running-the-server).
## Configuration
The server has no variables of its own; these are the common framework settings.
| Variable | Description | Default |
|:---|:---|:---|
| `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`, `warning`, `error`, etc.). | `info` |
| `LOGS_DIR` | Directory for log files (Node.js only). | `<app-root>/logs` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
See [`.env.example`](./.env.example) for the common framework overrides.
## Running the server
### Local development
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
- **Clone and install:**
```sh
git clone https://github.com/cyanheads/worms-marine-taxa-mcp-server.git
cd worms-marine-taxa-mcp-server && bun install
cp .env.example .env
```
- **Build and run the production version**:
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
```
- **Run checks and tests**:
```sh
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
```
### Docker
```sh
docker run --rm -p 3010:3010 ghcr.io/cyanheads/worms-marine-taxa-mcp-server:latest
```
The image defaults to HTTP transport, stateless session mode, and logs to `/var/log/worms-marine-taxa-mcp-server`. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/worms-marine-taxa-mcp-server:latest"]`. To build locally, `docker build -t worms-marine-taxa-mcp-server .`; OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
## Project structure
| Directory | Purpose |
|:---|:---|
| `src/mcp-server/tools/definitions` | Tool definitions (`*.tool.ts`). Eight read-only tools. |
| `src/services/worms` | WoRMS service: paced transport, normalization, domain schemas, response budgeting, and formatting. |
| `docs/` | Design contract (`design.md`) and the generated directory tree. |
| `tests/` | Service, tool-contract, and JSON-RPC integration tests. |
## Development guide
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches; optional lookups report classified failures beside the primary data instead of discarding it
- Register new tools in the `createApp()` arrays in `src/index.ts`
- Internal imports are relative ESM paths with `.js` extensions
- 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:
```sh
bun run devcheck
bun run test
```
## License
This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessOutdated data