protein-mcp-server

v0.8.1 pre-1.0

Federated protein structure & annotation across experimental (PDB) and predicted (AlphaFold) models via MCP. STDIO or Streamable HTTP.

protein.caseyjhand.com/mcp
claude mcp add --transport http protein-mcp-server https://protein.caseyjhand.com/mcp
codex mcp add protein-mcp-server --url https://protein.caseyjhand.com/mcp
{
  "mcpServers": {
    "protein-mcp-server": {
      "url": "https://protein.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http protein-mcp-server https://protein.caseyjhand.com/mcp
{
  "mcpServers": {
    "protein-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://protein.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "protein-mcp-server": {
      "type": "http",
      "url": "https://protein.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://protein.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

7

protein_search_structures

open-world

Search experimental (PDB) and predicted (computed-model) protein structures by free text, protein sequence (triggers an mmseqs2 similarity search), and/or organism, method, and resolution filters. Returns ranked hits; the experimental page is enriched with title, method, resolution, and organism. Chain hit IDs into protein_get_structure. Optionally returns a facet breakdown (counts by method / organism / release year / …) alongside the hits at no extra call. A facet on a dimension you are already filtering (e.g. the organism facet while organism is set) lists unfiltered alternatives by design — it does not constrain by its own active filter, so you can see sibling values to pivot to. Numeric histogram buckets (resolution, molecular weight) carry explicit rangeFrom/rangeTo bounds so a boundary label is unambiguous.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_search_structures",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "description": "Free-text query (protein name, gene, keyword, PDB title terms).",
      "type": "string"
    },
    "sequence": {
      "description": "One-letter amino-acid sequence; triggers an RCSB mmseqs2 sequence-similarity search.",
      "type": "string"
    },
    "organism": {
      "description": "Filter by source organism scientific name (e.g. \"Homo sapiens\").",
      "type": "string"
    },
    "method": {
      "description": "Filter by experimental method (e.g. \"X-RAY DIFFRACTION\", \"ELECTRON MICROSCOPY\").",
      "type": "string"
    },
    "max_resolution": {
      "description": "Maximum resolution in Å (lower is sharper); applies to experimental structures.",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "min_identity": {
      "description": "Minimum sequence identity (0–1) for a sequence search. Requires sequence — supplying it without one is rejected, since the threshold would never reach RCSB. Default 0.",
      "type": "number",
      "minimum": 0,
      "maximum": 1
    },
    "max_evalue": {
      "description": "Maximum E-value for a sequence search. Requires sequence — supplying it without one is rejected, since the threshold would never reach RCSB. Default 1.",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "content_type": {
      "default": "all",
      "description": "Which structure universe to search: experimental (PDB), predicted (computed models), or all.",
      "type": "string",
      "enum": [
        "experimental",
        "predicted",
        "all"
      ]
    },
    "facets": {
      "description": "Optional dimensions to summarize as a facet breakdown alongside the hits. Each dimension at most once; repeating one is rejected.",
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "method",
          "organism",
          "polymer_type",
          "resolution",
          "release_year",
          "molecular_weight"
        ]
      }
    },
    "limit": {
      "default": 25,
      "description": "Maximum hits to return (1–100).",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "start": {
      "default": 0,
      "description": "Zero-based result offset. Combine with limit to retrieve later pages.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "content_type",
    "limit",
    "start"
  ],
  "additionalProperties": false
}
view source ↗

protein_get_structure

open-world

Fetch structures with metadata and coordinate-file URLs. source "experimental" takes PDB entry IDs (batched in one call), and also resolves the computed-model IDs protein_search_structures returns (AF_*/MA_*), which come back marked source "predicted" with their modelling provider; "predicted" takes UniProt accessions (AlphaFold, with pLDDT/PAE confidence); "best_available" takes UniProt accessions and returns the top federated model — the highest-resolution experimental structure if one exists (optimizing resolution, not biological representativeness, so it can return an engineered mutant over the wild-type entry), else the best prediction. Records served by the RCSB entry endpoint also carry polymer entities with both chain namespaces (labelAsymIds for protein_compare_structures, authAsymIds for protein_get_annotations), bound ligands, molecular weight, and release date. Resolves up to the configured batch cap per call with per-ID partial success — missed IDs are listed in failed[], and IDs beyond the cap are reported in the notice. Set include_coords to inline coordinate content; if the inlined bytes exceed the response budget the content is withheld and overflow lists each structure's size — re-call with sections:[ids] for specific structures, or for a single oversized file download it from that record's coordinateUrls.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_get_structure",
    "arguments": {
      "ids": "<ids>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "ids": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "description": "PDB entry IDs or computed-model IDs such as AF_AFP69905F1 (source experimental), or UniProt accessions (predicted / best_available)."
    },
    "source": {
      "default": "experimental",
      "description": "Where to fetch: experimental (PDB), predicted (AlphaFold), or best_available (federated pick).",
      "type": "string",
      "enum": [
        "experimental",
        "predicted",
        "best_available"
      ]
    },
    "include_coords": {
      "default": false,
      "description": "Inline coordinate-file content (cif). Off by default — URLs are always returned.",
      "type": "boolean"
    },
    "sections": {
      "description": "Structure IDs to inline coordinates for, from a prior overflow outline.",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": [
    "ids",
    "source",
    "include_coords"
  ],
  "additionalProperties": false
}
view source ↗

protein_find_similar

open-world

Find structurally or evolutionarily related proteins. by:"sequence" runs an RCSB mmseqs2 sequence-similarity search (synchronous) over a sequence — supplied directly, or pulled from a PDB ID or UniProt accession. by:"structure" runs a Foldseek fold-similarity search (asynchronous) against experimental and predicted databases; if the job is still computing when the poll budget elapses, the response reports status "computing" with a ticket — re-call with ticket_id set to that value to resume the same job instead of resubmitting, and a complete response carries the same ticket so a finished job can be re-paged with a different start. Each mode reads only its own controls: sequence, max_evalue and min_identity belong to by:"sequence"; ticket_id and databases belong to by:"structure"; pdb_id, uniprot, start and limit are shared. A field the selected mode cannot consume is rejected rather than silently ignored. Output names the engine and database each hit came from.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_find_similar",
    "arguments": {
      "by": "<by>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "by": {
      "type": "string",
      "enum": [
        "sequence",
        "structure"
      ],
      "description": "Similarity axis: sequence (mmseqs2) or structure (Foldseek)."
    },
    "sequence": {
      "description": "One-letter amino-acid sequence to search from (by:sequence).",
      "type": "string"
    },
    "pdb_id": {
      "description": "PDB entry ID to derive the query from.",
      "type": "string"
    },
    "uniprot": {
      "description": "UniProt accession to derive the query from.",
      "type": "string"
    },
    "ticket_id": {
      "description": "Foldseek ticket ID from a prior by:structure response whose status was \"computing\". When set, polls that existing job instead of submitting a new search (by:structure only) — pdb_id/uniprot/databases are ignored.",
      "type": "string"
    },
    "databases": {
      "description": "Foldseek target databases (by:structure). Default pdb100 + afdb50. e.g. afdb-swissprot, BFVD.",
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "max_evalue": {
      "description": "Maximum E-value (by:sequence). Default 1.",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "min_identity": {
      "description": "Minimum sequence identity 0–1 (by:sequence). Default 0.",
      "type": "number",
      "minimum": 0,
      "maximum": 1
    },
    "limit": {
      "default": 25,
      "description": "Maximum hits to return (1–100).",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "start": {
      "default": 0,
      "description": "Zero-based result offset. Pages a by:sequence search, and a completed by:structure job when re-called with its ticket_id.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "by",
    "limit",
    "start"
  ],
  "additionalProperties": false
}
view source ↗

protein_track_ligands

open-world

Ligand discovery and binding-site analysis across the PDB. mode "find_ligand" resolves a name or formula to chemical component IDs with metadata (formula, weight, SMILES), ranked by deposition frequency — most-deposited component first, so the top hit is the most common match for the name, not necessarily an exact name-string match. mode "structures_with_ligand" returns PDB entries containing a ligand (by exact component ID — get the ID from find_ligand first), highest-resolution first, each with its resolution in Å. mode "binding_site" returns the protein residues lining a ligand's pocket in a given structure, with contact distances. Binding sites are experimental-only (computed from deposited coordinates; predicted models carry no bound ligands).

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_track_ligands",
    "arguments": {
      "mode": "<mode>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "mode": {
      "type": "string",
      "enum": [
        "find_ligand",
        "structures_with_ligand",
        "binding_site"
      ],
      "description": "Operation: resolve a ligand, find structures containing it, or analyze its binding site."
    },
    "query": {
      "description": "Ligand name or formula (mode find_ligand).",
      "type": "string"
    },
    "comp_id": {
      "description": "Exact chemical component ID (modes structures_with_ligand and binding_site).",
      "type": "string"
    },
    "pdb_id": {
      "description": "PDB entry ID (mode binding_site).",
      "type": "string"
    },
    "limit": {
      "default": 25,
      "description": "Maximum results to return (1–100).",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "start": {
      "default": 0,
      "description": "Zero-based result offset (mode structures_with_ligand only).",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "mode",
    "limit",
    "start"
  ],
  "additionalProperties": false
}
view source ↗

protein_compare_structures

open-world

Structurally align multiple structures (up to the configured batch cap) via the RCSB Structural Comparison service (TM-align / jFATCAT). reference:"first" aligns every structure to the first; reference:"all_pairs" computes the full pairwise matrix. Each pair is an independent async alignment job, fanned out with a concurrency cap and per-pair partial success — a pair still computing when the budget elapses returns status "computing" with its job UUID, and a failed pair degrades its row without sinking the others. Re-call with a matching entry in resume[] to poll a computing pair's UUID instead of resubmitting. Returns TM-score, RMSD, and aligned-residue count per pair, plus each structure's modeled-residue count and alignment coverage. TM-score is length-normalized and can shift sharply between structures that differ only by a terminal residue or two — the greedy superposition can settle into a worse local optimum — so read tmScore alongside rmsd, alignedResidues, modeledResidues and coverage, the columns that make such cases diagnosable.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_compare_structures",
    "arguments": {
      "structures": "<structures>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "structures": {
      "minItems": 2,
      "maxItems": 25,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "pdb_id": {
            "type": "string",
            "minLength": 1,
            "description": "PDB entry ID."
          },
          "chain": {
            "description": "mmCIF label_asym_id restricting the alignment to a single chain. Read it from polymerEntities[].labelAsymIds on protein_get_structure (source experimental) or the pdb://{entry_id} resource. Author chain IDs are a different namespace — polymerEntities[].authAsymIds, what protein_get_annotations.chain takes — and are not interchangeable with this one. Case-sensitive.",
            "type": "string"
          }
        },
        "required": [
          "pdb_id"
        ],
        "additionalProperties": false,
        "description": "A structure to align, by PDB entry ID with optional chain."
      },
      "description": "The structures to compare, up to the configured batch cap (excess is dropped with a notice). A structure repeated here is compared once."
    },
    "reference": {
      "default": "first",
      "description": "Align all to the first structure, or compute the full pairwise matrix.",
      "type": "string",
      "enum": [
        "first",
        "all_pairs"
      ]
    },
    "method": {
      "default": "tm-align",
      "description": "Alignment algorithm: tm-align, fatcat-rigid, or fatcat-flexible.",
      "type": "string",
      "enum": [
        "tm-align",
        "fatcat-rigid",
        "fatcat-flexible"
      ]
    },
    "timeout_s": {
      "description": "Poll budget per pair in seconds before returning \"computing\". Defaults to the server setting.",
      "type": "integer",
      "minimum": 5,
      "maximum": 120
    },
    "resume": {
      "description": "Resume tickets from a prior call: for each pair whose labels match an entry here, poll the existing UUID instead of submitting a new alignment job. Copy a, b, and uuid verbatim from a prior response's pairs[]; keep structures and reference unchanged.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "a": {
            "type": "string",
            "minLength": 1,
            "description": "First structure label (entry or entry.chain) of a pair from a prior response."
          },
          "b": {
            "type": "string",
            "minLength": 1,
            "description": "Second structure label (entry or entry.chain) of a pair from a prior response."
          },
          "uuid": {
            "type": "string",
            "minLength": 1,
            "description": "Alignment job UUID returned for that pair by a prior call."
          }
        },
        "required": [
          "a",
          "b",
          "uuid"
        ],
        "additionalProperties": false,
        "description": "A prior pair to resume by UUID instead of resubmitting."
      }
    }
  },
  "required": [
    "structures",
    "reference",
    "method"
  ],
  "additionalProperties": false
}
view source ↗

