Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

104 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fusion-annotation

What this tool does, in plain terms: when a tumor sequencing report turns up a gene fusion — two genes fused together, like the well-known EML4::ALK fusion found in some lung cancers — this tool answers two questions about it:

  1. What protein does the fusion actually make, and does it still work? Fusing two genes only matters if the two halves join up "in frame" (like joining two sentences without garbling the words) and if the piece that survives is the functionally important part. For EML4::ALK, the tool shows that the joined protein reads correctly with no premature stop, and that it keeps the whole ALK kinase domain — the enzymatic "motor" that, once switched on inappropriately, drives the cancer. It also shows exactly which piece of each original protein is lost.
  2. What is already known about this fusion clinically? — is it considered a driver of cancer, and which drugs (e.g., crizotinib, alectinib, lorlatinib for EML4::ALK) have evidence of activity against it, pulled from curated knowledgebases (CIViC, Open Targets) in the same open spirit as OncoKB.

The output is a short, human-readable line describing the fusion protein — for example:

EML4:p.Met1_Lys496::ALK:p.Tyr1059_Pro1620 — translation: the fusion keeps the first 496 amino acids of EML4 and everything from position 1059 onward in ALK, including its intact kinase domain.

— plus a figure like the one below, showing which parts of each parent protein are kept (colored) versus lost (grey) in the fusion.

EML4::ALK chimeric protein and domain retention

Regenerate this figure with pip install -e .[docs] && python docs/generate_domain_map.py. The web UI's domain diagram uses the same domain de-duplication and category-color scheme (see docs/generate_domain_map.py and web/src/components/DomainDiagram.tsx), so a given domain (e.g. the ALK kinase domain) always renders in the same color everywhere.

Here is the tool's full output for this example — no installation needed to read it, this is exactly what running the bundled example prints:

=== EML4::ALK variant 1 (E13;A20) ===

HGVS.p-like : EML4:p.Met1_Lys496::ALK:p.Tyr1059_Pro1620  (junction hybrid codon -> Val)
categorical : EML4::ALK
frame       : in-frame  (protein 1059 aa, internal stops 0)
junction    : EML4 Lys496 :: ALK Tyr1059  (hybrid codon -> V)

retained domains:
  [RETAINED] WD40 repeat (298-347)
  [RETAINED] WD40 repeat (398-437)
  [RETAINED] WD40 repeat (446-483)
  [RETAINED] HELP motif (255-293)
  [RETAINED] Protein kinase domain (1116-1392)
  [RETAINED] Serine-threonine/tyrosine-protein kinase, catalytic domain (1116-1382)
  [RETAINED] Tyrosine-protein kinase, catalytic domain (1116-1383)
  [RETAINED] Protein kinase-like domain superfamily (1089-1381)
  [RETAINED] Tyrosine-protein kinase, receptor class II, conserved site (1276-1284)
  [RETAINED] Tyrosine-protein kinase, active site (1245-1257)
  [RETAINED] Protein kinase, ATP binding site (1122-1150)

knowledge:
  oncogenic : Oncogenic
  therapies : Alectinib, Alvespimycin, Crizotinib, Entrectinib, Erlotinib, Lorlatinib,
              Nivolumab, Retaspimycin Hydrochloride, WHI-P154
  sources   : CIViC MP 5, Open Targets ENSG00000171094

A few notes on reading this:

  • The several near-duplicate "retained domain" lines under the kinase domain (catalytic domain, active site, ATP-binding site, …) all come from different InterPro entries that describe overlapping or nested regions of the same ALK kinase — that redundancy is a property of the underlying domain database, not an error.
  • Domain sources. When using the deployed server (backed by GenomeNexusDataProvider), domains come from GN's Pfam annotations as the primary source, enriched by InterPro (on by default, gracefully degraded to [] + warning if unavailable). The offline static example above uses hardcoded fixture data, so its domain list may differ slightly from a live annotation.
  • The therapies line mixes evidence of different strength: crizotinib, alectinib, and lorlatinib are the guideline-recognized ALK inhibitors with curated clinical evidence for this fusion (from CIViC); the rest of the list comes from a broader, less clinically curated drug-target database (Open Targets) and includes agents that are investigational or included for other reasons (e.g., HSP90 inhibitors, a JAK inhibitor). This heterogeneity is exactly the kind of thing we want feedback on — how should evidence tiers be separated or labeled for a clinical audience?

This is a research/informatics tool, not a diagnostic device — it is meant to support a molecular pathologist's or genomic analyst's interpretation, not replace it. Results should be reviewed by a qualified professional before they inform patient care.

