Generated banner for Clinvar Database
Generated logo for Clinvar Database

Clinvar Database

Google DeepMindOfficialPublished by Google DeepMind

  • Skill
  • Tool integration
  • Science and research
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Use when needing clinical significance, pathogenicity classifications (e.g., Pathogenic, Benign, VUS), clinical evidence rationales, or finding "hard positive" benchmark controls for human genomic variants.
— the description by Google DeepMind

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

ClinVar is the primary consensus record for clinical classifications of human genomic variations. This skill provides a Python-based interface to interact with the NCBI ClinVar database, allowing users to search for variant interpretations, retrieve clinical evidence, and screen for pathogenicity labels like Pathogenic, Benign, or VUS. It includes robust handling for rate limiting and complex XML parsing, ensuring reliable data retrieval for genomic research and clinical benchmarking.

What it does

  • Search for genomic variants using gene symbols, genomic coordinates, or clinical attributes.
  • Retrieve clinical significance labels and review status (star ratings) for specific variants.
  • Fetch detailed clinical evidence, including clinician rationales and assertion criteria.
  • Count matching variants for a given query to gauge result set size.
  • Extract precise genomic coordinates (GRCh38) and associated phenotype data.
  • Support complex queries using NCBI Entrez search syntax.

When to use it

  • When you need to determine the clinical significance of a specific human genomic variant.
  • When you need to find 'hard positive' benchmark controls for genomic analysis.
  • When you need to access clinician notes and assertion rationales for a variant classification.
  • When you need to identify conflicting interpretations for a variant across different laboratories.
  • When you need to retrieve HPO terms or condition names associated with a variant.

Advantages

  • Automated pagination and rate-limit handling for large-scale data retrieval.
  • Standardized JSON output for easy integration into downstream analysis pipelines.
  • Robust parsing of complex NCBI XML schemas via a dedicated wrapper script.
  • Supports triangulation methods to ensure structural variants are not overlooked.
  • Provides clear workflows for both quick screening and deep-dive evidence gathering.

Usage examples

“Find all pathogenic variants in the BRCA1 gene.”
The agent uses the search command with the query 'BRCA1[gene] AND pathogenic[clinsig]' to retrieve a list of variant IDs, then fetches summaries for those IDs.
“Get the clinical evidence and rationale for ClinVar variant 12345.”
The agent executes the evidence command for the specified ID to return a detailed report including submitter notes, citations, and allele information.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/google-deepmind/clinvar-database/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip clinvar-database.zip -d .agents/skills

Documentation by Google DeepMind

ClinVar Database

Prerequisites

  1. uv: Read the uv skill and follow its Setup instructions to ensure uv is installed and on PATH.

  2. User Notification: If .licenses/clinvar_database_LICENSE.txt does not already exist in the workspace root directory then (1) prominently notify the user to check the terms at https://www.ncbi.nlm.nih.gov/clinvar/, then (2) create the file recording the notification text and timestamp.

  3. .env file: Make sure the .env file exists in your home directory. Create one if it does not exist.

  4. NCBI_API_KEY (optional): Raises the NCBI rate limit from 3 to 10 requests/second. The skill works without it, but a key is recommended if the user plans many queries or encounters a 429 error. You can register for a key for free at https://www.ncbi.nlm.nih.gov/account/settings/. You MUST use the safe credentials protocol in the credentials skill to check for and request this key if this skill looks relevant to the user's request.

Overview

ClinVar is the primary consensus record for clinical classifications of human genomic variations. It provides the "clinical ground truth" for pathogenicity labels (Pathogenic, Likely Pathogenic, Benign, VUS) based on assertions from global laboratories.

When to Use

Use when you need to:

  • Find the current clinical significance and star rating (review status) for a specific variant.

  • Fetch clinician notes, assertion criteria, or rationales for previous clinical laboratory classifications.

  • Retrieve the preferred condition name and associated HPO terms for a specific variant.

  • Find a list of variant controls (e.g., "Find all Pathogenic variants in the HBB gene within 50bp of a signal").

  • Check for conflicting interpretations for a given variant and identify the organizations submitting each classification.

Do NOT use when you need to:

  • Find specific allele frequencies in global populations (use gnomAD).

  • Describe the normal biological role of a protein and typical inheritance patterns (use OMIM).

  • Predict mechanistic effects of novel mutations, like frameshifts or exon skipping (use AlphaGenome).

  • Find recommended surveillance schedules for patients with a pathogenic variant (use GeneReviews).

  • Generate or view 3D structural models of affected proteins (use PDB / AlphaFold).

Quick Start

ClinVar queries are executed via a robust Python wrapper script to handle strict rate limiting and XML/JSON parsing.

Example: Search for BRCA1 variants

uv run scripts/clinvar_api.py search --query "BRCA1[gene]" --output results.json

