MCP reference
ontologiq serve exposes the compiled ontology over the Model Context
Protocol on stdio. The tools are generated from your objects — you never
write tool code.
Connecting Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"ontologiq": {
"command": "ontologiq",
"args": ["serve", "--role", "support", "--project-dir", "/path/to/shop"]
}
}
}
Drop --role and the agent gets read tools only. That is the right default
for a first connection.
The generated tools
For an object called customer with a relation orders and an object
order with an action cancel:
| Tool | What it does |
|---|---|
get_customer | One record by identity |
list_customer | Many records, filterable by computed state |
traverse_customer_orders | From one customer to their orders |
propose_order_cancel | Propose the action; never executes |
Names are namespaced by object because MCP tool names are global: two
objects may both have a cancel action, and validate rejects a project
where generated names would collide.
Read tools
get_<object>
Input: the identity columns. Output:
{
"rows": [{"customer_id": 1, "full_name": "Ada Lovelace", "state": "active"}],
"count": 1,
"redacted_properties": ["email"],
"note": "Properties marked sensitive in the ontology are not returned. They were never read from the database."
}
If more than one row matches the identity, the call fails rather than returning an arbitrary one.
list_<object>
Input: optional state (constrained to the object’s declared states) and
limit (default 100, maximum 1000, always clamped server-side regardless of
what the client sends).
Results are ordered by identity, so paging by limit is deterministic, and truncation is reported explicitly:
{"rows": [...], "count": 100, "truncated": true}
An agent told nothing about truncation will confidently assert that there are exactly 100 churned customers.
traverse_<object>_<relation>
Input: the identity of the source record, plus an optional limit (same
default and ceiling as list_). Returns
the related object’s rows. Only the target’s columns come back — the source
record’s properties are never even read.
The propose tool
propose_<object>_<action> takes the object’s identity, the action’s
declared parameters, and an optional dry_run.
It cannot execute anything. Its outcomes:
outcome | Meaning |
|---|---|
dry_run | What would happen, evaluated against live data. Nothing changed. |
pending_approval | A proposal was stored. A human must approve it elsewhere. |
executed | The effect was applied. Only reachable for actions declaring no approval, and only when the server ran with --allow-auto-effects. |
unknown | Same path, but the request was sent and no response came back. The effect may or may not have been applied — do not retry. |
effect_failed | Same path; the endpoint refused (4xx, a redirect, or 5xx after retries). Nothing was applied. |
And its refusals, each an error the agent can act on:
error | Meaning |
|---|---|
denied_role | The asserted role is not in policy.roles. The warehouse was not touched. |
precondition_failed | requires does not hold; the message names the actual state. |
not_found | No record with that identity. |
ambiguous_identity | More than one row shares that identity. |
auto_effects_disabled | The action declares no approval and the server did not opt in. |
approval_unevaluable | approval.when references a parameter the call did not supply. |
Configuration and transport problems surface separately as
invalid_arguments, effect_misconfigured, unknown_object,
unknown_tool and internal_error. The table above is the governance
refusals — the ones an agent should reason about rather than retry.
The tool description handed to the model states the precondition, the allowed roles and the approval requirement, so the agent knows the rules before calling rather than discovering them by rejection.
What the model never gets
- Sensitive property values. They are not selected, so they never leave the warehouse.
- Resolved effect URLs. The ontology template and the host are reported; the resolved URL is not, because its path is frequently the secret (Slack, Teams and PagerDuty all put the token there).
- Raw exceptions. Adapter errors carry connection strings and hostnames; the model gets a stable message and the details go to stderr.
- An approval path. There is no approve tool, no
approved: trueinput, and no auto-approval on timeout.
Untrusted data
Values read from your warehouse flow into the model’s context, and a customer name can contain instructions. Before anything is returned, Ontologiq strips control characters, ANSI escapes, Unicode tag characters and bidi overrides — the last two hide text from the human reading the same output — and caps field length.
That reduces the surface; it does not eliminate prompt injection, and nothing at this layer can. The real mitigation is structural: an injected instruction still cannot complete a governed action, because approval lives in a process the model cannot address.
Protocol notes
- Transport: stdio. HTTP with proper authentication is on the roadmap; until then the role is asserted by whoever launches the process.
- SDK: the official
mcpPython package,>=2.0.0,<3. - stdout carries only JSON-RPC frames. Every diagnostic, including anything a database driver decides to print, is redirected to stderr.