For non-programmers: you don't need to run anything to give feedback — the block above is the real output. If you do want to run it yourself, the worked example in examples/eml4_alk_offline.py reproduces it with a single command (see Quickstart below). We're interested in feedback on whether this output is useful and clear from a clinical/biological point of view — what would you want to see added, removed, or presented differently?


For developers

Standards-aligned gene-fusion annotation: a protein-effect engine (VEP-like) and a knowledge engine (OncoKB-like), joined by an HGVS.p-like protein-level interface.

The core engine has zero runtime dependencies (Python stdlib only) and is fully offline-testable. Live annotation sources are reached through a pluggable DataProvider. The default backend (GenomeNexusDataProvider) uses Genome Nexus + UCSC + CIViC, reducing per-call latency from ~50–95 s (Ensembl REST) to ~1–2 s. A legacy Ensembl-backed provider (RestDataProvider) is also available, as is an MCP-backed provider for agentic workflows.

Why

Fusion callers (STAR-Fusion, Arriba, FusionInspector, …) tell you that a fusion exists. Oncogenicity scorers (OncoFuse, FusionPath, …) tell you how likely it is to be a driver. Neither emits a normalized, HGVS-style protein-level description of the chimeric product, nor a clean contract between the effect of a specific breakpoint and the knowledge about a categorical gene pair. This package fills that gap:

Layer Analogy Output
1 · Effect VEP chimeric protein: frame, junction, hybrid codon, retained/lost domains
2 · Interface HGVS.p / :: EML4:p.Met1_Lys496::ALK:p.Tyr1059_Pro1620 + categorical key
3 · Knowledge OncoKB oncogenicity, therapies, evidence for the gene-pair fusion

The interface layer follows the VICC Gene Fusion Specification distinction between an assayed fusion (a specific breakpoint, annotated by Layer 1) and a categorical fusion (a gene pair, keyed by Layer 3), and uses the HGVS :: adjoined-protein operator for the junction string.

Install

pip install -e .            # core only, no dependencies
pip install -e ".[test]"    # + pytest

Quickstart (offline)

from fusion_annotation import Transcript, build_exon_cds_map, annotate_fusion
from fusion_annotation.providers import StaticProvider

# ... seed a StaticProvider (see examples/eml4_alk_offline.py) ...
result = annotate_fusion(provider, "EML4", "ALK", five_exon=13, three_exon=20)
print(result["interface"]["hgvsp_like"])
# EML4:p.Met1_Lys496::ALK:p.Tyr1059_Pro1620  (junction hybrid codon -> Val)

The same fusion can be specified by genomic breakpoints instead of exon numbers — the coordinate is mapped through the exon table to a CDS base, which pins the isoform (see Why genomic breakpoints?):

result = annotate_fusion(
    provider, "EML4", "ALK",
    five_genomic="chr2:42295516",   # 3' end of EML4 exon 13 (GRCh38)
    three_genomic="chr2:29223528",  # 5' start of ALK exon 20 (GRCh38)
)
print(result["resolved"]["five"]["breakpoint"])
# {'type': 'genomic', 'genomic_position': 42295516, 'cds_coord': 1489}

Run the bundled worked examples:

python examples/eml4_alk_offline.py           # exon-number breakpoints
python examples/genomic_breakpoint_offline.py # genomic breakpoints

Live annotation via MCP

# In a Claude Science repl cell, host.mcp is the upstream connector callable.
from fusion_annotation.providers import MCPDataProvider
from fusion_annotation import annotate_fusion

provider = MCPDataProvider(host.mcp)          # MCP-host-backed provider (unchanged)
result = annotate_fusion(provider, "EML4", "ALK", five_exon=13, three_exon=20)

MCPDataProvider expects a callable with signature mcp(server, method, **kwargs) and uses these upstream servers:

  • genomes (Ensembl REST): ensembl_lookup, ensembl_sequence, ensembl_xrefs
  • protein-annotation (InterPro): get_domain_architecture
  • clinical-genomics (CIViC / Open Targets): civic_search_molecular_profiles, civic_search_evidence

Note: MCPDataProvider is a distinct mechanism from the deployed server's provider backend. The deployed server defaults to GenomeNexusDataProvider (GN + UCSC + CIViC); MCPDataProvider is for repl/agent contexts where an MCP host mediates the upstream calls.

As an MCP tool

fusion_annotation.mcp_tool exposes a single annotate_gene_fusion tool (TOOL_SCHEMA + annotate_fusion_tool() backend) that you can register with any MCP server framework, so an LLM agent can annotate a fusion in one call. See the module docstring for a FastMCP example.