protein_analyze_collection

open-world

Profile the PDB into distributions and trends over an optional scoping query: counts by method, organism, or polymer composition; resolution and molecular-weight histograms; release-year timelines; and multidimensional cross-tabs (e.g. method × release_year). Aggregation runs server-side at RCSB — one call returns compact buckets, no row pull. Pass one group_by dimension for a single breakdown, or two distinct dimensions for a cross-tab (the first nests the second). bucket_limit caps each dimension level separately rather than the response, so a cross-tab returns up to that many nested buckets under each of its capped parent buckets; bucketsReturned reports the realized total.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_analyze_collection",
    "arguments": {
      "group_by": "<group_by>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "group_by": {
      "minItems": 1,
      "maxItems": 2,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "method",
          "organism",
          "polymer_type",
          "resolution",
          "release_year",
          "molecular_weight"
        ]
      },
      "description": "1 dimension for a breakdown, or 2 distinct dimensions for a cross-tab (the first nests the second). Repeating a dimension is rejected."
    },
    "query": {
      "description": "Optional free-text scope (e.g. \"kinase\"); omit to profile the whole PDB.",
      "type": "string"
    },
    "organism": {
      "description": "Optional source-organism scope.",
      "type": "string"
    },
    "method": {
      "description": "Optional experimental-method scope.",
      "type": "string"
    },
    "max_resolution": {
      "description": "Optional maximum-resolution scope (Å).",
      "type": "number",
      "exclusiveMinimum": 0
    },
    "content_type": {
      "default": "experimental",
      "description": "Which structure universe to profile. Default experimental. Computed models carry no experimental metadata, so method and resolution return nothing under \"predicted\".",
      "type": "string",
      "enum": [
        "experimental",
        "predicted",
        "all"
      ]
    },
    "interval": {
      "description": "Bin width for a histogram dimension (a number, for resolution or molecular_weight) or period for a date histogram (\"year\", for release_year). Applies to whichever requested group_by dimension can consume that value type — primary or nested child — so a cross-tab like [\"method\",\"resolution\"] bins its nested resolution child. When both requested dimensions can consume it the primary takes it and the child keeps its default. When neither can, the call is rejected rather than silently ignoring the override.",
      "anyOf": [
        {
          "type": "number",
          "exclusiveMinimum": 0,
          "description": "Numeric bin width for a value histogram (e.g. resolution Å)."
        },
        {
          "type": "string",
          "enum": [
            "year"
          ],
          "description": "Period granularity for a date histogram. Only \"year\"."
        }
      ]
    },
    "bucket_limit": {
      "description": "Max buckets per dimension level, not per response. A cross-tab applies the cap separately to the parent dimension and to the nested child inside each parent bucket, so up to bucket_limit × (1 + bucket_limit) buckets can come back — 2550 at the default 50. The realized count comes back as bucketsReturned. Defaults to the server PROTEIN_FACET_BUCKET_CAP.",
      "type": "integer",
      "minimum": 1,
      "maximum": 500
    }
  },
  "required": [
    "group_by",
    "content_type"
  ],
  "additionalProperties": false
}
view source ↗

