An MCP server for querying the federated SPARQL endpoint
(https://apps.okn.us/federation/sparql) over the
Proto-OKN knowledge graphs.
It lets an LLM discover which knowledge graphs are relevant (from the
okn-registry descriptions), then run
SPARQL queries scoped to one or more named graphs of the form
https://purl.org/okn/frink/kg/{shortname}.
Proto-OKN — the Prototype Open Knowledge Network — is a National Science Foundation initiative (with NASA, NIH, the National Institute of Justice, NOAA, and the U.S. Geological Survey) that funds research teams to build a publicly accessible, interconnected set of data repositories and knowledge graphs. The graphs span domains such as health, the environment, criminal justice, space exploration, and supply-chain security, and are served together over the OKN federated SPARQL endpoint that this server queries. The okn-registry catalogs the participating knowledge graphs.
This repository ships one MCP server, two Agent Skills, and relies on two external literature MCPs — each with a distinct role:
| Component | Role |
|---|---|
![]() mcp-okn |
Federated OKN query service: discovers the relevant Proto-OKN graphs, writes SPARQL scoped to named graphs, aligns across KGs via crosswalks, and returns grounded rows with provenance and transcripts. |
![]() okn-report-style |
Report & reproducibility skill: turns an OKN analysis into a polished, reproducible deliverable (Markdown / HTML / Excel / figures / maps), tracking sources, versions, queries, and caveats. |
![]() okn-bioanalysis |
Biomedical workflow skill: cross-KG analysis of genes, diseases, chemicals, and drugs — enrichment, ortholog projection, mechanistic maps — ranking hypotheses by evidence and calling the literature MCPs to validate. |
![]() PubMed & Paperclip |
Literature evidence validation: search PubMed and full-text collections, corroborate OKN-derived claims against sources, add citations, and flag conflicts and uncertainty. |
Complementary by design: the skills augment mcp-okn, which queries the OKN;
okn-bioanalysis can call PubMed & Paperclip for literature evidence validation, and
okn-report-style turns the results into reproducible deliverables.
Once the server is configured in your MCP client (see Connecting your client), just ask in natural language — the assistant picks the graphs, writes the SPARQL, and combines the results for you. Some prompts to try:
- "List all Proto-OKN knowledge graphs as a table of shortname and description." — Result
- "List all verified crosswalks, grouped by domain, with an example of what each answers." — Result
- "For each crosswalk, list the join key and the SPARQL skeleton." — Result
- "Give a high-level overview of the spoke-genelab knowledge graph — its main classes and relationships — and draw the schema diagram." — Result
- "Which genes does rdkg associate with autism spectrum disorder?" — Result
- "What is the maximum PFAS measurement in each county?" — Result
- "How do I join spoke-okn and prokn? Show the verified recipe and shared identifier." — Result
- "Which knowledge graphs supply GO, pathway, or trait annotations for a gene I can join on Entrez?" — uses
find_context_sourcesto list every supplier with its join key and size - "What version of prokn is loaded, and when was it last updated?" — reads the
okn-voidprovenance viaget_kg_version - "Create a chat transcript of this analysis." — create a transcript in a downloadable Markdown file
- "Create a chat transcript of this analysis in PDF format." — the server returns Markdown and the client converts the
.mdto a.pdffile (Claude Desktop / claude.ai)
A crosswalk is a verified way to join two (or three) Proto-OKN knowledge graphs on a shared identifier. Because the graphs are built by different teams on different ontologies, each crosswalk is an integration opportunity: a question one graph can't answer alone becomes answerable by combining two.
The network map below shows all 244 crosswalks across 39 graphs as KG-to-KG edges
(edge width ∝ log of the verified join count; dashed/dotted lines mark joins bridged
through ubergraph/wikidata).
▶ Click the image to open the interactive, zoomable network.
- Proto-OKN Crosswalk Inventory — one page listing every verified crosswalk: the joined KGs, shared key, row count, and what it answers.
- Cross-KG crosswalk catalog — 496 example questions, at least two per crosswalk, each worked end-to-end as live federated SPARQL with a standalone, replayable transcript.
The catalog was generated with the
crosswalk generation prompt. Transcripts are
produced by create_chat_transcript; scripts/replay_transcript.py re-runs one, and
scripts/verify_transcripts.py re-runs every query in the corpus to flag ones that
now error or return zero rows (run it after a KG redeploy or recipe repair).
End-to-end analyses that federate many Proto-OKN graphs to answer a single
research question, spanning disease biology, spaceflight, environmental exposure
and justice, and infrastructure. Each finding is tagged with its source(s) and
ranked by cross-source agreement. Every case study is a self-contained folder under
docs/examples/{Name}/ with an interactive HTML report, a literature comparison, a
reproducibility record of every verbatim SPARQL query, an Excel workbook, and the
figures, scripts, and data that produced them.
Prerequisites for re-running a case study:
- The two Skills —
okn-bioanalysis(analysis) andokn-report-style(report format). - The PubMed and Paperclip MCP connectors — for the literature-comparison step only.
| Case study | Model | Report | Literature Comparison | Data | Reproducibility | Folder |
|---|---|---|---|---|---|---|
| Type 2 diabetes — 16 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Alzheimer's disease — 8 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Multiple sclerosis — 14 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Spaceflight-induced bone loss — 8 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Spaceflight-associated neuro-ocular syndrome (SANS) — 6 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Spaceflight-liver lipid dysregulation — 7 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Spaceflight-liver lipid dysregulation — 5 KGs | GPT-6-Astra |
HTML | md | xlsx | md | files |
| PFAS source prioritization — 5 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Bisphenol chemical exposome — 7 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Cumulative environmental-justice burden — 8 KGs | claude-opus-4-8 |
HTML | md | xlsx | md | files |
| Urban scaling of disease, mortality & crime — 3 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Flood-mobilised contamination cascade — 6 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Wildlife sentinel surveillance gap (Florida) — 12 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Supply-chain fragility — 6 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Earth-observation instrument criticality — 2 KGs | claude-opus-5 |
HTML | md | xlsx | md | files |
| Earth-observation instrument criticality — 2 KGs | gpt-5.6-sol |
HTML | md | xlsx | md | files |
The server is hosted at https://apps.okn.us/okn-mcp-dev/mcp — point any MCP
client at that URL, no local install required. (For a local install instead, see
Local installation; to check whether the hosted server is
current, see Identifying a deployment.)
- Open Settings → Connectors → Add custom connector.
- Name it
mcp-okn-devand enter the URLhttps://apps.okn.us/okn-mcp-dev/mcp. - Click Configure and set the tool permissions to Always allow.
- In a new chat, click the
+icon and enable themcp-okn-devtoggle.
A Claude Pro or Max subscription is required for MCP connectors in Claude Desktop.
Register it from the CLI:
claude mcp add --transport http mcp-okn-dev https://apps.okn.us/okn-mcp-dev/mcpOr add it to .mcp.json in your project root (or ~/.claude/settings.json for
universal access):
{
"mcpServers": {
"mcp-okn-dev": {
"type": "url",
"url": "https://apps.okn.us/okn-mcp-dev/mcp"
}
}
}Verify with /mcp — you should see mcp-okn-dev listed as connected.
Supported in the ChatGPT web app at https://chatgpt.com.
- Sign in to https://chatgpt.com.
- Click your profile name/avatar.
- Open Settings.
- Go to Apps.
- Open Advanced settings.
- Turn on Developer mode (required for custom MCP apps in ChatGPT).
- Return to Apps.
- Click Create app.
- Enter the MCP app details:
- Name:
mcp-okn-dev - URL:
https://apps.okn.us/okn-mcp-dev/mcp
- Name:
- Save or create the app.
- Start a new chat.
- Click the + button in the message box.
- Select mcp-okn-dev from the list of available apps/tools.
- Turn Web search off before testing, so ChatGPT uses the MCP app rather than web search.
- Run the verification prompt. If ChatGPT returns a graph list, the MCP app is working.
A subscription is required for MCP connectors in ChatGPT.
Use MCP in Agent mode with the GitHub Copilot extension, with the same URL:
https://apps.okn.us/okn-mcp-dev/mcp.
The case studies' Comparison with prior work step checks each finding against the primary literature, which needs two additional MCP connectors beyond the mcp-okn server:
- PubMed —
https://pubmed.mcp.claude.com/mcp - Paperclip —
https://paperclip.gxl.ai/mcp
Add them the same way as the mcp-okn server — in Claude Desktop / claude.ai via Settings → Connectors → Add custom connector, or from Claude Code:
claude mcp add --transport http pubmed https://pubmed.mcp.claude.com/mcp
claude mcp add --transport http paperclip https://paperclip.gxl.ai/mcpThese are needed only to reproduce that one step; the rest of each case study runs on the mcp-okn server alone.
Two optional Agent Skills ship
in the skills/ directory. They teach a client the repeatable
methodology for working with the mcp-okn tools, so you get consistent analyses and
report deliverables without re-explaining conventions each time:
okn-report-style— layout, figure, and style conventions for turning any OKN case study into a polished, reproducible report deliverable (interactive HTML + Markdown + multi-sheet Excel + figures + maps).okn-bioanalysis— methodology for biomedical knowledge-graph analysis and cross-KG hypothesis generation over the OKN federation's bio graphs (genes, proteins, diseases, phenotypes, pathways, chemicals, drugs, enrichment, ortholog projection, and linking bio entities to place-based data via geography).
Each skill is a self-contained folder: a SKILL.md plus references/ and
scripts/. A ready-to-upload zip of each is also committed for convenience:
Agent Skills work in these clients:
| Client | Install method | Notes |
|---|---|---|
| Claude Desktop (macOS / Windows) | Upload the zip under Settings → Capabilities → Skills | Pro, Max, Team, or Enterprise |
| claude.ai (web) | Upload the zip under Settings → Capabilities → Skills | Pro, Max, Team, or Enterprise |
| ChatGPT Desktop | Type the prompt: Install skill from GitHub: https://raw.githubusercontent.com/sbl-sdsc/mcp-okn/main/skills/{skill}.zip |
Replace {skill} with okn-report-style or okn-bioanalysis |
| Claude Code (CLI + IDE extensions) | Copy the folder into .claude/skills/ or ~/.claude/skills/ (see below) |
Free tier and up |
| Claude Agent SDK | Point the SDK at the skill folder | For building custom agents |
| Claude Developer Platform (API) | Load via the code-execution / Skills API | For programmatic use |
Download the skills straight from this repo into your personal skills directory (available in every project on this machine) — no clone required. Run the block for whichever skills you want:
mkdir -p ~/.claude/skills && cd ~/.claude/skills
# okn-report-style
curl -sL "https://raw.githubusercontent.com/sbl-sdsc/mcp-okn/main/skills/okn-report-style.zip" -o okn-report-style.zip
unzip -oq okn-report-style.zip && rm okn-report-style.zip
# okn-bioanalysis
curl -sL "https://raw.githubusercontent.com/sbl-sdsc/mcp-okn/main/skills/okn-bioanalysis.zip" -o okn-bioanalysis.zip
unzip -oq okn-bioanalysis.zip && rm okn-bioanalysis.zipFor a project-scoped install (shared with a repo), swap ~/.claude/skills for
.claude/skills in your project root.
Verify with /skills — you should see okn-report-style and okn-bioanalysis
listed. Claude invokes a skill automatically when a task matches its description.
To run the server yourself instead of using the hosted service:
Install uv (see the installation guide), then clone this repo:
git clone https://github.com/sbl-sdsc/mcp-okn.gitWorking inside this repo? It ships a project-scoped .mcp.json, so
Claude Code offers the local server automatically — no registration needed. Use it
when developing: it serves the crosswalk catalog from your working tree, whereas the
hosted mcp-okn-dev endpoint serves whatever was last deployed and will not reflect
local edits to crosswalks.json until it is redeployed.
For Claude Code, register it from the CLI:
claude mcp add mcp-okn -- uv --directory /path/to/mcp-okn run mcp-oknOr add it to any MCP client's config (e.g. Claude Desktop claude_desktop_config.json):
{
"mcpServers": {
"mcp-okn": {
"command": "uv",
"args": ["--directory", "/path/to/mcp-okn", "run", "mcp-okn"]
}
}
}Replace /path/to/mcp-okn with the absolute path to your checkout.
get_server_info() reports the running build, which every reproducibility header
also pins. Running from a checkout, it is the short git HEAD; a deployment should
set MCP_OKN_BUILD (to the commit it was built from) since a container has no git
metadata. Without either it reads unknown, and the header omits the build entirely
rather than claiming one. This is what lets you tell a hosted server that lags the
repo from a local one — and what distinguishes two records that name the same
version.
Two servers claiming the same version can be months apart, so
scripts/check_deployment.py answers the question directly: it speaks MCP to a live
endpoint, enumerates what that server actually exposes, and diffs it against this
checkout.
uv run python scripts/check_deployment.py # the hosted dev endpoint
uv run python scripts/check_deployment.py --url URL # any MCP endpointIt exits 0 when everything agrees, 1 on drift, and 2 when the endpoint
can't be reached or doesn't speak MCP — so it can gate a "did the redeploy land?"
check without reading a mid-restart 502 as either answer. Three things are
compared:
- build —
get_server_info().buildagainstgit rev-parse --short HEAD. When the hosted build is a commit this checkout knows, the report says how far behind it is (209 commits and 17 days behind HEAD). A deployment old enough to predateget_server_infocan't report a build at all; the script says so and falls back to the tool set. - tool and resource surface — the names the server lists versus the ones this tree registers, reported as missing/extra. This is what catches a lagging deployment with no build id: in July 2026 the hosted server served 19 tools against 23 in the repo for 17 days, and nothing flagged it until a reproducibility record could not be generated.
- bundled data — the crosswalk table's
countandverified_on, and the KG count. These ship inside the wheel, so they move only on a redeploy: a stale deployment can serve an identical tool surface over a months-oldcrosswalks.json.
The serverInfo.version returned by the MCP handshake is the MCP SDK's version,
not this project's (FastMCP is constructed without one), so it is never a build
signal; the script prints it labelled as such precisely so it isn't misread.
Instruction and tool-description drift is reported but not fatal by default — pass
--strict to fail on it too, which catches a redeploy that changed guidance without
changing any tool name. --json emits the same report for a polling loop.
The deploy pipeline for the hosted endpoint is operated externally, so a redeploy is a request rather than a command; this script is how you confirm one landed.
The tools follow a typical analysis arc — discover → inspect → plan a join → query → record. The single table below is grouped in that order.
| Tool | Purpose |
|---|---|
| 1. Discover graphs | |
list_kgs |
List all KGs with shortname, title, description, homepage, named_graph, and a payload list — the curated context types each graph supplies (e.g. digcfdekg → gene, gene_set, trait, disease), so you judge a graph by what it carries, not its name. Served from a bundled snapshot for instant cold start. |
describe_kg(shortname, long_description=False) |
Full registry doc (frontmatter + prose) for one KG, for deeper context. Set long_description=True for the registry's ~150-word prose body — useful for picking among near-overlapping KGs. Either way, the KG's description from metadata/descriptions follows under a Summary heading when it has one. For spoke-genelab, also appends its spaceflight assay-comparison rules. |
get_kg_version(shortname=None) |
A KG's release version and last_updated (ISO-8601 timestamp) — read live from the okn-void meta-graph's VoID provenance (pav:version, pav:lastUpdatedOn). Omit shortname for every registered KG that records provenance (42 of 44), sorted by shortname; a VoID record whose shortname is not in the registry is a stale entry and is listed under unregistered instead of being counted as a KG. Use it to check how current a graph is or cite the exact version behind an analysis. |
get_server_info() |
Identify the server answering you: service, version, build (the deployed commit, or unknown), and sparql_endpoint. The package version alone can't separate two deployments — a hosted server can lag the repo — so use this when a tool or argument seems to be missing, or to record which build produced a result. The build is also pinned in every reproducibility header. |
| 2. Inspect a graph's schema and identifiers | |
get_schema(shortname, compact=True, match=None) |
Schema for one KG — classes, predicates, edge properties (with reification query templates), and node properties. Reads the KG's VoID-built schema file (metadata/void_entities, shipped with the server), else probes the endpoint for distinct classes/predicates. Replies stay under 40,000 characters, with a schema_view block saying what was trimmed; pass match= (substrings, |-separated) to get just the matching part. Call before writing a query. Returns usage_notes (guidance + a reusable SPARQL snippet) for KGs with query-time domain rules, e.g. spoke-genelab. |
visualize_schema(shortname) |
Deterministic Mermaid classDiagram of a KG's schema, built server-side from get_schema — class boxes, labeled edges, and edge-property predicates as intermediary classes with typed fields (node classes light blue, edge classes orange, with a legend). When the schema file names predicates but not their endpoints, edges are recovered from the graph's rdfs:domain/rdfs:range scoped to the file's classes. Returns mermaid_block (already wrapped in a ```mermaid fence) — output it verbatim; don't redraw it as SVG/an image. Rendered examples: spoke-genelab, dreamkg, rdkg (details). |
probe_namespaces(shortname, predicate, sample=0) |
Report which identifier/ontology namespaces populate a predicate's objects. get_schema lists a KG's predicates but not which controlled vocabularies fill their values — call this before the main query whenever a predicate's objects are ontology terms (diseases, chemicals, genes, anatomy) to see the actual namespace distribution and pick the best identifier to join on. Exploratory — not logged. |
find_crosswalks(shortname, sample=0) |
Find ontology/database ids in a KG however they are encoded, profiling all three places at once: mapping predicates (rdfs:seeAlso, owl:sameAs, SKOS *Match, oboInOwl:hasDbXref), node IRIs that are the ontology term (role="subject"), and domain-specific predicates carrying an id (role="object"). The latter two are invisible to a mapping-predicate-only scan. Use whenever a KG seems to lack the identifier you need on its obvious predicates. |
| 3. Plan a cross-graph join | |
list_crosswalks(include_examples=True) |
List every verified cross-KG integration point in one call — a global map of which graphs connect and on what shared key. Rows are grouped by domain (Genes, Geospatial, Disease & phenotype, …) and sorted by ontology, ready to render as a table. Each row is a compact summary (domain, connected kgs in join order by official shortname, shared_key, bridge_kg, verified_count, and an example_question by default; set include_examples=False for a terser list). Use get_join_strategy(kg_a, kg_b) for a single pair's full recipe. |
get_join_strategy(kg_a, kg_b=None) |
Look up a precomputed, hand-verified recipe for joining two KGs — predicates, roles, shared identifier, bridge graph, verified count, and a runnable skeleton_query (the example SPARQL to copy and build on; it already encodes the IRI rewrites). Call before writing a federated join. Returns verified / known_non_join / unknown; with kg_b omitted, lists every join touching kg_a. |
find_context_sources(want=None, join_key=None) |
Reverse capability index — the inverse of get_join_strategy. Answers "which KGs supply pathway / GO / trait / disease … for an entity I can join on join_key?" by combining the per-KG payload tags with the verified crosswalk table. Returns, per requested context type, the supplier KGs with predicate + shared key + verified join size, sorted biggest-join-first, plus a payload_only bucket (KGs that carry the type but key it differently, e.g. Ensembl vs Entrez). A requested type that yields an empty list is positive evidence nothing supplies it on that key — so you never conclude a context is "unavailable" without checking. |
taxon_overlap(kg_a, kg_b) |
Compose the NCBITaxon overlap between two hub KGs through ubergraph. Returns two runnable skeletons — exact_id (same taxon id) and clade_membership (kg_b taxa under kg_a's clades via subClassOf*, which can be far larger when one side is coarser-grained) — plus, for a pair with a precomputed non-zero overlap, the materialized counts under materialized_overlap (the same per-pair counts list_crosswalks surfaces in the NCBITaxon hub row). Run a skeleton with sparql_query. |
point_to_s2(lat, lng, level=13) |
Convert a lat/long point to its spatialkg/KWG S2 cell IRI (Level-13 default) — the deterministic primitive behind the spatial bridge. Use when a KG carries POINT coordinates but no S2 key and you need the cell IRI spatialkg stores. |
spatial_bridge(point_query, target_pattern, select_vars="*", extra_prefixes="", limit=500) |
Generic point→S2 bridge for any point-bearing graph that lacks a stored S2 key (sudokn is the first such graph). point_query must SELECT ?site ?lat ?lng; the server computes each cell in Python and injects (?site ?cell) as a VALUES block into a federated query whose target_pattern joins ?cell to spatialkg/fiokg/sawgraph (e.g. county/FIPS). Nothing is persisted — the computed key lives only inside the request. |
| 4. Run queries | |
sparql_query(query, format="json", exploratory=False, compact=False) |
Run a SPARQL query on the federation endpoint. Substantive results are logged for the transcript unless exploratory=True. Pass compact=True for a token-efficient json shape — {"columns", "data", "count"} with positional rows — instead of the default repeated-key {"vars", "rows", "row_count"} (affects only the returned payload, not the transcript). A bracketed <https://schema.org/…> IRI is canonicalized to the http:// form most KGs store (string literals and IRI(CONCAT(…)) are left as written). A few KGs store the https:// form (nikg, ruralkg, ufokn); reach those predicates by binding the predicate as a variable and matching scheme-free, e.g. FILTER(STRENDS(STR(?p),'schema.org/location')). |
expand_ontology_term(term, relation="subClassOf", direction="descendants", include_self=True, limit=1000) |
Expand an ontology term to its full subtree/closure via the ubergraph graph. |
| 5. Record a reproducible transcript | |
reset_query_log() |
Clear the session query log. Call at the start of an analysis to scope a transcript. |
get_query_log() |
Return the queries logged so far this session (only those that returned rows and weren't exploratory). |
create_chat_transcript(model, exchanges, ...) |
Emit a reproducible markdown (or JSON) record of a session — prompts, answers, the verbatim queries + results that produced findings, and any visualize_schema diagrams. Call at the end of an analysis. |
create_reproducibility_record(model, supporting, ...) |
Emit a lean reproducibility record — header + the verbatim supporting queries + row counts + per-query diagrams (gated by size), no conversation prose or result tables. Small enough to return inline so it saves directly; use it for the reproducibility deliverable. supporting optionally curates the log to the queries that underpin the findings. |
| Resource | Purpose |
|---|---|
transcript://session/latest (text/markdown) |
The most recent record rendered by create_chat_transcript or create_reproducibility_record, so a client can fetch/save the document directly (transport-agnostic; works for remote servers). Cleared by reset_query_log. |
- mcp-okn redesign (also as a slide deck, PDF)
— a side-by-side comparison of the previous
mcp-proto-oknserver and thismcp-oknredesign across query model, KG coverage, schema/registry handling, cross-graph joins, and reproducibility.
The benchmark harness lives in its own repository,
mcp-okn-benchmark. It measures how
well an agent turns a prose question into a correct SPARQL answer, and how much of that
success comes from the server's tooling — running the same corpus against mcp-okn and
mcp-proto-okn, locally over stdio or remotely over HTTP.
It was split out of this repository (benchmark/, removed here) because a harness that
measures two servers cannot live inside one of them.
The package is organized by concern. server.py is a thin assembly point: it
imports the tool modules to trigger their @mcp.tool() registration and
re-exports their public symbols (so from mcp_okn.server import ... keeps
working). The shared mcp application instance lives in app.py, separate from
server.py, so the tool modules can import it without a circular dependency.
src/mcp_okn/
├── app.py # the shared FastMCP `mcp` instance + INSTRUCTIONS
├── server.py # assembly point: registers tools, re-exports, main()
├── registry.py # KG discovery from the okn-registry (+ bundled snapshot)
├── schema.py # get_schema / visualize_schema logic
├── sparql.py # federation endpoint client: shared connection, retries, schema.org normalization
├── crosswalks.py # curated cross-KG join table (data/crosswalks.json)
├── payloads.py # curated per-KG payload tags (data/kg_payloads.json)
├── void.py # per-KG version / last-updated from the okn-void graph
├── taxon.py # NCBITaxon hub: taxon-overlap skeleton composition
├── session.py # in-memory query/diagram log for transcripts
├── build_info.py # which BUILD is running (MCP_OKN_BUILD / git HEAD)
├── data/ # bundled snapshots: kgs.json, crosswalks.json, kg_payloads.json
└── tools/ # one module per concern; each registers via @mcp.tool()
├── _shared.py # helpers used by >1 tool module (_to_uri, …)
├── discovery.py # list_kgs, describe_kg, get_kg_version, get_server_info
├── schema_tools.py # get_schema, visualize_schema
├── probe.py # probe_namespaces, find_crosswalks
├── joins.py # get_join_strategy, taxon_overlap, list_crosswalks, find_context_sources
├── query.py # sparql_query, expand_ontology_term
└── transcript.py # reset/get_query_log, create_chat_transcript, create_reproducibility_record, resource
uv run python -m pytest # unit tests (offline)
uv run ruff check . # lint
uv run ruff format . # auto-format (use --check in CI)
uv run mypy # type-check src/mcp_okn
# live smoke test:
uv run python -c "import asyncio; from mcp_okn.sparql import run_sparql; \
print(asyncio.run(run_sparql('SELECT ?s WHERE { ?s ?p ?o } LIMIT 3')))"CI (.github/workflows/ci.yml) runs ruff lint, ruff format-check, and mypy on
every push/PR, plus the offline test suite on Python 3.10 and 3.12. A skills
job validates both Agent Skills against the
Agent Skills spec with the official
agentskills validator and asserts each committed .zip is in sync with its
skill folder (so an edited SKILL.md can't ship without a rebuilt zip).
ruff is pinned in uv.lock, and CI lints/format-checks the whole tree, so a
ruff version bump can surface new findings across the repo — reconcile them in one
pass when bumping. Scope (see [tool.ruff] in pyproject.toml):
- Excluded (
extend-exclude):docs/reproduction/anddocs/examples/— one-off, throwaway figure/PDF/reproduction scripts, not maintained tooling. - Docstrings not required (
Dignored viaper-file-ignores):tests/,scripts/, and the bundledskills/*/scripts/helpers. Everything else — chieflysrc/mcp_okn/— is held to the full rule set.
When committing, stage explicit paths (git add <path> …), not git add -A: the
working tree often carries untracked worked-example artifacts under docs/reproduction/
that should not be swept into an unrelated commit.
Deferred improvements are tracked as GitHub issues.
Reproducible checks of behaviors that aren't covered by the offline unit tests:
- schema.org http/https normalization
— a bracketed
<https://schema.org/…>IRI is canonicalized tohttp, so a query written with thehttpsform still hitshttp-stored data (dreamkgschema:Rating→ 3762), while string literals /IRI(CONCAT(…))are left intact (see below). - visualize_schema rendering — the
generated Mermaid renders cleanly as a class diagram via
mermaid-cliacross all three schema paths (curated, class-only, probe fallback), and survives thecreate_chat_transcriptround-trip. - transcript MCP resource — the
transcript://session/latestresource serves the full document via the resource API, with its embedded diagram still rendering.
A few graphs store schema.org terms under the https:// form. The canonicalization
hits only bracketed IRIs, so those predicates are unreachable by IRI (the bracketed
https form is rewritten to http, which the data isn't), but match when the predicate
is bound as a variable or the IRI is rebuilt from a string literal (now preserved).
Verified live against the federation:
| Graph (predicate) | <https://schema.org/X> (bracketed) |
IRI(CONCAT('https://schema.org/','X')) |
STRENDS(STR(?p),'schema.org/X') |
|---|---|---|---|
nikg (location) |
0 | 296,189 | 296,189 |
ruralkg (postalCode) |
0 | 9,037 | 9,037 |
ufokn (value, level-13 sample) |
0 | 5/5 (LIMIT 5) |
5/5 |
In each case the IRI(CONCAT) and STRENDS forms agree (same rows / same decimal S2
ids for ufokn) while the bracketed IRI returns 0 — confirming the literal-preservation
fix and the variable-predicate workaround documented in the ruralkg/ufokn crosswalk
notes.
spoke-genelab (NASA GeneLab) models each differential measurement as an Assay
with two arms — factor_space_1/factor_space_2 (the condition labels) and
factors_1/factors_2 (lists bundling the condition label plus extra factors
like dose, time, sex, strain). Reading any assay as a "spaceflight effect" is
wrong; two domain rules apply:
- Direction — keep only
factor_space_1 = "Space Flight"andfactor_space_2 = "Ground Control". Drop the reverse and every other pairing. Verified live: 680 assays are SF→GC vs 664 the reverse (plus SF/SF 1244, GC/GC 424, and Basal/Vivarium pairings). With this orientation group 1 = Space Flight, solog2fc/methylation_diff/lnfc> 0 means up in spaceflight. - Comparability — two separate
Assayrecords are comparable only if they share the same materials (prefermaterial_id_1/material_id_2) and the samefactors_1/factors_2after the experimental-condition labels are stripped, then test the remaining factors for equality (a shared extra factor is allowed if present on both sides).
Stripping removes exactly 15 distinct values, verified live: the spelled-out
labels (Space Flight, Ground Control, Basal Control, Vivarium Control,
Cell Culture Control; case-insensitive, so Ground control/Vivarium control
match too) and the short group codes via the anchored regex
^(GC|FLT|VIV|BSL|CC)(_C[0-9]+)?$ (GC, FLT, VIV_C2, BSL_C1, CC_C1, …).
The anchoring is deliberate: real factors that merely contain a control word —
Hardware 1G Ground Control, Ground Control Rerun, HLU_IR
(hindlimb-unloading) — stay in the list as legitimate distinguishing factors.
The rules live once in src/mcp_okn/contrasts.py (guidance prose + a reusable
comparability-signature SPARQL snippet) and are surfaced in the server
INSTRUCTIONS, as usage_notes on get_schema("spoke-genelab"), and appended to
describe_kg("spoke-genelab").
list_kgs serves a static snapshot bundled at src/mcp_okn/data/kgs.json (~45
KGs), so the first call returns instantly without fetching the individual
registry files. The live registry is only contacted when the snapshot is missing
(or when an internal refresh=True is passed). To refresh the snapshot after the
registry changes:
uv run python scripts/refresh_snapshot.pyKGs that are in the registry but not actually loaded under their expected
federation named graph (currently semopenalex) are filtered
out, so list_kgs only returns graphs that are queryable.
The curated crosswalk table is edited at metadata/crosswalks.json and bundled to
src/mcp_okn/data/crosswalks.json by the same refresh_snapshot.py run. Two helpers
recompute its live-verified counts against the federation and write them back:
scripts/verify_skeletons.py (per-crosswalk skeleton_query counts) and
scripts/refresh_taxon_overlaps.py (the NCBITaxon hub's pairwise exact_id + clade
counts — --inject to write, --pair A B for one pair). Run refresh_snapshot.py
afterwards to sync the bundled copy.
Each KG carries curated payload tags — the context types it supplies —
edited at metadata/kg_payloads.json and bundled to
src/mcp_okn/data/kg_payloads.json by refresh_snapshot.py (which validates that
every tag is a defined vocabulary term and every servable KG is tagged). These tags
are partly derived from each KG's entity schema, the *_entities.csv files in
metadata/void_entities that get_schema reads — so rebuilding those files after a
redeploy can change a KG's schema under the tags. scripts/check_payload_drift.py guards against that: it fingerprints each
KG's live class + predicate labels and diffs them against a committed baseline
(metadata/schema_fingerprints.json), flagging KGs whose schema moved so their
tags can be re-reviewed. Run it after rebuilding the schema files; re-ground any
affected tags, then --update to accept the new baseline.
uv run python scripts/check_payload_drift.py # report drift; exit 1 if any
uv run python scripts/check_payload_drift.py --update # accept current schemas as baselineThe schema files describe the served graph. The curated files mcp-okn used to
read could be refreshed before the graph was redeployed, so get_schema advertised
predicates with no triples (in October 2026, 152 of medical-device-kg's 315). The
VoID-built files can't run ahead like that. scripts/build_void_schema.py uses a
KG's VoID statistics only when its triple count matches the served graph, and
otherwise counts the served graph itself. Rebuild the files after a redeploy, then
run check_payload_drift.py.
When the served graph moves and the schema follows. The opposite drift is just
as silent: a redeploy renames or re-models predicates, the schema is updated to
match, and every stored query written against the old shape returns zero rows.
The 2026-10-13 refresh of 16 CSVs recorded four such redeploys — scales (the
has prefix dropped from ~190 predicates), prokn (relation predicates moved
from OBO RO to Biolink IRIs), sawgraph (S2 cells re-published with a reflexive
owl:sameAs, so feature→cell links live on kwg-ont:sfWithin) and dreamkg
(audiences re-modelled from literals to category IRIs). check_payload_drift.py
flags the schema change; scripts/verify_skeletons.py and
scripts/verify_transcripts.py are what show which stored recipes it broke.
mcp-okn is the next-generation successor to
mcp-proto-okn. If you use this
software, please cite the paper describing that predecessor:
Rose, P. W., Good, B. M., Saravia-Butler, A. M., Nelson, C. A., Balhoff, J. P., Kebede, Y., Whetzel, P. L., Bizon, C., Su, A. I., & Baranzini, S. E. (2026). mcp-proto-okn: Natural-language access to open scientific knowledge graphs through the Model Context Protocol. arXiv:2605.30283. https://arxiv.org/abs/2605.30283
@misc{rose2026mcpprotookn,
title = {mcp-proto-okn: Natural-language access to open scientific knowledge graphs through the Model Context Protocol},
author = {Rose, Peter W. and Good, Benjamin M. and Saravia-Butler, Amanda M. and Nelson, Charlotte A. and Balhoff, James P. and Kebede, Yaphet and Whetzel, Patricia L. and Bizon, Christopher and Su, Andrew I. and Baranzini, Sergio E.},
year = {2026},
eprint = {2605.30283},
archivePrefix = {arXiv},
primaryClass = {cs.AI},
url = {https://arxiv.org/abs/2605.30283}
}- National Science Foundation Award #2333819: "Proto-OKN Theme 1: Connecting Biomedical information on Earth and in Space via the SPOKE knowledge graph"
- National Science Foundation Award #2535091: "Proto-OKN Theme 2: OKN-Fabric"
This project is licensed under the BSD 3-Clause License.