Using the deployed MCP server

A hosted copy of the server is live. To get the URL, check the latest deploy workflow run or the Cloud Run console.

  • MCP endpoint: https://<your-cloud-run-url>/mcp
  • Health check: https://<your-cloud-run-url>/health

Backend. The server defaults to GenomeNexusDataProvider (Genome Nexus + UCSC + CIViC), which reduces per-call latency from ~50–95 s to ~1–2 s. To use the legacy Ensembl-backed provider, set FUSION_ANNOTATION_PROVIDER=rest in the Cloud Run service environment.

Note on canonical transcripts. GN's canonical transcript can differ from Ensembl's for some genes (confirmed: ROS1 uses ENST00000368508 on GN vs ENST00000368507 on Ensembl). For isoform-sensitive fusions, pass five_genomic / three_genomic breakpoints — or five_transcript / three_transcript — to pin the exact isoform.

To use it from Claude.ai or Claude Desktop as a remote connector:

  1. Open Settings → Connectors.
  2. Add a custom connector with URL https://<your-cloud-run-url>/mcp.
  3. Connect, then call the annotate_gene_fusion tool.

annotate_gene_fusion accepts:

{
  "five_gene": "EML4",
  "three_gene": "ALK",
  "five_exon": 13,
  "three_exon": 20,
  "five_genomic": null,
  "three_genomic": null,
  "five_transcript": null,
  "three_transcript": null,
  "genome_build": "GRCh38",
  "species": "homo_sapiens"
}

Each partner's breakpoint may be given either as an exon number (five_exon / three_exon) or as a genomic position (five_genomic / three_genomic — an integer, a "chr6:117324415" form, or an HGVS "g.117324415" term). A genomic position pins the transcript isoform and removes exon-numbering ambiguity between overlapping isoforms; when both are supplied for a partner, the genomic position wins.

Example — the same EML4::ALK fusion, specified by genomic breakpoints instead of exon numbers (GRCh38; both partners are on chr2). Any of the three coordinate forms below is accepted:

{
  "five_gene": "EML4",
  "three_gene": "ALK",
  "five_genomic": "chr2:42295516",
  "three_genomic": "chr2:29223528"
}
{
  "five_gene": "EML4",
  "three_gene": "ALK",
  "five_genomic": 42295516,
  "three_genomic": 29223528
}
{
  "five_gene": "EML4",
  "three_gene": "ALK",
  "five_genomic": "g.42295516",
  "three_genomic": "g.29223528"
}

All three resolve to the identical junction as the exon-number call (five_exon: 13, three_exon: 20) — EML4:p.Met1_Lys496::ALK:p.Tyr1059_Pro1620, in-frame — because chr2:42295516 is the 3′ end of EML4 exon 13 and chr2:29223528 is the 5′ start of ALK exon 20. To pin a specific isoform explicitly, add five_transcript / three_transcript.

The bundled examples/genomic_breakpoint_offline.py runs this end-to-end offline and prints the resolved breakpoints:

resolved (echoed back by the tool):
  EML4  ENST00000318522 (canonical) -- genomic breakpoint g.42295516 -> CDS coord 1489
  ALK   ENST00000389048 (canonical) -- genomic breakpoint g.29223528 -> CDS coord 3173

Genome build. genome_build selects the assembly the coordinates and transcripts come from — "GRCh38" (default) or "GRCh37" (aliases hg38 / hg19). This matters because a genomic breakpoint is only meaningful against the build it was called on: Ensembl serves each build from a different host, and the tool maps your coordinate against that build's exon table. If you pass GRCh37 coordinates, set genome_build: "GRCh37" — otherwise they are silently interpreted against GRCh38 and land on the wrong CDS base. (Exon-number input is build-agnostic, so it's unaffected.) The chosen build is echoed back under resolved.genome_build.

The transcript fields are optional and default to each gene's canonical Ensembl transcript. The response echoes, under a resolved block, the genome build, the transcript actually used for each partner, and how each breakpoint was interpreted; a warnings list flags a known oncogenic gene pair that reconstructs out-of-frame (usually a sign of a wrong exon number or isoform rather than a real frameshift):

{
  "resolved": {
    "genome_build": "GRCh38",
    "five":  {"gene": "EML4", "transcript": "ENST00000318522", "transcript_source": "canonical",
              "breakpoint": {"type": "exon", "exon": 13, "cds_coord": 1489}},
    "three": {"gene": "ALK",  "transcript": "ENST00000389048", "transcript_source": "canonical",
              "breakpoint": {"type": "exon", "exon": 20, "cds_coord": 3173}}
  },
  "warnings": []
}