Core Rules

  • Retmax Constraint: The search command defaults to --retmax 200. For any "List all" or gene-wide request, you MUST explicitly set --retmax higher (e.g., 1000) to ensure data completeness.

  • Use the Wrapper: Prefer the wrapper script for standard queries. It handles rate limiting, retries, and the complex XML parsing for you. If the script's parsed output does not contain the specific fields you need, you may modify the script or query the NCBI E-utilities API directly — but be aware that the raw XML schemas are complex and vary between record types.

  • If the rate limit is hit, the script will throw a clear error. You MUST use the safe credentials protocol in the credentials skill to check for and request the NCBI_API_KEY to help the user add it to their .env file.

  • Notification: If this skill is used, ensure this is mentioned in the output.

Utility Scripts

1. count — Count Matching Variants

Purpose: Check how many variants match a query without fetching IDs. Use to decide whether a full search is warranted.

Arguments:

  • --query: (Required) NCBI Entrez search query string.

  • --output: (Required) Output JSON file path.

Example: uv run scripts/clinvar_api.py count \ --query "TP53[gene] AND \"uncertain significance\"[clinsig]" \ --output count.json Output: {"total_count": <int>}

2. search — Search Variants

Purpose: Identify variants based on genomic location, gene symbols, or clinical attributes using NCBI Entrez search syntax. The search command automatically paginates through all matching results to ensure complete, deterministic retrieval.

# Fetch ALL matching variants (default behavior)
uv run scripts/clinvar_api.py search \
  --query "BRCA1[gene]" --output results.json

# Search by Chromosome and Position Range
uv run scripts/clinvar_api.py search \
  --query "11[chr] AND 5225000:5226000[chrpos]" --output results.json

# Combine terms using Entrez syntax
uv run scripts/clinvar_api.py search \
  --query "HBB[gene] AND pathogenic[clinsig]" --output results.json

# Cap results at 50
uv run scripts/clinvar_api.py search \
  --query "TP53[gene]" --retmax 50 --output results.json

Arguments:

  • --query: (Required) NCBI Entrez search query string.

  • --retmax: Maximum total number of variant IDs to return. Default is 0, which means "fetch all matching results." Set to a positive integer to cap the result set.

  • --page_size: Number of IDs to fetch per API request (default: 500, max: 10000 per NCBI limits).

  • --output: (Required) Output JSON file path.

Output: A JSON object containing:

  • total_count — Total number of matching variants in ClinVar.

  • fetched_count — Number of IDs actually retrieved.

  • variant_ids — List of ClinVar Variation ID strings.

3. summary — Get Interpretation Summary

Purpose: Retrieve top-line clinical significance labels, star ratings (review status), and basic phenotype data for rapid variant screening.

# Get summary for one or more Variation IDs
uv run scripts/clinvar_api.py summary \
  --variant_ids 12345 67890 --output summary.json

Arguments:

  • --variant_ids: (Required) One or more ClinVar Variation IDs.

  • --output: (Required) Output JSON file path.

Output: A JSON list of summary objects, each containing:

  • variant_id, title, clinical_significance, review_status,
    last_evaluated, phenotypes

  • genes — list of {gene_id, symbol, strand}

  • variation_type — e.g., single nucleotide variant, Deletion, Insertion

  • molecular_consequences — list of strings (e.g., ["missense variant",
    "nonsense"])

4. evidence — Get Clinical Evidence

Purpose: Fetch the full clinical record for a single variant, including free-text clinician rationales, assertion methods, and specific submitter notes.

# Get full evidence for a single Variation ID
uv run scripts/clinvar_api.py evidence \
  --variant_id 12345 --output evidence.json

Arguments:

  • --variant_id: (Required) A single ClinVar Variation ID.

  • --output: (Required) Output JSON file path.

Output: A JSON object containing:

  • variant_id

  • allele_info — {chromosome, position_start, position_stop, reference_allele, alternate_allele, cytogenetic_band, dbsnp_rsid} (GRCh38 preferred)

  • conditions — list of {name, medgen_cui, omim_id, orphanet_id, hpo_terms}

  • functional_consequences — list of {value, sequence_ontology_id}

  • structural_variant_details — {outer_start, inner_start, inner_stop, outer_stop, copy_number} (present only for CNVs, otherwise null)

  • citation_references — list of PubMed IDs cited in the global "Citations" section

  • submissions — list of per-submitter records, each containing:

    • submitter_name, classification, curator_notes, assertion_criteria

    • date_last_evaluated — when the submitter last reviewed the classification

Typical Workflows

For large or unknown result sets, use count first to decide whether to proceed, then search (which auto-paginates and returns total_count / fetched_count), then summary to screen.

# Step 1: Gauge size (optional — search also returns total_count)
uv run scripts/clinvar_api.py count \
  --query "HBB[gene] AND pathogenic[clinsig]" --output count.json

# Step 2: Fetch all variant IDs (auto-paginates)
uv run scripts/clinvar_api.py search \
  --query "HBB[gene] AND pathogenic[clinsig]" --output ids.json

