diff --git a/CHANGELOG.md b/CHANGELOG.md index 999d93e..16e0446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,41 @@ dominant client re-reads the schema each session. Only a removal/rename is MAJOR _Nothing yet._ +## [2.3.0] - 2026-07-31 + + + +### Added + +- **`gwascatalog` — the NHGRI-EBI GWAS Catalog, the 37th database.** Published genome-wide + association results: 955,930 SNP–trait associations (801,096 at genome-wide significance) over + 444,106 SNPs, 89,981 studies and 11,017 EFO traits. It sits on the `ebi` endpoint we already talk + to, so this is a registry row plus an MIE — no new infrastructure. + Picked from the production tool-call log rather than by guesswork: with the ~94% of traffic that is + a load-test harness and one automated pipeline filtered out, the largest remaining usage cluster is + disease-variant work chaining `clinvar` → `togovar` → `medgen` → `mondo` → `hco` → `hgnc`, and + trait association is the layer that thread was missing. + The MIE ships 10 live-verified examples (4 basic, 3 intermediate, 2 aggregation, 1 cross_db) and + documents three traps an agent cannot recover on its own. **The two vocabularies in the graph store + string literals in opposite term forms** — `gwas:has_snp_reference_id` needs `^^xsd:string` while + `m2r:snps` needs the plain form, and `DATATYPE()` reports `xsd:string` for both, so only an `ASK` + tells them apart; getting it backwards returns 0 rows silently. **Two parallel association models** + coexist: 955,930 med2rdf blank-node `Association`s carrying the flat row, versus 782,879 + IRI-addressable OBAN `TraitAssociation`s — different populations, and mixing them double-counts. + And **`?s a gwas:Study` returns 224,583 across two disjoint IRI families**, of which only 89,981 are + real GCST study records; the rest are EBI `Trackable/*` publication stubs. + Trait labels live only in the co-hosted `ontology/efo` graph, so the `cross_db` example is the + route to trait names, not a nicety. + +### Changed + +- Adding a database is now documented as touching **five** files, not one: `endpoints.csv`, the MIE, + the regenerated `02b_database_catalog.md`, the **hand-written** endpoint table in + `02_budgets_and_discovery.md` (no generator touches it — two `TestUsageGuideEndpointTable` tests + are what catch a miss), and the intro page's database grid. The README's **Contributing** section + now lists all five with the guarding test for each; it previously said two, which is how three of + them got missed on the first pass of this very release. A *removal* really is just the registry row. + ## [2.2.1] - 2026-07-30 ### Added @@ -751,7 +786,8 @@ their own file. No tool-surface change; the served MIE/guide content is correcte _MIE database onboarding and revisions land continuously and are summarised per release above; see git history for the full detail._ -[Unreleased]: https://github.com/dbcls/togomcp/compare/v2.2.1...HEAD +[Unreleased]: https://github.com/dbcls/togomcp/compare/v2.3.0...HEAD +[2.3.0]: https://github.com/dbcls/togomcp/compare/v2.2.1...v2.3.0 [2.2.1]: https://github.com/dbcls/togomcp/compare/v2.2.0...v2.2.1 [2.2.0]: https://github.com/dbcls/togomcp/compare/v2.1.3...v2.2.0 [2.1.3]: https://github.com/dbcls/togomcp/compare/v2.1.2...v2.1.3 diff --git a/README.md b/README.md index 5154d9e..a73b009 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ TogoMCP exposes tools for querying the following (via SPARQL or REST APIs): | Category | Resources | |---|---| | Proteins / Proteomics | UniProt, PDB, jPOST | -| Genes / Genomics | NCBI Gene, Ensembl, HGNC, OMA, Bgee, HCO, MCO, DDBJ, MoG+, TogoVar | +| Genes / Genomics | NCBI Gene, Ensembl, HGNC, OMA, Bgee, HCO, MCO, DDBJ, MoG+, TogoVar, GWAS Catalog | | Chemistry | ChEMBL, PubChem, ChEBI, Rhea, BRENDA, MassBank | | Pathways | Reactome | | Disease / Clinical | ClinVar, MedGen, MONDO, NANDO | @@ -232,7 +232,15 @@ togomcp/ Contributions are welcome! -**Adding a database**: add an MIE file under `togo_mcp/data/mie/` and a corresponding row in `togo_mcp/data/resources/endpoints.csv` (see the MIE spec in `togo_mcp/data/docs/`). +**Adding a database**: five places, not two. Only the first two affect what the server *validates*; the rest are documentation surfaces that drift silently, and the tests are what catch them. + +1. `togo_mcp/data/resources/endpoints.csv` — the registry row (this alone decides valid `database=` values). +2. `togo_mcp/data/mie/.yaml` — the MIE file (see the MIE spec in `togo_mcp/data/docs/`). +3. `uv run python scripts/generate_usage_guide_catalog.py` — regenerates the Usage Guide's database catalog. Guarded by `tests/test_catalog_in_sync.py`. +4. `togo_mcp/data/resources/usage_guide_v6/02_budgets_and_discovery.md` — a **hand-written** copy of the registry that no generator touches. Bump the per-endpoint count *and* add the key. Guarded by `TestUsageGuideEndpointTable` in `tests/test_server.py`. +5. `togo_mcp/data/docs/togomcp-intro.html` — add a card to the database grid (not generated). + +Note that a database *removal* really is just step 1: nothing validates against the other four. **Adding a tool**: pass `annotations=READ_ONLY_TOOL` to the `@mcp.tool` decorator. Every TogoMCP tool is read-only, and MCP's default for an *unannotated* tool is the unsafe one — clients such as ChatGPT treat a tool with no `readOnlyHint` as a write action, which means a confirmation prompt on every call. A test asserts this, so omitting it fails the build. diff --git a/pyproject.toml b/pyproject.toml index de5ecf9..ede9a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ togo_mcp = [ [project] name = "togo-mcp" -version = "2.2.1" +version = "2.3.0" description = "MCP Servers for using the RDF Portal" authors = [ {name = "Akira R. Kinjo"}, diff --git a/togo_mcp/data/docs/togomcp-intro.html b/togo_mcp/data/docs/togomcp-intro.html index f0045cb..906bd1c 100644 --- a/togo_mcp/data/docs/togomcp-intro.html +++ b/togo_mcp/data/docs/togomcp-intro.html @@ -767,6 +767,10 @@