Why genomic breakpoints? The CD74::ROS1 isoform trap

An exon number alone cannot name an isoform. For CD74::ROS1, the longer CD74 isoform ENST00000009530 (p41-type, with an extra invariant-chain exon) numbers its exons differently from the canonical breakpoint, so no CD74 exon on that transcript reproduces the real fusion — the frame math is correct but computed for the wrong isoform. A genomic breakpoint sidesteps this: it only resolves against the transcript whose exon table actually spans it. Supply five_genomic / three_genomic (and, if you want, pin the transcripts explicitly) and the tool maps the coordinate to the exact CDS base on that isoform.

The EML4::ALK worked example

EML4::ALK variant 1 (E13;A20) — EML4 exon 13 joined to ALK exon 20 — is the canonical NSCLC driver. This package reproduces, from primary data:

  • fusion CDS = 3180 nt → in-frame, zero internal stops, 1059 aa protein
  • junction: EML4 contributes 496 complete codons (…Lys496); the junction codon is a hybrid (1 nt EML4 + 2 nt ALK = GTG = Val); ALK continues from Tyr1059
  • ALK kinase domain (1116–1392) fully retained; EML4 second β-propeller lost → the mechanism: loss of ALK's extracellular/TM region + EML4-driven oligomerization of an intact kinase = constitutive activation

See docs/DESIGN.md for the full architecture and rationale (the domain-retention figure is shown at the top of this README).

A contrasting example: same gene pair, non-functional breakpoint

Not every breakpoint within an oncogenic gene pair yields a functional fusion product. For example, EML4::ALK with E13;A29 is predicted to be frameshift-truncating, and the critical ALK kinase domain is not retained intact:

HGVS.p-like : EML4:p.Met1_Lys496::ALK:p.Asp1389_Pro1417  (junction hybrid codon -> Gly)
categorical : EML4::ALK
frame       : frameshift-truncating  (protein 526 aa, internal stops 0)

retained domains:
  [RETAINED] WD40 repeat (298-347)
  [RETAINED] WD40 repeat (398-437)
  [RETAINED] WD40 repeat (446-483)
  [RETAINED] HELP motif (255-293)

lost / disrupted critical ALK features:
  [DISRUPTED] Protein kinase domain (1116-1392)
  [LOST] Tyrosine-protein kinase, active site (1245-1257)
  [LOST] Protein kinase, ATP binding site (1122-1150)

This is exactly why the package separates the assayed fusion effect (specific breakpoint, Layer 1) from the categorical gene-pair knowledge (Layer 3): the same EML4::ALK label can map to a clinically important driver breakpoint or to a protein product that is unlikely to be functional.

Web UI + REST API

For a browser-based lookup (rather than the MCP transport above), two more components sit on top of the same core engine:

  • api/ — a public FastAPI service (api/app.py). GET /api/annotate accepts the same fields as annotate_gene_fusion (see above) as query params — so the request URL itself is a shareable, stateless permalink; reopening it just re-runs the annotation, nothing is persisted server-side. POST /api/annotate accepts the same fields as a JSON body for programmatic callers. Interactive OpenAPI docs are served at /api/docs. Backed by the same GenomeNexusDataProvider / RestDataProvider as the MCP server (FUSION_ANNOTATION_PROVIDER env var, same convention). Unauthenticated by design, protected instead by per-IP rate limiting (FUSION_ANNOTATION_RATE_LIMIT, default 30/minute) and a configurable CORS allowlist (FUSION_ANNOTATION_CORS_ORIGINS, default *).
  • web/ — a React + TypeScript SPA (Vite) that calls api/ for a single-fusion lookup tab, a separate batch annotation tab, an HGVS.p-like result summary, a domain-retention table, and an interactive SVG domain diagram (the in-browser version of the figure at the top of this README). The current single-fusion lookup is always reflected in the URL query string, so the address bar doubles as the permalink. Deployed as a static site on GitHub Pages — no server, no container, $0 hosting cost.

The server-side curation endpoint accepts the same batch fusion rows as annotation, but it does not require every row to have exon or genomic breakpoints. When Genome Nexus can resolve the fusion structure, curation includes transcript, exon, protein-breakpoint, retained/lost domain, and kinase-domain context. When only the gene pair is known, curation still runs against PubMed and the output marks fusion_specificity=gene_pair_only, breakpoint_context_available=false, and a limitation explaining that exact exon/protein claims should not be made. The web batch input supports both forms:

