Docs
View as markdown

Connect over MCP

Serve a relate store over MCP to Claude Code, Claude Desktop, Cursor, Codex, Windsurf, Gemini CLI, Cline or OpenCode, every config block opening it --read-only.

mcpcliagents

Before you start: Install relate

This guide shows you how to connect an agent host to a relate store over 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. Read more in the glossary (the Model Context Protocol), with the store opened 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. Read more in the glossary. Every host on this page starts the same command, relate serve --mcp stdio --database <store> --read-only; only the file it lives in differs.

Prerequisites

  • relate on your PATH: relate --version prints its version (Install relate).
  • A store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary that already exists, a .rl file or a directory. A read-only open never creates one.
  • One of the hosts in Add the server to your host.

Choose a transport

relate serve --mcp takes either the keyword stdio or a port number. Both serve the same tools.

--mcp stdio--mcp <port>
Who starts itThe host, as a child process, until it closes stdinYou, as a long-running process, until SIGINT or SIGTERM
AuthenticationNone: the host spawned the process. --auth with stdio is a usage errorOne basic-auth credential from --auth or RELATE_AUTH; without either, a generated token printed at startup
Where it listensstdin and stdout127.0.0.1:<port> by default (--host); plaintext HTTP, no TLS
Which hostsEvery host on this pageHosts that connect to a URL

Opened with --read-only, the server lists two tools, read_cypher and get_schema, and takes no writer lease. write_cypher is listed only when the store is writable. A directory store can be served read-only beside a process that writes it; a .rl file opens only while no writer holds it. relate serve flags lists every flag.

What the agent sees

Once the host has started the server, the agent calls its tools with tools/call requests, and each reply carries the result as JSON text. The tryout below sends those calls to an in-browser database on this page that holds the same graph as the graph.rl that Verify creates, three people and two projects:

Cypher
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)
Recorded result
(no rows) · nodes created: 5, relationships created: 5, properties set: 13, labels added: 5

These calls ran when the page was recorded, on a database that holds what this page's cells write, through the same mapping the live tryout uses. Timings show as <ms>; live, each reply carries the timings of its own run. The server lists write_cypher, read_cypher, get_schema.

Live, the tryout starts as a server started --read-only, as every configuration on this page opens the store; with Serve read-only off it lists write_cypher as well.

get_schema: the request a host sends, then the reply.

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_schema","arguments":{}}}
{"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"{\"Person\":{\"count\":3,\"properties\":{\"name\":{\"type\":\"String\"},\"role\":{\"type\":\"String\"}},\"relationships\":{\"KNOWS\":{\"direction\":\"out\",\"labels\":[\"Person\"]},\"MAINTAINS\":{\"direction\":\"out\",\"labels\":[\"Project\"]},\"WORKS_ON\":{\"direction\":\"out\",\"labels\":[\"Project\"],\"properties\":{\"since\":{\"type\":\"Integer\"}}}},\"type\":\"node\"},\"Project\":{\"count\":2,\"properties\":{\"language\":{\"type\":\"String\"},\"name\":{\"type\":\"String\"}},\"type\":\"node\"}}","type":"text"}],"isError":false}}
LabelNodesPropertiesRelationships out
Person3name (String), role (String)WORKS_ON to Project; KNOWS to Person; MAINTAINS to Project
Project2language (String), name (String)-

read_cypher: the request a host sends, then the reply.

