---
title: Use relate with the Claude Agent SDK
description: "relate, the embedded Cypher graph database (relatedb.ai): Register a read-only relate MCP server with the Claude Agent SDK, and check the tools and rows the agent gets before the first model call."
type: how-to
surfaces:
  - mcp
  - cli
  - agents
status: stable
verified: 0.1.2
verified_by:
  - cypher
  - python
  - cli
samples:
  - https://relatedb.ai/docs/guides/integrations/claude-agent-sdk/samples/options.json
canonical_url: https://relatedb.ai/docs/guides/integrations/claude-agent-sdk/
prerequisites:
  - get-started/install
related:
  - guides/agents/connect-over-mcp
  - guides/agents/agent-safety
  - reference/errors#mcp-read-query-only
  - cookbook/agent-memory
last_updated: 2026-11-09
---

# Use relate with the Claude Agent SDK

Register a read-only relate MCP server with the Claude Agent SDK, and check the tools and rows the agent gets before the first model call.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

This guide shows you how to give an agent built with the Claude Agent SDK read access to a relate graph. relate serves a [store](https://relatedb.ai/docs/reference/glossary/#store) over [MCP](https://relatedb.ai/docs/reference/glossary/#mcp) on stdio with `--read-only`. The SDK starts that server as a child process, and the agent queries the graph through relate's `read_cypher` tool. Every step before the model call runs without a Claude credential, so you can check what the agent gets before it makes one.

```mermaid
flowchart LR
  accTitle: The SDK starts relate as a read-only MCP server
  accDescr {
    Your program calls the Claude Agent SDK, which starts relate serve as a child process and talks MCP to it over stdin and stdout.
    relate serve opens the team store read-only and answers read_cypher and get_schema calls.
    A separate writer, such as your app, can keep writing the directory store; the server reads it as of its start, or follows it with --read-only=live.
  }
  P("Your program") --> S["Claude Agent SDK"]
  S -- "MCP over stdio" --> R["relate serve --read-only"]
  R -- "reads" --> T[("./team store")]
  A("Your app") -. "writes" .-> T
```

Your program calls the Claude Agent SDK, which starts relate serve as a child process and talks MCP to it over stdin and stdout. relate serve opens the team store read-only and answers read_cypher and get_schema calls. A separate writer, such as your app, can keep writing the directory store; the server reads it as of its start, or follows it with --read-only=live.

## Prerequisites

- relate on your `PATH` ([Install relate](https://relatedb.ai/docs/get-started/install/)).
- The Claude Agent SDK: `pip install claude-agent-sdk` (Python 3.10 or later) or `npm install @anthropic-ai/claude-agent-sdk` (Node.js 18.14 or later). The SDK programs in [Register the server with the SDK](#register-the-server-with-the-sdk) were checked against `claude-agent-sdk` 0.2.179 and `@anthropic-ai/claude-agent-sdk` 0.3.319, as of 2026-11-07. The SDK changes faster than relate, so its own documentation wins if a name differs.
- A Claude credential for the model call, such as `ANTHROPIC_API_KEY` in the environment. Only the agent program in [Register the server with the SDK](#register-the-server-with-the-sdk) needs it.

## Create the store

The agent reads `./team`, a [directory store](https://relatedb.ai/docs/reference/glossary/#directory-store) holding the small team graph the Get started pages use. A directory store lets your app keep writing while a [read-only](https://relatedb.ai/docs/reference/glossary/#read-only) server reads it; a single-file `.rl` store that a writer holds refuses the read-only open. Plain `--read-only` serves the store as it was when the server started, so the agent sees later writes only after the server restarts. `--read-only=live` follows the writer's commits on a directory store; it refuses a `.rl` store and `:memory:`.

Write the seed script:

```bash verify
cat > team.cypher <<'EOF'
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
EOF
```

Run it into the store. This step writes, so it opens `./team` without `--read-only`:

```bash verify
relate query --database ./team --mode envelope < team.cypher
```

```text
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":1.769083,"execute_ms":1.4763330000000001,"lex_parse_ms":0.015375000000000002,"lower_ms":0.018000000000000002,"parse_ms":0.277375,"plan_ms":0.009708,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.00045900000000000004},"truncated":false}
```

The `CREATE` returns no rows; the store now holds three people and two projects.

## Check the tools the agent sees

`relate serve --mcp stdio` speaks MCP's JSON-RPC on stdin and stdout, one message per line, until stdin closes. It is the command the SDK starts. Send it the opening messages an MCP host sends:

```bash verify
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | relate serve --mcp stdio --database ./team --read-only 2>/dev/null
```

```text
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"relate-mcp","version":"0.1.2"}}}
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true,"title":"Read Cypher"},"description":"Execute one read-only Cypher statement. Returns the result's rows. Write and schema (DDL) statements are rejected.","inputSchema":{"properties":{"parameters":{"default":{},"description":"The statement's parameters; a `rows` array feeds `$rows`.","type":"object"},"statement":{"description":"The Cypher statement.","type":"string"}},"required":["statement"],"type":"object"},"name":"read_cypher"},{"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true,"title":"Get Schema"},"description":"Get the schema of the graph store: node labels with their property keys and inferred types, and the relationship types between them. `sample_size` bounds how many nodes and relationships are examined per label (larger is slower but more accurate; -1 examines all).","inputSchema":{"properties":{"sample_size":{"description":"The sample size used to infer the graph schema. Larger samples are slower, but more accurate. Smaller samples are faster, but might miss information. -1 samples the whole graph.","type":"integer"}},"type":"object"},"name":"get_schema"}]}}
```

Opened with `--read-only`, the server lists two tools, `read_cypher` and `get_schema`, both annotated `readOnlyHint: true`. `write_cypher` is not listed at all. The startup banner goes to stderr, which `2>/dev/null` drops, so stdout carries nothing but JSON-RPC.

## Call the query tool

An agent's query is a `tools/call` request for `read_cypher`, with a statement and its parameters. The response also carries timings that change on every run, so this check keeps only the rows:

```bash verify
printf '%s\n' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name, w.since AS since ORDER BY since","parameters":{"project":"relate"}}}}' \
  | relate serve --mcp stdio --database ./team --read-only 2>/dev/null \
  | grep -o '\\"rows\\":\[[^]]*\]'
```

```text
\"rows\":[{\"name\":\"Grace\",\"since\":2023},{\"name\":\"Ada\",\"since\":2024}]
```

Two people work on `relate`: Grace since 2023 and Ada since 2024. The whole response also carries the columns, column types, statistics, notifications, the plan, and timings.

A write sent to the same tool comes back as a tool error, with `isError: true`, and the store is unchanged:

```bash verify
printf '%s\n' \
  '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (p:Person {name: $name}) DETACH DELETE p","parameters":{"name":"Ada"}}}}' \
  | relate serve --mcp stdio --database ./team --read-only 2>/dev/null
```

```text
{"jsonrpc":"2.0","id":4,"result":{"content":[{"text":"Only MATCH queries are allowed for read-query","type":"text"}],"isError":true}}
```

## Register the server with the SDK

The SDK starts each stdio entry of its MCP server map as a child process. It names that server's tools `mcp__<server>__<tool>`, so relate's tools arrive as `mcp__relate__read_cypher` and `mcp__relate__get_schema`.

Build the options first. Constructing them makes no model call and needs no credential. The Python SDK raises `TypeError` on an option name it doesn't know, so this program checks every name before the agent runs:

```python run name=options title="options.py"
from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    mcp_servers={
        "relate": {
            "type": "stdio",
            "command": "relate",
            "args": ["serve", "--mcp", "stdio", "--database", "./team", "--read-only"],
        }
    },
    tools=[],
    allowed_tools=["mcp__relate__get_schema", "mcp__relate__read_cypher"],
    permission_mode="dontAsk",
    strict_mcp_config=True,
)

if __name__ == "__main__":
    print(" ".join([options.mcp_servers["relate"]["command"], *options.mcp_servers["relate"]["args"]]))
    print(options.allowed_tools)
    print(options.tools, options.permission_mode, options.strict_mcp_config)
```

```text
relate serve --mcp stdio --database ./team --read-only
['mcp__relate__get_schema', 'mcp__relate__read_cypher']
[] dontAsk True
```

The agent program uses the same options and makes the model call, so it needs the credential from the prerequisites. The Python version imports them from `options.py`:

### Python

```python title="agent.py"
import asyncio

from claude_agent_sdk import ResultMessage, query

from options import options


async def main() -> None:
    async for message in query(prompt="Who works on the relate project, and since when?", options=options):
        if isinstance(message, ResultMessage):
            print(message.result)


asyncio.run(main())
```

### Node.js

```js title="agent.mjs"
import { query } from "@anthropic-ai/claude-agent-sdk";

const options = {
  mcpServers: {
    relate: {
      type: "stdio",
      command: "relate",
      args: ["serve", "--mcp", "stdio", "--database", "./team", "--read-only"],
    },
  },
  tools: [],
  allowedTools: ["mcp__relate__get_schema", "mcp__relate__read_cypher"],
  permissionMode: "dontAsk",
  strictMcpConfig: true,
};

for await (const message of query({ prompt: "Who works on the relate project, and since when?", options })) {
  if (message.type === "result" && message.subtype === "success") console.log(message.result);
}
```

To allow writes, remove `--read-only`; the server then also lists `write_cypher`, which the SDK names `mcp__relate__write_cypher`.

The other options keep the agent to relate's two tools:

| Python option               | Node.js option              | Effect                                                                                                |
| --------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------- |
| `tools=[]`                  | `tools: []`                 | Turns off the SDK's built-in tools (shell, file reads and edits), so relate's tools are the only ones |
| `allowed_tools`             | `allowedTools`              | Pre-approves the listed tools, so they run without a permission prompt                                |
| `permission_mode="dontAsk"` | `permissionMode: "dontAsk"` | Denies a call to any tool that is not pre-approved, instead of prompting                              |
| `strict_mcp_config=True`    | `strictMcpConfig: true`     | Ignores MCP servers configured elsewhere, such as a project `.mcp.json`                               |

Each query the agent makes is the `tools/call` request from [Call the query tool](#call-the-query-tool), so the rows it reads are the rows that check printed.

> **Bound the agent's reads**
>
> `--safe` on the same `relate serve` command refuses a read with no `LIMIT` and no aggregate, so an exploratory `MATCH (n) RETURN n` can't pull the whole graph into the model's context. The refusal comes back as a tool error that says to add `LIMIT <n>`.

## Replay the agent's calls

An agent run with these options touches the graph only through `tools/call` requests: the model picks one of the two tools and its arguments, the SDK sends the request to the server, and the reply goes back to the model as the tool's result. The trace below replays the calls this page's question needs, one at a time, in the order this page checks them: the schema, the query from [Call the query tool](#call-the-query-tool), the delete that tool refuses, and the count from [Verify](#verify). It is a replay of those calls, not a transcript of a model run. No model chose them, and a model given the same prompt may make fewer calls, or other ones.

The trace runs in your browser, on an in-memory store holding the same graph as `./team`. This cell builds it with the statement from `team.cypher`:

```cypher run name=team
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
```

```table
(no rows) · nodes created: 5, relationships created: 5, properties set: 13, labels added: 5
```

Each step shows the request the SDK sends for `mcp__relate__<tool>` and what relate's tool returns for it, built from the engine's result:

```json data=agent-calls
{"steps": [
  {"tool": "get_schema",
   "note": "`mcp__relate__get_schema` with no arguments: the labels, their property keys and types, and the relationship types between them, which is what the agent can read about the graph before it writes a statement.",
   "arguments": {}},
  {"tool": "read_cypher",
   "note": "`mcp__relate__read_cypher` with the statement and `$project` of the request in Call the query tool: who works on `relate`, and since when.",
   "arguments": {"statement": "MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name, w.since AS since ORDER BY since",
    "parameters": {"project": "relate"}}},
  {"tool": "read_cypher",
   "note": "The delete sent to `read_cypher` in Call the query tool. The server's gate reads the statement's plan, finds a write, and answers with a tool error; the statement never runs.",
   "arguments": {"statement": "MATCH (p:Person {name: $name}) DETACH DELETE p",
    "parameters": {"name": "Ada"}},
   "expect": "error"},
  {"tool": "read_cypher",
   "note": "The count from Verify, sent as a tool call: the three people are all still there after the refused delete.",
   "arguments": {"statement": "MATCH (p:Person) RETURN count(p) AS people"}}
]}
```

The trace starts on the database the cells up to `team` build. Each step below ran when the page was recorded, one after another on that database, through the mapping the MCP tryout uses; each result is what relate serve's tool returns for that call, built from the engine's result. No agent and no server took part in the recording.

**Step 1 of 4: `get_schema`.** `mcp__relate__get_schema` with no arguments: the labels, their property keys and types, and the relationship types between them, which is what the agent can read about the graph before it writes a statement.

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_schema","arguments":{}}}
```

| Label   | Nodes | Properties                       | Relationships out                                          |
| ------- | ----- | -------------------------------- | ---------------------------------------------------------- |
| Person  | 3     | name (String), role (String)     | WORKS_ON to Project; KNOWS to Person; MAINTAINS to Project |
| Project | 2     | language (String), name (String) | -                                                          |

**Step 2 of 4: `read_cypher`.** `mcp__relate__read_cypher` with the statement and `$project` of the request in Call the query tool: who works on `relate`, and since when.

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name, w.since AS since ORDER BY since","parameters":{"project":"relate"}}}}
```

| name  | since |
| ----- | ----- |
| Grace | 2023  |
| Ada   | 2024  |

**Step 3 of 4: `read_cypher`.** The delete sent to `read_cypher` in Call the query tool. The server's gate reads the statement's plan, finds a write, and answers with a tool error; the statement never runs.

```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (p:Person {name: $name}) DETACH DELETE p","parameters":{"name":"Ada"}}}}
```

Tool error: `Only MATCH queries are allowed for read-query`.

**Step 4 of 4: `read_cypher`.** The count from Verify, sent as a tool call: the three people are all still there after the refused delete.

```json
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"MATCH (p:Person) RETURN count(p) AS people"}}}
```

| people |
| ------ |
| 3      |

Live: Step sends the next call and Play all sends the rest, each to the page's database in your browser on the store the cells build up to the trace's starting cell, and each step shows the result `relate serve`'s tool returns for that call, built from the engine's live result; a trace that would write twice starts on an emptied database, and the database is emptied again when the trace ends, so the page's cells run on what they write.

The second step's rows are the ones [Call the query tool](#call-the-query-tool) printed from the server, and the third step's error is the same text. A server opened with `--read-only` lists no `write_cypher`, so the trace has no write that succeeds.

## Verify

The agent's calls leave the store as it was. A read-only count from the CLI shows all three people, Ada included, after the refused delete:

```bash verify
relate --database ./team --read-only --mode envelope "MATCH (p:Person) RETURN count(p) AS people"
```

```text
{"columnTypes":["INTEGER"],"columns":["people"],"notifications":[],"planCache":"miss","rows":[{"people":3}],"timings":{"elapsed_ms":0.31437499999999996,"execute_ms":0.14966600000000002,"lex_parse_ms":0.028709,"lower_ms":0.0425,"parse_ms":0.105292,"plan_ms":0.051417,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.00825},"truncated":false}
```

## If it goes wrong

| Message                                                                                                                                   | Cause                                                                               | Remedy                                                                                                                                                  |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Only MATCH queries are allowed for read-query` (a tool error)                                                                            | The agent sent a write or a schema statement to `read_cypher`                       | Expected on a read-only server. To let the agent write, serve the store without `--read-only` and add `mcp__relate__write_cypher` to the allowed tools. |
| `Error: semantic error: ParameterMissing: Expected parameter(s): name` (a tool error)                                                     | The statement uses `$name`, and the call's `parameters` has no `name`               | Pass every `$parameter` in the call's `parameters` object.                                                                                              |
| `Error: Failed to open Relate: storage error: read-only open requires an existing database at <path>` on the server's stderr, exit code 1 | `--database` names a store that doesn't exist; a read-only open never creates one   | Create the store first, or fix the path. A relative path resolves against the directory the server starts in, so an absolute path avoids surprises.     |
| `Error: Failed to open Relate: storage locked: <path>` on the server's stderr, exit code 1                                                | `--database` names a single-file `.rl` store that another process holds for writing | Serve a directory store, which allows a read-only open beside a writer, or stop the writer first.                                                       |
| The agent's answers miss recent writes (no error)                                                                                         | Plain `--read-only` serves the store as it was when the server started              | Serve a directory store with `--read-only=live`, or restart the server.                                                                                 |

The [errors reference](https://relatedb.ai/docs/reference/errors/) lists every MCP refusal, with its JSON-RPC code where it has one.

## Related

- [Connect over MCP](https://relatedb.ai/docs/guides/agents/connect-over-mcp/): the same server for Claude Code, Cursor, and other agent hosts.
- [Keep an agent safe](https://relatedb.ai/docs/guides/agents/agent-safety/): read-only opens, safe mode, backups, and the permission matrix.
- [CLI reference](https://relatedb.ai/docs/reference/cli/): `relate serve` and every other command, with their flags.
- [Agent memory](https://relatedb.ai/cookbook/agent-memory/): a worked example of a graph an agent writes to and recalls from.

## Terms used on this page

- **store**: A relate database: a directory, a single `.rl` file, or `:memory:`, opened with `--database`.
- **MCP**: The Model Context Protocol, which agent hosts use to call tools; `relate serve --mcp` exposes a store as `read_cypher`, `get_schema` and `write_cypher`.
- **directory store**: A store kept as a folder of snapshot files, a WAL folder and lock files; the layout for any `--database` path that is not `:memory:` and does not end in `.rl`.
- **read-only**: An open mode (`--read-only`) that never writes, checkpoints or migrates the store; writes fail, and a directory store can be read beside its writer.