EML4::ALK,13,20
CD74::ROS1

In the web UI, AGCG-style curation appears as an expandable Gene information section at the bottom of each fusion annotation result rather than as a separate top-level results block. Curation runs fusion-first: it retrieves and summarizes literature for the exact fusion, including whether the fusion itself is identified in PubMed, driver/actionability rationale, supporting PMIDs, retrieved PMIDs, and any Genome Nexus fusion-position context used by the run. When exon, transcript, genomic, protein-breakpoint, frame, variant, or domain context is available, PubMed retrieval adds targeted structural queries and uses those matches to rank candidate abstracts. Retrieved literature is ordered for curation by evidence type: human clinical or patient evidence first, then cell-line/in-vitro functional evidence, then mouse/in-vivo model evidence, then review or general biological context. Within an evidence tier, breakpoint- or variant-matched records are preferred over generic gene-pair records. When exact-fusion literature is sufficient, per-gene PubMed/LLM retrieval is skipped to reduce latency, rate-limit pressure, and token use; the UI offers a per-gene override when deeper review is needed. When exact-fusion literature is sparse, the two involved gene sections can be expanded to review gene-level driver signal, confidence label, rationale, supporting PMIDs, and retrieved PMIDs. Gene-level curation checks OncoKB first via the curated-gene list and uses the OncoKB gene type, summary/background, and highest evidence levels when available. That avoids PubMed retrieval and LLM synthesis for genes already covered by OncoKB. PubMed/LLM gene synthesis is only used when an OncoKB token is not configured or the gene is not informative in OncoKB. Batch results are reviewed from a selectable list; choosing a completed row opens the same result view used by the single-fusion workflow.

Set ONCOKB_API_TOKEN in the API environment to enable OncoKB-backed per-gene curation. Store it as a local .env entry or a deployment secret; do not commit it. Set NCBI_API_KEY to use a higher PubMed E-utilities rate limit during fusion-level literature retrieval and PubMed/LLM gene fallback. The curation service also throttles PubMed requests across parallel gene workers and retries transient NCBI 429/5xx responses. FUSION_GENE_CURATION_NCBI_MIN_INTERVAL_SECONDS can be set to tune the minimum delay between NCBI requests for a deployment. The default curation model is claude-haiku-4-5-20251001; set FUSION_GENE_CURATION_MODEL to override it.

Run both locally:

pip install -e ".[api]"
python api/app.py                 # serves on :8080 by default

cd web && npm install && npm run dev   # proxies /api to localhost:8080

Deploy the API manually to Cloud Run with api/deploy.sh (same pattern as server/deploy.sh for the MCP server); build+deploy the web UI manually with cd web && npm run build, publishing dist/ to the gh-pages branch. Or automatically: every published GitHub Release triggers deploy-on-release.yml, which builds and deploys all three (MCP server + API to Cloud Run, web UI to GitHub Pages) — the API's URL is baked into the web build, and the API's CORS allowlist is locked to the Pages origin (https://genome-nexus.github.io), which is fixed and known in advance. For an ad-hoc redeploy between releases (e.g. to pick up a config change), trigger deploy-api-web.yml manually. The deployed web UI lives at https://genome-nexus.github.io/fusion-annotation/; to find the deployed API's URL, check the latest deploy-on-release workflow run or the Cloud Run console.

The web UI is published via GitHub Pages' classic branch-based source (Settings → Pages → Source: "Deploy from a branch", pointed at gh-pages): the deploy job builds the SPA from whichever commit was released and pushes the built output straight into the gh-pages branch. This means the live site always reflects exactly what was last released — main can keep moving ahead with unreleased work without affecting production — and, unlike the GitHub-Actions-artifact Pages method, it isn't gated by a GitHub Environment deployment-branch policy (which only matches branches, not the tag ref that release events run on).

Tests

pytest            # core effect engine, genomic-breakpoint mapping,
                  #   GenomeNexusDataProvider (fixture-backed, no network),
                  #   MCP server, and REST API (all offline/fixture-backed)

Status & roadmap

v0.1 handles exon-boundary and genomic-coordinate breakpoints (the latter map through the exon table to an exact CDS base, pinning the isoform — see issue #3), echoes the resolved transcript per partner, and flags known oncogenic pairs that come back out-of-frame. Planned: all-transcript enumeration, NMD prediction, full VICC GFS JSON schema + GA4GH VRS/Cat-VRS identifiers, hgvs library round-trip validation, and a proper OncoKB backend. See docs/DESIGN.md §6.

License

Apache-2.0 (see LICENSE)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages