
Connects Claude directly to RCSB PDB, PDBe, and UniProt for querying and analyzing protein structures. Exposes six tools for searching structures by keyword or filter, retrieving full structural data in mmCIF or PDB format, finding similar proteins via sequence or structure alignment, tracking ligands across the database, comparing multiple structures with RMSD calculations, and running statistical analyses across the entire PDB. Built on TypeScript with pluggable storage backends and OpenTelemetry instrumentation. Reach for this when you need Claude to answer questions about protein folds, binding sites, or experimental methods without manually parsing PDB files or writing custom API calls.
Federated protein structure & annotation across experimental (PDB) and predicted (AlphaFold) models via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://protein.caseyjhand.com/mcp
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, all keyless. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 |
| 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.
protein_search_structures toolquery, sequence (an mmseqs2 search; min_identity / max_evalue apply only with it), organism, method, or max_resolution; content_type is experimental, predicted, or all (default); up to 100 hits per page (default 25) via limit / startinvalid_sequence before searchsource and a chainable entry id (sequence hits add entityId); experimental hits carry title, method, resolution, and organism, AlphaFold models their uniprotAccessionfacets (method, organism, polymer_type, resolution, release_year, molecular_weight) add a flat breakdown capped at PROTEIN_FACET_BUCKET_CAP buckets per dimension; for non-sequence searches, protein_analyze_collection reaches the long tailprotein_get_structure toolsource: experimental (default) takes PDB entry IDs and the AF_* / MA_* computed-model IDs search returns (these come back as source: predicted); predicted takes UniProt accessions for AlphaFold models with pLDDT/PAE; best_available takes UniProt accessions and returns the highest-resolution experimental structure, else the best prediction. Up to PROTEIN_MAX_BATCH_IDS IDs per call (default 25)failed[], and requested / processed show IDs dropped past the cap; the call fails as all_failed when nothing resolves and mixed_id_types when the IDs don't fit source. Experimental-source records add polymerEntities, ligands, molecularWeight, and releaseDate, and coordinateUrls lists only files that existinclude_coords inlines coordinate text within a response budget; an over-budget batch returns an overflow size outline to re-call with sections: [ids], and a lone oversized file is withheld in favor of its coordinateUrlsprotein_find_similar toolby: sequence runs a synchronous RCSB mmseqs2 search from sequence, pdb_id, or uniprot, filtered by max_evalue / min_identity; by: structure runs an async Foldseek search from pdb_id or uniprot against pdb100 + afdb50 unless databases says otherwise. A field only the other mode reads fails as mode_mismatched_field; up to 100 hits per page (default 25)protein_search_structures; direct sequence takes precedence over identifierssource and, for structure hits, the database, ranked by score across all searched databases; responses report totalCount and nextStartstatus: computing with a ticketId; re-call with ticket_id to resume it or re-page a finished job. A multichain structure is one query per chain: each response covers query (0-based, default 0) out of queryCountprotein_track_ligands toolmode is find_ligand (query: a name or formula), structures_with_ligand (comp_id), or binding_site (pdb_id, optional comp_id); a missing mode input fails as missing_param. limit is 1–100 (default 25), and start pages the last two modesfind_ligand returns component IDs with formula, weight, SMILES, and InChIKey, ranked by depositionCount, with totalCount and candidatesConsidered showing when the candidate pool was cut short; structures_with_ligand lists entries highest resolution firstbinding_site returns pocket residues with contact distances, numbered in both label (asymId, seqId) and author (authAsymId, authSeqId) namespaces; it works on experimental structures onlyprotein_compare_structures toolPROTEIN_MAX_COMPARE_STRUCTURES (default 10, max 25), each a pdb_id with an optional label-namespace chain; method is tm-align (default), fatcat-rigid, or fatcat-flexible; reference is first (default) or all_pairs; timeout_s (5–120) sets the per-pair poll budgetstatus (complete, computing, failed), tmScore, rmsd, alignedResidues, and [a, b]-ordered modeledResidues and coverage; TM-score is normalized by a's length, so reversing a pair changes itsimilarityScore relays the raw method-specific alignment score and sequenceSimilarity the 0–1 fraction. Each gets a text column only when present in at least one pair; missing values remain unknown. Raw scores are not normalized across methodsuuid; re-call with { a, b, uuid } in resume[] to poll it instead of resubmitting. A resume under a different method or for another pair fails as resume_method_mismatch or resume_job_mismatchprotein_analyze_collection toolgroup_by takes one of method, organism, polymer_type, resolution, release_year, molecular_weight, or two distinct ones for a cross-tab; scope with query, organism, method, max_resolution, and content_type (default experimental). interval sets a numeric bin width for resolution / molecular_weight or "year" for release_yeartotal and bucketsReturned; buckets carry label and count (numeric bins add rangeFrom / rangeTo), and every dimension reports missingValueCountbucket_limit (1–500, default PROTEIN_FACET_BUCKET_CAP) caps each dimension level separately, so a cross-tab can return bucket_limit × (1 + bucket_limit) buckets; truncated and notice name the capped positionsprotein_get_annotations tooluniprot accession, or a pdb_id resolved through its UniProt cross-reference (author chain picks one accession); include is features, domains, variants, or all (default); limit pages each class separately (1–200, default 50), with independent zero-based features_offset, variants_offset, and domains_offset (default 0)ambiguityfeaturesTotal, variantsTotal, or domainsTotal; omitted classes omit their totals. A page with more records sets truncated and names its next offset in notice; an exhausted offset advises lowering it, distinct from genuine absence. Each call refreshes upstream data, so pages are not snapshot-stablefeatureId, originalSequence, and alternativeSequences; missing changes do not imply deletion. Domains retain the requested protein's separate locations[].fragments[] with start, end, and dcStatus, in UniProt residue numbering, preserving gaps and qualifiersmissing_identifier, invalid_accession, no_uniprot_mapping, or chain_not_foundpdb://{entry_id} resourceentry_id is a PDB entry ID (e.g. 4HHB); returns application/json with title, methods, resolution, organisms, ligands, and polymerEntities carrying both authAsymIds and labelAsymIdsprotein_get_structure with source: experimentalaf://{uniprot} resourceuniprot is a UniProt accession or an AlphaFold DB entry ID (e.g. AF-P69905-F1); returns application/json with meanPlddt, confidenceBuckets, cifUrl / pdbUrl / bcifUrl, and modelVersionprotein_get_structure with source: predictedBuilt 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:
PROTEIN_ASYNC_POLL_TIMEOUT_MS (default 30 s), then hand back a resume handle (ticketId, per-pair uuid) instead of blockingauthAsymIds feed protein_get_annotations chain, labelAsymIds feed protein_compare_structures chainAgent-friendly output:
source (experimental / predicted) and the engine or database behind it, and protein_get_structure and protein_get_annotations carry an attribution block (see Upstream data licensing)failed[] rows and per-pair status keep one bad ID or pair from failing the requesttotalCount and nextStart, a start past the end is flagged in notice separately from zero matches, and every advisory joins into that one noticeA 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"
}
}
}
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
git clone https://github.com/cyanheads/protein-mcp-server.git
cd protein-mcp-server
bun install
cp .env.example .env
# every variable has a working default
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). | 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.
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 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.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: registers tools and resources, 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. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.ts and register them in the createApp() arrays in src/index.tsStructure 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).
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.