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:
- 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. - 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.
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, andlorlatinibare 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?
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.
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.
pip install -e . # core only, no dependencies
pip install -e ".[test]" # + pytestfrom 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# 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:
MCPDataProvideris a distinct mechanism from the deployed server's provider backend. The deployed server defaults toGenomeNexusDataProvider(GN + UCSC + CIViC);MCPDataProvideris for repl/agent contexts where an MCP host mediates the upstream calls.
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.
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
ENST00000368508on GN vsENST00000368507on Ensembl). For isoform-sensitive fusions, passfive_genomic/three_genomicbreakpoints — orfive_transcript/three_transcript— to pin the exact isoform.
To use it from Claude.ai or Claude Desktop as a remote connector:
- Open Settings → Connectors.
- Add a custom connector with URL
https://<your-cloud-run-url>/mcp. - Connect, then call the
annotate_gene_fusiontool.
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": []
}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.
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).
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.
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/annotateaccepts the same fields asannotate_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/annotateaccepts the same fields as a JSON body for programmatic callers. Interactive OpenAPI docs are served at/api/docs. Backed by the sameGenomeNexusDataProvider/RestDataProvideras the MCP server (FUSION_ANNOTATION_PROVIDERenv var, same convention). Unauthenticated by design, protected instead by per-IP rate limiting (FUSION_ANNOTATION_RATE_LIMIT, default30/minute) and a configurable CORS allowlist (FUSION_ANNOTATION_CORS_ORIGINS, default*).web/— a React + TypeScript SPA (Vite) that callsapi/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:8080Deploy 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).
pytest # core effect engine, genomic-breakpoint mapping,
# GenomeNexusDataProvider (fixture-backed, no network),
# MCP server, and REST API (all offline/fixture-backed)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.
Apache-2.0 (see LICENSE)
