mcp-ubergraph-query
by twhetzel
README.md
# mcp-ubergraph-query
An [MCP](https://modelcontextprotocol.io) server for querying the
[Ubergraph](https://github.com/INCATools/ubergraph) biomedical ontology SPARQL endpoint.
Ubergraph is a merged knowledge graph of OBO ontologies including MONDO, UBERON, HP, CHEBI,
GO, CL, and more. This server exposes four tools that let AI assistants query it naturally.
## Tools
| Tool | Description |
|---|---|
| `query_ubergraph` | Execute custom SPARQL SELECT queries |
| `get_term_info` | Get label, definition, synonyms, and types for an ontology term |
| `search_terms` | Search terms by label or synonym across ontologies |
| `get_hierarchy` | Traverse parents, children, ancestors, or descendants |
## Quick Start
### Prerequisites
- Python 3.10+
- [uv](https://docs.astral.sh/uv/)
### Install
```bash
git clone https://github.com/twhetzel/mcp-ubergraph-query
cd mcp-ubergraph-query
uv sync --all-extras
```
### Run the server locally
The server uses **stdio** (stdin/stdout) for MCP transport. Start it with:
```bash
uv run mcp-ubergraph-query
```
Or:
```bash
uv run python -m ubergraph_query.server
```
Leave this process running; MCP clients (e.g. Claude Desktop, Cursor) connect by spawning this command and talking over stdin/stdout.
### Configure Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"ubergraph": {
"command": "uv",
"args": [
"--directory",
"/path/to/mcp-ubergraph-query",
"run",
"mcp-ubergraph-query"
]
}
}
}
```
## Configuration
Copy `.env.example` to `.env` and adjust as needed:
```bash
cp .env.example .env
```
| Variable | Default | Description |
|---|---|---|
| `UBERGRAPH_ENDPOINT` | `https://ubergraph.apps.renci.org/sparql` | SPARQL endpoint URL |
| `QUERY_TIMEOUT_DEFAULT` | `30` | Default query timeout (seconds) |
| `QUERY_LIMIT_MAX` | `1000` | Maximum allowed LIMIT value |
| `ENABLE_QUERY_CACHE` | `true` | Enable in-memory LRU result cache |
| `CACHE_TTL_SECONDS` | `3600` | Cache entry lifetime |
| `LOG_LEVEL` | `INFO` | Logging verbosity |
## Tool Reference
### `query_ubergraph`
Execute a custom SPARQL SELECT query against Ubergraph.
**Input:**
```json
{
"query": "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5",
"timeout": 30,
"limit": 100,
"format": "json"
}
```
**Output:**
```json
{
"results": [{"s": "...", "p": "...", "o": "..."}],
"query_time_ms": 234,
"result_count": 5,
"query_hash": "abc123def456"
}
```
Safety features: LIMIT is automatically injected if absent; write operations
(INSERT, DELETE, DROP, etc.) are rejected; timeout is capped at 60 s.
---
### `get_term_info`
Get comprehensive metadata for an ontology term by CURIE.
**Input:**
```json
{
"curie": "MONDO:0005015",
"include_hierarchy": false
}
```
**Output:**
```json
{
"curie": "MONDO:0005015",
"iri": "http://purl.obolibrary.org/obo/MONDO_0005015",
"label": "diabetes mellitus",
"definition": "A metabolic disorder characterized by...",
"synonyms": ["DM", "diabetes"],
"types": ["owl:Class"],
"in_ontology": "mondo"
}
```
With `include_hierarchy: true`, `parents` and `children` arrays are added.
---
### `search_terms`
Search ontology terms by label or synonym.
**Input:**
```json
{
"text": "diabetes",
"ontologies": ["MONDO", "HP"],
"limit": 10,
"exact_match": false
}
```
**Output:**
```json
{
"matches": [
{
"curie": "MONDO:0005015",
"label": "diabetes mellitus",
"match_type": "partial",
"ontology": "mondo",
"score": 0.6
}
],
"search_text": "diabetes",
"total_matches": 1
}
```
---
### `get_hierarchy`
Traverse hierarchical relationships for a term.
**Input:**
```json
{
"curie": "MONDO:0005015",
"relation": "parents",
"depth": 1
}
```
`relation` values: `parents`, `children`, `ancestors`, `descendants`
**Output:**
```json
{
"curie": "MONDO:0005015",
"relation": "parents",
"depth": 1,
"terms": [
{"curie": "MONDO:0005066", "label": "metabolic disease", "distance": 1}
]
}
```
## Example SPARQL Queries
### Get term label and definition
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX obo: <http://purl.obolibrary.org/obo/>
SELECT ?label ?definition WHERE {
obo:MONDO_0005015 rdfs:label ?label .
OPTIONAL { obo:MONDO_0005015 obo:IAO_0000115 ?definition }
}
```
### Search by label substring
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?term ?label WHERE {
?term rdfs:label ?label .
FILTER(CONTAINS(LCASE(?label), "diabetes"))
FILTER(STRSTARTS(STR(?term), "http://purl.obolibrary.org/obo/MONDO_"))
}
LIMIT 10
```
### Get immediate parents
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX obo: <http://purl.obolibrary.org/obo/>
SELECT ?parent ?label WHERE {
obo:MONDO_0005015 rdfs:subClassOf ?parent .
FILTER(!isBlank(?parent))
OPTIONAL { ?parent rdfs:label ?label }
}
```
### Get all ancestors (transitive)
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX obo: <http://purl.obolibrary.org/obo/>
SELECT ?ancestor ?label WHERE {
obo:MONDO_0005015 rdfs:subClassOf+ ?ancestor .
FILTER(!isBlank(?ancestor))
OPTIONAL { ?ancestor rdfs:label ?label }
}
LIMIT 100
```
### Find phenotype terms for a disease (HP + MONDO cross-ontology)
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX obo: <http://purl.obolibrary.org/obo/>
PREFIX oboInOwl: <http://www.geneontology.org/formats/oboInOwl#>
SELECT ?phenotype ?label WHERE {
?association obo:RO_0002200 obo:MONDO_0005015 ;
obo:RO_0002200 ?phenotype .
FILTER(STRSTARTS(STR(?phenotype), "http://purl.obolibrary.org/obo/HP_"))
OPTIONAL { ?phenotype rdfs:label ?label }
}
LIMIT 20
```
## Testing locally
The project is not on PyPI yet. Install and test from the repo:
```bash
# Install with dev dependencies (includes pytest)
uv sync --all-extras
# Run unit tests (no network)
uv run python -m pytest tests/ -v
# Test the MCP server: spawns server, lists tools, calls get_term_info, search_terms, get_hierarchy
uv run python examples/test_mcp_server.py
# Run direct SPARQL/query examples (hits Ubergraph)
uv run python examples/example_usage.py
```
**Manual testing with MCP Inspector:**
Run the server with `uv run mcp-ubergraph-query`, then use [MCP Inspector](https://github.com/modelcontextprotocol/inspector) and add a stdio server with command `uv`, args `--directory`, `<path-to-this-repo>`, `run`, `mcp-ubergraph-query`.
## Development
```bash
# Lint
uv run ruff check src/ tests/
```
## Project Structure
```
mcp-ubergraph-query/
├── src/
│ └── ubergraph_query/
│ ├── __init__.py # Package metadata
│ ├── server.py # MCP server + tool implementations
│ ├── sparql_client.py # Async HTTP SPARQL execution with retries
│ ├── query_builder.py # SPARQL query construction helpers
│ ├── cache.py # Thread-safe LRU cache with TTL
│ ├── validators.py # CURIE validation, query safety checks
│ └── config.py # Environment-based configuration
├── tests/
│ └── test_queries.py # Unit tests (no network required)
├── examples/
│ └── example_usage.py # Live query examples
├── pyproject.toml
├── .env.example
└── README.md
```
## Safety
- **Read-only**: Write operations (INSERT, DELETE, DROP, etc.) are rejected
- **LIMIT enforcement**: Queries without LIMIT get one injected; over-limit values are capped
- **Timeout cap**: Hard maximum of 60 seconds per query
- **Retry with backoff**: Transient 5xx/network errors are retried up to 3 times
- **Query logging**: Every query is logged with a SHA-256 hash for provenance
## License
MIT
TDQS
A3.8/5.0
Scored across 4 tools
Disambiguation5/5
Each tool serves a clearly distinct purpose: searching terms, retrieving term details, getting hierarchy, and executing custom SPARQL queries. No overlap or ambiguity.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern in snake_case (get_hierarchy, get_term_info, query_ubergraph, search_terms), making them predictable and easy to understand.
Tool Count5/5
With 4 tools, the server is well-scoped for ontology querying. Each tool is essential and covers the primary use cases without being too few or excessive.
Completeness4/5
The tool set covers the core needs: search, term retrieval, hierarchy, and custom queries. Minimal gaps exist, such as batch operations or ontology-wide listings, but the surface is sufficient for most tasks.
Maintenance
ActivityInactive
ResponsivenessNo issues