protein_get_annotations

open-world

Sequence and functional annotation for a protein: UniProt features (domains, binding sites, PTMs), natural variants, and InterPro domain/family memberships (Pfam, PROSITE, …) with GO terms. Provide a UniProt accession directly, or a PDB ID — it is resolved to its UniProt accession via the structure's sequence cross-reference. A multi-chain PDB entry can map to several accessions; the default pick is deterministic (lowest author chain ID) and the alternatives are listed in "ambiguity" — pass "chain" to select a specific one. Use "include" to scope which annotation classes are fetched. Every response carries an "attribution" block with the upstream data licenses and citations.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "protein_get_annotations",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "uniprot": {
      "description": "UniProt accession (e.g. P69905). Takes precedence over pdb_id.",
      "type": "string"
    },
    "pdb_id": {
      "description": "PDB entry ID; resolved to a UniProt accession via cross-reference.",
      "type": "string"
    },
    "chain": {
      "description": "Author chain ID (auth_asym_id, e.g. \"A\") that disambiguates a multi-chain PDB entry to a specific UniProt accession. Case-sensitive — must match the author chain ID exactly (large structures can carry distinct \"A\" and \"a\" chains). Only applies with pdb_id; ignored when uniprot is supplied directly. See polymerEntities[].authAsymIds in the pdb://{entry_id} resource for an entry's author chain IDs.",
      "type": "string"
    },
    "include": {
      "default": "all",
      "description": "Which annotation classes to fetch: features, domains (InterPro), variants, or all.",
      "type": "string",
      "enum": [
        "features",
        "domains",
        "variants",
        "all"
      ]
    },
    "limit": {
      "default": 50,
      "description": "Per-class cap (1–200): features, natural variants, and InterPro domains are each independently truncated to at most this many records. The default keeps a typical annotation set intact while bounding a densely-annotated protein (a well-studied protein can carry 150+ natural variants); a truncated class is disclosed in the response notice — raise it to retrieve more.",
      "type": "integer",
      "minimum": 1,
      "maximum": 200
    }
  },
  "required": [
    "include",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

Resources

2

Experimental structure summary for a PDB entry: title, method, resolution, organism, bound ligands, and per-entity chain IDs in both the author (auth_asym_id) and mmCIF label (label_asym_id) namespaces.

uri pdb://{entry_id} mime application/json

Predicted-structure summary for a UniProt accession from AlphaFold DB: mean pLDDT, confidence-band fractions, model URLs, and version.

uri af://{uniprot} mime application/json