Create an MCP server called "mcp-ubergraph-query" for querying the Ubergraph SPARQL endpoint.

**Project Overview:**
Ubergraph is a merged ontology knowledge graph containing biomedical ontologies 
(MONDO, UBERON, HP, CHEBI, GO, etc.). This MCP server provides tools to query it 
via SPARQL and retrieve ontology term information.

**Requirements:**
- Python 3.10+, use uv for dependency management
- MCP SDK (mcp package) for server implementation
- SPARQL queries via SPARQLWrapper or httpx
- Ubergraph endpoint: https://ubergraph.apps.renci.org/sparql
- Include query safety: LIMIT injection, timeout defaults
- Add caching for frequently accessed terms
- Comprehensive error handling and retries

**Four Main Tools:**

1. query_ubergraph - Execute custom SPARQL queries
2. get_term_info - Get detailed information about an ontology term
3. search_terms - Search for terms by label/synonym
4. get_hierarchy - Get parent/child terms

**Tool 1: query_ubergraph**
```python
{
    "name": "query_ubergraph",
    "description": "Execute a SPARQL query against the Ubergraph endpoint. Use for custom queries when other tools don't fit your needs.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "SPARQL query to execute"
            },
            "timeout": {
                "type": "integer",
                "default": 30,
                "description": "Query timeout in seconds (max: 60)"
            },
            "limit": {
                "type": "integer",
                "default": 100,
                "description": "Maximum results to return (max: 1000)"
            },
            "format": {
                "type": "string",
                "enum": ["json", "turtle", "xml"],
                "default": "json",
                "description": "Result format"
            }
        },
        "required": ["query"]
    }
}
```

**Tool 2: get_term_info**
```python
{
    "name": "get_term_info",
    "description": "Get comprehensive information about a specific ontology term (labels, definitions, synonyms, types).",
    "inputSchema": {
        "type": "object",
        "properties": {
            "curie": {
                "type": "string",
                "description": "Ontology term CURIE (e.g., 'MONDO:0005015', 'HP:0001945')"
            },
            "include_hierarchy": {
                "type": "boolean",
                "default": false,
                "description": "Include immediate parent and child terms"
            }
        },
        "required": ["curie"]
    }
}
```

**Tool 3: search_terms**
```python
{
    "name": "search_terms",
    "description": "Search for ontology terms by label or synonym. Returns matching terms with their IDs.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "text": {
                "type": "string",
                "description": "Search text (label or synonym)"
            },
            "ontologies": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Filter by ontology prefixes (e.g., ['MONDO', 'HP'])"
            },
            "limit": {
                "type": "integer",
                "default": 10,
                "description": "Maximum results"
            },
            "exact_match": {
                "type": "boolean",
                "default": false,
                "description": "Require exact match vs substring"
            }
        },
        "required": ["text"]
    }
}
```

**Tool 4: get_hierarchy**
```python
{
    "name": "get_hierarchy",
    "description": "Get hierarchical relationships for a term (parents, children, ancestors, descendants).",
    "inputSchema": {
        "type": "object",
        "properties": {
            "curie": {
                "type": "string",
                "description": "Ontology term CURIE"
            },
            "relation": {
                "type": "string",
                "enum": ["parents", "children", "ancestors", "descendants"],
                "default": "parents",
                "description": "Type of relationship to retrieve"
            },
            "depth": {
                "type": "integer",
                "default": 1,
                "description": "How many levels to traverse (1-5)"
            }
        },
        "required": ["curie"]
    }
}
```

**Response Formats:**

query_ubergraph response:
```python
{
    "results": [...],  # SPARQL results
    "query_time_ms": 234,
    "result_count": 42,
    "query_hash": "abc123"  # For provenance
}
```

get_term_info response:
```python
{
    "curie": "MONDO:0005015",
    "iri": "http://purl.obolibrary.org/obo/MONDO_0005015",
    "label": "diabetes mellitus",
    "definition": "A metabolic disorder...",
    "synonyms": ["diabetes", "DM", "diabetes mellitus"],
    "types": ["owl:Class"],
    "in_ontology": "mondo",
    "parents": [...],  # If include_hierarchy=true
    "children": [...]   # If include_hierarchy=true
}
```

search_terms response:
```python
{
    "matches": [
        {
            "curie": "MONDO:0005015",
            "label": "diabetes mellitus",
            "match_type": "exact_label",  # or "synonym", "partial"
            "ontology": "mondo",
            "score": 1.0
        }
    ],
    "search_text": "diabetes",
    "total_matches": 1
}
```

get_hierarchy response:
```python
{
    "curie": "MONDO:0005015",
    "relation": "parents",
    "terms": [
        {
            "curie": "MONDO:0005066",
            "label": "metabolic disease",
            "distance": 1
        }
    ]
}
```

**Environment Variables:**
```bash
UBERGRAPH_ENDPOINT=https://ubergraph.apps.renci.org/sparql
QUERY_TIMEOUT_DEFAULT=30
QUERY_LIMIT_MAX=1000
ENABLE_QUERY_CACHE=true
CACHE_TTL_SECONDS=3600
```

**Safety Features:**
1. Automatically inject LIMIT if not present (prevent huge results)
2. Enforce maximum timeout (60s)
3. Log all queries with timestamps and hashes
4. Retry logic with exponential backoff for transient failures
5. Validate CURIEs before querying

**Project Structure:**
```
mcp-ubergraph-query/
├── src/
│   └── ubergraph_query/
│       ├── __init__.py
│       ├── server.py          # MCP server
│       ├── sparql_client.py   # SPARQL execution
│       ├── query_builder.py   # Helper to build common queries
│       ├── cache.py           # Simple in-memory cache
│       ├── validators.py      # CURIE/query validation
│       └── config.py          # Configuration
├── tests/
│   └── test_queries.py
├── examples/
│   └── example_usage.py
├── pyproject.toml
├── README.md
└── .env.example
```

**Implementation Notes:**
1. Use SPARQLWrapper for SPARQL queries or httpx with proper headers
2. Implement simple LRU cache for get_term_info results
3. For query_ubergraph, validate and sanitize user queries
4. Include example SPARQL queries in the README
5. Add a health check that pings Ubergraph endpoint

**Example SPARQL Queries to Include:**

Get term info:
```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:
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?term ?label WHERE {
  ?term rdfs:label ?label .
  FILTER(CONTAINS(LCASE(?label), "diabetes"))
}
LIMIT 10
```

Get parents:
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?parent ?label WHERE {
  obo:MONDO_0005015 rdfs:subClassOf ?parent .
  ?parent rdfs:label ?label .
}
```

Start by setting up the project structure, then implement the SPARQL client, 
then build each tool incrementally. Begin with get_term_info (simplest) before 
query_ubergraph (most complex).