Content
# graph-mcp
A production-oriented MCP server that lets an LLM query an RDF graph database
**through a strict, validated `QueryPlan` IR** — never by emitting raw SPARQL
strings directly. The server validates, compiles, and executes plans; it also
explains what they will do.
> The LLM plans. The MCP server validates, compiles, executes, and explains.
## Documentation
The full documentation site lives in `docs-site/` and is published via
GitHub Pages on every push to `main`.
| Audience | Where to start |
| --- | --- |
| New users | [User guide](docs-site/docs/users/intro.md) — installation, configuration, MCP tools and resources, security |
| Contributors / maintainers | [Developer guide](docs-site/docs/developers/architecture.md) — architecture, IR, validator, renderer, evals |
| Operators | [Production-readiness checklist](docs/production_readiness.md) and the [security/deployment guide](docs-site/docs/users/security-and-deployment.md) |
| Reference | [Configuration](docs-site/docs/reference/configuration-reference.md), [Tools](docs-site/docs/reference/tools-reference.md), [Resources](docs-site/docs/reference/resources-reference.md), [Validation errors](docs-site/docs/reference/validation-errors.md), [Eval metrics](docs-site/docs/reference/eval-metrics.md) |
| ADRs | [`docs-site/docs/adr/`](docs-site/docs/adr/) |
### Local docs preview
```bash
cd docs-site
npm ci
npm run start # http://localhost:3000
npm run build # static site under docs-site/build/
```
The site is generated by [Docusaurus 3](https://docusaurus.io/). The
GitHub Pages deploy is configured in
[`.github/workflows/docs.yml`](.github/workflows/docs.yml). To enable
publishing on a fork, set:
```text
Settings → Pages → Build and deployment → Source → GitHub Actions
```
## Why an IR instead of free-form SPARQL?
Letting an LLM write SPARQL strings is convenient and unsafe: it conflates
intent with syntax, hides bugs, and makes safety review impossible. A typed
IR lets us:
- **enforce safety** — limits, depth, allowlists, no `Update`, no arbitrary
`SERVICE` — without parsing untrusted text;
- **catch semantic errors deterministically** — unbound variables, wrong
`HAVING` shape, `BIND` rebinds, unbounded property paths;
- **render canonical SPARQL** — stable output that diffs cleanly in PRs;
- **measure plan quality** — golden cases compare structure, not strings.
The deterministic eval baseline (a hand-coded keyword planner) ships in this
repo and exercises the full validator → renderer → executor pipeline against
20 golden cases. **Note:** that baseline is not an LLM and its case-pass rate
should not be read as evidence of LLM planning quality. The new structural,
safety, and repair metrics (see "Evaluations" below) are intended to score
real LLM planners.
## Architecture
```text
User question
↓
LLM planner / eval agent
↓
Strict QueryPlan IR (Pydantic v2)
↓
QueryPlanValidator ← SecurityPolicy
↓
SparqlRenderer ← deterministic, escaping-aware
↓
GraphEndpoint ← rdflib (local) or HTTP (remote)
↓
Structured QueryResult
```
| Layer | Module |
| --- | --- |
| IR | `graph_mcp/models/` |
| Validator | `graph_mcp/compiler/validator.py` |
| Renderer | `graph_mcp/compiler/renderer.py` |
| Executors | `graph_mcp/graph/endpoint.py` |
| MCP wiring | `graph_mcp/server.py`, `graph_mcp/mcp_tools/` |
| Security | `graph_mcp/security/policy.py` |
| Evals | `evals/` |
| RAG evals (experimental) | `evals_rag/` — see `evals_rag/README.md` |
## Installation
```bash
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # core + dev tools
pip install -e ".[dev,ai]" # add the optional PydanticAI planner
```
### Compatibility matrix
| Python | Pydantic | Status |
| --- | --- | --- |
| 3.11 | 2.6 – 2.13 | CI green |
| 3.12 | 2.6 – 2.13 | CI green |
| 3.13 | 2.6 – 2.13 | CI green; free-threaded build (``python3.13t``) is **not** supported (CFFI dependency) |
The recursive Pydantic IR is pinned to Pydantic ``>=2.6,<3``. The
import path is stress-tested across hash seeds (see
``tests/test_import_robustness.py``), so re-introducing a fragile
forward-ref strategy will fail CI before merge.
## Configuration
All settings come from environment variables (see `.env.example`):
| Variable | Default | Purpose |
| --- | --- | --- |
| `GRAPH_MCP_ENDPOINT_URL` | _(empty)_ | Remote SPARQL endpoint. Empty → in-memory rdflib. |
| `GRAPH_MCP_DEFAULT_LIMIT` | `100` | Auto-applied to `SELECT` queries without a `LIMIT`. |
| `GRAPH_MCP_MAX_LIMIT` | `1000` | Hard cap on any executed query. |
| `GRAPH_MCP_TIMEOUT_MS` | `5000` | Per-query timeout. |
| `GRAPH_MCP_ALLOWED_GRAPHS` | _(empty)_ | CSV allowlist; empty disables the GRAPH allowlist. |
| `GRAPH_MCP_ALLOWED_SERVICE_ENDPOINTS` | _(empty)_ | CSV allowlist for `SERVICE`; empty blocks all. |
| `GRAPH_MCP_ENABLE_RAW_SPARQL` | `false` | Expert-mode raw SPARQL tool. |
| `GRAPH_MCP_MAX_TRIPLE_PATTERNS` | `200` | Plan-size cap. |
| `GRAPH_MCP_MAX_QUERY_DEPTH` | `8` | Nesting cap. |
| `GRAPH_MCP_MAX_PROPERTY_PATH_COMPLEXITY` | `16` | Property-path AST cap. |
| `GRAPH_MCP_ALLOW_UNBOUNDED_PATHS` | `false` | Permit `*`/`+` paths. |
| `GRAPH_MCP_ALLOWED_PATH_PREDICATES` | _(empty)_ | CSV allowlist for property-path predicate IRIs. Empty = anything in `?p` etc. |
| `GRAPH_MCP_ALLOW_DEFAULT_PREFIX_OVERRIDE` | `false` | Permit plans to redefine `rdf`, `rdfs`, `xsd`, `owl`, `skos`, `dct`, `foaf`. |
| `GRAPH_MCP_LOCAL_GRAPH_FILE` | _(empty)_ | Turtle file to load into the local executor. |
| `GRAPH_MCP_SCHEMA_PROVIDER` | `auto` | One of `static`, `sparql`, `auto`. See the schema-provider section. |
| `GRAPH_MCP_SCHEMA_CACHE_TTL_SECONDS` | `300` | TTL for the cached schema snapshot. |
| `GRAPH_MCP_SCHEMA_DISCOVERY_TIMEOUT_MS` | `10000` | Per-query timeout for discovery SPARQL. |
| `GRAPH_MCP_SCHEMA_MAX_CLASSES` | `200` | Cap on discovered classes. |
| `GRAPH_MCP_SCHEMA_MAX_PROPERTIES` | `500` | Cap on discovered properties. |
| `GRAPH_MCP_SCHEMA_MAX_INDIVIDUALS` | `200` | Cap on discovered individuals. |
| `GRAPH_MCP_SCHEMA_MAX_NAMED_GRAPHS` | `200` | Cap on discovered named graphs. |
| `GRAPH_MCP_SCHEMA_DISCOVERY_ON_STARTUP` | `true` | Run an initial schema refresh when the server starts (only when using the SPARQL provider). |
| `GRAPH_MCP_LOG_LEVEL` | `INFO` | Logging level (logs go to stderr). |
### Schema-provider modes
| Mode | Behavior |
| --- | --- |
| `static` | Always use `StaticSchemaProvider`. Resources return only what the host injects via the `schema=` argument to `build_server`. |
| `sparql` | Use `SparqlSchemaProvider` and require an endpoint. Discovery runs at startup (configurable) and on every `refresh_schema` tool call. **Fail-fast:** if neither `GRAPH_MCP_ENDPOINT_URL` nor `GRAPH_MCP_LOCAL_GRAPH_FILE` is set, the server raises `ConfigurationError` instead of silently using an empty in-memory graph. |
| `auto` (default) | Use `SparqlSchemaProvider` when `GRAPH_MCP_ENDPOINT_URL` or `GRAPH_MCP_LOCAL_GRAPH_FILE` is set; otherwise fall back to `static`. |
The `SparqlSchemaProvider` discovers:
- declared classes (`rdfs:Class` / `owl:Class`) and instance-observed
classes (`?s a ?cls`);
- declared properties (`rdf:Property`, `owl:ObjectProperty`,
`owl:DatatypeProperty`) and observed predicates;
- `rdfs:label` and `skos:prefLabel`;
- `rdfs:domain` / `rdfs:range`;
- named graphs (`GRAPH ?g`);
- individuals (capped).
Discovery is best-effort: failed sub-queries are recorded in the snapshot's
``diagnostics`` list (visible at ``graph://schema/status``) rather than
raising. Generated `prefixed_name` values are filled in from configured
prefixes.
## Running the server
```bash
# stdio (recommended for MCP hosts like Claude Code)
python -m graph_mcp.server
# http transport
python -m graph_mcp.server --transport streamable-http
```
### Connecting from Claude Code (or any MCP client)
Add to your MCP client configuration (the exact path varies per client):
```json
{
"mcpServers": {
"graph-mcp": {
"command": "python",
"args": ["-m", "graph_mcp.server"],
"env": {
"GRAPH_MCP_LOCAL_GRAPH_FILE": "/absolute/path/to/your.ttl"
}
}
}
}
```
## End-to-end example
A plan, rendered, and executed against the bundled sample graph:
```python
from graph_mcp.models import (
Iri, Prefix, PrefixedName, Projection, SelectPlan, TriplePattern, Var,
)
from graph_mcp.compiler import QueryPlanValidator, SparqlRenderer
from graph_mcp.graph import LocalRdflibEndpoint
from graph_mcp.security import SecurityPolicy
from graph_mcp.config import Settings
policy = SecurityPolicy.from_settings(Settings())
validate = QueryPlanValidator(policy)
render = SparqlRenderer(policy)
endpoint = LocalRdflibEndpoint.from_turtle_file("evals/sample_graph.ttl")
plan = SelectPlan(
prefixes=[Prefix(prefix="ex", iri="http://example.org/")],
projection=[Projection(var=Var(name="person"))],
where=[
TriplePattern(
subject=Var(name="person"),
predicate=PrefixedName(prefix="ex", local="worksFor"),
object=PrefixedName(prefix="ex", local="Acme"),
),
],
)
assert validate.validate(plan).ok
print(render.render(plan).sparql)
# PREFIX ex: <http://example.org/>
# ...
# SELECT ?person
# WHERE {
# ?person ex:worksFor ex:Acme .
# }
# LIMIT 100
```
## Ontology concept discovery (RAG)
The `discover_ontology_concepts` MCP tool delegates **all** retrieval logic
to the [`ontology_vectorizer`](../ontology_vectorizer) library. graph-mcp
does not implement concept embedding, Qdrant queries, reranking, or
graph-aware scoring directly — it only owns the MCP boundary.
### Architecture
```
host LLM ──MCP──▶ graph-mcp.discover_ontology_concepts
│
▼
OntologyConceptRetriever (ontology_vectorizer.api)
│
┌─────────┼─────────┬───────────────┐
▼ ▼ ▼ ▼
embedding Qdrant reranking graph-aware scoring
client client client (parents / groups)
```
The MCP client never imports `ontology_vectorizer.qdrant_store`,
`ontology_vectorizer.retrieval`, or any other internal module — only the
public facade in `ontology_vectorizer.api`.
### Install
```bash
# Editable side-by-side checkouts (typical dev setup):
uv pip install -e ../ontology_vectorizer
uv pip install -e ".[rag]"
```
Or, with uv's `[tool.uv.sources]` entry already in `pyproject.toml`,
`uv sync --extra rag` will pick up the sibling checkout automatically.
### Required environment
The vectorizer reads its own variables (not prefixed with `GRAPH_MCP_`):
| Variable | Purpose |
| --- | --- |
| `QDRANT_URL` | Qdrant base URL |
| `QDRANT_API_KEY` | Qdrant API key (optional) |
| `QDRANT_COLLECTION_NAME` | Collection holding ingested concepts |
| `FOUNDRY_API_BASE_URL` | OpenAI-compatible / Foundry gateway URL |
| `FOUNDRY_API_TOKEN` | Bearer token for the gateway |
| `FOUNDRY_EMBEDDING_MODEL` | Embedding model name |
| `FOUNDRY_RERANKER_MODEL` | Reranker model name (optional; falls back to local lexical) |
| `FOUNDRY_LLM_MODEL` | LLM model used by enrichment (optional) |
| `ONTOLOGY_VECTORIZER_DEFAULT_ONTOLOGY_ID` | Default ontology id |
The MCP server has its own thin wrapper:
| Variable | Purpose |
| --- | --- |
| `GRAPH_MCP_CONCEPTS_ENABLED` | Master switch (default `true`) |
| `GRAPH_MCP_CONCEPTS_DEFAULT_ONTOLOGY_ID` | Used when a request omits `ontology_id` |
| `GRAPH_MCP_CONCEPTS_DEFAULT_TOP_K` | Default `top_k` |
| `GRAPH_MCP_CONCEPTS_INCLUDE_DEPRECATED_BY_DEFAULT` | Allow deprecated concepts even when the request doesn't ask for them |
### Ingestion (run once per ontology)
```bash
# Populate the Qdrant collection that the MCP tool reads.
ontology-vectorizer ingest --input ocean_demo.ttl --ontology-id ocean-demo
```
### Example MCP request / response
Request:
```json
{
"query": "sea surface temperature",
"ontology_id": "ocean-demo",
"top_k": 5,
"kind_filter": ["skos_concept"]
}
```
Response:
```json
{
"query": "sea surface temperature",
"ontology_id": "ocean-demo",
"retrieval_strategy": "hybrid_multi_stage_graph_aware",
"results": [
{
"concept_id": "...",
"iri": "https://example.org/ocean-demo/id/observable-property/sst",
"compact_id": "var:sst",
"preferred_label": "sea surface temperature",
"labels": ["sea surface temperature"],
"alt_labels": ["SST"],
"kind": "skos_concept",
"definition": "Temperature at the ocean surface.",
"score": 0.94,
"deprecated": false,
"parents": ["var:temperature"],
"group_ids": ["..."],
"explanation": "exact-label match"
}
]
}
```
Errors (vectorizer not installed, Qdrant unreachable, missing credentials)
are returned as `{"error": "..."}` rather than raised, so a host LLM can
surface them gracefully.
## Tools, resources, prompts
| MCP tool | Purpose |
| --- | --- |
| `resolve_terms` | Map natural-language mentions → ranked IRIs (label/alias/local-name match) |
| `validate_query_plan` | Static check; structured `ValidationResult` |
| `render_sparql` | Validates first, then renders canonical SPARQL |
| `query_graph` | Validate → render → execute (or `dry_run=true` to stop after rendering) |
| `explain_query_plan` | Human-readable plan summary |
| `execute_sparql_raw` | Off by default; gated by `GRAPH_MCP_ENABLE_RAW_SPARQL`; rejects updates and unauthorized `SERVICE` |
| `discover_ontology_concepts` | Delegates to [`ontology_vectorizer`](../ontology_vectorizer) for hybrid concept retrieval (embedding + Qdrant + reranking + graph-aware scoring). Requires `pip install graph-mcp[rag]`. |
| Resource | Body |
| --- | --- |
| `graph://schema/prefixes` | Prefix → IRI map |
| `graph://schema/classes` | Known classes |
| `graph://schema/properties` | Known properties |
| `graph://schema/named-graphs` | Known named graphs |
| `graph://schema/individuals` | Known individuals (capped) |
| `graph://schema/examples` | Example QueryPlan objects |
| `graph://policy/security` | Active policy |
| `graph://query-plan/schema` | JSON Schema of the QueryPlan IR |
| Prompt | Purpose |
| --- | --- |
| `build_query_plan` | Tells the host LLM how to plan, not write, SPARQL. |
## Tests, lint, type-check
The full local verification suite. The CI gate is **all five** of these
passing on every change:
```bash
python -c "import graph_mcp.models; print('ok')" # import smoke
python -m pytest -q # tests (offline)
python -m ruff check . # lint
python -m ruff format --check . # formatting
python -m mypy src evals # type-check
```
The Makefile targets `make test`, `make lint`, `make typecheck`, `make all`
are convenience wrappers; if you don't have `make`, run the commands above
directly.
## Evaluations
The eval harness scores planner output against golden cases. The deterministic
baseline runs fully offline; an LLM-backed planner is opt-in.
```bash
# Deterministic baseline — no API key, runs offline.
# This baseline is hand-coded; it exists to exercise the validator, renderer,
# executor, and metrics pipeline end-to-end without LLM cost or flakiness.
# Its scores are *not* evidence of LLM planning quality.
python -m evals.runner --planner deterministic
# LLM planner (requires `pip install -e .[ai]` and an API key).
# Schema, output JSON Schema, and golden examples are inserted into the
# system prompt; failed validations are fed back for up to 2 repair attempts.
python -m evals.runner --planner pydantic-ai --model anthropic:claude-sonnet-4-6
```
The runner emits structural-quality, safety, and repair metrics suitable for
comparing real LLM planners (and not for declaring victory based on the
keyword baseline):
| Metric | Meaning |
| --- | --- |
| `valid_plan_rate` | Fraction of generated plans that pass validation |
| `render_success_rate` | Fraction that also render to SPARQL |
| `execution_success_rate` | Fraction that also execute against the sample graph |
| `required_feature_recall` | Hit rate for required pattern kinds + required tokens in rendered SPARQL |
| `forbidden_feature_violation_rate` | Fraction of forbidden-feature checks that fired |
| `term_resolution_accuracy` | Fraction of expected schema terms that appeared |
| `structural_plan_score` | `required_feature_recall × (1 − forbidden_feature_violation_rate)` |
| `execution_result_accuracy` | Fraction of executed cases whose row count matched expectations |
| `safety_violation_count` | Hard-safety failures (e.g. SERVICE used) |
| `validation_error_rate` | Fraction whose plans the validator rejected |
| `repair_attempted_rate` | Fraction where the LLM planner needed at least one repair pass |
| `repair_success_rate` | Of those, fraction that became valid after repair |
| `case_pass_rate` | Cases that hit zero structural / safety / execution failures |
The runner can also produce a JSON+markdown report:
```bash
python -m evals.runner --report-dir build/eval_report
```
## Extending
### Add a new expression function
1. Add the function name to `ALLOWED_FUNCTIONS` in
`graph_mcp/models/expressions.py`.
2. Update the renderer if it requires a non-default rendering shape.
3. Add a test in `tests/test_renderer.py`.
### Add a new pattern type
1. Add the model in `graph_mcp/models/patterns.py` and to the `Pattern` union.
2. Update `QueryPlanValidator._validate_pattern` to handle scope/safety.
3. Update `SparqlRenderer._render_pattern` to emit it.
4. Update `_vars_in_pattern` in the validator if needed.
5. Add tests for both validator and renderer.
### Add schema-specific aliases
Inject a richer `SchemaProvider` into `build_server`:
```python
from graph_mcp.graph.schema_discovery import SchemaSnapshot, StaticSchemaProvider
schema = StaticSchemaProvider(SchemaSnapshot(...))
server = build_server(schema=schema)
```
### Add new golden eval cases
Append to `evals/golden_cases.yaml`. The `expected` block can specify
required pattern kinds, required tokens in the rendered SPARQL, forbidden
features, and execution expectations.
### Enable raw SPARQL safely
Raw SPARQL is **disabled by default**. To enable it, set
`GRAPH_MCP_ENABLE_RAW_SPARQL=true`. Even then, the tool runs every input
through a real token-aware scanner (`graph_mcp/mcp_tools/sparql_scanner.py`)
that distinguishes default-state code from string literals, comments, and
IRI references. Specifically:
- comments (`#…`) start a comment **only** in default state, so
`<http://example.org/#fragment>` is never mistaken for one;
- string literals (single, double, triple-quoted) are opaque to keyword
detection — `"# INSERT DATA"` is a string, not an INSERT;
- `INSERT`/`DELETE`/`DROP`/`CLEAR`/`LOAD`/`CREATE`/`COPY`/`MOVE`/`ADD` are
rejected via token-level matching (catches `INSERT\nDATA`,
`INSERT\tDATA`, `Insert\ndata`);
- `WITH … DELETE` is rejected (`WITH` itself is treated as forbidden);
- `DESCRIBE` is rejected;
- `SERVICE <iri>` is permitted only when the *exact* IRI matches the
allowlist; `SERVICE ?var` and `SERVICE prefix:name` are rejected;
- the actual query form is inferred from the first query keyword in the
token stream and must match `expected_query_type`;
- raw `SELECT` and `CONSTRUCT` queries must include an explicit top-level
`LIMIT` no greater than the effective `max_rows`; otherwise the request
is rejected. We never download an unbounded result and truncate
afterward.
Raw mode is still *not* a full SPARQL parser. It is best-effort
defence-in-depth around a feature you should generally leave off.
## Security model
- Read-only by default (no SPARQL Update, no arbitrary `SERVICE`).
- Validator enforces depth, triple-count, property-path complexity, and
limit caps **at every level** (top-level *and* subqueries).
- Named-graph allowlist: when configured, `GRAPH ?g` is rejected unless
`?g` is constrained by a sibling `VALUES` to allowlisted IRIs.
- SERVICE allowlist applies in both the IR validator and the raw-SPARQL
pre-flight check.
- All log output goes to stderr (stdio transport keeps stdout clean for
JSON-RPC).
- Errors and exceptions never include endpoint credentials.
- Raw-SPARQL tool is disabled by default and clearly tagged when enabled.
- EXISTS / NOT EXISTS sub-patterns are recursively validated for SERVICE,
unknown prefixes, depth, triple-count, and property-path policy; their
inner variables do not leak outward.
- Aggregate queries: variables outside aggregate expressions in projections,
HAVING, and ORDER BY must appear in GROUP BY.
- Prefixes are declared once at the top of the plan; subquery prefix blocks
are rejected.
## How `max_rows` affects rendered LIMIT
`query_graph(max_rows=N, dry_run=True)` caps the effective row limit
*before* rendering. Specifically:
```text
effective_max_rows = min(max_rows or default_limit, max_limit)
```
For a top-level `SELECT` or `CONSTRUCT`:
- if the plan has no `LIMIT`, it is set to `effective_max_rows`;
- if the plan's `LIMIT` is greater than `effective_max_rows`, it is capped;
- if the plan's `LIMIT` is smaller, it is preserved.
This means `dry_run=true` shows you what will actually be sent to the
endpoint, and remote endpoints are never asked to materialize unbounded
results before truncation. Subquery `LIMIT`s are capped at `policy.max_limit`
but never have a default injected (changing subquery semantics is unsafe).
For raw SPARQL, the rule is stricter: raw `SELECT` / `CONSTRUCT` queries
must include an explicit top-level `LIMIT` no greater than
`effective_max_rows` or the request is rejected.
## Known limitations
The following are explicit, current limitations — if any of these is a
blocker for you, please open an issue.
- **Deterministic planner is keyword-matching.** The hand-coded
`DeterministicPlanner` in `evals/agent.py` is aligned with the bundled
`golden_cases.yaml` keywords; its 100 % score on that file is *not*
evidence of LLM planning quality. The `evals/golden_cases_adversarial.yaml`
file contains paraphrased, plural/singular variations and clarification
traps the keyword baseline cannot answer; that file is the honest
benchmark for an LLM planner.
- **LLM eval is opt-in.** The PydanticAI planner is enabled by
`pip install -e .[ai]`. Running it requires an API key and is not part
of CI.
- **PydanticAI tool-backed term resolution is out of scope** for this
package. The optional PydanticAI planner currently receives schema
context in the prompt but does not yet use live PydanticAI tool calls
for term resolution. MCP hosts that need tool-backed resolution can
invoke the server's existing ``resolve_terms`` MCP tool directly. The
evals agent in ``evals/agent.py`` is intentionally a thin benchmarking
harness — production-grade term-resolution wiring belongs in the host
agent, not the MCP server.
- **Raw SPARQL.** Disabled by default. When enabled, the pre-flight check
is a token-aware scanner — it tracks string/IRI/comment states and
rejects update keywords, DESCRIBE, and unallowlisted SERVICE — but it is
*not* a full SPARQL parser. Raw `SELECT`/`CONSTRUCT` must include an
explicit top-level `LIMIT`; the server will reject otherwise.
- **Remote `CONSTRUCT`.** The HTTP endpoint asks for `text/turtle` /
`application/n-triples` / `application/rdf+xml` and parses the response
via rdflib. Endpoints that ignore the `Accept` header and return JSON or
HTML produce an `EndpointError("unsupported CONSTRUCT response
content-type")` — never a silent empty result.
- **Schema discovery is best-effort.** `SparqlSchemaProvider` records
per-section errors (timeouts, unsupported features) as
``SchemaDiagnostic`` entries on the snapshot rather than raising. Inspect
them via `graph://schema/status`.
- **Local timeout.** `LocalRdflibEndpoint` runs queries in a worker thread
under `asyncio.wait_for`. The timeout fires and the caller sees
`EndpointError`, but **rdflib has no first-class cancellation**, so a
runaway query continues to consume CPU on its worker thread until it
finishes. For hard cancellation, query a real SPARQL server via
`HttpSparqlEndpoint` against an engine that enforces query budgets.
- **`DESCRIBE`** is intentionally not in the IR.
- **SPARQL Update** is intentionally not in the IR and is rejected by the
raw-SPARQL pre-flight when raw mode is enabled.
- **Production claims.** This server has not been deployed under production
load by the authors. CI covers static checks, the import stress matrix
(Python 3.11–3.13 × 11 hash seeds), tests, and the deterministic eval.
Real-world load testing, multi-tenant authn, and billing/quota are out
of scope.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
markitdown
Python tool for converting files and office documents to Markdown.
Filesystem
Node.js MCP Server for filesystem operations with dynamic access control.
TrendRadar
TrendRadar: Your hotspot assistant for real news in just 30 seconds.
mempalace
The highest-scoring AI memory system ever benchmarked. And it's free.
mempalace
The highest-scoring AI memory system ever benchmarked. And it's free.