{"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"}}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"{\"columnTypes\":[\"STRING\",\"INTEGER\"],\"columns\":[\"name\",\"since\"],\"elapsed_ms\":<ms>,\"error\":null,\"execute_ms\":<ms>,\"lex_parse_ms\":<ms>,\"lower_ms\":<ms>,\"notifications\":[{\"category\":\"PERFORMANCE\",\"code\":\"Relate.Notification.Statement.MissingIndex\",\"description\":\"the equality predicate on name filters :Project without a range index on (:Project {name})\",\"gql_status\":\"03C91\",\"operator_id\":1,\"remedy\":\"CREATE INDEX relate_Project__name FOR (n:Project) ON (n.name)\",\"severity\":\"INFORMATION\",\"title\":\"Missing index\"}],\"parse_ms\":<ms>,\"plan\":{\"profiled\":false,\"root\":{\"actual\":null,\"children\":[{\"actual\":null,\"children\":[],\"details\":\"(:Project)<-[w:WORKS_ON]-(p:Person)\",\"estimated_rows\":null,\"identifiers\":[],\"operator\":\"Expand\",\"warnings\":[{\"category\":\"PERFORMANCE\",\"code\":\"Relate.Notification.Statement.MissingIndex\",\"description\":\"the equality predicate on name filters :Project without a range index on (:Project {name})\",\"gql_status\":\"03C91\",\"remedy\":\"CREATE INDEX relate_Project__name FOR (n:Project) ON (n.name)\",\"severity\":\"INFORMATION\",\"title\":\"Missing index\"}]}],\"details\":\"2 item(s), ordered\",\"estimated_rows\":null,\"identifiers\":[],\"operator\":\"ProduceResults\",\"warnings\":[]},\"schema\":\"relate.plan/v1\",\"statement\":\"MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name, w.since AS since ORDER BY since\"},\"plan_cache\":\"miss\",\"plan_ms\":<ms>,\"result_generation_ms\":<ms>,\"rewrite_ms\":<ms>,\"rows\":[{\"name\":\"Grace\",\"since\":2023},{\"name\":\"Ada\",\"since\":2024}],\"stats\":{\"constraints_added\":0,\"constraints_removed\":0,\"indexes_added\":0,\"indexes_removed\":0,\"labels_added\":0,\"labels_removed\":0,\"nodes_created\":0,\"nodes_deleted\":0,\"properties_set\":0,\"relationships_created\":0,\"relationships_deleted\":0}}","type":"text"}],"isError":false}}
namesince
Grace2023
Ada2024

write_cypher: the request a host sends, then the reply.

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"CREATE (:Person {name: $name, role: $role})","parameters":{"name":"Eve","role":"engineer"}}}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"{\"columnTypes\":[],\"columns\":[],\"elapsed_ms\":<ms>,\"error\":null,\"execute_ms\":<ms>,\"lex_parse_ms\":<ms>,\"lower_ms\":<ms>,\"notifications\":[],\"parse_ms\":<ms>,\"plan\":{\"profiled\":false,\"root\":{\"actual\":null,\"children\":[],\"details\":\"(:Person)\",\"estimated_rows\":null,\"identifiers\":[],\"operator\":\"Create\",\"warnings\":[]},\"schema\":\"relate.plan/v1\",\"statement\":\"CREATE (:Person {name: $name, role: $role})\"},\"plan_cache\":\"miss\",\"plan_ms\":<ms>,\"result_generation_ms\":<ms>,\"rewrite_ms\":<ms>,\"rows\":[],\"stats\":{\"constraints_added\":0,\"constraints_removed\":0,\"indexes_added\":0,\"indexes_removed\":0,\"labels_added\":1,\"labels_removed\":0,\"nodes_created\":1,\"nodes_deleted\":0,\"properties_set\":2,\"relationships_created\":0,\"relationships_deleted\":0}}","type":"text"}],"isError":false}}

Counters: nodes_created 1, properties_set 2, labels_added 1; rows is empty, as write_cypher returns counters.

write_cypher on a server started --read-only: it lists only read_cypher and get_schema, and answers a write_cypher call with a tool error before the engine sees the statement. This run is recorded from relate 0.1.2 (errors.json mcp-unknown-tool, replayed against the binary by docs:cli-check); the page quotes it as the binary printed it:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_cypher","arguments":{"query":"CREATE (:B)"}}}' | relate serve --mcp stdio --database ./graph.rl --read-only
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"relate-mcp","version":"0.1.2"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"unknown tool 'write_cypher'","type":"text"}],"isError":true}}

