Version License Docker MCP SDK npm TypeScript Bun
Install in Claude Desktop Install in Cursor Install in VS Code
Public Hosted Server: https://protein.caseyjhand.com/mcp
Overview
Experimental (PDB) and predicted (AlphaFold) protein structures behind one surface. Search, fetch, align, and annotate structures and their ligands across RCSB, AlphaFold DB, 3D-Beacons, UniProt, InterPro, and Foldseek. No API key required.
Tools
| Tool | Description |
|---|---|
protein_search_structures | Search experimental and predicted structures by text, sequence, or organism/method/resolution, with optional facets |
protein_get_structure | Fetch metadata and coordinate URLs for experimental, predicted, or best-available structures |
protein_find_similar | Find sequence homologs (RCSB mmseqs2) or fold homologs (Foldseek) |
protein_track_ligands | Resolve ligands to component IDs, find structures containing one, or map its binding site |
protein_compare_structures | Align structures with TM-align or jFATCAT, against a reference or all pairs |
protein_analyze_collection | Profile the PDB as counts, histograms, timelines, and cross-tabs |
protein_get_annotations | Fetch UniProt features and variants plus InterPro domains with GO terms |
Resources
| Resource | Description |
|---|---|
pdb://{entry_id} | Experimental structure summary for a PDB entry |
af://{uniprot} | AlphaFold DB predicted-structure summary for a UniProt accession |
Both resources are also reachable through protein_get_structure, for clients that don't surface resources.
Skills
| Skill | Description |
|---|---|
protein-guide | Identifier forms each tool takes, author vs label chains, residue numbering, and the gates for best_available picks, sequence and Foldseek hits, ligand counts, and PDB profiles |
Served over MCP as skill://protein-guide/SKILL.md; it also ships in plugin installs, the .mcpb bundle, the npm package, and the Docker image.
Capability reference
protein_search_structures tool
- At least one of
query,sequence(an mmseqs2 search;min_identity/max_evalueapply only with it),organism,method,max_resolution, orligand(an exact component ID held as a bound copy);content_typeisexperimental,predicted, orall(default); up to 100 hits per page (default 25) vialimit/start organismtakes a scientific name, an NCBI common name, or a taxon ID, and matches that taxon plus every taxon under it; a word fragment matches nothing.methodtakes one of the 13 RCSB experimental methods in any letter case, or an alias (cryo-EM,cryoEM,EM,X-ray,xray,NMRfor both NMR methods); anything else fails asunknown_method- A
sequenceneeds at least 10 residues; whitespace is stripped and case normalized, and an invalid one fails asinvalid_sequence. Each hit names itssourceand an entryidyou can pass to other tools, and carriesorganisms(a sequence hit's are the matched entity's). Sequence hits addidentity,evalue,bitscore, and the aligned ranges, withqueryLengthat the response level - Optional
facets(method,organism,polymer_type,resolution,release_year,molecular_weight) add a breakdown capped atPROTEIN_FACET_BUCKET_CAPbuckets per dimension; facets count the filtered set, so an active organism or method filter constrains its own facet
protein_get_structure tool
source: experimental(default) takes PDB entry IDs and theAF_*/MA_*computed-model IDs search returns (these come back assource: predicted);predictedtakes UniProt accessions, isoform accessions (P04637-2), or AlphaFold DB entry IDs (AF-P04637-2-F1) for AlphaFold models with pLDDT/PAE;best_availabletakes the same identifiers (an entry ID reduces to its accession) and returns the PDB entry with the widest UniProtcoverage(ties to the finest resolution), else the model with the highest confidence on the lDDT scale. Best-available records carrycoverage,uniprotStart, anduniprotEnd; a pick can come from a non-PDB provider, and only a record withpdbIdis a PDB entry. If the PDB check is unavailable, the pick ranks the federated models alone andnoticesays so- A UniProt accession merged into another entry resolves to its successor's model with a
notice; a demerged or deleted one lands infailed[]with its replacements - IDs are trimmed and de-duplicated (case-insensitive); up to
PROTEIN_MAX_BATCH_IDSare looked up per call (default 25) and the remainder comes back inignored. One 50 s budget covers all upstream lookups in a call; underpredictedandbest_available, an ID it stops, or whose upstream lookup fails (an error status, an HTML page, a network failure), lands infailed[]with a reason to re-call it - Unresolved IDs land in
failed[]; when nothing resolves the call fails asupstream_unavailable(retryable, withdata.failed) if every ID's lookup failed upstream or ran out of the 50 s budget, so none is known to lack a structure, and otherwise asall_failed(withdata.failed); it failsmixed_id_typeswhen the IDs don't fitsource. Records includecoordinateUrlsfor the files that exist, andorganismsunderexperimentalandpredicted include_coordsinlines coordinate text in 24,000-character windows: a single selected structure pages withcoords_offset, passing backcoordinatesNextOffsetuntil it is absent; several selected files inline whole only when together they fit one window, otherwise anoverflowoutline lists each file's length to re-call withsections: [id].coords_offsetabove 0 needs exactly one selected structure (coords_offset_needs_one_structure)
protein_find_similar tool
by: sequenceruns a synchronous RCSB mmseqs2 search over experimental PDB entries only (protein_search_structureswith asequencealso reaches computed models) fromsequence,pdb_id, oruniprot, filtered bymax_evalue/min_identity;by: structureruns an async Foldseek search frompdb_id(its mmCIF file) oruniprot(its AlphaFold DB model) againstpdb100+afdb50unlessdatabasessays otherwise.uniprottakes a canonical or isoform accession, or a UniProtKB entry name such asHBA_HUMAN(resolved to its entry's accession), in both modes and is shape-checked before any request (invalid_accession).databasestakes exactly these case-sensitive names:pdb100,afdb50,afdb-swissprot,afdb-proteome,bfvd_v3,cath50,mgnify_esm30,gmgcl_id,bfmd; any other fails schema validation (-32602), and a name Foldseek itself refuses fails asdatabases_rejected. A field only the other mode reads fails asmode_mismatched_field; up to 100 hits per page (default 25)- Sequences follow the same rules as
protein_search_structures, and a directsequencewins over identifiers. Apdb_idsequence query searches one protein entity: the one holdingchain(author chain, case-sensitive,by: sequenceonly, requirespdb_id), else the lowest-numbered protein entity of at least 10 residues; DNA and RNA entities are never searched. The response names it inqueryEntityId, and a notice lists the entry's other protein entities. An unmatchedchainfails aschain_not_found, andchainwithoutpdb_idaschain_without_pdb_id - Hits name their
source(anddatabasefor structure hits), ranked byscore; sequence hits addidentity,evalue,bitscore, and the aligned ranges. Responses reporttotalCount,nextStart, andqueryLength - A merged
uniprotaccession answers for its successor with a notice; a demerged or deleted one fails asinactive_accessionwith its replacements. Aby: structurecoordinate file is limited to 16,000,000 characters, and a larger one fails asstructure_too_large - A structure hit carries Foldseek's
targetname and anidonly when the target resolves to a PDB entry or UniProt accession;bfvd_v3,mgnify_esm30,gmgcl_id, andbfmdhits have none. Other structure-hit fields:chain(a-Nsuffix marks a symmetry copy),title,probability,organisms/taxId(the target's, where Foldseek classifies it), and the aligned spansqueryStart,queryEnd,targetStart,targetEnd,targetLength. A sequence hit'sorganismsare the matched entity's - A structure job still running at the poll budget returns
status: computingwith aticketId; re-call withticket_idto resume it. A job Foldseek accepted before the call's 50 s budget ran out comes back the same way. For a new search the poll window starts when the call does, so identifier lookups and the coordinate download come out of it. A multichain structure is searched one chain at a time viaquery(0-based, default 0) out ofqueryCount
protein_track_ligands tool
modeisfind_ligand(query: a name or formula),structures_with_ligand(comp_id), orbinding_site(pdb_id, optionalcomp_id); a missing mode input fails asmissing_param.limitis 1–100 (default 25), andstartpages the last two modesfind_ligandreturns component IDs with formula, weight, SMILES, and InChIKey, ranked bydepositionCount, withtotalCountandcandidatesConsideredshowing when the candidate pool was cut short;structures_with_ligandlists entries highest resolution first, each withtitle,method, andhostEntities(the polymer entities whose chains the depositor assigned the ligand to)- Matching counts bound non-polymer copies only, not a residue inside a polymer chain or a sugar inside a branched glycan;
depositionCountequals thetotalCountofstructures_with_ligandfor the same component binding_sitereturns pocket residues with contact distances, numbered in both label (asymId,seqId) and author (authAsymId,authSeqId) namespaces, for ligand instances in a fixed order (ligand entity number, author chain, author residue number) sostart/limitpages read each once; it works on experimental structures only
protein_compare_structures tool
- 2 structures up to
PROTEIN_MAX_COMPARE_STRUCTURES(default 10, max 25), each apdb_idwith an optional label-namespacechain;methodistm-align(default),fatcat-rigid, orfatcat-flexible;referenceisfirst(default) orall_pairs;timeout_s(5–120) sets the per-pair poll budget - Each pair row has a
status(complete,computing,failed),tmScore,rmsd,alignedResidues,modeledResidues, andcoverage; TM-score is normalized bya's length, so reversing a pair changes it.similarityScoreandsequenceSimilarityappear when the upstream supplies them - Structures past the cap return in
ignoredas{ pdb_id, chain? }, not compared. A computing pair returns its jobuuid; re-call with{ a, b, uuid }inresume[]to poll it instead of resubmitting. A mismatchedmethodor pair fails asresume_method_mismatchorresume_job_mismatch
protein_analyze_collection tool
group_bytakes one ofmethod,organism,polymer_type,resolution,release_year,molecular_weight, or two distinct ones for a cross-tab; scope withquery,organism,method,max_resolution, andcontent_type(defaultexperimental);organismandmethodtake the same forms as inprotein_search_structures, and an unknownmethodfails asunknown_method.intervalsets a numeric bin width forresolution/molecular_weightor"year"forrelease_year- Returns
totalandbucketsReturned; buckets carrylabelandcount(numeric bins addrangeFrom/rangeTo), and every dimension reportsmissingValueCount bucket_limit(1–500, defaultPROTEIN_FACET_BUCKET_CAP) caps each dimension separately;truncatedandnoticesay where results were cut
protein_get_annotations tool
- A canonical
uniprotaccession, or apdb_idresolved through its UniProt cross-reference (authorchainpicks one accession);includeisfeatures,domains,variants, orall(default);limitpages each class separately (1–200, default 50), with independent zero-basedfeatures_offset,variants_offset, anddomains_offset(default 0) - Returns UniProt features (domains, binding and active sites, PTMs, topology, processing products such as chains and signal peptides, mutagenesis, and more) and natural variants plus InterPro domains with GO terms; a PDB entry mapping to several accessions uses the lowest author chain and lists the rest in
ambiguity - An isoform accession fails as
invalid_accession, naming the canonical one. An accession UniProt merged into another entry answers as its successor, with the merge named first innotice; a demerged or deleted one fails asinactive_accessionwith itsreplacements - Each requested class carries
featuresTotal,variantsTotal, ordomainsTotal(InterPro's own membership count); a page with more records setstruncatedand names the next offset innotice, and an InterPro walk that stops short of its count setstruncatedand says how many membershipsdomains_offsetcan reach. One 50 s budget covers every upstream request in a call - Failures are typed as
missing_identifier,invalid_accession,accession_not_found,inactive_accession,no_uniprot_mapping, orchain_not_found
pdb://{entry_id} resource
entry_idis a PDB entry ID (e.g.4HHB); returnsapplication/jsonwith title, methods, resolution, organisms, ligands, andpolymerEntitiescarrying bothauthAsymIdsandlabelAsymIds- Same data as
protein_get_structurewithsource: experimental
af://{uniprot} resource
uniprotis a UniProt accession, an isoform accession (e.g.P04637-2), or an AlphaFold DB entry ID (e.g.AF-P69905-F1); returnsapplication/jsonwithmeanPlddt,confidenceBuckets,cifUrl/pdbUrl/bcifUrl, andmodelVersion, plus anoticewhen UniProt merged the accession into another entry (the successor's model answers)- Failures are typed as
invalid_accession,no_model,accession_not_found, orinactive_accession(with its replacements) - Same data as
protein_get_structurewithsource: predicted
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.
PDB / AlphaFold-specific:
- Search, fetch, and comparison treat experimental (PDB) and predicted (AlphaFold DB, ModelArchive, 3D-Beacons providers) structures the same way
- Keyless across RCSB, AlphaFold DB, 3D-Beacons, UniProt, InterPro, and Foldseek
- Collection analytics run on RCSB's facet engine and return bucket counts, not matching entries
- Foldseek and alignment jobs poll for up to
PROTEIN_ASYNC_POLL_TIMEOUT_MS(default 30 s), then hand back a resume handle (ticketId, per-pairuuid) instead of blocking - Chain namespaces stay separate:
authAsymIdsfeedprotein_get_annotationschain,labelAsymIdsfeedprotein_compare_structureschain
Agent-friendly output:
- Provenance: every hit names its
source(experimental/predicted) and the engine or database behind it, andprotein_get_structureandprotein_get_annotationscarry anattributionblock (see Upstream data licensing) - Graceful partial failure:
failed[]rows and per-pairstatuskeep one bad ID or pair from failing the request - Paging and advisories: paged tools return
totalCountandnextStart, astartpast the end is flagged innoticeseparately from zero matches, and every advisory joins into that onenotice
Getting started
Public Hosted Instance
A public instance is available at https://protein.caseyjhand.com/mcp, no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"protein-mcp-server": {
"type": "streamable-http",
"url": "https://protein.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file. No API key is needed.
{
"mcpServers": {
"protein-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/protein-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"protein-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/protein-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"protein-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/protein-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 accounts or API keys: every upstream is public.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/protein-mcp-server.git
- Navigate into the directory:
cd protein-mcp-server
- Install dependencies:
bun install
- Configure environment (optional):
cp .env.example .env
# every variable has a working default
Configuration
Every variable is optional.
| Variable | Description | Default |
|---|---|---|
PROTEIN_ASYNC_POLL_TIMEOUT_MS | Poll budget for an async alignment or Foldseek job before it returns computing (min 1000). For protein_find_similar keep it at or below 45000: the call's 50 s upstream budget ends a longer poll. | 30000 |
PROTEIN_MAX_BATCH_IDS | Max IDs per protein_get_structure call (1–100). | 25 |
PROTEIN_MAX_COMPARE_STRUCTURES | Max structures per protein_compare_structures call (2–25). | 10 |
PROTEIN_FACET_BUCKET_CAP | Buckets per dimension for protein_search_structures facets, and the protein_analyze_collection default (1–500). | 50 |
PROTEIN_FANOUT_CONCURRENCY | Max concurrent upstream requests for per-ID and per-pair fan-out (1–16). | 5 |
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. Overrides the stateless mode the server declares in code. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
OTEL_ENABLED | Enable OpenTelemetry. | false |
Each upstream's base URL can also be pointed at a mirror: RCSB_SEARCH_BASE_URL, RCSB_DATA_BASE_URL, RCSB_FILES_BASE_URL, RCSB_MODELS_BASE_URL, RCSB_ALIGNMENT_BASE_URL, BEACONS_BASE_URL, ALPHAFOLD_BASE_URL, MODELARCHIVE_BASE_URL, FOLDSEEK_BASE_URL, UNIPROT_BASE_URL, INTERPRO_BASE_URL. See .env.example for their defaults and 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 bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t protein-mcp-server .
docker run --rm -p 3010:3010 protein-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/protein-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 and resources, serves skills/, inits the provider services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) and shared schemas (_schemas.ts). |
src/mcp-server/resources | Resource definitions (*.resource.ts). |
src/services | Upstream clients: rcsb/ (search, data, facets), alphafold/, beacons/ (3D-Beacons), uniprot/ (UniProt, InterPro, GO), alignment/ (RCSB Structural Comparison), foldseek/, and shared/ HTTP, concurrency, identifier, and attribution helpers. |
skills/ | The published protein-guide agent skill, served over MCP and shipped in plugin installs, the .mcpb bundle, the npm package, and the Docker image. |
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 - Export new definitions from the barrels in
src/mcp-server/*/definitions/index.tsand register them in thecreateApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Upstream data licensing
Structure and annotation data comes from public databases, each under its own license. protein_get_structure and protein_get_annotations return an attribution block with the license, citation, and homepage of every source that contributed to that response. CC BY and CC BY-SA sources require attribution on redistribution; CC0 sources ask only for citation.
| Source | Contributes to | License |
|---|---|---|
| RCSB PDB | protein_get_structure — experimental records | CC0 1.0 Universal |
| AlphaFold DB | protein_get_structure — predicted models | CC BY 4.0 |
| ModelArchive | protein_get_structure — MA_* computed models | CC BY 4.0 |
| SWISS-MODEL | protein_get_structure — best_available models | CC BY-SA 4.0 |
| BFVD | protein_get_structure — best_available models | CC BY 4.0 |
| UniProt | protein_get_annotations | CC BY 4.0 |
| InterPro | protein_get_annotations — domain/family data | CC0 1.0 Universal |
| GO | protein_get_annotations — GO terms | CC BY 4.0 |
best_available models arrive through 3D-Beacons, so the block credits the provider that actually supplied the model; a provider with no curated entry gets a See provider terms fallback instead of a guessed license. InterPro and GO are credited separately, each only when it contributes. This table covers upstream data; the server's own code is licensed separately (see License).
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.