# Mithril Graph & Ontology database — context for AI agents The Knowledge Graph & Ontology database of Mithril. Until 2026-09-16 it was operated as the kotobase.net graph plane; the workbench is now https://graph.kotoba.cloud/ and the API is https://api.mithril.fund/v1/database. The kotobase.net XRPC and Client API paths remain served for existing clients with the same credential classes. Public operator: Kotoba Labs Inc. Contact: support@kotoba.cloud. Agent quickstart (executable): https://graph.kotoba.cloud/agent-quickstart.md Short index: https://graph.kotoba.cloud/llms.txt Human docs: https://docs.mithril.fund/graph/ · errors: https://docs.mithril.fund/reference/errors/ ## Data model - A database is a set of datoms `[e a v tx added]`. Entities are maps of attributes; an attribute whose value is another entity is an edge. - Every entity carries an ontology type tag: attribute `:ontology/type` in Datalog, the node label in Cypher, `hasLabel` in Gremlin bytecode, `` in SPARQL. The ontology inspector in the workbench visualizes the attributes and types observed in stored datoms. - Canonical truth is a signed immutable CID commit DAG. Query responses carry the basis (`basis_cid`) they were answered from; a transaction may pin `expected_parent` to that CID for a compare-and-swap write. - A write publishes only after a conditional head CAS wins. Same-graph losers retry with full-jitter exponential backoff (12 attempts by default); exhaustion is HTTP 409 with `retryable: true` and `retry_after_ms`. No acknowledged write is lost; throughput under contention is not promised. - Content identity is a CID; `GET /v1/database/ipld/` returns bytes. ## MCP server `https://graph.kotoba.cloud/mcp` — Model Context Protocol over streamable HTTP, stateless (no session id), JSON-RPC 2.0: `initialize`, `ping`, `tools/list`, `tools/call`. Descriptor with the client one-liners: `https://graph.kotoba.cloud/.well-known/mcp.json`. The connection's `Authorization` (Bearer service token or Biscuit) and `X-Kotobase-Tenant` travel verbatim into the same `/v1/database` routes below; every tool result is that route's own answer (a 401 stays a 401, rendered as `isError`). | Tool | Route | Arguments | | --- | --- | --- | | graph_docs | — | none (no credential) | | graph_list_databases | datomic.listDatabases | none | | graph_create_database | datomic.createDatabase | db_name | | graph_db_stats | datomic.dbStats | db_name | | graph_transact | datomic.transact | db_name, tx_edn, expected_parent? | | graph_query | datomic.q (datalog) / kg.query (cypher, sparql, sql) | db_name, query, lang?, limit? | | graph_usage | usageGet | none | ## Credentials (three classes, no fourth) 1. Browser session cookie (the workbench; same-origin POSTs only). 2. Tenant service account token `kb_sa_…` — minted once by the account holder in the workbench ("Create agent token"), shown once, revocable and rotatable. Sent as `authorization: Bearer ` on every call (an `editor` account holds `data:read` and `data:write`). Optionally attenuated at `POST https://auth.mithril.fund/v1/biscuit/token` (body `{tenantId, dbName, permissions}`) to a Biscuit bound to one tenant, one database, the requested permissions and 15 minutes; a Biscuit is attenuated, never refreshed. 3. A `kotoba cacao` CACAO (self-sovereign; migration path for existing clients). An inference personal token (`kc_pat_…`) is refused by name on every database route: inference keys do not grant database rights. Anonymous requests are HTTP 401 `sign-in-required`. Every database call names its tenant with `x-kotobase-tenant: t_…`. ## Routes (all `POST`, JSON, `https://api.mithril.fund/v1/database` prefix) XRPC, method = path suffix after `/xrpc/ai.gftd.apps.kotobase.`, body always carries `db_name`: - `datomic.listDatabases {}` → `{databases: [...]}` - `datomic.createDatabase {db_name}` / `datomic.deleteDatabase {db_name}` / `datomic.restoreDatabase` - `datomic.q {db_name, query_edn, inputs_edn?}` → `{rows | result, basis_cid, ...}` - `datomic.transact {db_name, tx_edn, expected_parent?}` — needs `data:write` - `datomic.dbStats {db_name}` · `datomic.asOf {db_name, ...}` - `kg.query {db_name, lang: "sql" | "cypher" | "sparql", query, limit?, external_tables?}` → `{ok, language, columns, rows, count, truncated, graph}` - `usageGet {}` · `savedQuery.list` / `savedQuery.save {name, db_name, lang, query}` / `savedQuery.delete` Datomic Client API shape (EDN or transit+json), same credential, database named by `x-datomic-db-name` or EDN `:db-name`: `/api/q` `/api/qseq` `/api/pull` `/api/datoms` `/api/db` `/api/db-stats` `/api/connect` `/api/list-databases` `/api/create-database` `/api/delete-database` `/api/transact` `/api/with` `/api/with-db` `/api/history` `/api/as-of` `/api/since` `/api/sync` `/api/tx-range` `/api/index-range` `/api/index-pull` `/api/seek-datoms` `/api/rseek-datoms`. Datoms are `{:e :a :v :tx :added}` maps; a transaction report is `{:db-before :db-after :tx-data :tempids}`. Pinning (IPFS Pinning Service API shape): `GET/POST /pins`, `GET/DELETE /pins/`. Audit: `GET /v1/audit`, `GET /v1/audit/export`. ## Read-only query lane for tool-calling models - Tool schema (OpenAI format, drop into `tools`): `GET https://api.mithril.fund/v1/graph/tools` (public) - Query: `POST https://api.mithril.fund/v1/graph/query` body `{"lang":"sparql|cypher|gremlin|datalog|sql","query":"…","db_name":"…","limit":100}` (`limit` ≤ 1000, query ≤ 65,536 chars, body ≤ 32 KiB) - READ-ONLY: a statement beginning with a write verb (create / merge / drop / alter / delete / insert / update / …), or Gremlin bytecode with addV / addE / drop, is refused before any hop. - Credential: the same Biscuit or CACAO as above. ## Query languages - Datalog: the kotobase dialect — Datomic-shaped EDN `{:find [?e ?v] :where [[?e :attr ?v]]}`; `inputs_edn` for parameters. - SQL (`kg.query`, `lang: "sql"`): bounded read-only `SELECT` over `datoms(e, a, v, v_edn, added)` and optional request-scoped `external_tables`. INNER/LEFT/RIGHT/FULL/CROSS joins, non-equality `ON`, derived tables to depth 2, GROUP BY, COUNT/SUM/AVG/MIN/MAX, DISTINCT, ORDER BY, LIMIT/OFFSET. Limits: 16 KiB query, four sources, 1,000 scanned/result rows, 10,000 intermediate rows; at most four external tables, 1,000 rows/table, 32 columns/table. HAVING, CTE/UNION, window functions, DML, comments and multiple statements fail closed. - Cypher: MATCH / OPTIONAL MATCH / WHERE with parenthesized NOT/AND/OR / WITH ORDER BY SKIP LIMIT / RETURN / CASE WHEN … IS NULL; labels are the type tag. Supported write: an authenticated standalone-node `CREATE` with a non-empty `id`. Relationship CREATE, MERGE, SET, REMOVE, DELETE fail closed. - SPARQL: SELECT over the datom head; `` binds the type tag. - Gremlin: bytecode-shaped traversal as a JSON array of `[step, args…]` (`[["V"],["hasLabel","greeting"],["values","greeting/text"]]`), not Groovy text. ## Measured evidence (receipts, not promises) - 2026-08-14, one live same-graph run with 25 simultaneous writers: 12 successes, 13 explicit HTTP 409 conflicts, no 5xx, all 12 acknowledged writes retained in the final head. A safety receipt, not a throughput or SLA claim. - 2026-08-15, authenticated standalone Cypher `CREATE` returned HTTP 200 and created `created/1`; a query combining NOT/AND/OR, WITH ORDER BY/SKIP/LIMIT, MATCH, OPTIONAL MATCH and CASE WHEN … IS NULL returned the expected rows. - 2026-09-17: `GET /v1/graph/tools` 200 without credentials; every `/v1/database/*` route 401 `sign-in-required` without credentials; `/v1/graph/query` 401 without credentials. ## Claim boundaries Immutable snapshots and CID identity make stored content portable and independently checkable; hosted write availability still depends on the provider's object store and head CAS. The ontology inspector reports observed attributes and types; it is not a reasoner over an external OWL ontology. Nothing here grants access: discovery is not write, query, or pin. ## The rest of the stack - Kotoba, the language: https://kotoba-lang.org/llms-full.txt - Mithril identity, inference and packages: https://mithril.fund/llms-full.txt