Skip to content

MCP

xmemory exposes a Model Context Protocol server over Streamable HTTP. Any MCP-compatible client — Claude Desktop, Cursor, Windsurf, pydantic-ai, LangChain, Mastra, or a plain SDK call — can connect and get access to xmemory’s read and write tools with no custom code.

The MCP server supports two authentication paths:

  • OAuth2 flow for interactive browser-based connectors.
  • Direct fixed API-key flow for headless agents, CI, evals, and scripts.

Connecting a specific client? See the step-by-step guides for Claude MCP (Manual) (claude.ai, Desktop, and Claude Code), ChatGPT, Codex MCP (Manual), and n8n. Any other MCP client (Cursor, Windsurf, VS Code, and other popular tools) connects with the same URL and Bearer token — see Authentication and Connection below.


xmemory connects to any MCP-compatible client as a remote (Streamable HTTP) server at https://mcp.xmemory.ai/ — no local install or custom code. Most clients add it through their built-in connector or MCP-server settings.

  1. In your client, add a new custom connector / MCP server with the URL https://mcp.xmemory.ai/.
  2. Authenticate — either interactively (OAuth2), where you pick a connection type (instance or admin) on the xmemory connect page, or headless, by pointing at a qualified shortcut URL and sending your xmem_... API key as a Bearer token. See Authentication below for exact tokens and headers.
  3. Approve the connection — xmemory’s read and write tools then appear in the client.

API key: To use xmemory APIs or integrations (including MCP), you need an API key. Get one from the xmemory console. Copy and securely store the key. Never share your API key publicly.

Point your client to https://mcp.xmemory.ai/ and complete the OAuth2 login flow. The client receives an opaque MCP token (format: xmem_mcp_...) that it sends as the Authorization: Bearer token on every request. For OAuth-issued sessions, binding metadata (instance/admin/status) is stored server-side.

Headless clients can skip OAuth and send an account API key directly as the Bearer token on MCP shortcut paths:

Authorization: Bearer <your xmemory API key>

