# MCP

> MCP integration guide.

Canonical page: https://xmemory.ai/mcp/

xmemory exposes a [Model Context Protocol](https://modelcontextprotocol.io/) 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)](https://xmemory.ai/claude/) (claude.ai, Desktop, and Claude Code), [ChatGPT](https://xmemory.ai/chatgpt/), [Codex MCP (Manual)](https://xmemory.ai/codex/), and [n8n](https://xmemory.ai/n8n/). Any other MCP client (Cursor, Windsurf, VS Code, and other popular tools) connects with the same URL and Bearer token — see [Authentication](#authentication) and [Connection](#connection) below.

---

## Adding xmemory as a connector

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](#authentication) below for exact tokens and headers.
3. Approve the connection — xmemory's read and write tools then appear in the client.

---

## Authentication

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

### Option 1: OAuth2 (interactive)

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.

### Option 2: Direct fixed API key (headless)

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:

```sh
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.

---

## Connection

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

```json
{
  "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](https://xmemory.ai/pydantic/), [LangChain](https://xmemory.ai/langchain/), [Google ADK](https://xmemory.ai/google-adk/), and more. For chat apps and CLIs, see [Claude MCP (Manual)](https://xmemory.ai/claude/) and [ChatGPT](https://xmemory.ai/chatgpt/).

---

## Tools

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) tools** — `read`, `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.

**`get_instance_id`** — Return the instance ID bound to the current session

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

Details: Takes no parameters.

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

**`get_instance_schema`** — Return the bound instance's schema (object types, fields, relations, deduplication keys)

Returns: A flat object `{ status, data_schema }` — `data_schema` holds the instance schema.

Details: 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_instructions`** — Return how to connect this instance on an agent surface, ordered for where it is likely to be used

Parameters:
- `format` (`string`) — `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"`.

Returns: 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.

Details: 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.

**`write`** — Persist data — extract structured objects from text, or apply explicit structured mutations (synchronous — blocks until fully committed)

Parameters:
- `text` (`string`) — Free-form text containing facts to extract and remember. Exactly one of `text` / `structured_mutations` — leave `text` empty when passing mutations.
- `structured_mutations` (`array`) — 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_id` (`string | null`) — Session ID for tracing (e.g. `claude-qwxhjkmrtz`).
- `scope` (`array`) — Anchor the write to existing records — a list of `{ "type": ..., "key": { "xuid": ... } }` or `{ "type": ..., "key": { "key": { <pk field>: <value> } } }` entries. The write is confined to the scope: it may only modify or delete these records and create new ones linked to them, and `scope_mode` decides what happens to anything else. Extraction is unchanged by a scope, so the text still names the records it means. Text writes only — it cannot be combined with `structured_mutations`. It also requires read permission on the instance. See [Scoped writes](https://xmemory.ai/api/#scoped-writes).
- `scope_mode` (`string`) — Only with `scope`. `"reject"` (default) fails the whole write when it would touch a record outside the scope. `"drop"` skips those changes instead and reports them under `changes.skipped_out_of_scope`, creates only records the scope names by primary key — such a record need not be stored yet — and never creates a type that declares no primary key. The tool turns the diff engine on for a drop-mode write, whatever the connection is configured for. Defaults to `"reject"`.

Returns: 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, and on a drop-mode scoped write it also carries `skipped_out_of_scope`, what the scope kept out; `console_url` links to the run in the console.

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

Details: 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_async`** — Same as `write`, but enqueues the operation and returns immediately with a `write_id`

Parameters:
- `text` (`string`) — Free-form text containing facts to extract and remember. Exactly one of `text` / `structured_mutations` — leave `text` empty when passing mutations.
- `structured_mutations` (`array`) — 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_id` (`string | null`) — Session ID for tracing.
- `scope` (`array`) — Anchor the write to existing records — a list of `{ "type": ..., "key": { "xuid": ... } }` or `{ "type": ..., "key": { "key": { <pk field>: <value> } } }` entries. The write is confined to the scope: it may only modify or delete these records and create new ones linked to them, and `scope_mode` decides what happens to anything else. Extraction is unchanged by a scope, so the text still names the records it means. Text writes only — it cannot be combined with `structured_mutations`. It also requires read permission on the instance. See [Scoped writes](https://xmemory.ai/api/#scoped-writes).
- `scope_mode` (`string`) — Only with `scope`. `"reject"` (default) fails the whole write when it would touch a record outside the scope. `"drop"` skips those changes instead and reports them under `changes.skipped_out_of_scope`, creates only records the scope names by primary key — such a record need not be stored yet — and never creates a type that declares no primary key. The tool turns the diff engine on for a drop-mode write, whatever the connection is configured for. Defaults to `"reject"`.

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

Details: **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.

Writes to an instance — `write` and `write_async`, text and structured — share one queue and are applied one at a time, in the order they were queued. See [Write ordering](https://xmemory.ai/api/#write-ordering).

**`write_status`** — Check the status of an async write previously submitted via `write_async`

Parameters:
- `write_id` (`string`, required) — The write ID returned by `write_async`.

Returns: 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`.

Details: | `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; `extracted` waits for older writes to be applied first |
| `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.

**`read`** — Query the instance and return an answer, in the shape you ask for

Parameters:
- `query` (`string`, required) — A natural-language question about the stored data.
- `scope` (`object`) — 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_mode` (`string`) — Shape of the result: `"single-answer"` (default), `"raw-tables"`, or `"xresponse"`. See the return shapes below. Defaults to `"single-answer"`.
- `include_related_types` (`string`) — `"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](https://xmemory.ai/mcp/#related-types). Defaults to `"none"`.
- `related_types_depth` (`integer`) — 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](https://xmemory.ai/mcp/#related-types). Defaults to `1`.
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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](https://xmemory.ai/api/#related-types). `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 `raw-tables` read that matched nothing returns `{"columns": [], "rows": []}`, and an `xresponse` one `{"objects": [], "relations": []}` — the same value a zero-row sub-question carries in `reader_results`, so one emptiness check works against the whole response. It means the query ran and found nothing. In both modes `null` means something different: the schema provably cannot represent the concept, so try a better-matching instance rather than concluding the data is absent. A read whose query could not be executed fails instead of returning either.

**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.

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

Details: 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.

### Schema evolution (suggestion engine)

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`.

**`review_suggestions`** — Return the consolidated proposal and a `proposal_version` token

Parameters:
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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. An item with `apply_blocked: true` cannot be applied as proposed, and its `rationale` says why: accepting it fails the whole apply, so leave it out of a bulk accept. The flag is re-derived on every review.

**`decide_suggestions`** — Record an `accept` / `reject` / `defer` per item, in bulk

Parameters:
- `proposal_version` (`string`, required) — Proposal version token from `review_suggestions`.
- `decisions` (`array`, required) — Per-item decisions, each `{ "item_fingerprint": "...", "decision": "accept" | "reject" | "defer", "edits"?: {...} }`.
- `session_id` (`string | null`) — Session ID for tracing.

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

**`apply_pending_decisions`** — Commit accepted decisions as one migration

Parameters:
- `proposal_version` (`string`, required) — Proposal version token (the `next_proposal_version` from `decide_suggestions`).
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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.

### Schema management (opt-in)

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`).