What's New

Full changelog →

@@ -1121,6 +1121,10 @@

Available Databases

MoG+ (Mouse Genomes plus)
Genome-wide sequence variation across 62 inbred and wild-derived mouse strains on the GRCm39 assembly: ~146M variants (SNVs and indels) with per-strain genotypes, Ensembl VEP and SnpEff functional consequences, and Ensembl gene-overlap links.
+
+
GWAS Catalog
+
NHGRI-EBI catalog of published genome-wide association studies: 955,930 SNP–trait associations (801,096 at genome-wide significance) across 89,981 studies and 444,106 SNPs, with p-values, effect sizes, risk alleles, mapped genes, and 11,017 EFO trait terms joinable to the co-hosted EFO ontology.
+
TogoVar
Integrated human genome variation database (GRCh38): ~390M variants (SNV, deletion, insertion, MNV, indel) with normalized/VCF coordinates, Ensembl-VEP consequences (SIFT/PolyPhen/AlphaMissense, HGVS), dbSNP and HGNC/Ensembl gene links, and an embedded ClinVar annotation layer. Per-population allele frequencies are served via the TogoVar REST API tools.
diff --git a/togo_mcp/data/mie/gwascatalog.yaml b/togo_mcp/data/mie/gwascatalog.yaml new file mode 100644 index 0000000..b6f4aa2 --- /dev/null +++ b/togo_mcp/data/mie/gwascatalog.yaml @@ -0,0 +1,354 @@ +database: gwascatalog + +discovery: + title: NHGRI-EBI GWAS Catalog + description: > + Genome-wide association study results linking SNPs (rsIDs) to human traits and diseases, + with p-values, effect sizes, risk alleles, mapped genes and EFO trait terms. + keywords: + - gwas + - genome-wide association + - snp + - variant + - polymorphism + - trait + - phenotype + - p-value + - odds ratio + - effect size + - risk allele + - heritability + - efo + - association study + categories: [variant, genomics, disease] + +endpoint: https://rdfportal.org/ebi/sparql +base_uri: http://rdf.ebi.ac.uk/dataset/gwas/ +graphs: + primary: http://rdf.ebi.ac.uk/dataset/gwas_catalog + co_hosted: + ontology/efo: > + 2g probe 2026-07-31 — JOIN TARGET, not an inflator. Holds the ONLY rdfs:label and + obo:IAO_0000115 definition for the EFO trait IRIs this DB points at (500/500 sampled traits + labelled here, ×1 each; gwas_catalog itself stores NO label for a trait IRI). Cross-graph join + required for trait names on the OBAN side — see example xdb_trait_label_efo. + ontology/clo: > + Re-declares rdfs:label on 8 of 500 sampled trait IRIs (×2 on the label leg for those). + Harmless if you pin ontology/efo; only bites an unpinned `?trait rdfs:label ?l`. + expressionatlas: > + Carries rdfs:label + dct:identifier for the `identifiers.org/ensembl/ENSG*` gene IRIs used by + m2r:snp_gene_ids (the main `ensembl` graph does NOT). Join target for gene symbols; ×1. + ensembl_grch37: "Re-types the same ENSG gene IRIs (rdf:type only, ×1). No label, no value conflict." + biosamples: "Types 81 of 500 sampled trait IRIs. Contributes to rdf:type inflation only — see the type_triple_inflation gotcha." + chembl/chebi/reactome/amrportal/biomodels/ensembl*: > + Also on this endpoint but share no IRI with gwas_catalog — probed clean (reverse probe on 6 + representative + hub IRIs, all legs). + +entity_counts: + associations_med2rdf: 955930 # gwas:Association (blank nodes) + trait_associations_oban: 782879 # gwas:TraitAssociation (IRI-addressable) + snps: 444106 + distinct_efo_traits: 11017 + studies_typed_total: 224583 # MIXED — see study_dual_population gotcha + studies_real_gcst: 89981 # the actual GWAS Catalog study records + cytogenetic_regions: 828 + chromosomes: 24 + genome_wide_significant_assocs: 801096 # p_value < 5e-8 + verified: "2026-07-31" + +global_gotchas: + - id: literal_form_split + say: > + THE #1 silent-failure mode. The two vocabularies store string literals in OPPOSITE term forms, + and DATATYPE() reports xsd:string for BOTH so it cannot tell them apart. Verified by ASK + 2026-07-31 on the same rsID: `gwas:has_snp_reference_id "rs1296720"` → 0 rows, needs + `^^xsd:string`; `m2r:snps "rs1296720"^^xsd:string` → 0 rows, needs the plain form. RULE: + `gwas:` predicates → ^^xsd:string; `m2r:`/`dct:` → plain. When unsure, + FILTER(STR(?x) = "value") matches both. + - id: two_parallel_models + say: > + This graph merges TWO independent association models over the same science, with DIFFERENT + populations. (1) med2rdf `gwas:Association` — 955,930 BLANK NODES carrying the full flat row + (p_value, odds_ratio, risk allele, genes, study). Blank nodes are NOT addressable by IRI: + you must reach them via a property (m2r:snps, m2r:mapped_trait_uri, m2r:study). (2) EBI/OBAN + `gwas:TraitAssociation` — 782,879 real IRIs joining a SNP IRI to an EFO trait IRI via + oban:has_subject / oban:has_object. Pick ONE model per query; mixing them double-counts. + - id: empty_string_sentinel + say: > + Absent values are stored as EMPTY STRING literals, not omitted triples. m2r:mapped_genes, + m2r:snp_gene_ids, m2r:upstream_gene_id, m2r:downstream_gene_id and the *_distance predicates + are all present on essentially every association. So OPTIONAL always binds and BOUND(?x) is + always true — "associations with a mapped gene" counted by triple presence returns all + 955,930. FIX: FILTER(?g != "") or, for the IRI-valued ones, FILTER(isIRI(?g)). + - id: study_dual_population + say: > + `?s a gwas:Study` returns 224,583 but that is TWO disjoint IRI families: 89,981 real study + records at http://www.ebi.ac.uk/gwas/studies/GCST* (these carry dct:identifier, dct:description, + m2r:initial_sample_size) and 134,602 EBI-side http://rdf.ebi.ac.uk/dataset/gwas/Trackable/* + publication stubs (these carry only gwas:has_author + gwas:has_publication_date). Reporting + 224,583 as "studies in the GWAS Catalog" is wrong. FIX: filter + STRSTARTS(STR(?s), "http://www.ebi.ac.uk/gwas/studies/") or require dct:identifier. + - id: type_triple_inflation + say: > + Every EFO trait IRI carries THREE rdf:type triples inside gwas_catalog alone — owl:Class, + owl:NamedIndividual, and a self-type (`?trait a `) — so `?t a ?type` is ×3 + before any co-tenant, and ×4.18 unpinned (measured: 500 traits → 2,089 type triples = + gwas_catalog 1,500 + ontology/efo 500 + biosamples 81 + ontology/clo 8). FIX: never count + traits via rdf:type — COUNT(DISTINCT ?trait) anchored on oban:has_object or + m2r:mapped_trait_uri, with FROM . + +examples: + - id: snp_associations_by_rsid + intent: all trait associations reported for one rsID, with effect statistics + question: "What traits is rs1296720 associated with, and how strong are the associations?" + complexity: basic + sparql: | + PREFIX gwas: + PREFIX m2r: + SELECT ?trait ?pvalue ?riskAllele ?orValue ?study + FROM + WHERE { + ?a a gwas:Association ; + m2r:snps "rs1296720" ; # PLAIN literal — m2r: side + m2r:mapped_trait ?trait ; # on an Association this is the trait LABEL + m2r:p_value ?pvalue ; + m2r:strongest_snp_risk_allele ?riskAllele ; + m2r:study ?study . + OPTIONAL { ?a m2r:odds_ratio ?orValue } + } + ORDER BY ?pvalue + LIMIT 20 + verified: {rows: 4, top_trait: "QT interval", top_pvalue: 2e-23, date: "2026-07-31"} + teaches: "Reach the blank-node Association through m2r:snps with a PLAIN rsID literal; the whole flat row hangs off that one node." + traps_avoided: + - "m2r:odds_ratio is MIXED-TYPED: xsd:decimal where a value exists, xsd:string \"NA\" where it does not (roughly half each). FILTER(?orValue > 1.5) silently drops the \"NA\" rows (harmless), but AVG()/MAX() over it breaks and ORDER BY sorts strings and numbers separately. Same for m2r:beta and m2r:risk_allele_frequency." + + - id: study_by_accession + intent: resolve a GCST study accession to its trait, cohort description and publication + question: "What did GWAS Catalog study GCST000032 investigate, and in what cohort?" + complexity: basic + sparql: | + PREFIX gwas: + PREFIX m2r: + PREFIX dct: + SELECT ?accession ?trait ?initialSample ?pubmed + FROM + WHERE { + ?study a gwas:Study ; + dct:identifier "GCST000032" ; # PLAIN literal — dct: side + dct:identifier ?accession . + OPTIONAL { ?study dct:description ?trait } + OPTIONAL { ?study m2r:initial_sample_size ?initialSample } + OPTIONAL { ?study gwas:has_pubmed_id ?pubmed } + } + LIMIT 10 + verified: {rows: 1, trait: "Stroke", pubmed: "17434096", date: "2026-07-31"} + teaches: "Requiring dct:identifier is also the cheapest way to restrict ?s a gwas:Study to the 89,981 real study records." + traps_avoided: + - "dct:description holds the study's REPORTED trait as free text (\"Stroke\"), not an EFO IRI — the EFO IRI is on m2r:mapped_trait for a Study (but is a label string on an Association)." + + - id: snp_location_ebi + intent: genomic position and cytogenetic band for an rsID via the EBI/OBAN entity + question: "Where in the genome is rs1296720, and which cytogenetic band is it in?" + complexity: basic + sparql: | + PREFIX gwas: + PREFIX ro: + PREFIX xsd: + SELECT ?snp ?position ?region + FROM + WHERE { + ?snp a gwas:SingleNucleotidePolymorphism ; + gwas:has_snp_reference_id "rs1296720"^^xsd:string . # TYPED — gwas: side + OPTIONAL { ?snp gwas:has_basepair_position ?position } + OPTIONAL { ?snp ro:located_in ?region } + } + LIMIT 10 + verified: {rows: 1, position: 3823641, region: "CytogeneticRegion/16p13.3", date: "2026-07-31"} + teaches: "The gwas: vocabulary needs ^^xsd:string on every string match; has_basepair_position (unlike m2r:chr_pos) is a real integer." + traps_avoided: + - "You cannot build the SNP IRI from an rsID: .../SingleNucleotidePolymorphism/10064763 holds rs1296720 — the numeric tail is an internal key, NOT the rs number. Always look the SNP up by has_snp_reference_id." + + - id: trait_snps_oban + intent: SNPs associated with an EFO trait through the IRI-addressable OBAN model + question: "Which SNPs are most strongly associated with QT interval?" + complexity: intermediate + sparql: | + PREFIX gwas: + PREFIX oban: + SELECT ?rsid ?pvalue ?traitName + FROM + WHERE { + ?ta a gwas:TraitAssociation ; + oban:has_object ; # trait + oban:has_subject ?snp ; # SNP + gwas:has_p_value ?pvalue ; + gwas:has_gwas_trait_name ?traitName . + ?snp gwas:has_snp_reference_id ?rsid . + } + ORDER BY ?pvalue + LIMIT 10 + verified: {rows: 10, top_rsid: "rs2074238", distinct_trait_names: ["QT interval", "QTc interval"], date: "2026-07-31"} + teaches: "OBAN reification: has_subject is always the SNP and has_object always the EFO trait — the direction never varies, so it is safe to anchor on either end." + traps_avoided: + - "gwas:has_p_value is xsd:double, so p-values below ~1e-308 UNDERFLOW TO 0.0 and become mutually indistinguishable (two rows here are both 0.0). To rank the extreme tail, use the rank_by_mlog route below instead." + - "has_gwas_trait_name is the REPORTED trait string and varies within one EFO trait (\"QT interval\" and \"QTc interval\" both map to EFO_0004682) — group on the EFO IRI, not on this label." + + - id: rank_by_mlog + intent: rank the most significant associations without hitting double underflow + question: "Which variants show the strongest statistical evidence for QT interval?" + complexity: intermediate + sparql: | + PREFIX gwas: + PREFIX m2r: + SELECT ?rsid ?trait ?mlog + FROM + WHERE { + ?a a gwas:Association ; + m2r:mapped_trait_uri ; + m2r:p_value_mlog ?mlog ; # -log10(p), xsd:decimal — no underflow + m2r:snps ?rsid ; + m2r:mapped_trait ?trait . + } + ORDER BY DESC(?mlog) + LIMIT 10 + verified: {rows: 10, top_rsid: "rs12143842", top_mlog: 776, second_mlog: 399.39794000867204, date: "2026-07-31"} + teaches: "Rank significance on m2r:p_value_mlog (xsd:decimal), never on the raw p-value: -log10 keeps the extreme tail ordered where xsd:double collapses it to 0.0." + traps_avoided: + - "The top two rows here are BOTH p = 0.0 on the raw p-value (see trait_snps_oban) — mlog separates them as 776 vs 399.4. Ranking on the raw p-value silently ties the strongest hits in the catalog." + - "A larger mlog means MORE significant, so this needs ORDER BY DESC — the opposite direction from ORDER BY ?pvalue." + + - id: enum_snps_for_trait + intent: enumerate ALL SNPs recorded for one trait (set-level route) + question: "How many distinct SNPs has the GWAS Catalog associated with QT interval?" + complexity: basic + sparql: | + PREFIX gwas: + PREFIX m2r: + SELECT (COUNT(DISTINCT ?rsid) AS ?distinct_snps) (COUNT(DISTINCT ?a) AS ?associations) + FROM + WHERE { + ?a a gwas:Association ; + m2r:mapped_trait_uri ; # IRI, not a string + m2r:snps ?rsid . + } + verified: {distinct_snps: 631, associations: 858, date: "2026-07-31"} + teaches: "m2r:mapped_trait_uri is a real IRI — enumerate a trait's variants by that IRI, never by matching the trait NAME." + traps_avoided: + - "Associations (858) exceed distinct SNPs (631) because the same SNP is reported by several studies — COUNT(?a) is a study-report count, COUNT(DISTINCT ?rsid) is the variant count. Decide which one the question asks for." + - "Matching m2r:mapped_trait \"QT interval\" instead undercounts: that label is free text and its variants (\"QTc interval\", \"trait in response to thiazide, QT interval\") sit under the same EFO IRI." + + - id: enum_traits_for_gene + intent: enumerate ALL traits associated with variants mapped to one gene (set-level route) + question: "Which traits have GWAS variants mapped to the CREBBP gene?" + complexity: intermediate + sparql: | + PREFIX gwas: + PREFIX m2r: + SELECT ?trait (COUNT(DISTINCT ?a) AS ?n) + FROM + WHERE { + ?a a gwas:Association ; + m2r:snp_gene_ids ; # CREBBP + m2r:mapped_trait ?trait . + } + GROUP BY ?trait ORDER BY DESC(?n) LIMIT 15 + verified: {rows: 13, top: {trait: "QT interval", n: 4}, date: "2026-07-31"} + teaches: "m2r:snp_gene_ids carries an identifiers.org Ensembl gene IRI, so gene-anchored enumeration is a direct IRI match with no text search and no ID bridge." + traps_avoided: + - "m2r:snp_gene_ids is IRI-valued ONLY where a gene was mapped; elsewhere it is an EMPTY STRING (241 IRI vs 89 \"\" in a 330-row sample). `?a m2r:snp_gene_ids ?g` with no filter binds junk — add FILTER(isIRI(?g)) when you leave it as a variable." + - "m2r:mapped_genes looks like the gene field but was EMPTY on 300/300 sampled associations — snp_gene_ids is the populated one." + - "m2r:mapped_trait flips type by subject class: a LABEL string on an Association (used here), but an EFO IRI on a Study. Never write a query that reads it off both." + + - id: agg_genome_wide_significant + intent: count associations passing the genome-wide significance threshold + question: "How many GWAS Catalog associations reach genome-wide significance (p < 5e-8)?" + complexity: aggregation + sparql: | + PREFIX gwas: + PREFIX m2r: + SELECT (COUNT(DISTINCT ?a) AS ?genome_wide_significant) + FROM + WHERE { + ?a a gwas:Association ; + m2r:p_value ?p . + FILTER(?p < 5.0e-8) + } + verified: {genome_wide_significant: 801096, of_total_associations: 955930, date: "2026-07-31"} + teaches: "FROM-pin + COUNT(DISTINCT) is the union-safe counting recipe; m2r:p_value is uniformly xsd:double so the numeric FILTER needs no cast." + traps_avoided: + - "m2r:p_value is the only cleanly-typed numeric on the association row (500/500 xsd:double). Its neighbours are not — see the mixed-typing trap on snp_associations_by_rsid before filtering on odds_ratio or beta." + + - id: agg_study_populations + intent: separate the two IRI families hiding behind gwas:Study + question: "How many studies does the GWAS Catalog actually contain?" + complexity: aggregation + sparql: | + PREFIX gwas: + SELECT ?family (COUNT(DISTINCT ?s) AS ?n) (SAMPLE(?s) AS ?example) + FROM + WHERE { + ?s a gwas:Study . + BIND(IF(STRSTARTS(STR(?s), "http://www.ebi.ac.uk/gwas/studies/GCST"), + "real_gcst_study", "ebi_trackable_stub") AS ?family) + } + GROUP BY ?family + verified: {real_gcst_study: 89981, ebi_trackable_stub: 134602, date: "2026-07-31"} + teaches: "When one class spans two IRI namespaces, BIND(STRSTARTS(...)) partitions it in a single pass and shows the split instead of hiding it." + traps_avoided: + - "The headline answer is 89,981, NOT the 224,583 that `?s a gwas:Study` returns. The 134,602 Trackable/* stubs carry only has_author and has_publication_date — they are publication nodes, not studies." + + - id: xdb_trait_label_efo + intent: "cross-graph: GWAS trait IRI -> its EFO label and definition (the only source of trait names)" + question: "What is EFO_0004682, and how many GWAS associations use it?" + complexity: cross_db + endpoint_name: ebi + sparql: | + PREFIX gwas: + PREFIX m2r: + PREFIX rdfs: + PREFIX obo: + SELECT ?label ?definition (COUNT(DISTINCT ?a) AS ?associations) + WHERE { + GRAPH { + rdfs:label ?label . + OPTIONAL { obo:IAO_0000115 ?definition } + } + GRAPH { + ?a a gwas:Association ; m2r:mapped_trait_uri . + } + } + GROUP BY ?label ?definition + verified: {label: "QT interval", associations: 858, has_definition: true, date: "2026-07-31"} + teaches: "The EFO IRI is shared verbatim between gwas_catalog and ontology/efo on the SAME endpoint — a direct two-GRAPH join, no ID bridge and no TogoID hop." + traps_avoided: + - "gwas_catalog stores NO rdfs:label for a trait IRI (0 of 500 sampled). Querying trait names inside the gwas graph alone returns nothing — the label only exists in ontology/efo." + - "Pin ontology/efo explicitly: unpinned, ontology/clo re-declares rdfs:label for a minority of trait IRIs and doubles those rows." + +schema_delta: + - "m2r:p_value_mlog — -log10(p) as xsd:decimal; the reliable way to rank the extreme significance tail that m2r:p_value underflows to 0.0." + - "m2r:chr_id and m2r:chr_pos are xsd:string, NOT numbers — a positional range filter needs xsd:integer(?chr_pos); gwas:has_basepair_position (EBI side) is already an integer." + - "m2r:context — the variant consequence as a bare SO term name (\"intron_variant\"), a string not an SO IRI." + - "m2r:upstream_gene_id / m2r:downstream_gene_id (+ the matching *_distance predicates) give the flanking genes when the SNP is intergenic; same empty-string sentinel as snp_gene_ids." + - "m2r:intergenic, m2r:cnv, m2r:merged are string flags (\"0\"/\"1\", \"N\"), not booleans." + - "ro:part_of / ro:has_part link a TraitAssociation to a parent TraitAssociation, grouping the sub-associations reported under one analysis." + - "gwas:has_author and gwas:has_publication_date (134,602 each) live ONLY on the Trackable/* study stubs — they are the publication metadata for the EBI side." + - "m2r:genotyping_technology, m2r:platform_snps_passing_qc, m2r:replication_sample_size describe study methodology; note replication_sample_size is often the literal string \"NA\"." + +id_join_map: + stable_anchor: > + Study = http://www.ebi.ac.uk/gwas/studies/GCST (accession also on dct:identifier as a PLAIN + literal). Trait = the EFO IRI http://www.ebi.ac.uk/efo/EFO_<7 digits>. SNP = an opaque internal + IRI .../gwas/SingleNucleotidePolymorphism/ whose number is NOT the rs number — anchor on the + rsID instead, via gwas:has_snp_reference_id "rsN"^^xsd:string (EBI side) or m2r:snps "rsN" plain + (med2rdf side). gwas:Association is a BLANK NODE and has no stable anchor at all. + same_endpoint_joins: + efo: "EFO trait IRIs are shared verbatim with GRAPH — the only source of trait labels/definitions/synonyms. See example xdb_trait_label_efo." + expressionatlas: "m2r:snp_gene_ids Ensembl gene IRIs (http://identifiers.org/ensembl/ENSG*) carry rdfs:label + dct:identifier in GRAPH ; the main ensembl graph does not." + ensembl_grch37: "Same ENSG IRIs are typed in GRAPH (rdf:type only — no labels)." + xrefs: + dbsnp: "rdfs:seeAlso -> http://identifiers.org/dbsnp/rs as an IRI, on the Association node (~841k rdfs:seeAlso triples graph-wide)." + pubmed: "dct:references -> http://rdf.ncbi.nlm.nih.gov/pubmed/ as an IRI; gwas:has_pubmed_id carries the bare PMID string alongside it (1,180,513 triples)." + ensembl: "m2r:snp_gene_ids -> http://identifiers.org/ensembl/ENSG as an IRI, but an EMPTY STRING where no gene was mapped — filter isIRI() before joining." + bridged_via_togoid: + - "ncbigene, hgnc, clinvar, togovar — not co-hosted on this endpoint; bridge with togoid_convertId using the dbSNP rsID or the Ensembl gene ID as the source key." diff --git a/togo_mcp/data/resources/endpoints.csv b/togo_mcp/data/resources/endpoints.csv index 7b6330c..934079e 100644 --- a/togo_mcp/data/resources/endpoints.csv +++ b/togo_mcp/data/resources/endpoints.csv @@ -35,3 +35,4 @@ hco,https://rdfportal.org/primary/sparql,primary,sparql mco,https://rdfportal.org/primary/sparql,primary,sparql togovar,https://grch38.togovar.org/sparql,togovar,togovar_search_(gene|disease) ontology,https://rdfportal.org/primary/sparql,primary,OLS4:searchClasses +gwascatalog,https://rdfportal.org/ebi/sparql,ebi,OLS4:searchClasses diff --git a/togo_mcp/data/resources/usage_guide_v6/02_budgets_and_discovery.md b/togo_mcp/data/resources/usage_guide_v6/02_budgets_and_discovery.md index fcfdd3e..e215c77 100644 --- a/togo_mcp/data/resources/usage_guide_v6/02_budgets_and_discovery.md +++ b/togo_mcp/data/resources/usage_guide_v6/02_budgets_and_discovery.md @@ -82,7 +82,7 @@ is the bold row label. | Endpoint | n | `database` keys | |---|---:|---| | **primary** | 16 | `mesh` `go` `taxonomy` `mondo` `nando` `bacdive` `mediadive` `brenda` `hgnc` `jpostdb` `massbank` `nbrc` `mogplus` `hco` `mco` `ontology` | -| **ebi** | 5 | `chembl` `chebi` `reactome` `ensembl` `amrportal` | +| **ebi** | 6 | `chembl` `chebi` `reactome` `ensembl` `amrportal` `gwascatalog` | | **ncbi** | 5 | `clinvar` `pubmed` `pubtator` `ncbigene` `medgen` | | **sib** | 4 | `uniprot` `rhea` `bgee` **`oma`** | | **pubchem** | 1 | `pubchem` | diff --git a/togo_mcp/data/resources/usage_guide_v6/02b_database_catalog.md b/togo_mcp/data/resources/usage_guide_v6/02b_database_catalog.md index af2d2ee..dd916bd 100644 --- a/togo_mcp/data/resources/usage_guide_v6/02b_database_catalog.md +++ b/togo_mcp/data/resources/usage_guide_v6/02b_database_catalog.md @@ -1,6 +1,6 @@ ## 📚 DATABASE CATALOG -All 36 RDF databases, with what each is *for*. Scan by the KIND of data you need (not by entity name), pick 1–3 candidates, then `get_MIE_file(database)` before any `run_sparql`. The exact `database=` key is **bold**. +All 37 RDF databases, with what each is *for*. Scan by the KIND of data you need (not by entity name), pick 1–3 candidates, then `get_MIE_file(database)` before any `run_sparql`. The exact `database=` key is **bold**. Quick hints: "MANE" → `ensembl` · "drug targets" → `chembl` · "clinical variants" → `clinvar` · "pathways" → `reactome` · "gnomAD" / "variants" → `togovar` · "orthologs" → `oma` · "expression" → `bgee` · "glycobiology" → `glycosmos` · "superconductor" → `supercon`. @@ -9,11 +9,11 @@ Quick hints: "MANE" → `ensembl` · "drug targets" → `chembl` · "clinical va - **annotation** — `pubtator` `uniprot` - **antimicrobial** — `amrportal` - **compound** — `chebi` `chembl` `massbank` `pubchem` -- **disease** — `clinvar` `glycosmos` `medgen` `mesh` `mondo` `nando` `togovar` +- **disease** — `clinvar` `glycosmos` `gwascatalog` `medgen` `mesh` `mondo` `nando` `togovar` - **drug_target** — `chembl` - **enzymology** — `brenda` - **gene** — `bgee` `ensembl` `glycosmos` `hgnc` `medgen` `ncbigene` -- **genomics** — `hco` `hgnc` `mco` `mogplus` `oma` `togovar` +- **genomics** — `gwascatalog` `hco` `hgnc` `mco` `mogplus` `oma` `togovar` - **glycan** — `glycosmos` - **literature** — `pubmed` `pubtator` - **materials** — `supercon` @@ -26,7 +26,7 @@ Quick hints: "MANE" → `ensembl` · "drug targets" → `chembl` · "clinical va - **sequence** — `ddbj` `ensembl` - **structure** — `pdb` - **taxonomy** — `bgee` `taxonomy` -- **variant** — `clinvar` `mogplus` `togovar` +- **variant** — `clinvar` `gwascatalog` `mogplus` `togovar` **All databases** (alphabetical): @@ -52,6 +52,8 @@ Quick hints: "MANE" → `ensembl` · "drug targets" → `chembl` · "clinical va keywords: glycan, glycosylation, glycoprotein, saccharide, wurcs, glytoucan, n-glycan, o-glycan, glycogene, monosaccharide, glycan motif, epitope, lectin, cazy, carbohydrate-active enzyme, glycoside hydrolase, glycosyltransferase, ec number, disease gene, doid, gene ontology, tissue expression, human protein atlas, glycolipid - **go** — Gene Ontology (GO). Cross-species controlled vocabulary for gene-product function, organized into three independent domains (biological_process, molecular_function, cellular_component) with a DAG hierarchy, definitions,… _(categories: ontology)_ keywords: gene ontology, ontology, biological process, molecular function, cellular component, go term, functional annotation, obo, controlled vocabulary, hierarchy, subclassof, dag +- **gwascatalog** — NHGRI-EBI GWAS Catalog. Genome-wide association study results linking SNPs (rsIDs) to human traits and diseases, with p-values, effect sizes, risk alleles, mapped genes and EFO trait terms. _(categories: disease, genomics, variant)_ + keywords: gwas, genome-wide association, snp, variant, polymorphism, trait, phenotype, p-value, odds ratio, effect size, risk allele, heritability, efo, association study - **hco** — HCO — Human Chromosome Ontology (cytobands). The human cytogenetic map: every Giemsa-stained chromosome band (ISCN name, e.g. _(categories: genomics, ontology)_ keywords: cytoband, chromosome band, karyotype, cytogenetic, giemsa stain, ideogram, genomic coordinates, grch37, grch38, genome build, faldo, chromosome location, iscn, human chromosome - **hgnc** — HGNC — HUGO Gene Nomenclature Committee. Authoritative approved human gene nomenclature: official symbol, full name, HGNC ID, chromosomal band, and a central hub of typed cross-references (NCBI Gene, Ensembl, RefSeq, UniProt, OMIM, Orphanet… _(categories: gene, genomics)_ diff --git a/uv.lock b/uv.lock index e5df233..100656c 100644 --- a/uv.lock +++ b/uv.lock @@ -1820,7 +1820,7 @@ wheels = [ [[package]] name = "togo-mcp" -version = "2.2.1" +version = "2.3.0" source = { editable = "." } dependencies = [ { name = "fastmcp" },