# Step 3: Get summaries (extract variant_ids from search output)
uv run scripts/clinvar_api.py summary \
  --variant_ids 12345 67890 --output summary.json
Deep Dive: search → evidence

When you need the full clinical picture for a specific variant — including submitter rationales, PubMed citations, ontology-linked conditions, and allele coordinates — use evidence.

uv run scripts/clinvar_api.py evidence \
  --variant_id 12345 --output evidence.json
Workflow: Robust Variant Discovery (Triangulation)

ClinVar metadata is inconsistent. To fulfill "List all" requests, do not rely on a single filter. Perform the following in a single turn and merge results:

  1. Search by exact label (e.g., "3 prime UTR variant"[molecular_consequence]).

  2. Search by HGVS nomenclature pattern (e.g., c.*).

  3. Search by genomic coordinate range (using [chrpos]).

This "triangulation" ensures structural variants with missing labels are not overlooked.

Verifying Coding vs. Non-Coding Status via HGVS

molecular_consequences alone can be ambiguous (e.g., splice donor variant appears in both coding and non-coding contexts). Always cross-check the title field for HGVS patterns:

  • c.-… — 5' UTR (non-coding)

  • c.*… — 3' UTR (non-coding)

  • c.123+N / c.123-N — intronic (non-coding)

  • p.Trp146Arg etc. — protein effect (coding)

A variant with UTR/intronic HGVS and no p. annotation is non-coding, even with splicing labels. Conversely, any p. annotation indicates a coding effect.

ClinVar Metadata Reference
  • 3' UTR

    • Search String: "3 prime UTR variant"[mol_consequence]

    • HGVS: c.*

  • 5' UTR

    • Search String: "5 prime UTR variant"[mol_consequence]

    • HGVS: c.-

  • To find "high-confidence" variants or expert-reviewed consensus, use the review_status filter. This is the most efficient way to distinguish between single-laboratory assertions and panel-reviewed ground truth.

When to Use Which Fields
  • Quick pathogenicity label — Use summary → clinical_significance

  • Gene symbol and strand — Use summary → genes

  • Variant type (SNV, del, etc.) — Use summary → variation_type

  • Protein-level effect — Use summary → molecular_consequences

  • Genomic coordinates (GRCh38) — Use evidence → allele_info

  • Linked conditions (ontology) — Use evidence → conditions

  • SO functional consequence — Use evidence → functional_consequences

  • CNV breakpoints/copy number — Use evidence → structural_variant_details

  • PubMed references — Use evidence → citation_references

  • Date of last lab review — Use both → last_evaluated

  • Clinician rationales — Use evidence → submissions[].curator_notes

Retrieving Genomic Coordinates (Default HG38/GRCh38)

To get precise genomic coordinates in the format <chrom>:<pos>:<ref>><alt> (e.g., chr5:70951945:G>A), you must use the evidence command, as these details are not available in the summary output.

You MUST always include genomic coordinates in the format <chrom>:<pos>:<ref>><alt> when listing or presenting variants, even if not explicitly requested by the user. If coordinates are missing from the summary, use the evidence command or dbSNP fallback to retrieve them.

  1. Fetch Evidence: Use uv run scripts/clinvar_api.py evidence --variant_id <ID> --output evidence.json.

  2. Extract VCF Attributes: The evidence command parses the XML. Extract:

    • Chromosome: Chr

    • Position: positionVCF (or start)

    • Ref: referenceAlleleVCF (or referenceAllele)

    • Alt: alternateAlleleVCF (or alternateAllele) from the SequenceLocation element with Assembly="GRCh38".

Fallback for Imprecise Coordinates (Gene Range): ClinVar often returns the full gene range for non-coding variants. If the extracted coordinates correspond to the gene range instead of a specific position, use the dbsnp-database skill to resolve the precise coordinates using the dbsnp_rsid or HGVS title: 1.Check for dbsnp_rsid in the evidence output. 2. Run uv run scripts/dbsnp_cli.py resolve-rsid {rsid} to get precise GRCh38 coordinates. 3. Format as <chrom>:<pos>:<ref>><alt> using the SPDI or HGVS data from dbSNP.

Structural Variant Note

The structural_variant_details field is only populated for copy number variants (CNVs). For standard SNVs and small indels this field will be null. Use the allele_info fields (position_start, position_stop, reference_allele, alternate_allele) instead.

CNV / Large Deletion Note

Large copy-number variants (CNVs) frequently have empty molecular_consequences. If a variant title mentions "del" and coordinates overlap your target region, it is relevant regardless of missing labels.

Obtaining and Using an API Key

You can register for a key for free at https://www.ncbi.nlm.nih.gov/account/settings/. You MUST use the safe credentials protocol in the credentials skill to check for and request this key if this skill looks relevant to the user's request.