**`enhance_schema`** — LLM: improve the bound instance's schema — returns the new YAML and a migration plan; does not apply it

Parameters:
- `schema_description` (`string`, required) — Free-form description of what should change in the schema.
- `schema_to_improve` (`string`, required) — The current YAML schema to improve (from `get_instance_schema`).
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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_schema`** — Replace the bound instance's schema, applied as a migration

Parameters:
- `schema_yml` (`string`) — New schema as YAML (one of `schema_yml` / `schema_json`).
- `schema_json` (`string`) — New schema as JSON (one of `schema_yml` / `schema_json`).
- `migration_plan` (`object`) — Serialized migration plan (from `enhance_schema`) for non-additive changes.
- `confirm_destructive` (`boolean`) — Set `true` to acknowledge ops that drop data. Defaults to `false`.
- `session_id` (`string | null`) — Session ID for tracing.

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

**`dry_run_schema_migration`** — Preview the DDL `update_instance_schema` would run, without applying it

Parameters:
- `schema_yml` (`string`) — Candidate schema as YAML (one of `schema_yml` / `schema_json`).
- `schema_json` (`string`) — Candidate schema as JSON (one of `schema_yml` / `schema_json`).
- `migration_plan` (`object`) — Serialized migration plan, same shape as for `update_instance_schema`.
- `confirm_destructive` (`boolean`) — Set `true` to preview destructive ops without the gate rejecting. Defaults to `false`.
- `session_id` (`string | null`) — Session ID for tracing.

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

**`list_schema_migrations`** — List the bound instance's applied migrations, newest first

Parameters:
- `limit` (`integer`) — Max records to return (1–200). Defaults to `50`.
- `before_id` (`string`) — Pagination cursor: return migrations applied before this ID.
- `include_yaml` (`boolean`) — Include the before/after YAML snapshots for each record (can be large). Defaults to `false`.
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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_migration`** — Get one applied-migration record of the bound instance by ID

Parameters:
- `migration_id` (`string`, required) — The migration record ID (from `list_schema_migrations`).
- `include_yaml` (`boolean`) — Include the before/after YAML snapshots. Defaults to `false`.
- `session_id` (`string | null`) — Session ID for tracing.