The session type is determined by the URL path, so a direct API-key request must target a qualified shortcut path (the bare root https://mcp.xmemory.ai/ is for OAuth only):

  • /instance/<instance_id> — instance tools bound to that instance.
  • /admin — global admin (fleet) tools. Admin is global-only; /admin/<instance_id> is not a recognized shortcut.
  • /status — status-only tool.

The API key is revalidated on every request.

An example of adding an xmemory instance to Claude Code using the direct API key method:

Terminal window
claude mcp add --transport http xmemory-<FIRST8> https://mcp.xmemory.ai/instance/<instance_id> --header "Authorization: Bearer <xmemory API key>"

Name each entry xmemory- plus the first eight characters of the instance id — the same name the Connect tab and xmemcli instance setup render for it. A client holds one connection per server entry, so two instances registered under one name leave one entry, the second having quietly replaced the first.


Clients that use a config file — e.g. .mcp.json — expect an entry like:

{
"mcpServers": {
"xmemory-<FIRST8>": {
"url": "https://mcp.xmemory.ai/instance/<instance_id>",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}

The url and token depend on which authentication path you use:

Auth path url Bearer token
OAuth2 (interactive) https://mcp.xmemory.ai/ <xmemory_mcp_token>
Direct API key (instance) https://mcp.xmemory.ai/instance/<instance_id> <xmemory_api_key>
Direct API key (admin) https://mcp.xmemory.ai/admin <xmemory_api_key>

For framework-specific setup, see the Focused integrations guides in the sidebar — Pydantic AI, LangChain, Google ADK, and more. For chat apps and CLIs, see Claude MCP (Manual) and ChatGPT.


The xmemory MCP server exposes 10 default tools to instance connections, all operating on the instance bound at login, plus an opt-in Schema management group (5 more) the instance owner can grant on the connect page.

For direct API-key usage, /instance/<instance_id> creates the same instance-scoped tool surface, while /admin switches to the global admin (fleet) tools. Admin is global-only — it owns no per-instance state, so /admin/<instance_id> is not a recognized shortcut; a specific instance’s schema is managed from that instance’s own connection.

Two channels tell an agent which memory it is talking to. The server instructions carry the instance’s name and purpose, a Holds: line naming its object types, the owner’s standing instructions, and a pointer to get_instance_schema — loaded at session start by clients that use the field. The whole block is budgeted against a 2 KB ceiling, and the Holds: line is the part that yields first: a long instance description can crowd it out entirely, while the name, the owner’s instructions and the pointer are kept. Not every client uses the field at all, so read, write and write_async also name the instance themselves.

Tool descriptions carry no schema. read, write and write_async instead name the bound instance in their first line, and carry its purpose and object type names at the end. The remaining tools are the same for every instance.

The first line is what does the work. Many clients defer tool definitions behind a tool search, and a deferred definition is not in context until that search runs — though not every client or configuration defers, so this is a common case rather than a rule. Either way the first line earns its place: it is what a search matches on, since tool descriptions are themselves the search corpus, and it is the first thing read once a definition expands.

They carry the instance because the instructions field does not reach every client: Claude Code receives it, while some others drop it entirely. So the instance’s name, in that first line, is the routing signal that travels furthest. The purpose and object-type tail travels less far — Claude Code caps each description at 2 KB and the tail sits past that, so on that client the tail is carried by instructions instead. Where instructions had to drop its own Holds: line to fit, the object-type names reach that client by neither route; the instance is still named and described, and get_instance_schema returns the object types, fields and relations in full.

Tool responses. There are two response families. Instance (bound) toolsread, write, the schema tools, etc. — return a flat JSON object with a top-level status ("ok" or "error") and the payload keys inline (no wrapper); get_instance_id is the one exception and returns a bare ID string. Admin tools instead return a JSON envelope — the parts that matter are items (the result; a single record is items[0]) and errors (populated on failure). Both families also carry a console_url linking to that operation in the console. Each tool below documents its own payload.

Tools

get_instance_idReturn the instance ID bound to the current session

A bare instance-ID string (32-char UUID hex, e.g. a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6) — not a JSON envelope.

Takes no parameters.

Useful for display, logging, or confirming which instance the agent is operating on.

get_instance_schemaReturn the bound instance's schema (object types, fields, relations, deduplication keys)

A flat object { status, data_schema }data_schema holds the instance schema.

Takes no parameters — it always returns the schema of the instance bound at login.

The LLM can call this to understand what kinds of data the instance stores, which helps it formulate better write and read calls.

get_setup_instructionsReturn how to connect this instance on an agent surface, ordered for where it is likely to be used
formatstring
agent (default) for the steps to run now; project to also return the shared setup files a team commits once — each teammate still approves and signs in themselves. Defaults to "agent".

A JSON object of per-surface steps and commands. With format=project it also carries committable file fragments, each stating how it merges into whatever the repository already has.

For adding the same memory somewhere else — "how do I also connect this on my desktop?" — rather than for reaching it from here, which is already done by the time the tool is callable.

Commands only. The sign-in step hands over credentials out of band, so nothing returned here is a secret, and an instance id is an identifier rather than a credential.

Computed from the instance's current settings, so editing them changes the next response.

writePersist data — extract structured objects from text, or apply explicit structured mutations (synchronous — blocks until fully committed)
textstring
Free-form text containing facts to extract and remember. Exactly one of text / structured_mutations — leave text empty when passing mutations.
structured_mutationsarray
Ordered list of exact, LLM-free object and relation edits applied as one batch, mutually exclusive with text. Available only where the deployment enables structured writes; the tool returns an error otherwise.
session_idstring | null
Session ID for tracing (e.g. claude-qwxhjkmrtz).
scopearray
Anchor the write to existing records — a list of { "type": ..., "key": { "xuid": ... } } or { "type": ..., "key": { "key": { <pk field>: <value> } } } entries. Their current values steer the extractor to update them instead of duplicating them, and the write is confined to the scope: it may only modify or delete these records and create new ones linked to them, so touching anything else fails. Text writes only — it cannot be combined with structured_mutations. A scoped write is always extracted by the fast pipeline: a connection configured for deep extraction is resolved to fast for that call rather than rejected. It also requires read permission on the instance, because the scoped records' current values are shown to the extractor.

A flat object { status, write_id, cleaned_objects: { objects, relations }, changes: { created, updated, deleted }, trace_id, console_url }. changes is the per-object breakdown the diff engine applied; console_url links to the run in the console.

{
"status": "ok",
"write_id": "<uuid>",
"cleaned_objects": { "objects": [], "relations": [] },
"changes": { "created": {}, "updated": {}, "deleted": {} },
"trace_id": "...",
"console_url": "https://console.xmemory.ai/..."
}

Internally, the server runs a two-phase pipeline: an LLM extracts structured objects according to your instance's schema, then a diff engine compares them against existing data and applies inserts, updates, and deletes.

Because write blocks until committed, you can call read immediately after and get consistent results.

write_asyncSame as `write`, but enqueues the operation and returns immediately with a `write_id`
textstring
Free-form text containing facts to extract and remember. Exactly one of text / structured_mutations — leave text empty when passing mutations.
structured_mutationsarray
Ordered list of exact, LLM-free object and relation edits applied as one batch, mutually exclusive with text. Available only where the deployment enables structured writes; the tool returns an error otherwise.
session_idstring | null
Session ID for tracing.
scopearray
Anchor the write to existing records — a list of { "type": ..., "key": { "xuid": ... } } or { "type": ..., "key": { "key": { <pk field>: <value> } } } entries. Their current values steer the extractor to update them instead of duplicating them, and the write is confined to the scope: it may only modify or delete these records and create new ones linked to them, so touching anything else fails. Text writes only — it cannot be combined with structured_mutations. A scoped write is always extracted by the fast pipeline: a connection configured for deep extraction is resolved to fast for that call rather than rejected. It also requires read permission on the instance, because the scoped records' current values are shown to the extractor.

A flat object { status, write_id, trace_id, console_url }. Poll completion with write_status.

Important: do not call read immediately after write_async — the data may not be committed yet. Use write_status to poll, or use write (synchronous) when you need to read right after.

write_statusCheck the status of an async write previously submitted via `write_async`
write_idstringrequired
The write ID returned by write_async.

A flat object { write_id, write_status, error_detail, error, completed_at, trace_id, console_url, status }. The lifecycle state is in write_status (see below); status is the call's own ok/error.

write_status Meaning
queued Waiting to be picked up
processing Currently being extracted and applied
extracting · extracted · applying Intermediate states of the two-phase pipeline
completed Successfully committed — safe to read
failed Extraction or persistence failed; see error_detail
not_found No write with this ID exists

Treat anything other than completed / failed / not_found as still in flight.

readQuery the instance and return an answer, in the shape you ask for
querystringrequired
A natural-language question about the stored data.
scopeobject
Restrict the read to specific, already-identified objects: { "objects": [...], "relations_scope": "no_relations" | "all_relations" }. Each objects entry names one object by type plus a key that is either { "xuid": ... } or { "key": { <pk field>: <value> } }. relations_scope defaults to no_relations, so relations are not exposed unless you ask for them. Everything else in the instance is invisible to the read, and an unknown or ambiguous scope fails rather than silently widening. Omit for a normal read over the whole instance.
read_modestring
Shape of the result: "single-answer" (default), "raw-tables", or "xresponse". See the return shapes below. Defaults to "single-answer".
include_related_typesstring
"types" adds related_types to the result: the object types the read touched, each with the fields it did not return and the object types a declared relation links it to, plus a catalog describing every named type and relation once. Schema-level only — no extra rows are read. Ask for it to phrase a follow-up when an answer looks incomplete, not on every read. Needs the same access as get_instance_schema: instance.get_own on the API key behind the connection, and that tool enabled for the connection; refused otherwise. See Related types. Defaults to "none".
related_types_depthinteger
With include_related_types: "types", how many relation levels the catalog follows out from the touched types, 1 to 3. Every objects entry then carries its distance and, below the last level, its own related edges; the result echoes the depth asked for as depth and counts the types the budgets kept out of the catalog on omitted_objects. The default serves the payload exactly as without the parameter. Ignored without the option. See Related types. Defaults to 1.
session_idstring | null
Session ID for tracing.

A flat object { status, reader_result, sql, pending_suggestions, trace_id, console_url }, plus related_types when the call asked for it with include_related_types: "types" — the same shape as on the REST API. read_mode decides the shape of reader_result:

read_mode reader_result Best for
single-answer {"answer": "Bob Lee joined last Monday."} A written answer
raw-tables {"columns": [{"name": ..., "type": ...}], "rows": [[...]]} Exact stored values, no LLM formatting pass
xresponse {"objects": [...], "relations": [...]} The matching records as structured data

A successful raw-tables read that matched no rows returns reader_result: null rather than an empty table.

Composite questions. A question holding several independent parts is split server-side into sub-questions, one answered per part. The response then carries an extra reader_results key — one entry per sub-question, each { sub_query, reader_result, error }, and each reader_result in the mode you asked for. reader_result at the top level stays the combined value. The key is absent for a single-intent question, so treat its presence, not its length, as the signal that the question was split. A sub-question that could not be answered sets its own error and leaves the rest intact.

{
"status": "ok",
"reader_result": { "answer": "Bob Lee joined last Monday." },
"sql": null,
"pending_suggestions": 0,
"trace_id": "...",
"console_url": "https://console.xmemory.ai/..."
}

Internally, the server translates the question into SQL against the instance's knowledge graph, executes it with automatic retry and empty-result verification, and formats the result into a plain-text answer.

xmemory learns from reads that couldn’t be fully answered and, on demand, surfaces a single rolling proposal of schema improvements for the bound instance. The flow is three tools — review → decide → apply — and a change is only applied when you call apply_pending_decisions.

Tools

review_suggestionsReturn the consolidated proposal and a `proposal_version` token
session_idstring | null
Session ID for tracing.

A flat object { status, instance_id, proposal, retry_after_seconds }, where proposal = { proposal_version, schema_version, items }. When a migration is already running, status is evolution_in_progress and retry_after_seconds is set.

decide_suggestionsRecord an `accept` / `reject` / `defer` per item, in bulk
proposal_versionstringrequired
Proposal version token from review_suggestions.
decisionsarrayrequired
Per-item decisions, each { "item_fingerprint": "...", "decision": "accept" | "reject" | "defer", "edits"?: {...} }.
session_idstring | null
Session ID for tracing.

A flat object { status, decisions_recorded, warnings, next_proposal_version }. Pass next_proposal_version to apply_pending_decisions.

apply_pending_decisionsCommit accepted decisions as one migration
proposal_versionstringrequired
Proposal version token (the next_proposal_version from decide_suggestions).
session_idstring | null
Session ID for tracing.

A flat object { status, migration_id, prior_version, new_version, applied_items, summary }. status is nothing_to_apply when no accepted items remained.

Always confirm with the user before deciding or applying. Rejecting an item suppresses that exact suggestion in future proposals.

A separate, opt-in permission group lets an instance connection own its schema lifecycle directly — distinct from the suggestion engine above. These tools default off and are exposed only when the Schema management permission is granted on the OAuth connect page, so headless direct-key bindings can’t reach them. Schema dry-runs and migration history now live here: admin connections are global-only and no longer edit a specific instance’s schema.

A bound instance already has an XMD schema, so it evolves that schema with enhance_schema — there is no per-instance generate_schema (generating a fresh schema from a plain-language description is a new-instance concern that lives only on the admin connection, as admin_generate_schema).

Tools

enhance_schemaLLM: improve the bound instance's schema — returns the new YAML and a migration plan; does not apply it
schema_descriptionstringrequired
Free-form description of what should change in the schema.
schema_to_improvestringrequired
The current YAML schema to improve (from get_instance_schema).
session_idstring | null
Session ID for tracing.

A flat object { status, generated_schema, migration_plan, summary, warnings, repair_log } — the improved YAML and a structured migration plan for non-additive changes. Apply it with update_instance_schema.

update_instance_schemaReplace the bound instance's schema, applied as a migration
schema_ymlstring
New schema as YAML (one of schema_yml / schema_json).
schema_jsonstring
New schema as JSON (one of schema_yml / schema_json).
migration_planobject
Serialized migration plan (from enhance_schema) for non-additive changes.
confirm_destructiveboolean
Set true to acknowledge ops that drop data. Defaults to false.
session_idstring | null
Session ID for tracing.

A flat object { status, migration_id, prior_version, new_version, statements_executed } when a migration ran.

dry_run_schema_migrationPreview the DDL `update_instance_schema` would run, without applying it
schema_ymlstring
Candidate schema as YAML (one of schema_yml / schema_json).
schema_jsonstring
Candidate schema as JSON (one of schema_yml / schema_json).
migration_planobject
Serialized migration plan, same shape as for update_instance_schema.
confirm_destructiveboolean
Set true to preview destructive ops without the gate rejecting. Defaults to false.
session_idstring | null
Session ID for tracing.

A flat object { status, current_version, statements, plan_summary, warnings } (statements are the planned DDL).

list_schema_migrationsList the bound instance's applied migrations, newest first
limitinteger
Max records to return (1–200). Defaults to 50.
before_idstring
Pagination cursor: return migrations applied before this ID.
include_yamlboolean
Include the before/after YAML snapshots for each record (can be large). Defaults to false.
session_idstring | null
Session ID for tracing.

A flat object { status, items, next_before_id, has_more }; each record in items = { id, applied_at, source, prior_version, new_version, ops, ops_summary }.

get_schema_migrationGet one applied-migration record of the bound instance by ID
migration_idstringrequired
The migration record ID (from list_schema_migrations).
include_yamlboolean
Include the before/after YAML snapshots. Defaults to false.
session_idstring | null
Session ID for tracing.

A flat object { status, record }, the migration record { id, applied_at, source, prior_version, new_version, ops, ops_summary, yaml_before?, yaml_after? }.

Important: an agent may edit the current XMD contract directly or use enhance_schema to generate a candidate from natural-language instructions. In either path, retrieve the current schema first, preview with dry_run_schema_migration, test representative inputs with extract, then apply with update_instance_schema only after approval.

Selecting the admin connection type at login switches the tool surface to the global fleet management tools, all prefixed admin_. Admin operates across your whole fleet rather than a single instance, so it has no bound-instance state. To edit a specific instance’s schema, use that instance’s own connection and its Schema management group (above); admin’s by-id schema tool (admin_get_instance_schema_by_id) is read-only.

Important: for a new instance, either author valid XMD directly or pass the memory requirements as schema_description to admin_generate_schema; then pass that XMD to admin_create_instance. Natural-language generation is a convenience, not a restriction. Use admin_enhance_schema when improving existing XMD; see the XMD guide for the supported contract.

Stateless schema helpers

Tools

admin_generate_schemaGenerate a YAML schema from a free-form description
schema_descriptionstringrequired
Free-form description of the schema to generate.
cluster_idstring
Target cluster; required when authenticating with an API key.
session_idstring | null
Session ID for tracing.

items[0] = { data_schema } (the generated schema).

admin_enhance_schemaImprove an existing YAML schema
schema_descriptionstringrequired
Free-form description of the desired improvements.
schema_to_improvestringrequired
The existing YAML schema to improve.
cluster_idstring
Target cluster; required when authenticating with an API key.
session_idstring | null
Session ID for tracing.

items[0] = { data_schema, migration_plan, summary, warnings, repair_log } — the enhanced schema and a structured migration plan for applying non-additive changes safely.

Instance lifecycle

Tools

admin_create_instanceCreate a new empty instance from a YAML schema
schema_yamlstringrequired
YAML data schema (from admin_generate_schema / admin_enhance_schema).
cluster_idstring
Target cluster; required when authenticating with an API key.
namestring
Instance name; required when cluster_id is provided.
descriptionstring
Instance description.
session_idstring | null
Session ID for tracing.

items[0] = { instance_id } (the new instance). Bind to it via a fresh /instance/<instance_id> connection.

Cluster-scoped management

Tools

admin_list_clustersList clusters accessible to the API key

items = clusters, each { id, org_id, name, description }.

Takes no parameters — the API key determines which clusters are visible.

admin_get_clusterGet a single cluster by ID
cluster_idstringrequired
The cluster ID to fetch.

items[0] = a cluster { id, org_id, name, description }.

admin_list_instancesList instances in a cluster
cluster_idstringrequired
The cluster ID to list instances from.
verboseboolean
When false, return instance IDs only. Defaults to true.

items = instances in the cluster.

admin_get_instanceGet instance metadata within a cluster
cluster_idstringrequired
The cluster the instance belongs to.
instance_idstringrequired
The instance ID to fetch.

items[0] = the instance { id, cluster_id, name, description, data_schema, ... }.

admin_delete_instanceDelete an instance within a cluster
cluster_idstringrequired
The cluster the instance belongs to.
instance_idstringrequired
The instance ID to delete.

ids = the deleted instance ID.

admin_update_instance_metadataReplace an instance's name/description within a cluster
cluster_idstringrequired
The cluster the instance belongs to.
instance_idstringrequired
The instance ID to update.
namestringrequired
New instance name.
descriptionstring | null
New instance description (omit or null to clear).
agent_owner_instructionsstring | null
Standing instructions shown word for word to every agent that connects to this instance. Omit to leave unchanged; pass an empty string to clear it.

items[0] = the updated instance.

admin_patch_instance_metadataPartially update an instance's metadata within a cluster
cluster_idstringrequired
The cluster the instance belongs to.
instance_idstringrequired
The instance ID to patch.
namestring
New instance name.
descriptionstring
New instance description.
agent_owner_instructionsstring | null
Standing instructions shown word for word to every agent that connects to this instance. Omit to leave unchanged; pass an empty string to clear it.

items[0] = the updated instance.

Instance-scoped management (no cluster_id needed)

Tools

admin_list_own_instancesList all instances across linked clusters
verboseboolean
When false, return instance IDs only. Defaults to true.

items = instances across every cluster the API key can reach.

admin_get_instance_by_idGet instance metadata by instance ID
instance_idstringrequired
The instance ID to fetch.

items[0] = the instance.

admin_delete_instance_by_idDelete an instance by instance ID
instance_idstringrequired
The instance ID to delete.

ids = the deleted instance ID.

admin_get_setup_instructionsReturn how to connect a given instance on an agent surface
instance_idstringrequired
The instance to produce connect instructions for.
formatstring
agent (default) for the steps to run now; project to also return the shared setup files a team commits once — each teammate still approves and signs in themselves. Defaults to "agent".

A JSON object of per-surface steps and commands. With format=project it also carries committable file fragments, each stating how it merges into whatever the repository already has.

The natural follow-up to admin_create_instance — though that call already returns the same payload, so this is for instances created earlier, or for adding one to another surface.

Commands only. The sign-in step hands over credentials out of band, so nothing returned here is a secret, and an instance id is an identifier rather than a credential.

admin_get_instance_schema_by_idGet an instance's schema by instance ID
instance_idstringrequired
The instance ID whose schema to fetch.

items[0] = the instance schema.

admin_update_instance_metadata_by_idReplace an instance's name/description by instance ID
instance_idstringrequired
The instance ID to update.
namestringrequired
New instance name.
descriptionstring
New instance description.
agent_owner_instructionsstring | null
Standing instructions shown word for word to every agent that connects to this instance. Omit to leave unchanged; pass an empty string to clear it.

items[0] = the updated instance.

admin_patch_instance_metadata_by_idPartially update an instance's metadata by instance ID
instance_idstringrequired
The instance ID to patch.
namestring
New instance name.
descriptionstring
New instance description.
agent_owner_instructionsstring | null
Standing instructions shown word for word to every agent that connects to this instance. Omit to leave unchanged; pass an empty string to clear it.

items[0] = the updated instance.


Use write when you need to read the data back immediately — it blocks until committed, guaranteeing consistency.

Use write_async + write_status when throughput matters more than immediate consistency — the client isn’t blocked, and you can poll for completion later.


read answers in one of three shapes, chosen with read_mode: single-answer (default) for a written answer, raw-tables for the selected rows with no LLM formatting pass, and xresponse for the matching records as objects and relations. A successful raw-tables read that matched no rows returns reader_result: null rather than an empty table.

The exact shapes and an example response are on the read tool above.


Pass include_related_types: "types" to read and the result also carries related_types: the object types the read touched, the fields it did not return, and the object types a declared relation links them to, with a catalog of every named type and relation — derived from the schema, no extra rows read — so you can phrase a deliberate follow-up read for a field or a linked type the answer left out. Ask for it when an answer looks incomplete rather than on every read; a field counts as returned only when it reaches the result, so a column a CTE selected but the outer query dropped is listed as not returned. The shape is documented on the REST API.

related_types_depth (1 to 3, default 1) follows the listed edges further: every entry under objects then carries its distance from the touched types and, below the last level, its own related edges; the result echoes the depth asked for as depth and counts on omitted_objects the types within that many levels that the budgets kept out of the catalog (shape). It needs nothing beyond the option and is ignored without it.

It needs the same access as get_instance_schema: the API key behind the connection must hold instance.get_own, and the connection’s tool grant must include get_instance_schema — a connection that unchecked that tool does not get the schema through read either. Without them the tool returns { "error": ... } naming what is missing, and a read without the parameter is unaffected.


You can ask read a question holding several independent parts — “who owns the billing service, and when did the last incident close?” — in one call. The server splits it into sub-questions, answers each on its own, and runs them against one shared snapshot, so the parts cannot disagree with each other.

The response then carries a reader_results key alongside the usual reader_result: one entry per sub-question, each { sub_query, reader_result, error }, and each answer in whatever read_mode you asked for. So a raw-tables composite question comes back as one table per part. The top-level reader_result stays the combined answer, so a client that ignores reader_results keeps working.

Two things worth knowing. The key is absent for a single-intent question rather than being a one-entry list, so check whether it is present rather than counting its length — the SDKs differ here and hand you an empty array instead. And a part that could not be answered sets its own error and leaves the other answers intact, so a composite read can be partly successful.

Nothing has to be enabled or passed for this. A scope, if you supply one, applies read-only to every sub-question.


Tools never raise — on failure they return a parseable JSON error instead, in one of three shapes: {"error": "<message>"} (a guard failure, e.g. no instance bound to the session), {"status": "error", "error_message": "<message>"} (the flat instance-tool response on failure), or the admin envelope’s errors array — with schema-evolution failures carrying a structured error_type (stale_proposal_version, dependency_closure_failed, destructive_confirmation_required, …). Common messages:

Error Cause
"no instance bound to this session" Token is invalid or not linked to an instance
"text size (N bytes) exceeds maximum (M bytes)" Write payload too large (limit: 1 MB)
"write queue not ready" Background processor hasn’t started
"write failed: <detail>" Extraction or persistence failure