Best Practices

  • Always use uv run to execute python.

  • If jq is unavailable pivot immediately to using Python one-liners for processing JSON (e.g., uv run python3 -c "import json; ...").

  • Use count before search to understand the result set size.

  • The search command fetches all results by default and includes total_count and fetched_count in the output — always verify these match to confirm complete retrieval.

  • Entrez results are unsorted. To order by date, fetch all results and sort locally by last_evaluated.

Common Mistakes

  • Attempting to parse the E-utilities XML yourself — Always use the provided clinvar_api.py client which handles the unpredictable XML schemas robustly.

  • Getting HTTP 429 Too Many Requests — The client throws an exception telling you to pause. You MUST use the safe credentials protocol in the credentials skill to check for and request the NCBI_API_KEY to help the user add it to their .env file, then retry.

  • Sending raw DNA sequences to the API — The API expects HGVS nomenclature, RS IDs, or proper Entrez coordinate syntax (11[chr] AND 1234[chrpos]), not raw ATCG strings.

  • For synonymous or non-coding variants — HGVS nomenclature (e.g., CAPN3 AND "c.551C>T") is more reliable than coordinate searches ([chrpos]), as many ClinVar records for these types lack precise genomic mappings.

  • Case sensitivity in molecular consequences — ClinVar returns mixed-case strings. Always use case-insensitive matching (.lower()) when filtering.

  • Parsing search output as a bare list — search returns a JSON object with total_count, fetched_count, and variant_ids — not a bare list.

Files