Transport answers (HTTP 401, 403 and 415, the body cap, the header deadline) are not part of the tryout; the MCP server reference lists them.

Live: each call is a button that sends its tools/call request to the page's database in your browser and shows the request and the reply, built from the engine's result the way relate serve --mcp builds it, with the statement and parameters editable; "Serve read-only" drops write_cypher from the tool list, as --read-only does, and shows the engine's own refusal of the write on a read-only database beside the server's recorded answer.

In a browser the tryout starts with Serve read-only on, because every block in the next section opens the store read-only: the tool list has no write_cypher, and a call to it is refused before the engine sees it. Turned off, write_cypher writes to this page's in-browser database and nothing else. The rows and the plan come from the engine; apart from the timings, a host gets the same reply from relate serve.

Add the server to your host

Each block starts relate on stdio against graph.rl, read-only. Host configuration formats change between releases. Each host's block follows that host's own documentation as of 2026-11-07.

A relative store path resolves against the directory the host starts the server in, which is not the same for every host. Blocks for a host that starts servers from your project folder use ./graph.rl; the others use <absolute-path>/graph.rl, which works everywhere.

Claude Code

A person adds the server with claude mcp add. An agent working in the repository writes the same server into .mcp.json at the project root, a file the person can review and commit.

At a terminal

claude mcp add relate -- relate serve --mcp stdio --database ./graph.rl --read-only

To allow writes, remove --read-only.

That adds the server for you in the current project (the default local scope, kept in ~/.claude.json). With --scope project, Claude Code writes the same entry to .mcp.json at the project root instead, so it can be committed.

From an agent

.mcp.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "./graph.rl", "--read-only"]
    }
  }
}

To allow writes, remove "--read-only" from args.

The entry is the one claude mcp add --scope project writes. Claude Code asks before it uses a project-scoped server from .mcp.json, so the person approves the server the agent added.

claude mcp list shows each server with its health status, and /mcp inside a session shows its tools.

Claude Desktop

The file is claude_desktop_config.json: ~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows, ~/.config/Claude/ on Linux. Settings, then Developer, then Edit Config opens it.

claude_desktop_config.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]
    }
  }
}

To allow writes, remove "--read-only" from args.

Quit and restart Claude Desktop after saving. The server's stderr, including relate's startup lines, goes to mcp-server-relate.log in Claude Desktop's log folder (~/Library/Logs/Claude on macOS).

Cursor

The file is .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project. With --read-only in args, Cursor can query the graph and cannot write to it.

mcp.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]
    }
  }
}

To allow writes, remove "--read-only" from args.

The same server entry as a Cursor install link, generated from the JSON shown with it:

The link carries the path as written, <absolute-path> included, so the entry needs the store's real path in mcp.json once it is added.

Codex

The file is ~/.codex/config.toml, or .codex/config.toml in a trusted project. The Codex CLI, the IDE extension and the ChatGPT desktop app share it.

config.toml
[mcp_servers.relate]
command = "relate"
args = ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]

To allow writes, remove "--read-only" from args.

The CLI writes the same entry:

codex mcp add relate -- relate serve --mcp stdio --database <absolute-path>/graph.rl --read-only

To allow writes, remove --read-only. codex mcp list and /mcp show the configured servers.

Windsurf

The file is ~/.codeium/windsurf/mcp_config.json, in the same shape as Claude Desktop's.

mcp_config.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]
    }
  }
}

To allow writes, remove "--read-only" from args.

As of 2026-11-07, Windsurf's English documentation redirects to Devin Desktop's. This page does not describe Devin Desktop's MCP configuration.

Gemini CLI

The file is .gemini/settings.json in the project, or ~/.gemini/settings.json for every project.

settings.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]
    }
  }
}

To allow writes, remove "--read-only" from args.

