pubchem-mcp-server
<div align="center">
<h1>@cyanheads/pubchem-mcp-server</h1>
<p><b>Search the PubChem chemical database for compounds, properties, safety data, bioactivity, cross-references, and entity summaries via MCP. STDIO or Streamable HTTP.</b>
<div>10 Tools • 6 Resources</div>
</p>
</div>
<div align="center">
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/pubchem-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/pubchem-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
</div>
<div align="center">
[](https://github.com/cyanheads/pubchem-mcp-server/releases/latest/download/pubchem-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=pubchem-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvcHViY2hlbS1tY3Atc2VydmVyIl19) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22pubchem-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fpubchem-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
</div>
<div align="center">
**Public Hosted Server:** [https://pubchem.caseyjhand.com/mcp](https://pubchem.caseyjhand.com/mcp)
</div>
---
## Overview
Chemical compounds and bioassays from PubChem. Search compounds by name, formula, or structure; fetch properties, safety data, bioactivity, drug interactions, and 3D structures; and find bioassays by biological target.
### Tools
| Tool | Description |
|:---|:---|
| `pubchem_search_compounds` | Search for compounds by name, SMILES, InChIKey, formula, substructure, superstructure, or 2D similarity, or list the compounds a bioassay found active. |
| `pubchem_get_compound_details` | Get physicochemical properties, descriptions, synonyms, drug-likeness, and classification for compounds by CID. |
| `pubchem_get_compound_image` | Fetch a 2D structure diagram (PNG) for a compound by CID. |
| `pubchem_get_compound_3d_structure` | Fetch a 3D conformer (atomic coordinates and bonds) for a compound by CID, as parsed JSON or raw SDF. |
| `pubchem_get_compound_xrefs` | Get external database cross-references (PubMed, patents, genes, proteins, etc.). |
| `pubchem_get_compound_safety` | Get GHS hazard classification and safety data for one or more compounds by CID (batch). |
| `pubchem_get_bioactivity` | Get a compound's bioactivity profile: assay results, targets, and activity values; filter by outcome or molecular target. |
| `pubchem_get_compound_interactions` | Get drug-drug, drug-food, and chemical-target interactions for a compound by CID. |
| `pubchem_search_assays` | Find bioassays by biological target (gene symbol, protein, Gene ID, UniProt accession). |
| `pubchem_get_summary` | Get summaries for PubChem entities: assays, genes, proteins, taxonomy. |
### Resources
Compound and assay records are also exposed as URI-templated resources, backed by the same client methods as the tools; many MCP clients are tool-only and never surface resources.
| Resource | Description |
|:---|:---|
| `pubchem://compound/{cid}` | Core physicochemical properties (JSON). |
| `pubchem://compound/{cid}/safety` | GHS hazard classification (JSON). |
| `pubchem://compound/{cid}/image` | 2D structure diagram (PNG). |
| `pubchem://compound/{cid}/xrefs` | External cross-references (JSON). |
| `pubchem://compound/{cid}/bioactivity` | Bioassay activity profile (JSON). |
| `pubchem://assay/{aid}` | BioAssay summary (JSON). |
## Capability reference
### `pubchem_search_compounds` <sub>tool</sub>
- Six search strategies: identifier (`identifierType` + `identifiers`; name/SMILES/InChIKey, 25 per call, the rest returned in `unprocessed`), formula (`formula`, Hill notation, optional `allowOtherElements`), substructure/superstructure and 2D similarity (`query` + `queryType`; similarity threshold 70-100, default 90), or assay (`aid`; compounds the bioassay marked active, or every compound tested with `activeOnly: false`)
- Up to 200 CIDs per page (default 20; a larger `maxResults` is served at 200 and reported in `cappedInputs`); `offset` pages to 10,000. Optional `properties` saves a follow-up `pubchem_get_compound_details` call
- Reports `totalFound`, or a `totalFoundAtLeast` floor when an upstream search saturated, and `unresolvedIdentifiers` for inputs with no CID. Fails with `aid_not_found` for an unknown AID and `search_query_rejected` (with a hint) for a query PubChem cannot search
---
### `pubchem_get_compound_details` <sub>tool</sub>
- Up to 100 CIDs per call (the rest returned in `unprocessed`); 27 available properties, defaulting to a core set of 14
- Optional descriptions (`descriptionOffset`/`maxDescriptions`, default 3, max 20) and classification (FDA, MeSH, ATC) cover the first 10 CIDs only, the rest listed in `skippedCids`. Optional synonyms (`synonymOffset`/`maxSynonyms`, default 20, max 100) and a Lipinski/Veber drug-likeness check cover every found CID
- Per-CID `found: false` separates a nonexistent CID from a compound with no data
---
### `pubchem_get_compound_image` <sub>tool</sub>
- Single CID; `size` is `"small"` (100x100) or `"large"` (300x300, default)
- Returns base64-encoded PNG plus width/height
- Typed `cid_not_found` error when PubChem has no record for the CID
---
### `pubchem_get_compound_3d_structure` <sub>tool</sub>
- Single CID; `format="json"` (default) returns parsed atoms (element + x/y/z) and bonds, `format="sdf"` returns the raw V2000 SDF text
- `maxAtoms`/`maxBonds` cap the JSON preview (default 200 each); `atomCount`/`bondCount` always report the full totals, with any capping disclosed via enrichment
- `includeRawSdf` bypasses the default 500-line cap on the raw SDF text
- Optional `includeAlternateConformerIds` lists conformer IDs beyond the default
- Typed `no_3d_structure` error when PubChem has no computed 3D coordinates (large molecules, mixtures, some salts)
---
### `pubchem_get_compound_xrefs` <sub>tool</sub>
- Single CID; one or more `xrefTypes`: `RegistryID`, `RN` (CAS numbers), `PatentID`, `PubMedID`, `GeneID`, `ProteinGI`, `TaxonomyID`
- `maxPerType` up to 500 (default 50), with one `offset` applied to every type; each type reports `totalAvailable` and `truncated`. `PubMedID` comes newest first
- An empty result says whether the compound has none of the requested types or the CID may be mistyped
---
### `pubchem_get_compound_safety` <sub>tool</sub>
- Up to 25 CIDs per call (the rest returned in `unprocessed`)
- Returns the GHS signal word, pictograms, H-codes, and P-codes merged across sources, plus `classifications[]` with each source's own label; precautionary statements carry a `decoded` flag
- Per-CID `status`: `ok`, `not_classified` (notifiers found no GHS hazard), `no_ghs_data` (no deposited classification), or `cid_not_found`, so a bad CID never reads as "no hazards on file"
---
### `pubchem_get_bioactivity` <sub>tool</sub>
- Single CID; filter by `outcomeFilter` (`active`/`inactive`/`all`, default `all`) and/or `targetGeneId`/`targetAccession`
- A target filter matches any identifier tied to the same target, so Gene ID 5742, UniProt P23219, and RefSeq NP_542158 all reach the same COX-1 assays
- Up to 100 results per page (default 20); `offset` reaches the rest. A multi-target assay reports the matched target and lists the others under `otherTargets`
- Reports `totalAssays`/`activeCount`/`inactiveCount` for the whole compound and `filteredCount`/`returnedCount` for the page; notices say whether there is no data, the filter excluded everything, or `offset` is past the end
---
### `pubchem_get_compound_interactions` <sub>tool</sub>
- Single CID; one or more `kinds` — `drug-drug` (DrugBank), `drug-food`, `target` (binding/activity from BindingDB, ChEMBL, and others); default `["drug-drug"]`
- `maxEntries` per kind per page (max 50, default 10); `offset` counts source records, up to 2,147,483,646. Each kind pages independently, reported in `paging[]` (`totalRecords`/`nextOffset`/`truncated`)
- A kind that fails is named in `failedKinds` and the others still return. `drug-drug` and `target` can be refused by PubChem's SDQ service, and a retry will not help; `pubchem_get_bioactivity` also covers per-target activity
---
### `pubchem_search_assays` <sub>tool</sub>
- Search by `targetType`: `genesymbol`/`proteinname` (text), `geneid` (NCBI Gene ID), `proteinaccession` (UniProt)
- Up to 200 AIDs per page (default 50); `offset` pages through the results
- Rejects a blank `targetQuery` and a non-numeric `geneid`
- Reports `totalFound` and distinguishes "no match" from "offset past the end"
---
### `pubchem_get_summary` <sub>tool</sub>
- `entityType`: `assay` (AID), `gene` (NCBI Gene ID), `protein` (UniProt accession), or `taxonomy` (Tax ID); up to 10 identifiers per call
- A comma-joined string is split into identifiers; any past the first 10 are returned in `unprocessed`, and more than 1000 fails as `too_many_identifiers`
- AIDs, Gene IDs, and Tax IDs must be positive integers; other pieces get an `invalid: true` row, and the call fails as `invalid_identifier` only when none is usable
- Per-identifier `found` flag; fields depend on `entityType` (taxonomy: ordered `lineage`; gene: `symbol`/`taxonomy`; assay: `method`, the first 25 `targets` with `targetCount`, tested/active compound counts, `lastDataChange`)
---
### `pubchem://compound/{cid}` <sub>resource</sub>
- Core physicochemical properties (the same default 14-property set as `pubchem_get_compound_details`), as `application/json`
- Throws a typed not-found when the CID doesn't exist in PubChem
- Use `pubchem_get_compound_details` to select specific properties or add descriptions, synonyms, drug-likeness, and classification
---
### `pubchem://compound/{cid}/safety` <sub>resource</sub>
- GHS hazard classification as `application/json`, including the per-source `ghs.classifications` and the contributor `source` list
- `status` (`ok`/`not_classified`/`no_ghs_data`/`cid_not_found`) is the only signal distinguishing a bad CID from a compound with no deposited classification — a resource read has no notice surface
---
### `pubchem://compound/{cid}/image` <sub>resource</sub>
- 2D structure diagram, 300x300 PNG, returned as a base64 blob
- Use `pubchem_get_compound_image` for the 100x100 size option
---
### `pubchem://compound/{cid}/xrefs` <sub>resource</sub>
- Focused default set — `RN` (CAS), `RegistryID`, `PubMedID` — up to 25 IDs per type, as `application/json`; the PubMed IDs are the 25 highest (most recently assigned), highest first
- Use `pubchem_get_compound_xrefs` for the full set of xref types, a higher per-type cap, and offset paging
---
### `pubchem://compound/{cid}/bioactivity` <sub>resource</sub>
- Up to 25 assays as `application/json`, plus `totalAssays`/`activeCount` for the whole compound; a multi-target assay lists its further targets under `otherTargets`
- Use `pubchem_get_bioactivity` to filter by outcome or target, raise the cap, or page with offset
---
### `pubchem://assay/{aid}` <sub>resource</sub>
- BioAssay summary as `application/json` — name, description, source, protocol, substance counts
- Throws a typed not-found when the AID doesn't exist
## 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.
PubChem-specific:
- Covers PUG REST, PUG View, and SDQ endpoints
- Rate-limited client (5 req/s) with request queuing and one retry on transient failures
- A 50 s budget per call fails a slow upstream as `Timeout`, and a cancelled call fails with `RequestCancelled`
- Built-in V2000 SDF parser for 3D conformers; drug-likeness computed from already-fetched properties
- All tools are read-only and idempotent, with no API keys required
Agent-friendly output:
- Per-CID `status` and `found` flags let callers branch on data instead of matching error strings
- Batch tools return per-item results alongside `unresolvedIdentifiers`, `skippedCids`, and `failedKinds` rather than failing the whole call
- Capped lists disclose `truncated`, `shown`/`cap`, and `nextOffset`; oversized batches are served up to the cap, with `cappedInputs` and `unprocessed` reporting the rest
- Failures carry a typed `reason` (e.g. `cid_not_found`, `missing_identifier_args`, `invalid_cid_query`) with recovery text
## Getting started
### Public Hosted Instance
A public instance is available at `https://pubchem.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
```json
{
"mcpServers": {
"pubchem-mcp-server": {
"type": "streamable-http",
"url": "https://pubchem.caseyjhand.com/mcp"
}
}
}
```
### Self-Hosted / Local
Add the following to your MCP client configuration file.
```json
{
"mcpServers": {
"pubchem-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/pubchem-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio"
}
}
}
}
```
Or with npx (no Bun required):
```json
{
"mcpServers": {
"pubchem-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/pubchem-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio"
}
}
}
}
```
Or with Docker:
```json
{
"mcpServers": {
"pubchem-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/pubchem-mcp-server:latest"]
}
}
}
```
For Streamable HTTP, set the transport and start the server:
```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```
### Prerequisites
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
- No API keys required — PubChem's API is freely accessible.
### Installation
1. **Clone the repository:**
```sh
git clone https://github.com/cyanheads/pubchem-mcp-server.git
```
2. **Navigate into the directory:**
```sh
cd pubchem-mcp-server
```
3. **Install dependencies:**
```sh
bun install
```
4. **Configure environment (optional):**
```sh
cp .env.example .env
# edit .env to override transport, session mode, storage, or logging defaults
```
## Configuration
| Variable | Description | Default |
|:---------|:------------|:--------|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
| `MCP_HTTP_HOST` | Host for HTTP server. | `127.0.0.1` |
| `MCP_SESSION_MODE` | `stateless`, `stateful`, or `auto`. | `stateless` |
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (RFC 5424). | `info` |
| `STORAGE_PROVIDER_TYPE` | Storage backend. | `in-memory` |
| `OTEL_ENABLED` | Enable OpenTelemetry. | `false` |
See [`.env.example`](./.env.example) for the full list of optional overrides.
## Running the server
### Local development
- **Build and run:**
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
```
- **Run checks and tests:**
```sh
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
```
### Docker
```sh
docker build -t pubchem-mcp-server .
docker run --rm -p 3010:3010 pubchem-mcp-server
```
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/pubchem-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 — registers tools/resources and inits the PubChem client. |
| `src/mcp-server/tools/definitions/` | Tool definitions (`*.tool.ts`). |
| `src/mcp-server/resources/definitions/` | Resource definitions (`*.resource.ts`). |
| `src/services/pubchem/` | PubChem API client — rate limiting, retry, and response/SDF parsing. |
| `scripts/` | Build, clean, devcheck, and tree generation scripts. |
| `tests/` | Unit and integration tests. |
## Development guide
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for request-scoped logging
- Wrap external API calls: validate the raw PubChem response → normalize to a domain type → return the output schema; never fabricate missing fields
- Register new tools and resources in the `index.ts` barrel files
## Contributing
Issues are welcome. Run checks before submitting:
```sh
bun run devcheck
bun run test
```
## License
Apache-2.0 — see [LICENSE](LICENSE) for details.
TDQS
Scored across 10 tools
Each tool targets a distinct data facet of PubChem (search, details, image, 3D, xrefs, safety, bioactivity, interactions, assays, summary), so boundaries are largely clear. The only mild overlap is between pubchem_get_bioactivity and pubchem_get_compound_interactions (both surface target activity) and details vs. summary, but descriptions clarify the intended split.
All tools follow a consistent pubchem_<verb>_<noun> pattern (pubchem_search_*, pubchem_get_compound_*, pubchem_get_summary), with uniform snake_case and only get/search as verbs. The convention is predictable and readable throughout.
10 tools is well-scoped for a large multi-faceted database like PubChem, with each tool earning its place across the compound/bioassay/target domains. No redundancy or padding.
Covers a broad read-only surface: compound search (name/SMILES/formula/substructure/similarity), details, image, 3D, xrefs, safety, bioactivity, interactions, plus assay search and entity summaries. Minor gaps exist (e.g., no dedicated gene/protein/taxonomy detail endpoint beyond the generic summary), but core workflows are well supported.