scripts/clinvar_api.py

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""A Python client for querying the NCBI ClinVar database via E-utilities."""

# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "polite-http",
#   "python-dotenv",
# ]
# ///

from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Any
import urllib.parse
import xml.etree.ElementTree as ET

import dotenv
from polite_http import http_client

DEFAULT_TIMEOUT_SECONDS: float = 30.0


class _Response:

  def __init__(self, status_code: int, content: bytes):
    self.status_code = status_code
    self.content = content


class RateLimitError(Exception):
  """Raised when the NCBI API rate limit is exceeded."""


class ClinVarClient:
  """A robust Python client for querying the NCBI ClinVar database.

  This client uses NCBI E-utilities and handles rate limiting, API key
  authentication, and complex XML/JSON response parsing.
  """

  BASE_URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/'

  def __init__(self):
    # Look for NCBI configuration in the environment
    self.api_key = os.environ.get('NCBI_API_KEY')
    self.tool = os.environ.get('NCBI_TOOL')
    self.email = os.environ.get('NCBI_EMAIL') or os.environ.get('USER_EMAIL')

    # NCBI limits: 10 req/sec with key, 3 req/sec without key
    self.rate_limit = 10 if self.api_key else 3
    self.client = http_client.HttpClient(self.BASE_URL, qps=self.rate_limit)

  def _request(self, endpoint: str, params: dict[str, Any]) -> '_Response':
    """Makes an HTTP request to the given E-utilities endpoint.

    Args:
      endpoint: The API endpoint to call (e.g. 'esearch.fcgi').
      params: Query parameters for the request.

    Returns:
      A `_Response` with status code and content bytes.

    Raises:
      RateLimitError: If a 429 status is received.
      RuntimeError: On any other HTTP or network error.
    """
    if self.tool:
      params.setdefault('tool', self.tool)
    if self.email:
      params.setdefault('email', self.email)
    if self.api_key:
      params['api_key'] = self.api_key

    url = urllib.parse.urljoin(self.BASE_URL, endpoint)
    query_string = urllib.parse.urlencode(params, doseq=True)
    full_url = f'{url}?{query_string}'

    try:
      resp = self.client.fetch(full_url, timeout=DEFAULT_TIMEOUT_SECONDS)
      return _Response(resp.status_code, resp.data)
    except http_client.HttpError as exc:
      if exc.status_code == 429:
        raise RateLimitError(
            'HTTP 429 Too Many Requests hit on NCBI E-utilities.\nAGENT'
            ' INSTRUCTION: Pause execution and inform the user that an NCBI API'
            ' Key is required to increase the rate limit, providing the URL'
            ' https://www.ncbi.nlm.nih.gov/clinvar/docs/api_http/ for'
            ' instructions on how to obtain one. The user will need to set the'
            ' NCBI_API_KEY environment variable and advise the agent to retry.'
        ) from exc
      raise RuntimeError(
          f'HTTP {exc.status_code} error from NCBI E-utilities: {exc}'
      ) from exc
    except Exception as exc:
      raise RuntimeError(
          f'Failed to fetch data from NCBI E-utilities: {exc}'
      ) from exc

  def _parse_json(self, content: bytes) -> dict[str, Any]:
    """Decodes and parses a JSON response from NCBI E-utilities."""
    return json.loads(content.decode('utf-8', errors='replace'), strict=False)

  def _esearch(self, params: dict[str, Any]) -> dict[str, Any]:
    """Performs an esearch request and returns the validated esearchresult dict.

    Args:
      params: Query parameters for esearch.fcgi.

    Returns:
      The `esearchresult` dictionary.

    Raises:
      RuntimeError: If NCBI Entrez search returns an error.
    """
    response = self._request('esearch.fcgi', params)
    data = self._parse_json(response.content)
    esearchresult = data.get('esearchresult', {})
    if 'ERROR' in esearchresult:
      raise RuntimeError(f"NCBI Entrez search error: {esearchresult['ERROR']}")
    return esearchresult

  def count_variants(self, query: str) -> int:
    """Returns the total number of variants matching a query.

    This is a lightweight call that does not fetch any variant IDs. Use it
    to check result set size before committing to a full search.

    Args:
        query: NCBI Entrez search query string.

    Returns:
        Total number of matching variant IDs.

    Raises:
        RuntimeError: If NCBI Entrez search returns an error.
    """
    params = {
        'db': 'clinvar',
        'term': query,
        'rettype': 'count',
        'retmode': 'json',
    }
    esearchresult = self._esearch(params)
    return int(esearchresult.get('count', 0))

  def search_variants(
      self,
      query: str,
      retmax: int = 0,
      page_size: int = 500,
  ) -> dict[str, int | list[str]]:
    """Identifies variants based on genomic location, gene symbols, etc.

    Automatically paginates through all results using `retstart` to ensure
    complete, deterministic retrieval. Uses the NCBI Entrez Standard Search
    Syntax.

    Args:
        query: The search query string.
        retmax: Maximum total number of variant IDs to return. A value of 0 (the
          default) means "fetch all matching results".
        page_size: Number of IDs to fetch per API request (default 500, max
          10000 per NCBI limits).

    Returns:
        A dictionary with keys:
        - ``total_count``: Total number of matching variants in ClinVar.
        - ``fetched_count``: Number of IDs actually retrieved.
        - ``variant_ids``: List of ClinVar Variation ID strings.

    Raises:
        RuntimeError: If NCBI Entrez search returns an error.
    """
    page_size = min(page_size, 10000)

    # Step 1: Get total count.
    total_count = self.count_variants(query)
    target = total_count if retmax == 0 else min(total_count, retmax)

    print(f'Total matching variants: {total_count}. Fetching {target}...')

    # Step 2: Paginate.
    all_ids: list[str] = []
    retstart = 0
    page_num = 0
    total_pages = (target + page_size - 1) // page_size if target > 0 else 0

    while retstart < target:
      page_num += 1
      current_page_size = min(page_size, target - retstart)
      print(
          f'  Fetching page {page_num}/{total_pages}'
          f' (IDs {retstart + 1}-{retstart + current_page_size}'
          f' of {target})...'
      )

      params = {
          'db': 'clinvar',
          'term': query,
          'retmode': 'json',
          'retmax': current_page_size,
          'retstart': retstart,
      }
      esearchresult = self._esearch(params)
      ids = esearchresult.get('idlist', [])

      if not ids:
        break

      all_ids.extend(ids)
      retstart += len(ids)

    print(f'Fetched {len(all_ids)} variant IDs.')

    return {
        'total_count': total_count,
        'fetched_count': len(all_ids),
        'variant_ids': all_ids,
    }

  def get_interpretation_summary(
      self, variant_ids: list[str | int]
  ) -> list[dict[str, str | list[str]]]:
    """Retrieves top-line clinical significance labels and star ratings.

    Args:
        variant_ids: A list of variant IDs to summarize.

    Returns:
        A list of summary dictionaries for rapid variant screening.
    """
    if not variant_ids:
      return []

    # Ensure all IDs are strings and join with commas
    ids_str = ','.join(map(str, variant_ids))

    params = {'db': 'clinvar', 'id': ids_str, 'retmode': 'json'}

    response = self._request('esummary.fcgi', params)
    data = self._parse_json(response.content)

    result_data = data.get('result', {})
    uids = result_data.get('uids', [])

    summaries = []
    for uid in uids:
      var_data = result_data.get(uid, {})
      significance = 'Unknown'
      review_status = 'Unknown'
      last_evaluated = 'Unknown'

      # Extract primary classification and date from possible classification
      # blocks
      for sig_key in [
          'clinical_significance',
          'germline_classification',
          'clinical_impact_classification',
          'oncogenicity_classification',
      ]:
        sig_data = var_data.get(sig_key)
        if sig_data and isinstance(sig_data, dict):
          desc = sig_data.get('description')
          if desc and significance == 'Unknown':
            significance = desc
            review_status = sig_data.get('review_status', 'Unknown')

          date = sig_data.get('last_evaluated')
          if date and last_evaluated == 'Unknown':
            last_evaluated = date
        elif sig_data and significance == 'Unknown':
          significance = str(sig_data)

      # Extract phenotypes from classification trait sets
      phenotypes = []
      for class_key in [
          'germline_classification',
          'clinical_impact_classification',
          'oncogenicity_classification',
      ]:
        classification = var_data.get(class_key, {})
        for trait in classification.get('trait_set', []):
          name = trait.get('trait_name')
          if name and name not in phenotypes:
            phenotypes.append(name)

      # Extract gene information
      genes = []
      for gene in var_data.get('genes', []):
        genes.append({
            'gene_id': gene.get('geneid', ''),
            'symbol': gene.get('symbol', ''),
            'strand': gene.get('strand', ''),
        })

      # Extract variation type (uses obj_type in esummary)
      variation_type = var_data.get('obj_type', '')

      # Extract molecular consequence (uses molecular_consequence_list)
      molecular_consequences = var_data.get('molecular_consequence_list', [])

      summaries.append({
          'variant_id': uid,
          'clinical_significance': significance,
          'review_status': review_status,
          'last_evaluated': last_evaluated,
          'phenotypes': phenotypes,
          'title': var_data.get('title', ''),
          'genes': genes,
          'variation_type': variation_type,
          'molecular_consequences': molecular_consequences,
      })

    return summaries

  def get_clinical_evidence(self, variant_id: str) -> dict[str, Any]:
    """Fetches full records including free-text clinician rationales.

    Note: Efetch for clinvar returns XML; parsed here into a clean dictionary.

    Args:
        variant_id: The variant ID to retrieve clinical evidence for.

    Returns:
        A dictionary containing clinical evidence submissions, allele
        information, conditions, and structural variant details.
    """
    # Ensure we're using a VCV accession for the efetch call
    if not str(variant_id).startswith('VCV'):
      try:
        vcv_id = f'VCV{int(variant_id):09d}'
      except ValueError:
        vcv_id = variant_id
    else:
      vcv_id = variant_id

    params = {'db': 'clinvar', 'id': vcv_id, 'rettype': 'vcv', 'retmode': 'xml'}

    response = self._request('efetch.fcgi', params)

    try:
      root = ET.fromstring(response.content.decode('utf-8'))
    except ET.ParseError as e:
      raise RuntimeError(f'Failed to parse NCBI XML response: {e}') from e

    # Navigate the ClinVarSet XML structure
    # Submissions are typically found under ClinVarAssertion or
    # ClinicalAssertion nodes
    assertions = root.findall('.//ClinVarAssertion') + root.findall(
        './/ClinicalAssertion'
    )

    submissions: list[dict[str, str | None]] = []
    for assertion in assertions:
      # Extract Submitter Name
      submitter_node = assertion.find('.//ClinVarAccession')
      if submitter_node is not None:
        submitter_name = (
            submitter_node.attrib.get('submitter')
            or submitter_node.attrib.get('SubmitterName')
            or 'Unknown'
        )
      else:
        submitter_name = 'Unknown'

      # Extract Classification (Clinical Significance)
      clin_sig_node = assertion.find('.//ClinicalSignificance/Description')
      if clin_sig_node is None:
        clin_sig_node = assertion.find(
            './/Classification/GermlineClassification'
        )

      classification = (
          clin_sig_node.text if clin_sig_node is not None else 'Unknown'
      )

      # Extract Curator Notes / Comments
      curator_notes = []
      for comment in assertion.findall('.//Comment'):
        if comment.text:
          curator_notes.append(comment.text.strip())

      # Extract Assertion Criteria if present
      assertion_criteria = None
      criteria_node = assertion.find(
          './/AttributeSet/Attribute[@Type="AssertionMethod"]'
      )
      if criteria_node is None:
        # In VCV XML, ReviewStatus might be what we want
        criteria_node = assertion.find('.//Classification/ReviewStatus')

      if criteria_node is not None and criteria_node.text:
        assertion_criteria = criteria_node.text.strip()

      # Extract date of last evaluation
      date_last_evaluated = None
      date_node = assertion.find('.//ClinicalSignificance')
      if date_node is not None:
        date_last_evaluated = date_node.attrib.get('DateLastEvaluated')
      if date_last_evaluated is None:
        date_node = assertion.find('.//Classification')
        if date_node is not None:
          date_last_evaluated = date_node.attrib.get('DateLastEvaluated')

      submissions.append({
          'submitter_name': submitter_name,
          'classification': classification,
          'curator_notes': '; '.join(curator_notes) if curator_notes else None,
          'assertion_criteria': assertion_criteria,
          'date_last_evaluated': date_last_evaluated,
      })

    return {
        'variant_id': variant_id,
        'allele_info': self._extract_allele_info(root),
        'conditions': self._extract_conditions(root),
        'functional_consequences': self._extract_functional_consequences(root),
        'structural_variant_details': self._extract_structural_variant(root),
        'citation_references': self._extract_global_citations(root),
        'submissions': submissions,
    }

  def _extract_global_citations(self, root: ET.Element) -> list[str]:
    """Extracts PMIDs for variant classification."""
    pmids = []

    # 1. Target VCV structure: ClassifiedRecord/Classifications/*
    # We only pick Citations that are DIRECT children of the classification
    # block to avoid picking up phenotype/gene citations in ConditionList.
    for classifications in root.findall('.//Classifications'):
      for classification in classifications:
        for citation in classification.findall(
            './Citation/ID[@Source="PubMed"]'
        ):
          if citation.text:
            pmids.append(citation.text.strip())

    # 2. Target ClinVarSet structure: InterpretedRecord
    # Similarly, only pick citations that are direct children of the
    # top-level interpretation blocks.
    for interpreted in root.findall('.//InterpretedRecord'):
      for child in interpreted:
        for citation in child.findall('./Citation/ID[@Source="PubMed"]'):
          if citation.text:
            pmids.append(citation.text.strip())

    # 3. Fallback: If still empty, check for Variation level citations
    # These are directly under the Measure element.
    if not pmids:
      for measure in root.findall('.//Measure'):
        for citation in measure.findall('./Citation/ID[@Source="PubMed"]'):
          if citation.text:
            pmids.append(citation.text.strip())

    return sorted(list(set(pmids)))

  def _extract_allele_info(self, root: ET.Element) -> dict[str, str | None]:
    """Extracts allele location and identity from the VCV XML."""
    info = {
        'chromosome': None,
        'position_start': None,
        'position_stop': None,
        'reference_allele': None,
        'alternate_allele': None,
        'cytogenetic_band': None,
        'dbsnp_rsid': None,
    }

    # Try SequenceLocation nodes (GRCh38 preferred)
    for loc in root.findall('.//SequenceLocation'):
      if loc.attrib.get('Assembly') == 'GRCh38':
        info['chromosome'] = loc.attrib.get('Chr')
        info['position_start'] = loc.attrib.get('start')
        info['position_stop'] = loc.attrib.get('stop')
        info['reference_allele'] = loc.attrib.get('referenceAllele')
        info['alternate_allele'] = loc.attrib.get('alternateAllele')
        break

    # Fallback: first SequenceLocation if GRCh38 not found
    if info['chromosome'] is None:
      first_loc = root.find('.//SequenceLocation')
      if first_loc is not None:
        info['chromosome'] = first_loc.attrib.get('Chr')
        info['position_start'] = first_loc.attrib.get('start')
        info['position_stop'] = first_loc.attrib.get('stop')
        info['reference_allele'] = first_loc.attrib.get('referenceAllele')
        info['alternate_allele'] = first_loc.attrib.get('alternateAllele')

    # Cytogenetic band
    cyto_node = root.find('.//CytogeneticLocation')
    if cyto_node is not None and cyto_node.text:
      info['cytogenetic_band'] = cyto_node.text.strip()

    # dbSNP rsID from XRef
    for xref in root.findall('.//XRef'):
      if xref.attrib.get('DB') == 'dbSNP':
        info['dbsnp_rsid'] = f"rs{xref.attrib.get('ID', '')}"
        break

    return info

  def _extract_conditions(
      self, root: ET.Element
  ) -> list[dict[str, str | None]]:
    """Extracts condition/trait details with ontology cross-references."""
    conditions = []
    seen_names = set()

    for trait in root.findall('.//TraitSet/Trait'):
      name_node = trait.find('./Name/ElementValue[@Type="Preferred"]')
      if name_node is None:
        name_node = trait.find('./Name/ElementValue')
      condition_name = (
          name_node.text.strip()
          if name_node is not None and name_node.text
          else 'Unknown'
      )

      # Deduplicate by name
      if condition_name in seen_names:
        continue
      seen_names.add(condition_name)

      # Collect ontology cross-references
      medgen_cui = None
      omim_id = None
      orphanet_id = None
      hpo_terms = []

      for xref in trait.findall('.//XRef'):
        db = xref.attrib.get('DB', '')
        ref_id = xref.attrib.get('ID', '')
        if db == 'MedGen':
          medgen_cui = ref_id
        elif db == 'OMIM':
          omim_id = ref_id
        elif db == 'Orphanet':
          orphanet_id = ref_id
        elif db == 'HP' or db == 'Human Phenotype Ontology':
          hpo_terms.append(ref_id)

      conditions.append({
          'name': condition_name,
          'medgen_cui': medgen_cui,
          'omim_id': omim_id,
          'orphanet_id': orphanet_id,
          'hpo_terms': hpo_terms,
      })

    return conditions

  def _extract_functional_consequences(
      self, root: ET.Element
  ) -> list[dict[str, str]]:
    """Extracts molecular consequence terms from Sequence Ontology."""
    consequences = []
    seen = set()

    # FunctionalConsequence nodes carry SO terms
    for fc in root.findall('.//FunctionalConsequence'):
      value = fc.attrib.get('Value', '')
      if value and value not in seen:
        seen.add(value)
        so_id = None
        xref = fc.find('./XRef[@DB="Sequence Ontology"]')
        if xref is None:
          xref = fc.find('./XRef[@DB="SO"]')
        if xref is not None:
          so_id = xref.attrib.get('ID')
        consequences.append({'value': value, 'sequence_ontology_id': so_id})

    return consequences

  def _extract_structural_variant(
      self, root: ET.Element
  ) -> dict[str, str | None] | None:
    """Extracts structural variant details for CNVs."""
    # Only present for structural variants / CNVs
    loc = None
    for seq_loc in root.findall('.//SequenceLocation'):
      if seq_loc.attrib.get('Assembly') == 'GRCh38':
        loc = seq_loc
        break
    if loc is None:
      loc = root.find('.//SequenceLocation')

    if loc is None:
      return None

    # Structural variant fields are only present for CNVs
    outer_start = loc.attrib.get('outerStart')
    inner_start = loc.attrib.get('innerStart')
    inner_stop = loc.attrib.get('innerStop')
    outer_stop = loc.attrib.get('outerStop')
    copy_number = None

    # Check for copy number in Attribute nodes
    for attr in root.findall(
        './/AttributeSet/Attribute[@Type="AbsoluteCopyNumber"]'
    ):
      if attr.text:
        copy_number = attr.text.strip()
        break

    # Only return if there is at least one structural-specific field
    if not any([outer_start, inner_start, inner_stop, outer_stop, copy_number]):
      return None

    return {
        'outer_start': outer_start,
        'inner_start': inner_start,
        'inner_stop': inner_stop,
        'outer_stop': outer_stop,
        'copy_number': copy_number,
    }


def write_output(data, output_file):
  """Writes output to a JSON file."""
  try:
    with open(output_file, 'w', encoding='utf-8') as f:
      json.dump(data, f, indent=2)
    print(f'Success! Data written to: {output_file}')
  except (OSError, TypeError) as e:
    print(f'Error writing to file {output_file}: {e}')
    sys.exit(1)


def main():
  dotenv.load_dotenv(os.path.expanduser('~/.env'))
  parser = argparse.ArgumentParser(
      description='ClinVar Database API Wrapper Script'
  )
  subparsers = parser.add_subparsers(dest='command', required=True)

  # count
  p_count = subparsers.add_parser(
      'count',
      help='Get the total number of variants matching a query (no ID fetch)',
  )
  p_count.add_argument(
      '--query',
      required=True,
      help='NCBI Entrez search query (e.g. "BRCA1[gene]")',
  )
  p_count.add_argument('--output', required=True, help='Output JSON file path')

  # search
  p_search = subparsers.add_parser(
      'search',
      help='Search for variants by gene, coordinates, or clinical attributes',
  )
  p_search.add_argument(
      '--query',
      required=True,
      help='NCBI Entrez search query (e.g. "BRCA1[gene]")',
  )
  p_search.add_argument(
      '--retmax',
      type=int,
      default=0,
      help=(
          'Maximum total number of variant IDs to return. 0 (default) means'
          ' fetch all matching results.'
      ),
  )
  p_search.add_argument(
      '--page_size',
      type=int,
      default=500,
      help='Number of IDs to fetch per API request (default: 500, max: 10000).',
  )
  p_search.add_argument('--output', required=True, help='Output JSON file path')

  # summary
  p_summary = subparsers.add_parser(
      'summary',
      help=(
          'Get clinical significance, star rating, and phenotypes for'
          ' variant IDs'
      ),
  )
  p_summary.add_argument(
      '--variant_ids',
      nargs='+',
      required=True,
      help='One or more ClinVar Variation IDs',
  )
  p_summary.add_argument(
      '--output', required=True, help='Output JSON file path'
  )

  # evidence
  p_evidence = subparsers.add_parser(
      'evidence',
      help='Fetch full clinical evidence record for a single variant',
  )
  p_evidence.add_argument(
      '--variant_id',
      required=True,
      help='A single ClinVar Variation ID',
  )
  p_evidence.add_argument(
      '--output', required=True, help='Output JSON file path'
  )

  args = parser.parse_args()
  client = ClinVarClient()

  if args.command == 'count':
    total = client.count_variants(args.query)
    data = {'total_count': total}
    print(f'Total matching variants: {total}')
  elif args.command == 'search':
    data = client.search_variants(
        args.query, retmax=args.retmax, page_size=args.page_size
    )
  elif args.command == 'summary':
    data = client.get_interpretation_summary(args.variant_ids)
  elif args.command == 'evidence':
    data = client.get_clinical_evidence(args.variant_id)
  else:
    raise AssertionError(f'Unknown command: {args.command}')
  write_output(data, args.output)


if __name__ == '__main__':
  main()

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs Python

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Google DeepMind
Publisher
Google DeepMind (Official)
Source
google-deepmind/science-skills · skills/clinvar_database · commit 4de6786
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Google DeepMind or Google DeepMind.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clinvar-database.zip