Returns: 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](https://xmemory.ai/xmd/) 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.

### Admin tools

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](https://xmemory.ai/xmd/) for the supported contract.

**Stateless schema helpers**

**`admin_generate_schema`** — Generate a YAML schema from a free-form description

Parameters:
- `schema_description` (`string`, required) — Free-form description of the schema to generate.
- `cluster_id` (`string`) — Target cluster; required when authenticating with an API key.
- `session_id` (`string | null`) — Session ID for tracing.

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

**`admin_enhance_schema`** — Improve an existing YAML schema

Parameters:
- `schema_description` (`string`, required) — Free-form description of the desired improvements.
- `schema_to_improve` (`string`, required) — The existing YAML schema to improve.
- `cluster_id` (`string`) — Target cluster; required when authenticating with an API key.
- `session_id` (`string | null`) — Session ID for tracing.

Returns: `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**

**`admin_create_instance`** — Create a new empty instance from a YAML schema

Parameters:
- `schema_yaml` (`string`, required) — YAML data schema (from `admin_generate_schema` / `admin_enhance_schema`).
- `cluster_id` (`string`) — Target cluster; required when authenticating with an API key.
- `name` (`string`) — Instance name; required when `cluster_id` is provided.
- `description` (`string`) — Instance description.
- `session_id` (`string | null`) — Session ID for tracing.

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

**Cluster-scoped management**

**`admin_list_clusters`** — List clusters accessible to the API key

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

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

**`admin_get_cluster`** — Get a single cluster by ID

Parameters:
- `cluster_id` (`string`, required) — The cluster ID to fetch.

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

**`admin_list_instances`** — List instances in a cluster

Parameters:
- `cluster_id` (`string`, required) — The cluster ID to list instances from.
- `verbose` (`boolean`) — When `false`, return instance IDs only. Defaults to `true`.

Returns: `items` = instances in the cluster.

**`admin_get_instance`** — Get instance metadata within a cluster

Parameters:
- `cluster_id` (`string`, required) — The cluster the instance belongs to.
- `instance_id` (`string`, required) — The instance ID to fetch.

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

**`admin_delete_instance`** — Delete an instance within a cluster

Parameters:
- `cluster_id` (`string`, required) — The cluster the instance belongs to.
- `instance_id` (`string`, required) — The instance ID to delete.

Returns: `ids` = the deleted instance ID.

**`admin_update_instance_metadata`** — Replace an instance's name/description within a cluster

Parameters:
- `cluster_id` (`string`, required) — The cluster the instance belongs to.
- `instance_id` (`string`, required) — The instance ID to update.
- `name` (`string`, required) — New instance name.
- `description` (`string | null`) — New instance description (omit or `null` to clear).
- `agent_owner_instructions` (`string | 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.

Returns: `items[0]` = the updated instance.

**`admin_patch_instance_metadata`** — Partially update an instance's metadata within a cluster

Parameters:
- `cluster_id` (`string`, required) — The cluster the instance belongs to.
- `instance_id` (`string`, required) — The instance ID to patch.
- `name` (`string`) — New instance name.
- `description` (`string`) — New instance description.
- `agent_owner_instructions` (`string | 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.

Returns: `items[0]` = the updated instance.

**Instance-scoped management** (no `cluster_id` needed)

**`admin_list_own_instances`** — List all instances across linked clusters

Parameters:
- `verbose` (`boolean`) — When `false`, return instance IDs only. Defaults to `true`.

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

**`admin_get_instance_by_id`** — Get instance metadata by instance ID

Parameters:
- `instance_id` (`string`, required) — The instance ID to fetch.

Returns: `items[0]` = the instance.

**`admin_delete_instance_by_id`** — Delete an instance by instance ID

Parameters:
- `instance_id` (`string`, required) — The instance ID to delete.

Returns: `ids` = the deleted instance ID.

**`admin_get_setup_instructions`** — Return how to connect a given instance on an agent surface

Parameters:
- `instance_id` (`string`, required) — The instance to produce connect instructions for.
- `format` (`string`) — `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"`.

Returns: 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.

Details: 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_id`** — Get an instance's schema by instance ID

Parameters:
- `instance_id` (`string`, required) — The instance ID whose schema to fetch.

Returns: `items[0]` = the instance schema.

**`admin_update_instance_metadata_by_id`** — Replace an instance's name/description by instance ID

Parameters:
- `instance_id` (`string`, required) — The instance ID to update.
- `name` (`string`, required) — New instance name.
- `description` (`string`) — New instance description.
- `agent_owner_instructions` (`string | 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.

Returns: `items[0]` = the updated instance.

**`admin_patch_instance_metadata_by_id`** — Partially update an instance's metadata by instance ID

Parameters:
- `instance_id` (`string`, required) — The instance ID to patch.
- `name` (`string`) — New instance name.
- `description` (`string`) — New instance description.
- `agent_owner_instructions` (`string | 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.

Returns: `items[0]` = the updated instance.

---

## Sync vs async writes

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.

Both tools feed one queue per instance. Writes are processed in parallel but applied one at a time, in the order they were queued, so a `write` never lands ahead of a `write_async` queued before it. See [Write ordering](https://xmemory.ai/api/#write-ordering).

---

## Read modes

`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 `raw-tables` read that matched nothing returns `{"columns": [], "rows": []}`, and an `xresponse` one `{"objects": [], "relations": []}` — the same value a zero-row sub-question carries in `reader_results`, so one emptiness check works against the whole response. It means the query ran and found nothing. In both modes `null` means something different: the schema provably cannot represent the concept, so try a better-matching instance rather than concluding the data is absent. A read whose query could not be executed fails instead of returning either.

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

---

## Related types

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](https://xmemory.ai/api/#related-types).

`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](https://xmemory.ai/api/#following-relations-further)). 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.

---

## Composite questions

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.

---

## Error handling

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 |
