Tools
Eight tools — five for the ChEMBL compound/target/bioactivity surface, plus three for SQL analytics over the DuckDB-backed canvas that chembl_get_bioactivities spills to (the third is opt-in):
| Tool | Description |
|---|---|
chembl_search_molecules | Find compounds by name / ChEMBL ID / InChIKey, or run a structure search (exact | similarity | substructure) from a SMILES. |
chembl_get_bioactivities | The flagship compound↔target bridge: bioactivity measurements for a molecule or a target, ranked on pchembl_value. Large sets spill to a canvas. |
chembl_search_targets | Resolve a protein / gene symbol / UniProt accession to the ChEMBL target ID chembl_get_bioactivities needs. |
chembl_get_drug_info | Drug pharmacology — mechanism(s) of action, molecular target(s), action type, first-approval year, and clinical indications. |
chembl_get_assay | Assay provenance behind a bioactivity row — type, target, organism, and ChEMBL's 1–9 confidence score. |
chembl_dataframe_query | Run a read-only SQL SELECT over the bioactivity rows spilled to a canvas — rank, group, dedupe, aggregate across the full set. |
chembl_dataframe_describe | List the tables and columns staged on a canvas, so you can write correct SQL before querying. |
chembl_dataframe_drop | Drop a named staged table from a canvas. Opt-in via CHEMBL_DATAFRAME_DROP_ENABLED=true — absent from tools/list when off, since TTL already reclaims staged tables. |
chembl_search_molecules
The discovery entry point for compounds.
- Default
search_type=namematches drug names, synonyms, ChEMBL IDs, and InChIKeys in one query - Structure search via
search_type:exact(exact match),similarity(Tanimoto ≥ threshold), orsubstructure(contains the query structure) — supplystructureas a SMILES similarity_thresholdis an integer 40–100 (default 70; ChEMBL rejects values below 40)max_phase_minrestricts name searches to compounds at or above a max clinical phase (e.g.4for marketed drugs only)- Every row carries
max_phase— the cheap druggability signal (4 = marketed, 0 = research) — plus MW, AlogP, Lipinski rule-of-five violations, and QED; structure searches also return a Tanimoto similarity percent - Chain
molecule_chembl_idintochembl_get_bioactivitiesorchembl_get_drug_info
chembl_get_bioactivities
The flagship tool and the reason the server exists — the curated compound↔target↔assay link.
- Supply exactly one of
molecule_chembl_id(target deconvolution / selectivity) ortarget_chembl_id(lead finding); both or neither is amissing_filtererror - Filter by
standard_type(IC50 / Ki / EC50 / …), minimum potencypchembl_value_min,assay_type, andorganism; rows are ranked onpchembl_value(−log10 molar potency) - Ranking trap:
pchembl_valueis comparable only within onestandard_type— set the filter, because mixing IC50 and Ki is a scientific error - Numerics are coerced from upstream JSON strings to
number | nullat the service boundary — a missing potency reads asnull, never0 - A popular target carries tens of thousands of measurements: when the set exceeds the inline preview it spills to a DataCanvas table (
bioactivities) you SQL withchembl_dataframe_queryfor honest aggregates across the full set, while the inline preview answers the immediate question - Spilling requires
CANVAS_PROVIDER_TYPE=duckdb; without it the preview is the full inlined set (capped atlimit) - The optional
canvas_idreuses an existing canvas, but thebioactivitiestable is always re-registered — a second query replaces the prior rows on that canvas rather than appending; omitcanvas_idto mint a fresh one
chembl_search_targets
Resolve a protein into the ChEMBL target ID downstream tools need.
- Supply at least one of
accession(UniProt, e.g.P00533),gene_symbol(e.g.EGFR), orquery(free-text name); narrow further withorganismandtarget_type - A UniProt accession is the most precise input — chain it from a
uniprot/proteinserver - Each row carries the target type, organism, and component UniProt accessions + gene symbols (flattened from ChEMBL's nested component synonyms)
- Chain
target_chembl_idintochembl_get_bioactivities
chembl_get_drug_info
Drug pharmacology for a molecule — distinct from the openfda server's label / adverse-event view.
- Supply
molecule_chembl_id(fromchembl_search_molecules) - Returns mechanism(s) of action, the molecular target(s), action type (inhibitor / agonist / …), first-approval year, and clinical indications with the max phase reached for each
- Composed from molecule + mechanisms + indications with
Promise.allSettled, so a missing mechanism or indication list degrades to an empty array rather than failing the call - A mechanism's
target_chembl_idchains intochembl_get_bioactivitiesfor compounds hitting the same target
chembl_get_assay
Assay provenance behind a bioactivity row — call it to judge whether two measurements are comparable before ranking them together.
- Supply
assay_chembl_idfrom achembl_get_bioactivitiesrow - Returns the description, assay type (binding / functional / ADMET / toxicity), the target it measures, organism, and ChEMBL's 1–9 confidence score (9 = direct assay on the protein target, lower = homologous or indirect)
chembl_dataframe_query / chembl_dataframe_describe / chembl_dataframe_drop
In-conversation SQL analytics over the bioactivities table that chembl_get_bioactivities spills to a DuckDB-backed canvas. When a query spills, the tool returns a canvas_id; pass it to chembl_dataframe_query for ranking, grouping, deduplication, and aggregation across the full set — standard DuckDB SQL.
- Read-only.
chembl_dataframe_queryaccepts a singleSELECT; writes, DDL, and non-SELECT statements are rejected by the framework SQL gate. Reference the staged table by the namechembl_get_bioactivitiesreturned (bioactivities), and discover its columns withchembl_dataframe_describefirst. - The spilled table holds the full
Activityrow — 18 columns including the normalizedstandard_*/pchembl_valuefields (rank on these) and the raw upstreamtype/value/units/relation(audit only). Compute aggregates here, never over the inline preview. chembl_dataframe_dropis the only destructive tool and is opt-in (CHEMBL_DATAFRAME_DROP_ENABLED=true) — absent fromtools/listwhen off, because per-table and per-canvas TTL already reclaim staged tables. Reach for it only to free a large table early in a long session.- All three require
CANVAS_PROVIDER_TYPE=duckdb; without it they return acanvas_disablederror andchembl_get_bioactivitiesdegrades to a preview-only response.
Resources and prompts
| Type | Name | Description |
|---|---|---|
| Resource | chembl://molecule/{chemblId} | A molecule record by ChEMBL ID — the same shape a chembl_search_molecules row carries (ID, names, structures, properties, max clinical phase). |
| Resource | chembl://target/{chemblId} | A target record by ChEMBL target ID — preferred name, type, organism, and component UniProt accessions + gene symbols. |
All resource data is also reachable via the tools, so tool-only MCP clients lose nothing — the resources are convenience injectable-context mirrors of the per-record fetch. {chemblId} is validated against the CHEMBL\d+ pattern. There are no prompts; the canonical workflows are short tool chains an agent composes directly, and the cross-server chain guidance ships as server-level instructions instead.
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
- Unified error handling — handlers throw, framework catches, classifies, and formats
- Typed error contracts with recovery hints (
missing_filter,missing_input,canvas_disabled) - Pluggable auth (
none,jwt,oauth) and swappable storage backends - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
ChEMBL-specific:
- Single keyless upstream client for the ChEMBL REST data API — Django-style filtered URL builder,
page_metapagination,withRetry-wrapped fetch + parse - String →
number | nullnumeric coercion at the service boundary (a missing potency becomesnull, never0— the scientific-data fidelity rule) - Bidirectional bioactivity: one tool serves both compound→target and target→compound, ranked on
pchembl_value - Structure search (exact / similarity / substructure) consolidated under one discovery tool via a
search_typeenum - DataCanvas spill on the flagship: tens-of-thousands-of-row activity sets stream to a DuckDB table queryable via
chembl_dataframe_query - Server-level
instructionscarry the cross-server chain guidance and the ChEMBL CC BY-SA 3.0 attribution
Agent-friendly output:
- Provenance on every response — total-found counts, applied-filter echo, and a spill notice so agents know whether the preview is the full set or a slice of a canvas table
- Truncation disclosure — capped search results report
shown/cap/totalCountso a page is never mistaken for the complete set - Typed, recoverable errors —
missing_filter/missing_input/canvas_disabledcarry recovery hints, so callers correct the call without parsing prose - Never fabricates — normalization and
format()preservenullpotency / units; a missing measurement renders as "not reported", never0
Getting started
ChEMBL is keyless — no API key or account is required.
Add the following to your MCP client configuration file:
{
"mcpServers": {
"chembl-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/chembl-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"chembl-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/chembl-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"chembl-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/chembl-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
To unlock the analytical SQL path (the bioactivities spill and the chembl_dataframe_* tools), add "CANVAS_PROVIDER_TYPE": "duckdb" to the env.
Prerequisites
- Bun v1.3.0 or higher (or Node.js v24+).
- Optional: set
CANVAS_PROVIDER_TYPE=duckdbto enable the DataCanvas SQL path for large bioactivity sets.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/chembl-mcp-server.git
- Navigate into the directory:
cd chembl-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env to override any defaults (all optional)
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. ChEMBL is keyless, so every variable is optional.
| Variable | Description | Default |
|---|---|---|
CANVAS_PROVIDER_TYPE | Set to duckdb to enable the bioactivity spill and the chembl_dataframe_* SQL tools. When none, large sets inline a preview but never spill. | none |
CHEMBL_API_BASE_URL | Base URL for the ChEMBL REST data API. Override for a private mirror or pinned host. | https://www.ebi.ac.uk/chembl/api/data |
CHEMBL_REQUEST_TIMEOUT_MS | Per-request timeout in milliseconds for upstream ChEMBL fetches. | 30000 |
CHEMBL_MAX_PAGE_SIZE | ChEMBL per-page cap when streaming activity pages for the spill (max 1000). | 1000 |
CHEMBL_DEFAULT_LIMIT | Default result limit applied when callers omit it. | 25 |
CHEMBL_DATAFRAME_DROP_ENABLED | Register the opt-in chembl_dataframe_drop tool (absent from tools/list when off). | false |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
# 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, changelog sync bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t chembl-mcp-server .
docker run --rm -e MCP_TRANSPORT_TYPE=stdio chembl-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/chembl-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them. The fully-resolved @duckdb native binary is copied from the build stage so CANVAS_PROVIDER_TYPE=duckdb works at runtime.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources and inits the ChEMBL service + optional canvas. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts). Five ChEMBL tools plus the three chembl_dataframe_* canvas tools. |
src/mcp-server/resources/definitions | Resource definitions (*.resource.ts). Molecule and target record mirrors. |
src/services/chembl | The single ChEMBL upstream client — URL builder, pagination, numeric coercion, nested-structure flattening, activity page stream. |
src/services/canvas-accessor.ts | Module-level holder for the optional DataCanvas wired in createApp({ setup }). |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources in the
createApp()arrays - Wrap the ChEMBL API: validate raw → normalize to the flat domain type → return the output schema; never fabricate missing fields (absent numerics become
null, never0)
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.