# MCP server for AI agents

Source: https://docs.littlebigbrain.com/sdks/mcp/

`@littlebigbrain/mcp` is an [MCP](https://modelcontextprotocol.io) server. It
gives an agent a small set of tools over your graph. With them, an agent can:

- search by meaning,
- follow links with SPARQL,
- cite the commit it read,
- write records that your schema checks.

A typical agent turn has three calls. Search finds a starting record. A SPARQL
query follows its links to the answer. The answer cites the rows and the
commit.

The [quickstart](https://docs.littlebigbrain.com/start/quickstart/) shows these steps in code.
[Your first search through MCP](#your-first-search-and-query-through-mcp) shows
them as tool calls.

Run the server locally over stdio with `npx @littlebigbrain/mcp`. Or connect to
the hosted endpoint with OAuth sign-in. The server wraps the
[`@littlebigbrain/client`](https://docs.littlebigbrain.com/sdks/typescript/) SDK. This page describes version
0.16.0, which uses client 0.31.0.

Source and issues: [github.com/littlebigbrains/lbb-mcp](https://github.com/littlebigbrains/lbb-mcp) (Apache-2.0).

## Discover the hosted MCP server

The [agent skills index](https://littlebigbrain.com/.well-known/agent-skills/index.json)
provides focused instructions for connecting to MCP and querying a graph with
its real vocabulary and commit evidence. It follows Agent Skills Discovery
v0.2.0; each entry points to a Markdown skill and includes a SHA-256 digest.
Verify the downloaded file against that digest before loading its instructions.

The [AI catalog](https://littlebigbrain.com/.well-known/ai-catalog.json) lists the
MCP server, both OpenAPI schemas and two agent skills, with stable identifiers
and example queries for semantic discovery. The same manifest is available at
the current [ARD discovery path](https://littlebigbrain.com/.well-known/ard.json).
Follow its [MCP server card](https://littlebigbrain.com/mcp-server-card.json) to connect. The card describes
the hosted Streamable HTTP endpoint and requires your existing stack slug in
`X-LBB-Stack`. Follow the [OAuth protected-resource metadata](https://mcp.littlebigbrain.com/.well-known/oauth-protected-resource)
for sign-in, then connect to `https://mcp.littlebigbrain.com` with that header.
You can also use the stack-specific URL described below.

The current [SEP-2127 format](https://github.com/modelcontextprotocol/experimental-ext-server-card)
describes identity and connection details. The server reports capabilities and
the available tools when your MCP client connects. A
[compatibility card](https://littlebigbrain.com/.well-known/mcp/server-card.json)
also provides the earlier `serverInfo` and `capabilities` fields for clients
that use the original well-known discovery path.

## Browser tools on the marketing site

The [marketing site](https://littlebigbrain.com/) also exposes browser tools
through [WebMCP](https://webmachinelearning.github.io/webmcp/) where the browser
supports it. Agents can find and open public pages, read the current page and
estimate Cloud or Pro pricing. The contact form lets an agent prepare an
enquiry for the user to review and send. These browser tools operate on the
marketing site; graph queries use the authenticated MCP server described here.

## Discover the service documentation through DNS

Query the SVCB record at `_index._agents.littlebigbrain.com` to find the public
service documentation index. It advertises `docs.littlebigbrain.com`, HTTPS
on port 443 and the path `/llms.txt` through the experimental DNS-AID
`well-known` parameter, encoded as `key65409` pending registration:

```text
_index._agents.littlebigbrain.com. 3600 IN SVCB 1 docs.littlebigbrain.com. (
  mandatory=alpn,port,key65409 alpn="h2,http/1.1" port=443 key65409="/llms.txt"
)
```

Fetch [the index](https://docs.littlebigbrain.com/llms.txt) to find the MCP,
HTTP API and authentication documentation. A DNS-AID client needs to understand
the mandatory path parameter to use this record. The hosted MCP connection
still uses your stack's URL and OAuth sign-in, as described below.

## Task-shaped tools

The set has one tool per task. It does not mirror the HTTP routes.

| Tool | Purpose |
| --- | --- |
| `lbb_inspect` | graph context and exact reads: guide, graphs, paginated ontology/schema, publication status, conformance, metadata, entity, ontology change suggestions |
| `lbb_rdf` | import full RDF/OWL or add axioms using INSERT DATA (an update that removes asks the person to approve) |
| `lbb_query` | analytical reads: structured query, SPARQL text, search by meaning, questions in plain words, canned analysis, and four tools for writing queries (names, describe, commit_at, compare) |
| `lbb_embeddings` | search setup, read only: list and inspect embeddings, preview the text and cost of embedding a class |
| `lbb_embeddings_manage` | declare or refresh an embedding (spends model tokens) |
| `lbb_embeddings_delete` | delete an embedding and its vectors (asks the person to approve) |
| `lbb_fit_sources` | fit from text, read only: list the fit sources, preview the text the fit reads |
| `lbb_fit_sources_manage` | declare, dry-run or refresh a fit source (spends model tokens) |
| `lbb_fit_sources_delete` | delete a fit source (asks the person to approve) |
| `lbb_workflows` | starter workflows, read only: the starter workflows this server offers, the graph's triggered workflows with their status, the preview of creating one, the workflows with triggers, what a watch has not sent yet, the history of a workflow's settings, workflow tests, and what the runs did with each input |
| `lbb_workflows_manage` | create a workflow from a starter workflow on a class or change one, ask it to run now, pause and resume, define a workflow with triggers of your worker, use earlier settings of a workflow again, or start, answer and cancel a workflow test |
| `lbb_workflows_delete` | delete a triggered workflow or a workflow with triggers (asks the person to approve) |
| `lbb_reviews` | review items, read only: the questions for a person, one page at a time, one item, and the items grouped by pattern |
| `lbb_reviews_manage` | file a review item, answer one or a batch, comment, or withdraw an item (an answer that commits asks the person to approve) |
| `lbb_files` | files, read only: the graph's files with their parse status, filtered by status or name, and the parsed lines of a page range, each with its box on the page |
| `lbb_commit` | preview/write statements or entity properties, and retract them (a retract asks the person to approve) |
| `lbb_model_choice` | the model each use runs, trials of other models with their reports, and the model choices of the workflow steps |
| `lbb_model_choice_manage` | start or stop a trial, switch a use to a tested model, revert (a trial spends model and judge tokens); set or remove the model of a workflow step |
| `lbb_models` | evaluation and training inputs, and the managed models' activity |
| `lbb_evals` | label results as relevant or not, save evaluation queries, and check them at the current commit; read the model checks and correct one; keep datasets of examples |
| `lbb_evals_delete` | delete a dataset of examples (asks the person to approve) |
| `lbb_configure` | preview/define/evolve native metadata, list and apply ontology starters, suggest an ontology change, activate SHACL shapes, or keep the graph's notes for questions in plain words (an evolve that removes and a `reject`-mode publish ask the person to approve) |

Call `lbb_inspect` with `action: "ontology"` before the agent writes a query or
a statement. The agent then uses only the types, relations and properties of
the pinned commit.

Read tools accept `detail: "compact" | "standard" | "full"`. The default is
`compact`, which returns this envelope:
`{summary, data, counts?, truncated?, rows_shown?, next?}`.

## Local (stdio): Claude Code, Cursor, Codex

The client starts the server as a local process. They talk over standard input
and output (stdio). This needs Node.js 18 or later.

Copy the complete endpoint and a stack API key from **Connect** in the
[console](https://cloud.littlebigbrain.com). Add them to your client's MCP
configuration, such as `.mcp.json` or `mcp.json`:

```json
{
  "mcpServers": {
    "lbb": {
      "command": "npx",
      "args": ["-y", "@littlebigbrain/mcp"],
      "env": {
        "LBB_BASE_URL": "https://<tenant-short-id>--<stack-slug>.db.eu.littlebigbrain.com",
        "LBB_API_KEY": "<your-stack-api-key>"
      }
    }
  }
}
```

| Variable | Meaning |
| --- | --- |
| `LBB_BASE_URL` | Required. The complete stack endpoint from **Connect**. Without it, the server starts and lists its tools, and each tool call fails with a message that asks for it. |
| `LBB_API_KEY` | The stack API key. Keep it in your local configuration. |
| `LBB_GRAPH` | Optional. The graph for calls that pass no `graph`. The default is `main`. |
| `LBB_MCP_DESTRUCTIVE` | Optional. `allow` runs a call that deletes or removes data without a question when the client cannot show one. Leave it unset to keep the question. See [Approve destructive writes](#approve-destructive-writes). |

## Hosted (streamable-HTTP, OAuth)

The hosted endpoint is `https://mcp.littlebigbrain.com`. It uses the MCP OAuth
sign-in. You sign in to your little big brain account. No stack API key is
stored on your machine. Claude-style clients use the URL of one stack:

```json
{
  "mcpServers": {
    "lbb": {
      "url": "https://mcp.littlebigbrain.com/mcp/<your-stack-slug>"
    }
  }
}
```

A client that supports remote MCP auth runs the sign-in by itself:

1. It receives an authorization challenge from the endpoint.
2. It opens the browser sign-in.
3. It sends the resulting access token with each call.

The endpoint checks the token and confirms that your account owns the stack. It
then creates a short-lived session and runs the tools on that stack. Add
`?graph=<name>` to the URL to change the default graph from `main`.

| Response | Cause |
| --- | --- |
| `401` with an authorization challenge | The access token is missing or not valid. The client signs in again. |
| `402 subscription_required` | The account's subscription setup is not complete. Finish it in the console. |
| `403 insufficient_scope` | A claimed agent token lacks the trusted `mcp:access` permission. |
| `404` `unknown stack` | Your account does not own a stack with that slug. |
| `405` | The request was not a `POST`. |
| `503` | The stack's endpoint is not ready yet. Retry from the console. |

For agent identity registration, follow [auth.md](https://littlebigbrain.com/auth.md).
The guide uses WorkOS's generated instructions once the issuer enables Agent
Registration, and the existing human OAuth flow until then. A claimed agent's
access token must carry `mcp:access`, granting read and write access to its
organization's stacks. Each request checks revocation, the completed
service-auth claim and the delegated user's active membership before opening
a database session. See [agent authentication](https://docs.littlebigbrain.com/start/authentication/#authmd-agent-registration).

> [!CAUTION] Codex Desktop
Codex sends the MCP URL as an OAuth `resource` parameter. It also appends any
configured `oauth_resource`. The authorization server then receives two
`resource` values and rejects the request.

For Codex, use the origin as the URL. Pass the stack in the `X-LBB-Stack`
header, and leave `oauth_resource` unset:

```json
{
  "mcpServers": {
    "lbb": {
      "type": "http",
      "url": "https://mcp.littlebigbrain.com",
      "headers": { "X-LBB-Stack": "<your-stack-slug>" }
    }
  }
}
```

You can also serve the tools over streamable HTTP with a stack API key. Run the
HTTP server from your own Node process, as in
[Embed in a Node process](#embed-in-a-node-process).

## Your first search and query through MCP

Run the [quickstart](https://docs.littlebigbrain.com/start/quickstart/) first. It creates the `support` graph
with tickets, bugs, releases and a ticket embedding. Then connect one of the
transports above to the same stack. Ask your agent: **which release fixed the
login bug?**

The agent reads the schema first, with `lbb_inspect`:

```json
{ "action": "ontology", "graph": "support" }
```

It lists the types `Ticket`, `Bug` and `Release` and the links `REPORTED_AS`
and `FIXED_IN`. Next, the agent searches by meaning with `lbb_query`:

```json
{
  "mode": "search",
  "graph": "support",
  "text": "users cannot sign in after the upgrade",
  "top_k": 3
}
```

The top hit is the ticket `login fails after 3.1`. The agent takes its `iri`
and the response's `served_at_seq` into the next call. It follows the links
with `lbb_query` at that commit:

```json
{
  "mode": "sparql",
  "graph": "support",
  "as_of_commit_seq": 1,
  "query": "PREFIX dr: <https://littlebigbrain.com/r/> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?bug ?release WHERE { <iri-of-the-top-hit> dr:reported_as ?b . ?b rdfs:label ?bug ; dr:fixed_in ?r . ?r rdfs:label ?release . }"
}
```

The result has one row:

| bug | release |
| --- | --- |
| session token expiry | Release 3.2 |

The answer comes from two stored links. The agent does not infer the
connection from similar text. It can cite the rows and commit `1`.
[Replay an answer](https://docs.littlebigbrain.com/guides/time-travel-audit/) explains how long a commit stays
readable.

## Build a graph through MCP

An agent can also build a graph with tools alone. The calls below write the
quickstart records into a new graph, `support-agent`. The search and query
above then work on it with `"graph": "support-agent"`.

Use a new graph for this. `lbb_commit` writes records by type and name, and
the quickstart writes them with a `key`. On one graph, that gives two records
for each ticket.

**1. Preview the schema.** Call `lbb_configure`. With `dry_run: true`, the
call shows the result and writes nothing:

```json
{
  "action": "define_ontology",
  "graph": "support-agent",
  "dry_run": true,
  "entity_types": ["Ticket", "Bug", "Release"],
  "relations": [
    { "name": "REPORTED_AS", "source": "Ticket", "target": "Bug" },
    { "name": "FIXED_IN", "source": "Bug", "target": "Release" }
  ]
}
```

Send the same call without `dry_run` to create the graph. A repeat of the same
call returns `changed: false`.

**2. Add a field.** Call `lbb_configure` again:

```json
{
  "action": "evolve_ontology",
  "graph": "support-agent",
  "ops": [{ "op": "add_property", "name": "description", "value_type": "text" }]
}
```

**3. Write the records.** Call `lbb_commit` with links (`triplets`) and field
values (`entity_properties`):

```json
{
  "graph": "support-agent",
  "triplets": [
    { "source": { "type": "Ticket", "name": "login fails after 3.1" }, "relation": "REPORTED_AS", "target": { "type": "Bug", "name": "session token expiry" }, "evidence": "Support triage, 2 Sep" },
    { "source": { "type": "Ticket", "name": "invoice PDF is blank" }, "relation": "REPORTED_AS", "target": { "type": "Bug", "name": "PDF fonts missing" }, "evidence": "Support triage, 3 Sep" },
    { "source": { "type": "Ticket", "name": "CSV export times out" }, "relation": "REPORTED_AS", "target": { "type": "Bug", "name": "export runs on one thread" }, "evidence": "Support triage, 4 Sep" },
    { "source": { "type": "Bug", "name": "session token expiry" }, "relation": "FIXED_IN", "target": { "type": "Release", "name": "Release 3.2" }, "evidence": "Release notes 3.2" },
    { "source": { "type": "Bug", "name": "PDF fonts missing" }, "relation": "FIXED_IN", "target": { "type": "Release", "name": "Release 3.1.1" }, "evidence": "Release notes 3.1.1" }
  ],
  "entity_properties": [
    { "type": "Ticket", "name": "login fails after 3.1", "properties": { "description": "Since 3.1, people are sent back to the login screen right after they type their password." } },
    { "type": "Ticket", "name": "invoice PDF is blank", "properties": { "description": "Downloaded invoices open as empty pages." } },
    { "type": "Ticket", "name": "CSV export times out", "properties": { "description": "Exports with more than 10,000 rows stop after 30 seconds." } }
  ]
}
```

The response reports `commit_seq` and echoes `written_properties`, the fields
that were stored.

**4. Wait for publication.** Call `lbb_inspect` until `state` is `current`:

```json
{ "action": "publication", "graph": "support-agent" }
```

**5. Turn on search.** Call `lbb_embeddings_manage` to declare an embedding on
the `Ticket` type:

```json
{
  "action": "declare",
  "graph": "support-agent",
  "class": "https://littlebigbrain.com/class/ticket",
  "from": ["label", "description"]
}
```

A declaration spends model tokens. Preview the text first with `lbb_embeddings`
and `action: "preview"`. Then call `lbb_embeddings` with `action: "get"` and
`name: "ticket"` until the embedding reports `serving`.
[Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/) covers each option.

### Schema calls

`lbb_configure` has these schema actions. Each one that writes takes
`dry_run: true` to preview without writing.

| Action | Arguments | Behavior |
| --- | --- | --- |
| `define_ontology` | `graph` (required), `entity_types` and `relations`, or `source` with `format`; `merge_default` | Creates the graph with its types and links. On an existing graph, it adds what is new. |
| `evolve_ontology` | `ops` (required, in order), `graph` | Adds, renames, widens, narrows or removes types, links and fields. |
| `list_starters` | `graph` | Lists the [ontology starters](https://docs.littlebigbrain.com/guides/ontology-starters/) (`crm`, `documents`, `work`) with each one's status on the graph. |
| `apply_starter` | `starter` (required), `expected_ontology_version`, `graph` | Adds what the graph lacks of a starter in one ontology version. Refused with `starter_conflict` when the graph holds a term differently. |
| `suggest_ontology_change` | `title` (required), `change`, `facts`, `rationale`, `anchor`, `key`, `evidence`, `agent`, `graph` | Files the change as a [suggestion](https://docs.littlebigbrain.com/guides/ontology-suggestions/) for a person to review. `change` holds up to 128 `evolve_ontology` operations. `facts` holds graph facts (`triplets`, `entity_properties`, `replace_owned`, `summary`) that accepting commits; `change` may be empty when `facts` is set. Nothing changes until a person accepts it. |
| `publish_schema` | `shapes` (required), `desired_mode`, `confirm_restrictive`, `ontology`, `graph` | Activates SHACL rules. In `reject` mode it asks the person to approve first. See [Validate writes](#validate-writes-with-shacl). |
| `get_rewrite_profile` | `graph` | Reads the graph's [profile](https://docs.littlebigbrain.com/guides/answer-questions/#notes-for-every-question): the notes and worked examples the model reads for every question in plain words, and its `version`. |
| `set_rewrite_profile` | `notes`, `examples` (up to 20 `{question, sparql, note?}`), `expected_version`, `graph` | Stores the profile. Pass the `version` you read as `expected_version`; another write first answers `409 conflict`. |

For `define_ontology`, give both `entity_types` and `relations`. Each type is a
name or `{ "name": … }`. A type object accepts `name`, `description`,
`resolvable` and `super_types`. Another field fails with HTTP 400, which lists
the accepted fields.

A link object has `name`, `source` and `target`, as in the example. It also
accepts `description`, `inverse`, `symmetric`, `transitive`, `reducer`,
`cardinality` and `temporal_semantics`.

`merge_default: true` adds the built-in AI-context vocabulary under your types.
The tool's default is `false`.

On an existing graph, `define_ontology` refuses a change that narrows a link's
source or target types. The error is `ontology_restrictive_change`. Use
`evolve_ontology` with `narrow_relation` for that change.

`super_types` and `add_super_types` store each parent as an `rdfs:subClassOf`
statement. A query with `entailment: "subclass"` then matches the records of
each subtype. See [Reasoning](https://docs.littlebigbrain.com/guides/reasoning/#add-a-type-hierarchy-to-json-records).

`define_ontology` also takes a `source` document instead of names. Its `format`
is `auto` (the default), `turtle`, `json_ld`, `rdf_xml`, `csv`, `tsv`,
`lbb_json` or `spec`. `lbb_json` expects a serialized ontology. `spec` is the
JSON form above.

From an RDF or OWL document, `define_ontology` keeps only types and links. To
store the whole document as queryable facts, use `lbb_rdf`.

`evolve_ontology` takes these `op` values:

| `op` | Arguments |
| --- | --- |
| `add_entity_type` | `name` |
| `add_super_types` | `entity_type`, `super_types` |
| `add_relation` | `name`, `domain`, `range`, `cardinality`, `temporal_semantics`, `reducer`, `inverse_name`, `transitive`, `symmetric` |
| `add_property` | `name`, `value_type` (`bool`, `i64`, `f64`, `date_time`, `keyword`, `text` or `bytes`; default `text`), `required`, `unit` (of an `i64` or `f64` field, such as `mm`, `kg` or `h`), `allowed_values` |
| `set_property_unit` | `property`, `unit` (leave it out to clear the unit) |
| `set_property_constraint` | `property`, `allowed_values` (an empty list clears the constraint) |
| `widen_relation` | `relation`, `add_domain`, `add_range` |
| `narrow_relation` | `relation`, `remove_domain`, `remove_range` |
| `rename_entity_type`, `rename_relation` | `from`, `to` |
| `set_relation_inverse` | `relation`, `inverse_name` |
| `set_relation_cardinality` | `relation`, `cardinality` |
| `remove_entity_type`, `remove_relation` | `name` |

`allowed_values` is enforced: a commit with another value for the field
fails. It works on `keyword`, `text`, `i64` and `bool` fields. Values stored
before a constraint or a unit change stay as they are: a unit change converts
nothing, and a constraint checks only later commits.

A removed type or link stays readable in old records. New commits that use it
fail. Preview a narrowing or removing change with `dry_run: true`. The preview
reports conflicts with current data. Repair them, then apply.
`allow_data_conflicts` does not skip that check. A call that removes asks the
person to approve first. See
[Approve destructive writes](#approve-destructive-writes).

### Write calls

`lbb_commit` has three modes. It picks `retract` when you pass
`retract_edges` or `retract_entities`, and `search_feedback` when you pass
`search_feedback`. Otherwise it writes facts.

| Argument | Meaning |
| --- | --- |
| `triplets` | Links. Each has `source` and `target` as `{ "type", "name" }`, a `relation`, and optional `evidence`, `confidence` (0 to 1) and `valid_time` (`start`, `end`, `granularity`, `source_text`). |
| `entity_properties` | Field values. Each item is `{ "type", "name", "properties" }`. |
| `entity_embeddings` | Vectors you computed yourself. Each item has `type`, `name`, optional `key` or `entity_id`, `model_id`, `vector`, and optional `metric`. |
| `dry_run` | Checks a facts commit, returns its SHACL report, and writes nothing. |
| `observed_at` | An RFC 3339 time. The commit is recorded as of that time, for backfills. |
| `edge_idempotency` | `append` (the MCP default) or `skip_unchanged`. |
| `idempotency_key` | Optional. See [Idempotent writes](#idempotent-writes). |
| `retract_edges` | Links to remove, each `{ source, relation, target }`. |
| `retract_entities` | Records whose current links are all removed. Their history stays readable. |
| `search_feedback` | Relevance labels for search results. |

Values are converted to each field's declared type. The string `"52"` lands in
an `i64` field, and `"2026-06-26"` in a `date_time` field. An undeclared field
fails the commit with HTTP 400. Declare the field with `add_property` first.

The `source` and `target` of a triplet carry only `type` and `name`. The tool
drops a `key` there. `entity_properties` items pass a `key` through.

`retract_edges` and `retract_entities` also match records by type and name. A
record written with a `key` does not match, and the call returns `no_op: true`.
A retract asks the person to approve first. See
[Approve destructive writes](#approve-destructive-writes).

A graph whose first write is RDF refuses `lbb_commit` with HTTP 400
`rdf_native_graph`. See [Add RDF and OWL](#add-rdf-and-owl).

For `search_feedback`, send the `query`, an optional `search_id`, and
`labels`. Each label has a `target`, a `grade`, and optional `rank`, `score`,
`reason` and `split`. Grades are `3` (good), `1` (partly relevant) and `0`
(bad).

A target is `{ "kind": "entity", "entity": { "entity_type", "name" } }`. The kinds
`assertion`, `observation` and `concept` name other targets.

## Query options

`lbb_query` has nine modes. Each read answers from one commit.

### SPARQL text

`mode: "sparql"` runs SPARQL 1.1 `SELECT` and `ASK` queries. `CONSTRUCT` and
`DESCRIBE` return HTTP 400. Use the [`/sparql` endpoint](https://docs.littlebigbrain.com/sdks/http-api/) for
them.

| Argument | Meaning |
| --- | --- |
| `query` | The query text. Required unless `cursor` is set. |
| `entailment` | `none` (default), `subclass`, `rdfs` or `owl`. See [Reasoning](https://docs.littlebigbrain.com/guides/reasoning/). |
| `consistency` | `eventual` reads the latest published commit. `strong` also applies the newest writes. |
| `min_indexed_seq` | Read at this commit or a later one. |
| `as_of_commit_seq` | Read the graph as it was at this commit. |
| `row_limit` | Rows per page. See [Pages and large results](#pages-and-large-results). |
| `cursor` | The `next.cursor` of the previous page. |
| `request` | The user's words behind the query. The server records an eval trace, and the result names its `trace_id`. |
| `detail`, `graph` | As for every tool. |

Without `as_of_commit_seq`, the tool reads the graph's latest commit number
first. Every page of that query then reads that commit.

A valid-time `as_of` is refused before any HTTP call, also inside an older
cursor. Use `as_of_commit_seq` for an earlier commit.

A SPARQL query can also search by meaning. The pattern
`?x <https://littlebigbrain.com/search#similarTo> "words"` binds the records
nearest to the words, and the other patterns of the query filter and join
them. The result then carries `search`: the plan (`nearest`, `filter_first`
or `search_first`), the hits asked for and bound, and `complete`. When
`complete` is false, `notes` says so. With `search:rerank true` and
`search:relevance ?r`, the rerank model keeps the hits that answer the words
and binds its answer; `search.rerank` reports how it went.
[Search inside a SPARQL query](https://docs.littlebigbrain.com/guides/search-by-meaning/#search-inside-a-sparql-query)
lists the patterns.

> [!TIP] Lowercase IRIs
SPARQL text (`mode: "sparql"`) addresses little big brain relations, types, and
properties as `<https://littlebigbrain.com/{r,class,p}/name>`, always
**lowercase**. The tool lowercases those local names for you and reports each
rewrite under `notes`. An uppercase name then still matches. IRIs from imported
RDF keep their original form.

### Structured JSON

`mode: "structured"` takes a JSON `body` instead of query text. A `predicate`
is a link name, in any case. This body counts the tickets fixed in each release:

```json
{
  "mode": "structured",
  "graph": "support",
  "body": {
    "patterns": [
      { "subject": { "var": "t" }, "predicate": "REPORTED_AS", "object": { "var": "b" } },
      { "subject": { "var": "b" }, "predicate": "FIXED_IN", "object": { "var": "r" } }
    ],
    "group_by": ["r"],
    "aggregates": [{ "func": "count", "as": "tickets" }]
  }
}
```

Each release comes back in `groups`, with its record under `keys.r` and the
count under `aggregates.tickets`.

| Body field | Shape |
| --- | --- |
| `patterns` | `[{ subject, predicate, object }]`. A term is `{ "var": "x" }` or a fixed `{ "entity": { "entity_type", "name" } }`. |
| `filters` | `{ "compare": { "op", "left", "right" } }`, or `and`, `or`, `not` of filters. `op` is `eq`, `ne`, `lt`, `le`, `gt` or `ge`. |
| Filter operand | `{ "var" }`, `{ "property": { "var", "field" } }`, or `{ "value": … }` with one of `str`, `i64`, `f64`, `bool`, `date_time`, `entity`. |
| `group_by` | Variables to group by record. |
| `group_keys` | `{ "property": { var, field, as } }`, or `{ "date_bucket": { var, field, granularity, as } }` with `year`, `month`, `week`, `day` or `hour`. Values come back under `value_keys`. |
| `aggregates` | `{ func, as }` with `count`, `sum`, `avg`, `min` or `max`, and optional `operand` and `distinct`. |
| `having` | Filters over the groups. |
| `order_by` | `[{ var, descending }]` |
| `select`, `distinct`, `as_of_commit_seq` | Projection, duplicate removal, and a commit pin. |

To count groups without reading them, add an equality `having` and set
`row_limit: 1`. Then read `row_page.total`. A body with `combinators` fails:
write `OPTIONAL`, `UNION`, `MINUS` or `EXISTS` as SPARQL text.

### Search by meaning

`mode: "search"` takes `text`. It searches every searchable type, or one
`embedding`. Other arguments are `top_k` (at most 200), `probe`,
`include: ["text"]`, `filter`, `explain` and `request`.

A `filter` item names a class or a link. A class item is `{ "class": … }`. A
link item is `{ "via": …, "to": … }`. Its `direction` is `out` (the default) or
`in`.

With `explain: true`, the call returns the plan without calling the model.
[Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/) explains filters and planning.

### Questions in plain words

`mode: "question"` takes a `question` in plain words and calls
`POST /v1/query/ask`. The server describes the graph and picks the kind of
question. Its model runs queries in a short loop, reads the rows, and
answers. The result has the answer with the records it cites, the steps, the
route, the query whose rows hold the answer, the rows and the eval
`trace_id`. It takes about 6 to 12 seconds.

| Argument | Meaning |
| --- | --- |
| `question` | The question, 1 to 4,000 characters. Required. |
| `context` | Notes on what the data means: units, names to prefer. Send the same text with each question. |
| `route` | Fixes the kind of question: `lookup`, `aggregate`, `search`, `history`, `schema` or `unanswerable`. |
| `limit` | The rows each query returns, 1 to 1,000. |
| `anchor` | The IRIs of records the question is about, at most 10. The queries use them directly. |
| `timeline` | Dates that stand for the graph's commits, at most 200: `[{date, as_of_commit_seq, label?}]`. A question about a date reads the commit of the latest point on or before it. |
| `detail`, `graph` | As for every tool. |

The result lists in `linked` the names of the question that the server found
in the graph, and in `anchors` what it read about each IRI of `anchor`. A
history question returns `history`: the commit the server read for its date,
and for an answer from a comparison the rows `added`, `removed` and
`changed` with `totals`. Each list shows at most 20, 100 or 500 entries by
`detail`; `history.shown` and a note say when a list is cut. The rows are
those of the query the answer stands on. The `answer` and `run` arguments of
earlier versions are gone: the mode always answers.

When the client asks for progress (a `progressToken` in the call's
`_meta`), the tool sends each step as a progress notification: the route,
each tool call of the loop, and the answer. The result stays the same.
Cancelling the call stops the server's work.

Use this mode when the agent has not read the schema. When it has, a query
it writes itself in `mode: "sparql"` needs no model call. An `unanswerable`
route means the graph does not hold the data.
[Answer questions in plain words](https://docs.littlebigbrain.com/guides/answer-questions/) covers the
routes and the limits. It also lists the data that goes to the model
providers.

### Tools for writing queries

Four modes give an agent that writes its own SPARQL what only the server
knows. None calls a model.

| Mode | Arguments | Use it when |
| --- | --- | --- |
| `names` | `text`, `limit` (1 to 10) | The question names a record. The result groups the candidates by name, the one to prefer first; put its IRI in the query. |
| `describe` | `question`, or `classes` and `properties` as IRIs | The agent needs classes or properties it has not seen. The result is the server's text: how many sampled instances hold each property, and the values of small classes. `detail: "full"` adds the JSON. |
| `commit_at` | `date` (`YYYY-MM-DD`) or `moment` (RFC 3339) | The question names a date. Run the query with `mode: "sparql"` and the commit it returns as `as_of_commit_seq`. |
| `compare` | `query`, `before`, `after`, `key`, `max_rows`, `limit` | The question asks what changed. `before` and `after` are each `{as_of_commit_seq}`, `{date}` or `{moment}`. The result has `added`, `removed` and `changed` as lexical rows, with `totals`; pass `next` back for the next page. |

### Summary charts

`mode: "analyze"` returns counts with a Vega-Lite chart. Set `metric` to
`entity_types`, `relations`, `overview`, or `sparql` with a structured body in
`sparql`. `chart` is `bar` (default) or `pie`, and `top_k` keeps the largest
values.

## Pages and large results

A query page holds at most `row_limit` rows. The default depends on `detail`:
20 for `compact`, 100 for `standard`, 1,000 for `full`. The maximum is 5,000.

Each page reports
`row_page: {returned, total, offset, limit, has_more, next_offset?}`. Its
`summary` says `returned X of Y rows`. When more rows exist, `next` holds the
arguments for the next page:

```json
{ "mode": "sparql", "cursor": "eyJhc19vZl9jb21taXRfc2Vx…", "row_limit": 2, "detail": "compact" }
```

Pass `next` back to `lbb_query` until it is absent. You do not write `LIMIT`
and `OFFSET`. The cursor holds the query, the graph and the commit of page 1.
New writes between pages do not change the rows of later pages.

A cursor must match the arguments sent with it. A different `query`, `graph`,
`row_limit`, `entailment`, `consistency` or `min_indexed_seq` fails.

A page keeps whole values within 80,000 bytes of UTF-8. So a page can be
shorter than `row_limit`. Follow `next` even then. A cursor moves on only by
the rows you received.

If one row alone is too large, project fewer fields. Or read that row through
the [SPARQL HTTP endpoint](https://docs.littlebigbrain.com/sdks/http-api/).

Other read tools shorten large values by `detail`. `compact` keeps 5 items per
list and 300 characters per string. `standard` keeps 20 and 1,000. `full` keeps
100 and 5,000. A shortened result sets `truncated` and names the next `detail`
level in `next`.

A list page (`data` with `has_more` and `next_cursor`) asks the server for at
most the items its `detail` level keeps, so `detail` never shortens the page.
A page over 80,000 characters is asked for again with fewer items. Its
`next_cursor` then continues after the last item shown. `next_cursor` is
never shortened.

## Read the schema and records

`lbb_inspect` reads graph context and exact records:

| Action | Returns |
| --- | --- |
| `guide` | Counts, types, links and suggested next calls. For a missing graph, the list of graphs and how to create one. |
| `graphs` | The graphs of the stack. Works before any graph exists. |
| `publication` | Whether writes are published: `state`, `head_seq`, `published_seq`, `lag_commits`. |
| `ontology` | Types, links and fields, with counts, in complete pages. |
| `schema` | The active schema and SHACL metadata, in complete pages. It runs no validation. |
| `ontology_conformance` | The published SHACL report: `conforms`, `result_count`, `validated_at_seq`. |
| `ontology_search` | Types, terms and links that match `query`, up to `top_k`. |
| `metadata` | Graph status, including the latest commit. |
| `entity` | One record by `entity_id`, or by `entity_type` and `name`: its fields and its links. Accepts `as_of_commit_seq`. A valid-time `as_of` is refused before any HTTP call. |
| `ontology_suggestions` | The [ontology change suggestions](https://docs.littlebigbrain.com/guides/ontology-suggestions/) and their state, with `identity_count` and `fact_count` (the links and field rows accepting commits). `status` keeps one state: `open`, `accepted`, `dismissed` or `superseded`. `limit` is 50 by default. |

`ontology` and `schema` return complete entries. `page_size` sets entries per
page: 50 by default, at most 500. `section` limits the pages to one list, such
as `entity_type_defs`, `relation_defs`, `property_defs`, `classes` or
`relations`.

Pass the returned `next` arguments until they are absent. Nested values are
never shortened, also with `detail: "compact"`.

A single entry can exceed the output budget. The result then carries
`entry_fragment`. Join its `serialized_json` parts in `char_offset` order, then
parse the JSON.

A schema change between pages fails the cursor, so pages never mix versions.
Start again without `cursor` after a change.

## Add RDF and OWL

`lbb_rdf` stores RDF statements as graph facts. It has two actions:

| Action | Arguments | Behavior |
| --- | --- | --- |
| `import` | `source` (required), `format` (`turtle` default, `ntriples`, `nquads`, `trig`), `base_iri`, `blank_node_scope`, `idempotency_key`, `graph` | Stores a complete document: lists, labels, comments and OWL statements. |
| `update` | `update` (required), `idempotency_key`, `graph` | Runs SPARQL Update `INSERT DATA`. |

`import` accepts the default RDF graph only. An N-Quads or TriG document must
contain only default-graph statements.

`update` accepts only `INSERT DATA`. `DELETE`, `WHERE`, `CLEAR` and the other
forms return HTTP 400 and change nothing. Named graphs are refused too.

A graph whose **first** write is RDF is RDF-native. It refuses later
`lbb_commit` writes with `rdf_native_graph`. Decide the write path before the
first write. A graph first written with `lbb_commit` accepts `lbb_rdf` as well.

This call adds a type hierarchy to the `support` graph. It states that every
bug is an issue:

```json
{
  "action": "update",
  "graph": "support",
  "update": "INSERT DATA { <https://littlebigbrain.com/class/bug> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <https://example.org/support/Issue> }"
}
```

The response has `accepted: true`, which means the statement is stored.
Queries use it once its commit is published.

Wait for publication with `lbb_inspect` `action: "publication"`. Then check the
answer with `entailment`:

```json
{
  "mode": "sparql",
  "graph": "support",
  "entailment": "subclass",
  "query": "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?issue WHERE { ?b a <https://example.org/support/Issue> ; rdfs:label ?issue } ORDER BY ?issue"
}
```

The query returns the three bugs. Without `entailment`, it returns no rows.
[Reasoning & inference](https://docs.littlebigbrain.com/guides/reasoning/#supported-owl-statements) lists the
OWL statements that `entailment: "owl"` applies.

To read the stored statements, query with `entailment: "none"` and follow each
`next` cursor:

```sparql
SELECT ?s ?p ?o WHERE {
  ?s ?p ?o
} ORDER BY ?s ?p ?o
```

RDF statements cannot be removed or replaced. An import of a changed document
adds its new statements and keeps the old ones.

To revise a document, import it into a new graph and check it. Then move your
clients to that graph. Keep the original graph until you choose to delete it.

A retry with the same content writes nothing new. To write the same content
again on purpose, pass a new `idempotency_key`.

## Validate writes with SHACL

SHACL is the W3C standard for validation rules. `lbb_configure` with
`action: "publish_schema"` activates a set of rules:

```json
{
  "action": "publish_schema",
  "graph": "support",
  "dry_run": true,
  "desired_mode": "warn",
  "shapes": {
    "format": "turtle",
    "source": "@prefix sh: <http://www.w3.org/ns/shacl#> . @prefix dclass: <https://littlebigbrain.com/class/> . @prefix dr: <https://littlebigbrain.com/r/> . <https://example.org/shapes/Ticket> a sh:NodeShape ; sh:targetClass dclass:ticket ; sh:property [ sh:path dr:reported_as ; sh:maxCount 1 ; sh:message \"A ticket is reported as at most one bug.\" ] ."
  }
}
```

- `shapes.format` is `auto`, `turtle`, `n_triples`, `n_quads` or `trig`.
- `desired_mode` is `off`, `warn` or `reject`.
- `confirm_restrictive` is accepted. The server does not read it.
- `ontology` must match the current schema. Change the schema with
  `define_ontology` or `evolve_ontology`.

The preview parses the rules and checks them against the schema. It writes
nothing and starts no validation job. It does not check your data.

Tighten rules in this order:

1. Publish in `warn` mode.
2. Read the report with `lbb_inspect` `action: "ontology_conformance"`.
3. Repair the records it lists.
4. Publish the same rules in `reject` mode.

In `reject` mode, a write that breaks a rule fails, and the error lists the
violations. A publish in `reject` mode asks the person to approve first, with
the target classes of the rules. See
[Approve destructive writes](#approve-destructive-writes). `lbb_commit` with `dry_run: true` returns the same report without
writing. See [Query and validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).

## Errors

A failed call sets `isError`. Its `structuredContent.error` keeps every field
of the server error:

```json
{
  "error": {
    "type": "not_found_error",
    "code": "graph_not_found",
    "message": "graph 'nope' does not exist for this tenant",
    "param": "graph",
    "request_id": "8661a030-…",
    "doc_url": "https://docs.littlebigbrain.com/sdks/errors/#graph_not_found",
    "status": 404
  }
}
```

An error the tool finds before any HTTP call has `type` and `code`
`tool_error`. Examples are an unknown argument and a cursor that does not match
its query.

A graph error can name only a storage key. The tool then rewrites the message.
The new message lists the stack's graphs and the `graph` value to pass.

## Idempotent writes

Without an `idempotency_key`, `lbb_commit` and `lbb_rdf` derive one from the
graph and the content. A retry with the same content writes nothing new.
`lbb_commit` then returns the first result with `idempotent_replay: true`.

To repeat an identical write on purpose, pass a new `idempotency_key`.

`lbb_commit` sets `edge_idempotency` to `append` by default. New evidence on an
existing link is then recorded. Pass `skip_unchanged` for backfills that you
run again. It skips exact copies of current links, and repeats that differ only
in evidence.

## Approve destructive writes

A call that deletes or removes data asks the person first. The server sends
the question through the client with MCP form elicitation. The question
names what the call removes first, with the counts, for example
`Delete the embedding "ticket" of graph "support" and its vectors?`. It has
one confirm box.

- When the person approves, the call runs.
- When the person declines or cancels, nothing is sent. The tool returns an
  error with the code `approval_declined`.
- When the approval request fails, nothing is sent either. The code is
  `approval_failed`.

These calls ask:

| Call | What the question names |
| --- | --- |
| `lbb_embeddings_delete` | the embedding and its graph |
| `lbb_fit_sources_delete` | the fit source and its graph |
| `lbb_workflows_delete` | the workflow, its starter workflow and its graph |
| `lbb_evals_delete` | the dataset, its id, its item count and its graph |
| `lbb_commit` with `mode: "retract"` | the counts, then each link and record it removes (the first 20 of each, then a count) |
| `lbb_rdf` with an `update` that holds `DELETE`, `CLEAR`, `DROP`, `MOVE` or `COPY` | each operation that removes, quoted on lines of its own, then the forms of the other operations |
| `lbb_configure` with `action: "evolve_ontology"` and `remove_entity_type`, `remove_relation`, or `narrow_relation` with `remove_domain` or `remove_range` | the counts, then each type and link it removes and each type it takes out of a link's source or target types (the first 20, then a count) |
| `lbb_configure` with `action: "publish_schema"` and `desired_mode: "reject"` | the target classes of the rules and the size of the source |
| `lbb_reviews_manage` `answer` or `answer_batch` that accepts or corrects a `value` item, or accepts a `change` item | the count and the effect: how many items commit a value and how many apply an ontology change, then each item with its value, or with what its change removes |

The question escapes newlines and other control characters in names and
values, so a name cannot add lines of its own.

`lbb_model_choice_manage` `put_choice` and `delete_choice` ask nothing. They
change only the model of a workflow step's future calls, remove no data,
and a choice is set again in one step.

Some calls read first, so the question can name what they change.
`lbb_evals_delete` reads the dataset. When it does not exist, that read fails
with 404, and nobody is asked. A review answer reads its items, and the
suggestion of each `change` item. An answer that commits nothing asks
nothing: a `reject`, a `cant_tell` or a `choose`, and an answer to an item
that is answered, withdrawn or in `test` scope. The answer to an item that
Evals filed (a check, a calibration question, a golden) is a grade or a
choice, so it asks nothing either. A preview (`dry_run: true`)
of an `evolve_ontology` or a `publish_schema` writes nothing, so it does not
ask.

Some clients cannot show the question: a client without elicitation, or
with URL elicitation only. The HTTP transport of this server, which the
hosted endpoint uses, handles each call as a request of its own, so it
cannot ask either. Then the call fails with `approval_required`, and nothing
is sent. No tool argument changes that, since the agent sets the arguments.
The person who runs the MCP server can set `LBB_MCP_DESTRUCTIVE=allow` in
its environment. The server then runs these calls without a question when
the client cannot show one.

The hosted endpoint does not set it. There, every call that deletes or
removes data answers `approval_required`. Make that change in the console
or with the TypeScript or Python SDK instead.

The server writes one JSON line per tool call to standard error:

```json
{"event":"mcp_tool_call","tool":"lbb_embeddings_delete","outcome":"error","code":"approval_declined","approval":"declined","request_id":7}
```

`action` names the call's action, `approval` what the approval step decided
(`approved`, `declined`, `cancelled`, `failed`, `unavailable` or
`allowed_by_setting`), and `request_id` the JSON-RPC request. A line holds no
arguments, names or texts.

## Other tools

| Tool | Actions |
| --- | --- |
| `lbb_embeddings` | `list`: each embedding's status, progress and lag, and the graph's model. `get`: one embedding by `name`. `preview`: the text of sample records and the facts of a class, with no model call. |
| `lbb_embeddings_manage` | `declare`: a new embedding or a changed recipe (`class`, `from`, `exclude`, `title`, `name`). `refresh`: one embed step now, by `name`. `model`: move every embedding of the graph to `model` (and `dim`). |
| `lbb_embeddings_delete` | Deletes the embedding `name` and its vectors, once the person approves. |
| `lbb_fit_sources` | `list`: each fit source's status, the suggestions it filed and this month's spend. `get`: one by `name`. `preview`: the text the fit reads for a few records of a class, with no model call. |
| `lbb_fit_sources_manage` | `declare`: a fit source on a class (`class`, `from`, `exclude`, `context`, `name`); the server reads every record and files ontology suggestions. `dry_run`: the proposals for up to 3 records, nothing filed (spends model tokens). `refresh`: ask the job to run now, by `name` (returns at once). |
| `lbb_fit_sources_delete` | Deletes the fit source `name`, once the person approves; its suggestions stay. |
| `lbb_workflows` | `starters`: the starter workflows this server offers, with their group, their settings and whether each can run here. `list`: the graph's triggered workflows with their state, lag, counters, spend and last error. `get`: one, by `starter` and `name`, with its own status in `details`. `preview`: what creating a workflow would do for `starter`, `class`, `fields`, `exclude` and `params` (and `workflow`, `owns` and `batch` for a developer workflow, whose preview shows its first trigger messages): the resolved watch, the instance count and the starter workflow's own preview. It writes nothing. `workflows`: the workflows with triggers, or one by `name`, with each one's target type, shards, routing field, window, trigger and shard inboxes. `queue`: what a workflow's watch has not sent yet, by `name`, each record with its shard and origin, one page in `data` (`cursor` takes the answer's `next_cursor`; `limit`, `trigger`). `run_results`: what a workflow's runs did with each input, by `name`, newest run first: the input, its outcome and its values with their quotes and review items, one page in `data` (`run` for one run with `run_counts`, `outcome`, `iri`, `cursor`, `limit`). `revisions`: the history of a workflow's settings by `name`, newest change first (`cursor`, `limit` 1 to 100): each item a revision (its `settings`, its id `wrev_…`, the hash of those settings) with who changed it, when, how, and `bound` (the one in use). `revision`: one revision by `name` and `revision`; compare two by their `settings`. `tests`: one page of the graph's [workflow tests](https://docs.littlebigbrain.com/guides/test-a-workflow/), newest first (`name`, `limit`, `cursor`). `tests_get`: one test by `id`, with each input's text and, per value, its quote, its support and what the live workflow does with it. |
| `lbb_workflows_manage` | `use`: create a workflow from a starter workflow on a class, or change one (`starter`, `class`, `fields`, `exclude`, `params`, `name`, `gate`, `budget_usd_per_month`); the workflow reads every instance, then follows each published commit. A developer workflow (`starter: "workflow"`) also takes `workflow` (`workflow_type`, `version`), `owns` (`classes`, `relations`, `properties`) and `batch` (1 to 64). `refresh`: ask the workflow to run now, by `starter` and `name` (returns at once). `pause` and `resume`: stop a workflow from reading and sending, or let it catch up. Every workflow pauses but `search.embed` and `ontology.fit`, which answer `workflow_pause_unsupported`. Without `starter` they name a workflow with triggers. `define`: create or change a workflow with triggers of your worker (`name`, `workflow`, one trigger, `owns`, `batch`, `shards`, `route_by`, `window`, `trigger`). The trigger is `class` and `fields` (a watch), `media_types` (a content trigger on uploads), `schedule` (`{cron, timezone}` or `{every, offset}`, and `jitter`) or `stream` (`{max_wait}`: the events a client sends, in batches). `run`: run one now, by `name`. `activate`: make a kept revision the one in use, by `name` and `revision`, with `expected_revision` (the revision you read as bound) so a change by someone else since fails with `workflow_revision_conflict`. An activation changes what the workflow does from then on and deletes no data, so it is not a destructive write and asks for no confirmation. Show the user what changes first. `tests_create`: test a `values.extract` workflow on chosen inputs before it writes. Name a workflow of the graph (`name`), or pass `install`, the `use` request of a workflow you have not created. Add `iris` (at most 20) or `sample` (1 to 20, default 6). `tests_answer`: the person's answer to one value (`id`, `iri`, `property`, `answer`); `answer: null` removes it. `tests_cancel`: stop a test by `id`. A test writes nothing to the graph. |
| `lbb_workflows_delete` | Deletes the triggered workflow of `starter` named `name`, or without `starter` the workflow with triggers named `name`, once the person approves; what it wrote stays. |
| `lbb_reviews` | `list`: one page of [review items](#review-items), the latest update first, as the server sends it (`object`, `data`, `has_more`, `next_cursor`). Filters: `status`, `kind` (one or a list), `scope`, `origin_id`, `workflow`, `run`, `key`, `eval_id` (the items Evals filed for one record: a calibration session, a model call's check, a golden); page with `limit` and `cursor`. `get`: one item by `id`. `patterns`: the live items grouped by the decision they ask (`status`, `origin_id`, `type`, `eval_id`). |
| `lbb_reviews_manage` | `create`: file an item of kind `value`, `choice`, `grade` or `answer` (`kind`, `title`, `question`, and `subject`, `evidence`, `rationale`, `key`, `scope`, `facts`, `origin`). `answer`: answer one item (`id`, `answer`, `revision`). `answer_batch`: one answer for up to 500 `ids`. `comment` (`id`, `text`, `author`). `withdraw` (`id`, `reason`). |
| `lbb_files` | `list`: 50 files a call, newest upload first, with their SHA-256, name, size, `Document` IRI and parse status; when more follow, `next` names the `after` of the next call. `parse_status` keeps the files of one status: `parsed`, `waiting`, `unsupported` or `failed`. `name` keeps the files whose name holds a text, in any case. A filtered list reads the published generation and returns its commit (`as_of_commit_seq`). A new upload can be missing from it until publication. Its `next` repeats the filters. `pages`: the parsed pages of one file, by `sha256` or by its exact `name` (a file of any age), from `from` to `to` (0-based, `to` excluded, one page by default). Each line comes as `<line id> [x0,y0,x1,y1] <text>`, the box normalized to the page. `contains` keeps the lines that hold a text, `max_lines` caps the lines (200 by default, at most 1,000), and `next` names the `from` and `line_offset` of the next call. |
| `lbb_evals` | `summary`, `traces`, `trace`, `label`, `judge`, `goldens`, `golden`, `accept`, `delete`, `run`, `results`, `settings`. Model checks: `checks_summary`, `checks`, `review_check`. Datasets: `datasets`, `datasets_get`, `datasets_export`, `datasets_create`, `datasets_items`. |
| `lbb_evals_delete` | Deletes the dataset `id` with its items, once the person approves. A later `datasets_get` or `datasets_export` of it fails with 404, and the next save of its workflow test makes a new dataset. |
| `lbb_model_choice` | `options`: per use (`ask`, `route`, `rerank`, `fit`, `label`) the model it runs now, LBB's model, a switch, its checked calls and the models a trial can test. `trials` (`job`, `limit`): the trials with their reports. `trial` (`trial_id`): one trial with its compared calls. `trial_call` (`trial_id`, `call_id`): both answers and the ground truth. `switches`: the switched uses. `choices` (`workflow`, `use`, `cursor`, `limit`): the model choices of the workflow steps (`extract`, `decide`, `embed`): the environment's rows, and with `workflow` the row in force for each step of that workflow; one page in `data`, with `has_more` and `next_cursor`. |
| `lbb_model_choice_manage` | `start`: test a candidate (`provider`, `model`, `effort`) on the uses in `jobs`, with an optional `target` and `days`; it changes no model. `stop` (`trial_id`). `switch` (`trial_id`): the use runs the candidate from the next call on; only a trial that qualifies and compared with the model in use. `revert` (`job`): back to LBB's model. Agents show the report and switch only what the user confirmed. `put_choice` (`use`, `provider`, `model`, `effort`, `workflow`): the step runs that model from its next call, for the environment or one workflow. `delete_choice` (`use`, `workflow`): the step runs the next level again; it answers `{id, object, deleted: true}`. |
| `lbb_models` | `shadow_eval` (with a `body`), `suggest_dataset`, `extractor_dataset` (with `limit`, `split_seq`). `activity`: one month of the stack's managed-model use, by feature, day and graph (`month` as `yyyy-mm`; the current month by default). |

With `lbb_evals`, an agent records what the user confirmed about an answer:

1. Run a query or search with `request`, the user's words. The result names a
   `trace_id`.
2. For a question, label the whole answer with `action: "label"`, `trace_id`
   and `valid`: `true` when the query is right for the question. With
   `valid: false`, `sparql` gives the right query.
3. For the results, read their ids with `action: "trace"` and label them with
   `item` and `valid`, or `items`. A search by meaning needs these labels: no
   query says which hits are right.

`golden` saves a query and treats its current results as relevant. `run`
checks every golden at the current commit: a question is asked again through
Ask (`POST /v1/query/ask`, its answer loop), and a saved query or search runs
again. The verdict of a question shows the loop's final query and its answer
with the records it cites. `judge` lets the platform's
judge model label unlabeled results.

`lbb_evals` also reads the [model checks](https://docs.littlebigbrain.com/guides/check-model-quality/). A
judge model checks a sample of the model calls LBB makes for the graph:

- `checks_summary` sums a month per job and model, with the judge's agreement
  with people. `month` is `yyyy-mm`; the default is the current month.
- `checks` lists the month's checks, newest first. Each has the judge's
  verdict, score and reason, and the ground truth. `job`, `verdict` and
  `reviewed` filter the list, and `after` reads the next page.
- `review_check` records a review of one check by its `call_id`.
  `agree: true` keeps the judge's verdict. `agree: false` with `verdict`
  corrects it, with an optional `score`, `reference` and `note`.

A review becomes the call's ground truth. Show the check to the user first,
and review only what the user confirmed:

```text
lbb_evals action=checks verdict=wrong reviewed=false
lbb_evals action=review_check call_id=<call> agree=false verdict=right note="The user confirmed the order."
```

### Review items

A review item is a question for a person. Workflows, integrations, agents and
people file them, and the console's Review inbox shows them. Each kind takes
its own decisions:

| Kind | Asks | Decisions |
| --- | --- | --- |
| `change` | accept an ontology change, identities or facts | `accept`, `reject`, `cant_tell` |
| `value` | confirm one value of a property | `accept`, `correct` (with `value`), `reject`, `cant_tell` |
| `choice` | pick one of `question.options` | `choose` (with `option`), `cant_tell` |
| `grade` | grade `question.output` on the scale `question.options` | `choose` (with `option`), `cant_tell` |
| `answer` | answer `question.prompt` in words | `accept` (the text in `note`), `cant_tell` |

An accepted or corrected `value` item commits the value to the graph. Show
the item to the user, and answer only what the user decided:

```text
lbb_reviews action=list status=open kind=value
lbb_reviews action=get id=rev_…
lbb_reviews_manage action=answer id=rev_… answer={"decision": "correct", "value": 45} revision=3
```

An answer that accepts or corrects a `value` item, or accepts a `change`
item, commits facts or applies an ontology change. It asks the person to
approve first. See [Approve destructive writes](#approve-destructive-writes).

An item has one answer. The same answer again returns the item. Another
answer fails with `409 review_answered`. `revision` is the revision the
person saw; a later one fails with `409 review_stale`.

A list page holds at most the items the `detail` level shows: 5 (`compact`),
20 (`standard`) or 100 (`full`). The page then comes back whole, and
`next_cursor` continues after the last item you read. Each write sends an
idempotency key. A retry after a network failure or a 5xx sends the same
key, so the write happens once.
[Ask a person](https://docs.littlebigbrain.com/guides/review-in-workflows/) shows how workflows file items
and use the answers.

### Workflow tests and datasets

A [workflow test](https://docs.littlebigbrain.com/guides/test-a-workflow/) runs a `values.extract` workflow
on a few inputs and writes nothing to the graph. The answers to its values
become a dataset of examples:

```text
lbb_workflows_manage action=tests_create name=sinks sample=6
lbb_workflows action=tests_get id=wtest_…
lbb_workflows_manage action=tests_answer id=wtest_… iri=<input> property=width_cm answer={"decision": "correct", "value": 45}
lbb_evals action=datasets_create name=sinks source={"kind": "test", "id": "wtest_…"}
lbb_evals action=datasets_export id=ds_…
```

`datasets_create` also takes the examples themselves: `name`, `items`,
`install` (each property's value type, the units, the context) and `about`.
Each item has a `name`, its `text`, the `values` a correct run writes and
the ones it must `leave_out`. `datasets_items` adds items to a dataset; an
item with the name of one it holds replaces it. `lbb_evals_delete` deletes
a dataset by `id`, once the person approves. A dataset ends with its graph.

Storage inspection and other operator actions are not in the tool set.
Publication and cleanup run automatically.

## Embed in a Node process

The package exports three functions:

| Export | What it does |
| --- | --- |
| `createMcpHttpServer(options)` | Returns a Node `http.Server` that serves the tools over streamable HTTP. Each caller sends a stack API key as a bearer token. |
| `buildLbbServer(client, options?)` | Returns an MCP server bound to one `LbbClient`. |
| `registerLbbTools(server, client, options?)` | Adds the tools to an MCP server you already have. |

This process serves the tools on `http://127.0.0.1:8080/mcp`:

```ts
import { createMcpHttpServer } from "@littlebigbrain/mcp";

const server = createMcpHttpServer({
  baseUrl: process.env.LBB_URL!,
  mcpPath: "/mcp",
  allowedHosts: ["127.0.0.1", "localhost"],
  onError: (error) => console.error("MCP request failed", error),
});
server.listen(8080, "127.0.0.1");
```

A client sends `POST /mcp` with a JSON body and `Authorization: Bearer
<stack API key>`. Add `?graph=` to change the default graph. `GET /healthz`
needs no key.

| Option | Default | Meaning |
| --- | --- | --- |
| `baseUrl` | Required | The complete stack endpoint. |
| `mcpPath` | `/mcp` | The path that serves MCP. |
| `maxBodyBytes` | 1,048,576 | Larger bodies get `413`. |
| `requestTimeoutMs` | 30,000 | The request timeout, in milliseconds. |
| `allowedHosts` | None | Host names that the `Host` header must match. A present `Origin` must match too. Others get `403`. Set it when the server is reachable from a browser. |
| `allowedOrigins` | None | Extra exact `Origin` values to accept. |
| `onError` | None | Called with each failed request's error. |
| `clientFactory` | `new LbbClient(…)` | Builds the `LbbClient` for each request from `baseUrl`, `apiKey` and `graph`. |

| Response | Cause |
| --- | --- |
| `401` | No bearer token. |
| `415` | The `Content-Type` is not `application/json`. |
| `400` | The body is not valid JSON. |
| `413` | The body is larger than `maxBodyBytes`. |
| `403` | The `Host` or `Origin` is not allowed. |
| `404` | Another path, or a method other than `POST` on `mcpPath`. |
| `500` | An unexpected error. The body is `internal server error`, and `onError` receives the details. |

This server has no OAuth. Put your own authentication in front of it before
you expose it to a network. The hosted OAuth endpoint is a separate service.

To add the tools to your own MCP server, next to your own tools:

```ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { LbbClient } from "@littlebigbrain/client";
import { registerLbbTools } from "@littlebigbrain/mcp";

const lbb = new LbbClient({
  baseUrl: process.env.LBB_URL!,
  apiKey: process.env.LBB_API_KEY!,
  graph: "support",
});

const server = new McpServer({ name: "support-agent", version: "1.0.0" });
registerLbbTools(server, lbb);
await server.connect(new StdioServerTransport());
```

`buildLbbServer(lbb)` does the same with a new server named `lbb`. Both accept
these options:

| Option | Default | Meaning |
| --- | --- | --- |
| `queryTextFormat` | `compact` | `compact` or `pretty`: the format of query results. |
| `destructiveWithoutApproval` | `allow` when `LBB_MCP_DESTRUCTIVE=allow`, else `refuse` | What a call that deletes or removes data does when the client cannot ask the person. See [Approve destructive writes](#approve-destructive-writes). |
| `log` | A JSON line on standard error | Receives one event per tool call. |

## Related

- [Quickstart](https://docs.littlebigbrain.com/start/quickstart/): the `support` graph these examples use.
- [Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/): embeddings, filters and
  search planning.
- [Query and validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/): SPARQL and SHACL in depth.
- [Reasoning & inference](https://docs.littlebigbrain.com/guides/reasoning/): the `entailment` regimes.
- [Replay an answer](https://docs.littlebigbrain.com/guides/time-travel-audit/): pin a read to a commit and
  replay it.
