Sitemap
Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science

Best practices, tips & tricks from Snowflake experts and community

Using Life-Science Ontologies from a Snowflake AI Agent: Gene Ontology and the OLS4 MCP

8 min read13 hours ago

--

Introduction

In enterprise data work, the word “ontology” comes up more and more. The idea is to give an AI agent an ontology as the context layer it refers to when it works with company data.

Life science has had ontologies such as the Gene Ontology (GO) for more than 20 years, and anyone can query them through public APIs. EMBL-EBI’s Ontology Lookup Service (OLS4) also offers an MCP server, so an AI agent can look up GO definitions and hierarchies directly.

In this article, I run a GO enrichment in Snowflake on the genes that changed in an RNA-seq experiment (differentially expressed genes, DEGs), and then have an AI agent interpret the result while it looks terms up in OLS4.

Life-science ontologies

In the data-platform world, the usual example is the Palantir Ontology. Its documentation calls it “an operational layer for the organization.” It holds the organization’s objects and links, plus actions and functions. It is an ontology for modelling and running your own business.

Healthcare and life-science ontologies such as GO, the Human Phenotype Ontology (HPO), Mondo and ChEBI are different. They are reference ontologies that define scientific concepts independent of any organization. They give shared identifiers and hierarchies to gene functions, phenotypes, diseases and chemicals, and researchers use them as the standard when they annotate data.

  • Purpose: Palantir Ontology represents and runs the business; GO / HPO / Mondo standardize scientific concepts.
  • Core: Palantir has objects, links and actions; GO / HPO / Mondo have classes, relations and definitions.
  • Relation to data: Palantir holds the actual data; GO / HPO / Mondo are the standard used to annotate data.

The timeline below shows how life-science ontologies and the infrastructure for distributing and querying them developed.

Press enter or click to view image in full size
How life-science ontologies and their infrastructure developed

Of these, GO, which appeared in 2000, is widely used in enrichment analysis downstream of RNA-seq. Life-science researchers use ontologies every day.

An architecture for using life-science ontologies from Snowflake

Using life-science ontologies from Snowflake looks like this.

Press enter or click to view image in full size
Architecture for using life-science ontologies from Snowflake

In this article, I build the part where the agent uses the OLS4 MCP, using the Cortex Code Agent SDK. I did not build the semantic view, the ontology gateway or the cache.

A Cortex Agent picks the tool for internal data or the tool for ontologies, depending on the question.

Internal data (left side of the diagram) such as expression, variants and sample information lives in Snowflake tables or Iceberg tables, and a semantic view defines the relationships and metrics between them. Masking policies and row access policies in Horizon Catalog control access here.

For external ontologies (right side), I use the OLS4 MCP server. OLS4 is a service for searching many ontologies, including GO. The MCP server is provided in the official repository and needs no authentication. One server gives access to GO, HPO, Mondo, ChEBI and more.

However, the Cortex Agent MCP connector requires OAuth authentication, so it cannot connect to OLS4 as is, because OLS4 has no authentication. In that case, you register a stored procedure or UDF as a custom tool and call the OLS4 REST API through an external access integration (EAI). Resources without an MCP server, such as Open Targets and ChEMBL, can be called the same way.

Finally, the internal and external results are joined with SQL in Snowflake. If the mapping table between internal IDs and ontology terms also records the ontology release you used, you can track differences after the source is updated.

Looking up GO through the OLS4 MCP server

With the Python MCP SDK, I call searchClasses, fetch and getAncestors to get the definition and hierarchy of GO:0006915 (apoptotic process). I run it in a Snowflake Notebook, so asyncio.run runs in a separate thread to avoid clashing with the notebook's event loop.

import asyncio
import json
from concurrent.futures import ThreadPoolExecutor
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
OLS4_MCP_URL = "https://www.ebi.ac.uk/ols4/api/mcp"
GO_IRI = "http://purl.obolibrary.org/obo/GO_0006915"
def text_of(result):
return "".join(c.text for c in result.content if getattr(c, "type", "") == "text")
async def main():
async with streamable_http_client(OLS4_MCP_URL) as streams:
async with ClientSession(streams[0], streams[1]) as s:
await s.initialize()
term = await s.call_tool("searchClasses", {"query": "GO:0006915", "ontologyId": "go"})
definition = await s.call_tool("fetch", {"id": f"go+{GO_IRI}"})
ancestors = await s.call_tool("getAncestors", {"ontologyId": "go", "classIri": GO_IRI})
return term, definition, ancestors
term, definition, ancestors = ThreadPoolExecutor(1).submit(asyncio.run, main()).result()t = json.loads(text_of(term))["items"][0]
d = json.loads(text_of(definition))
a = json.loads(text_of(ancestors))["items"]
print("term:", t["curie"], t["label"][0])
print("definition:", d["text"])
print("parent:", [p["label"][0] for p in t["directParent"]])
print("ancestors:", " → ".join(x["label"][0] for x in a))
term: GO:0006915 apoptotic process
definition: A programmed cell death process which begins when a cell receives an internal (e.g. DNA damage) or external signal ...
parent: ['programmed cell death']
ancestors: occurrent → process → biological_process → cell death → cellular process → programmed cell death
Press enter or click to view image in full size
Calling the OLS4 MCP from a Snowflake Notebook

The official GO label is apoptotic process, so a searchClasses keyword search for apoptosis returns child terms at the top. That is why I look the term up by its GO ID here.

