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 --versionprints its version (Install relate). - A store A relate database: a directory, a single
.rlfile, or:memory:, opened with--database. Read more in the glossary that already exists, a.rlfile 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 it | The host, as a child process, until it closes stdin | You, as a long-running process, until SIGINT or SIGTERM |
| Authentication | None: the host spawned the process. --auth with stdio is a usage error | One basic-auth credential from --auth or RELATE_AUTH; without either, a generated token printed at startup |
| Where it listens | stdin and stdout | 127.0.0.1:<port> by default (--host); plaintext HTTP, no TLS |
| Which hosts | Every host on this page | Hosts 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:
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)(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}}| 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) | - |
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}}| name | since |
|---|---|
| Grace | 2023 |
| Ada | 2024 |
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-onlyTo 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
{
"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.
{
"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.
{
"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:
relate to Cursor with this configuration; its link is generated from it:{
"command": "relate",
"args": ["serve", "--mcp", "stdio", "--database", "<absolute-path>/graph.rl", "--read-only"]
}The link as text
cursor://anysphere.cursor-deeplink/mcp/install?name=relate&config=eyJjb21tYW5kIjoicmVsYXRlIiwiYXJncyI6WyJzZXJ2ZSIsIi0tbWNwIiwic3RkaW8iLCItLWRhdGFiYXNlIiwiPGFic29sdXRlLXBhdGg%2BL2dyYXBoLnJsIiwiLS1yZWFkLW9ubHkiXX0%3DThe 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.
[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-onlyTo 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.
{
"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.
{
"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.
{
"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.
{
"$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.
Related
- Connect your agent to relate: install, rehearse a change, then connect over MCP.
- Keep an agent safe: read-only opens, safe mode, and backups before an agent writes.
- MCP errors: every MCP refusal and JSON-RPC error code.
- Run relate in a sandbox: what to allow when the host runs inside one.
- CLI reference:
relate serveand its flags. - Use relate with the Claude Agent SDK: the same read-only server, started by the SDK.