/mcp in a session and gemini mcp list show each server and its tools. In a folder Gemini CLI does not trust, stdio servers show as disconnected until you trust it.

Cline

The file is cline_mcp_settings.json. In the IDE extension, the MCP Servers icon in the Cline panel, then the Configure tab, then Configure MCP Servers opens it; the Cline CLI reads ~/.cline/data/settings/cline_mcp_settings.json.

cline_mcp_settings.json
{
  "mcpServers": {
    "relate": {
      "command": "relate",
      "args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"],
      "disabled": false,
      "autoApprove": []
    }
  }
}

To allow writes, remove "--read-only" from args.

autoApprove lists the tools Cline runs without asking; empty, it asks for every call.

OpenCode

The file is opencode.json (or opencode.jsonc) in the project, or ~/.config/opencode/opencode.json for every project. Servers go under mcp.servers, and a local server's command is one array. OpenCode starts a local server in the project folder by default.

opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "relate": {
        "type": "local",
        "command": ["relate", "serve", "--mcp", "stdio", "--database", "./graph.rl", "--read-only"]
      }
    }
  }
}

To allow writes, remove "--read-only" from command. opencode mcp list shows each server and its connection state.

Serve over HTTP

If your host connects to a URL, or several clients share one server, start relate yourself on a port as in Serve over HTTP: relate serve --mcp 8000 --database ./graph.rl --read-only listens on 127.0.0.1:8000 and checks a credential from RELATE_AUTH or --auth, or else a token it generates for the user relate and saves beside the store as <store>.auth-token. The MCP server reference describes the transport and every answer.

In another terminal, register the URL with your host. For Claude Code, <credential> is the base64 of <user>:<password> (printf '%s' '<user>:<password>' | base64 prints it):

claude mcp add --transport http relate http://127.0.0.1:8000 --header "Authorization: Basic <credential>"

The server opened the store read-only, so this entry cannot write.

Verify

Check the server from a shell before a host is involved. This step creates a small graph.rl to check against, the shared example graph of three people and two projects:

relate --database ./graph.rl "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)"

Then send three JSON-RPC messages the way a host does, one per line: initialize, tools/list, and a write sent to read_cypher. The 2>/dev/null drops relate's startup lines on stderr:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"CREATE (:Person {name: \"Eve\"})"}}}' \
  | relate serve --mcp stdio --database ./graph.rl --read-only 2>/dev/null
{"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"}]}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"Only MATCH queries are allowed for read-query","type":"text"}],"isError":true}}

The tool list names read_cypher and get_schema and no write_cypher, and the write came back as a tool error with the store unchanged. In the host, the same two tools appear in its server list (/mcp in Claude Code, Codex and Gemini CLI).

For the HTTP transport, the same tools/list with curl:

curl -s -u <user>:<password> -H 'Content-Type: application/json' -X POST http://127.0.0.1:8000 -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

If it goes wrong

The host reports the server failed, and its log shows Error: Failed to open Relate: storage error: database not found: <path>. The read-only open never creates a store, and a relative path resolved against a directory you did not expect. Put the store's absolute path in the block, or create the store first.

The host cannot start relate at all. The host's PATH does not include the directory relate is installed in (~/.local/bin for install.sh), which is common for desktop apps. Put the binary's absolute path in command.

A write fails with Only MATCH queries are allowed for read-query, or unknown tool 'write_cypher'. The store is open read-only, as configured. To let the agent write, remove --read-only; Keep an agent safe covers what to put in place first.

HTTP answers 401 Unauthorized or 415 Unsupported Media Type. A 401 carries WWW-Authenticate: Basic realm="relate": the request has no credential, or the wrong one. A 415 means the POST had no Content-Type, or a browser-simple one (text/plain or a form type).

relate serve exits 2 with an error that begins Error: serve --mcp stdio skips auth. --auth applies to the port listeners only. Drop it from a stdio block.