Building DEGs from the airway data

The Bioconductor airway package is RNA-seq data from four human airway smooth muscle cell lines treated with dexamethasone. I use DESeq2 and adjust for the differences between cell lines.

https://www.bioconductor.org/packages/release/data/experiment/html/airway.html

The analysis is done in R.

library(airway); library(DESeq2)
data("airway")
airway$dex <- relevel(airway$dex, ref = "untrt")
dds <- DESeqDataSet(airway, design = ~ cell + dex)
dds <- DESeq(dds[rowSums(counts(dds)) >= 10, ])
res <- results(dds, contrast = c("dex", "trt", "untrt"), alpha = 0.05)

With padj < 0.05 and |log2FoldChange| > 1, there are 999 DEGs (519 up, 480 down).

Get Tatsuya Koreeda’s stories in your inbox

Join Medium for free to get updates from this writer.

This R code also runs in a Snowflake Notebook (Container Runtime). I create an R and DESeq2 environment with micromamba and call Rscript from a Python cell.

Press enter or click to view image in full size
Running DESeq2 in a Snowflake Notebook

Computing GO enrichment in Snowflake

I load the DESeq2 results into AIRWAY_DEG and define the DEGs as a view.

CREATE OR REPLACE VIEW AIRWAY_DEG_SIG AS
SELECT
*,
IFF(LOG2_FOLD_CHANGE > 0, 'UP', 'DOWN') AS DIRECTION
FROM AIRWAY_DEG
WHERE PADJ < 0.05
AND ABS(LOG2_FOLD_CHANGE) > 1;

Counting the view by UP / DOWN gives 519 / 480, the same as in R.

Press enter or click to view image in full size
Counting AIRWAY_DEG_SIG by UP / DOWN

For GO enrichment, I load the gene-to-GO mapping from org.Hs.eg.db into Snowflake and compute it in a Python stored procedure. SQL counts the genes per term, and the p-values come from the hypergeometric distribution. Here is the part that computes the p-values (bh is a function that applies the Benjamini-Hochberg correction).

counts["PVALUE"] = hypergeom.sf(k - 1, N, M, n)
counts["P_ADJUST"] = bh(counts["PVALUE"])

With the same settings as clusterProfiler::enrichGO (BP, gene set size 10 to 500, BH), all 4,540 terms matched. Let's look at the top UP terms.

CALL GO_MCP_DEMO.AIRWAY.GO_ENRICHMENT('UP', 5);

Run in a Snowsight worksheet, it returns a table with the gene count, p-values and gene list for each term.

Press enter or click to view image in full size
Running GO_ENRICHMENT in Snowsight

Letting the Cortex Code Agent SDK interpret the result

The Cortex Agent MCP connector cannot connect to the OLS4 MCP, which has no authentication, so I use the Cortex Code Agent SDK for the agent. LLM inference runs on Snowflake, and you connect an MCP server just by passing its URL. I limited the tools to SQL against Snowflake and the OLS4 MCP.

from cortex_code_agent_sdk import query, CortexCodeAgentOptions
options = CortexCodeAgentOptions(
connection="default",
model="auto",
system_prompt=open("agent/prompts/system_prompt_en.md").read(),
mcp_servers={"ols4": {"type": "http", "url": "https://www.ebi.ac.uk/ols4/api/mcp"}},
allowed_tools=["SQL", "mcp__ols4__*"],
)
async for msg in query(prompt=question, options=options):
...

The system prompt tells the agent to take enrichment numbers only from GO_ENRICHMENT and GO definitions and hierarchies only from OLS4. The answer is split into four sections: Statistics, Meaning in Gene Ontology, AI interpretation (hypotheses to verify) and Limitations.

Here is the question.

Get the genes that changed significantly with dexamethasone treatment.
Run a Gene Ontology Biological Process enrichment on them, separately for up-regulated (UP) and down-regulated (DOWN) genes.
For the top enriched GO terms, look up their definitions and ontology hierarchy with the OLS4 MCP,
and explain the biological changes caused by dexamethasone treatment.

Running it in a terminal, the agent computed the enrichment with SQL and then looked up eight GO terms with the OLS4 MCP searchClasses tool.

Press enter or click to view image in full size
The order of tools the agent called

Here is the statistics part of the answer. The numbers match the GO_ENRICHMENT results.

Press enter or click to view image in full size
Statistics (Snowflake) in the answer

These are the definitions and parent terms pulled from OLS4.

Press enter or click to view image in full size
Meaning in Gene Ontology (OLS4 MCP) in the answer

Finally, the interpretation based on the statistics and the GO definitions. I asked the agent to mark knowledge that did not come from the tools as “Outside knowledge”.

Press enter or click to view image in full size
AI interpretation in the answer

Because it looked up the hierarchy in OLS4, the agent could tell that GO:0030198 and its parents GO:0043062 and GO:0045229 are one signal from the same 28 genes. From the enrichment table alone, these three look like separate findings.

Summary

  • I put the airway DEGs in Snowflake and computed GO enrichment in a stored procedure (matching clusterProfiler).
  • I got GO term definitions and hierarchies from the official OLS4 MCP server.
  • An agent built with the Cortex Code Agent SDK answered by calling tools in the order DEGs, enrichment, OLS4, interpretation.

The OLS4 MCP server connects with just a URL, so it is an easy way to let an agent use GO definitions and hierarchies when it interprets analysis results.

References

--

--