Roam Research
A Roam Research MCP server (and CLI) that lets AI assistants or scripts read, search, create, update, and organize blocks, pages, tasks, tables, outlines, and memories in one or more Roam graphs.
Fetch content: Read pages by title in markdown/raw/structure form, fetch blocks with children and ancestors, get full page views with linked references, and list namespace sub-pages.
Create content: Create pages with mixed text/table content, build nested outlines, create Roam tables, import nested markdown, and add TODO items to today’s daily page.
Update content: Rewrite pages with smart diffing that preserves block UIDs, move blocks, rename pages, and batch create/update/move/delete actions with UID placeholders.
Search the graph: Full-text search, tag search, TODO/DONE status search, block-reference/backlink search, hierarchy search, date-based search, pages modified today, and raw Datalog queries with optional regex filtering.
Agent memory: Store and recall tagged memories on daily pages, with categories, headings, parent blocks, and filtering/sorting.
Graph conventions and syntax help: Retrieve per-graph agent guidelines plus safety rules, and load the full Roam markdown cheatsheet.
Multi-graph support: Target named graphs from
ROAM_GRAPHS, use a default graph, and pass awrite_keyfor protected non-default graphs.Safety controls: Honors
#.rm-hide/#.rm-privatefiltering, preserves hidden blocks during page rewrites, supportsdry_runpreviews, and marks destructive/write tools with annotations.
Supports loading configuration from .env files for managing environment variables like API tokens and graph names.
Enables parsing and conversion of markdown content, with support for importing nested markdown structures into Roam Research with proper hierarchy preservation.
Provides comprehensive access to Roam Research's API functionality, allowing AI assistants to interact with Roam Research graphs through tools for fetching, creating, and updating pages and blocks, importing markdown, searching content, and executing Datalog queries.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Roam Research search for notes about project planning from last month"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.

Roam Research MCP + CLI
Introduction
I created this project to solve a personal problem: I wanted to manage my Roam Research graph directly from Claude Code (and other LLMs). As I built the Model Context Protocol (MCP) server to give AI agents access to my notes, I realized the underlying tools were powerful enough to stand on their own.
What started as an backend for AI agents evolved into a full-featured Standalone CLI. Now, you can use the same powerful API capabilities directly from your terminal—piping content into Roam, searching your graph, and managing tasks—without needing an LLM at all.
Whether you want to give Claude superpowers over your knowledge base or just want a robust CLI for your own scripts, this project has you covered.

Related MCP server: Roam Research MCP Server
What's New in v5.0
In one line: roam save now has one flag for each way of naming a parent block, and neither one guesses. --parent-uid is always a UID; --parent is always text.
Until 4.1.0, --parent took either a block UID or heading text and decided which by looking for a digit in the value. About one Roam UID in five has no digit, so those saves were silently misrouted. 4.1.0 added --parent-uid and deprecated passing a UID to --parent. 5.0 finishes the split.
--parent-uid <uid>nests under the block with that UID. It acceptsuidor((uid)), and if no such block exists the command exits non-zero and writes nothing.--parent "<text>"nests under the block with that text on the target page, creating it if missing. It is never read as a UID.--parent "((uid))"finds or creates a block whose content is that reference, which was not reliably possible before.Creating a parent block is announced on stderr, with its UID. If the text you gave
--parentlooks like a UID, a second line says what happened and gives the--parent-uidcommand to use instead. Stdout is unchanged.roam save --helpleads with the choice. A "Choosing a parent block" section sits above the examples, so the difference between the two flags is the first thing you read.
Why a major. roam save --parent "((uid))" and roam save --parent <uid> used to nest under that block and now nest under a block containing that text. If a script passes a UID to --parent, change the flag to --parent-uid. Only the CLI is affected: the MCP tools take parent-uid as a typed field and behave exactly as before, so if you use the server through an AI assistant, nothing is required of you.
Full detail is in the changelog.
What's New in v4.0
In one line: a block containing a soft line break (Shift+Enter) now survives a page rewrite. Read a page, write it back, and nothing moves.
Until now, a multi-line block rendered as two physical lines, the second at column 0. That reset the parser's indentation baseline, so every block after it collapsed toward the root and roam_update_page_markdown dutifully generated the moves to make your real page match. Reading a page and writing back a revision, the documented purpose of the tool, was enough to trigger it. Callout bodies and fenced code blocks are exactly the blocks that carry soft breaks.
Soft breaks render as
⏎. A page containing one gains a leading<!-- roam:escaped-newlines -->marker line; keep it if you write the markdown back. Pages with no multi-line block render byte-identical to 3.x, with no marker and no encoding.Backslashes are never special. The earlier design escaped newlines as
\n, which is also a common prefix in authored text:\nabla,\neq,C:\newdir. The sentinel needs no such rule, so all of those are written exactly as typed, everywhere.Verbatim round-trips are no-ops. Renderer output submitted back unchanged, title header included, produces zero actions. The CLI shares the fix:
roam getpiped intoroam save --updateleaves the page as it was.Two more guards on page rewrites. Non-empty markdown that parses to zero blocks is now refused rather than deleting every block on the page (genuinely empty markdown still clears a page, as documented). And a hand-authored first block that happens to be an H1 echoing the page title is no longer stripped on an ordinary update.
Linked references are encoded too.
roam_fetch_page_full_viewescapes soft breaks in referring blocks and breadcrumbs, not just the page's own content.Browser clients pass CORS preflight. The HTTP transport now allows
MCP-Protocol-VersionandLast-Event-ID, both of which a client must send after initialization. Non-browser clients were never affected.The MCP SDK is pinned to the version the test suite runs against, so a fresh install gets the protocol surface that was tested rather than whatever npm serves that day.
Why a major. Four read surfaces return different bytes for any page containing a multi-line block: roam_fetch_page_by_title (format: "markdown"), roam_fetch_page_full_view, roam_get_subpages, and roam get. If you use the server through an AI assistant, nothing is required of you. A script that parses markdown output of multi-line pages sees the new encoding. If you pinned roam-research-mcp@3, you keep 3.2.0's fixes and its documented multi-line limitation until you re-pin.
Full detail, including the corner cases and how each fix was verified against the prior state, is in the changelog.
How this differs from Roam's official MCP server
Roam Research ships its own MCP server and CLI (@roam-research/roam-mcp). It is a good tool, and this project is not trying to replace it. They talk to two different Roam APIs, which is the difference everything else follows from.
This project | Official | |
Talks to | Roam's backend REST API (graph token + graph name) | Roam Desktop's local HTTP API |
Needs Roam running | No — works headless | Yes, the desktop app must be open (it deep-links to launch it) |
Where it can run | Anywhere: laptop, server, container, CI | The machine running Roam Desktop |
Shared daemon | Yes — | Per-client stdio |
Multi-graph |
|
|
Web-only graphs | Works | Desktop only |
Reach for the official server when you want Roam's own supported path, or you need things only the running app can do: controlling the Desktop UI (open a page, read the current selection, drive the sidebar), semantic/embeddings search, link suggestions, file upload, comments, or invoking tools that Roam extensions register.
Reach for this one when Roam isn't running or isn't installed — a server, a container, a cron job, a CI step. Or when you want the extras this project has grown: a full standalone CLI with stdin piping, a shared HTTP daemon with optional bearer auth, smart page diffing that preserves block UIDs (and therefore your block references), batch operations with UID placeholders for building nested structures in one call, and agent memory tools.
One deliberate omission: there is no page-delete tool here. Roam has no undo that can reverse a bulk API deletion. The official server does offer delete_page; this project takes the more conservative line.
They interoperate
The two servers share conventions on purpose, so running both costs you nothing:
[[roam/agent guidelines]]— both read the same page for your conventions. Write them once; both honour them. See Agent guidelines.#.rm-hide/#.rm-private— both withhold tagged blocks from AI-facing content. Tag once, hidden from both. See Hiding content from the AI.
Standalone CLI: roam
The roam CLI lets you interact with your graph directly from the terminal. It supports standard input (stdin) piping for all content creation and retrieval commands, making it perfect for automation workflows.
Quick Examples
# Save a quick thought to your daily page
roam save "Idea: A CLI for Roam would be cool"
# Pipe content from a file to a new page
cat meeting_notes.md | roam save --title "Meeting: Project Alpha"
# Create a TODO item on today's daily page
echo "Buy milk" | roam save --todo
# Prepend to top of page (newest-first ordering)
roam save -p "Changelog" --order first "v2.18.0 release"
# Search your graph and pipe results to another tool
roam search "important" --json | jq .
# Search for pages by namespace prefix
roam search --namespace "Convention" # Finds all Convention/* pages
# Fetch a page by title
roam get "Roam Research"
# Fetch daily pages using any date format (auto-normalized)
roam get today # Today's daily page
roam get 2026-03-21 # ISO date → "March 21st, 2026"
roam get "03/21/2026" # US date → "March 21st, 2026"
roam get "March 21" # Named (assumes current year)
# Fetch a block with ancestors (parent chain to page root)
roam get abc123def -a # Block + children + ancestors
roam get abc123def -a -d 0 # Ancestors only, no children
# Fetch page by UID or Roam URL
roam get page abc123def
roam get page "https://roamresearch.com/#/app/my-graph/page/abc123def"
# Sort and group results
roam get --tag Project --sort created --group-by tag
# Find references (backlinks) to a page
roam refs "Project Alpha"
# Update a block (e.g., toggle TODO status)
roam update ((block-uid)) --todo
# Multi-graph: read from a specific graph
roam get "Page Title" -g work
# Multi-graph: write to a protected graph
roam save "Note" -g work --write-key "$ROAM_SYSTEM_WRITE_KEY"Available Commands: get, search, save, refs, update, batch, rename, status, server.
Run roam <command> --help for details on any command.
Installation
npm install -g roam-research-mcp
# The 'roam' command is now available globallyMCP Server Tools
The MCP server exposes these tools to AI assistants (like Claude), enabling them to read, write, and organize your Roam graph intelligently.
Multi-Graph Support: All tools accept optional
graphandwrite_keyparameters. Usegraphto target a specific graph from yourROAM_GRAPHSconfig, andwrite_keyfor write operations on protected graphs.
Tool Name | Description |
| Fetch page content by title. |
| Fetch a page's content plus all linked references with breadcrumb context and children. |
| Fetch a block by UID with optional children (depth) and/or ancestors (up to page root). |
| Create new pages, optionally with mixed text and table content. |
| Update a page using smart diff (preserves block UIDs). |
| List sub-pages under a namespace prefix (e.g. "Project/") with optional tag filter. |
| Full-text search across the graph or within specific pages. Supports namespace prefix search for page titles. |
| Find blocks that reference a page, tag, or block UID. |
| Find TODO or DONE items. |
| Find blocks containing specific tags (supports exclusion). |
| Find blocks/pages by creation or modification date. |
| List pages modified since midnight. |
| Add TODO items to today's daily page. |
| Create properly formatted Roam tables. |
| Create hierarchical outlines. |
| Execute multiple low-level actions (create, move, update, delete) in one batch. |
| Move a block to a new parent or position. |
| specialized tools for AI memory management within Roam. |
| Execute raw Datalog queries for advanced filtering. |
| Retrieve the Roam-flavored markdown reference. |
| Retrieve this graph's user-defined agent conventions. |
Structured results from write tools (v3.0.0+)
The ten write tools declare an outputSchema and return structuredContent — a validated object — alongside the usual text. A client can read page_uid, uid_map or success directly instead of hunting for JSON inside a string, which makes chaining calls more reliable:
// roam_process_batch_actions
{ "success": true, "uid_map": { "parent1": "Xk7mN2pQ9" },
"validation_passed": true, "actions_attempted": 4 }Three things worth knowing:
Nothing was taken away. The text channel is unchanged, so a client that ignores
structuredContentbehaves exactly as before.Read tools deliberately have neither. They already serialise their whole result into the text channel, so a schema would just double the payload.
These fields are additive-only. Some clients validate live responses against a cached tool list, so a field will be added or deprecated — never renamed or removed outside a major version.
Upgrading to 5.0.0: CLI only.
roam save --parentis now always text, so a script passing a block UID to it (--parent "((uid))"or--parent <uid>) must switch to--parent-uid. Left unchanged, such a call exits 0 and nests your content under a new block containing the reference, on the daily page unless-pis given. MCP tools are unaffected. See the changelog.
Upgrading to 4.0.0: markdown reads of a page containing a soft line break (Shift+Enter) now render that break as
⏎and carry a leading<!-- roam:escaped-newlines -->marker, so the page survives a write-back intact. Pages without multi-line blocks are byte-identical to 3.x. AI-assistant users need do nothing; scripts parsing markdown output of multi-line pages see the new encoding. See the changelog.
Upgrading from 2.x: three write-result fields were renamed —
uid→page_uid(roam_create_page),created_uids→created_blocks(roam_create_outline,roam_import_markdown) andpreservedUids→preserved_uids(roam_update_page_markdown). This only affects code that reads those names; if you use the server through an AI assistant, nothing changes. See the changelog for why.
Agent guidelines (per-graph)
roam_get_guidelines reads a page inside the graph — [[roam/agent guidelines]] by default — holding your own conventions: how you tag, how you namespace pages, what an agent should never do. Roam's official MCP server reads the same page title, so one page serves both.
This is distinct from CUSTOM_INSTRUCTIONS_PATH, and the two compose:
|
| |
Lives in | a file on disk | a page in the graph |
Scope | server-wide, all graphs | per-graph |
To change it | edit the file, restart the server | edit the page |
Answers | how to write Roam markdown | how this user wants this graph handled |
Just create the page. With no configuration at all, roam_get_guidelines reads [[roam/agent guidelines]] — the same title Roam's own server reads, so writing it once makes both honour it. Creating a page with that exact namespaced title is the opt-in; nothing is read from the graph unless an agent explicitly calls the tool.
If the page doesn't exist, the tool returns exists: false rather than failing, so it is always safe to call.
It also returns the rules that aren't yours to set
Alongside your conventions, every roam_get_guidelines response carries a roamSyntax field: the short list of things that destroy content — roam_update_page_markdown deleting every block your markdown omits, truncated structure previews written back as if they were content, block references retyped as plain text — plus a caution that reads silently exclude #.rm-hide subtrees, and the handful of places Roam's markdown inverts standard markdown.
Two reasons it rides here rather than in the cheatsheet. It reaches every client, including one that never calls roam_markdown_cheatsheet; and it is returned even when a graph has no guidelines page, which is exactly the case where an agent has least context. The layering is deliberate: your conventions win on style, roamSyntax wins on data safety. No convention can make a truncated preview complete.
The full syntax reference — components, queries, embeds, tool selection — stays in roam_markdown_cheatsheet. roamSyntax is ~800 tokens and deliberately capped.
Each graph can point at a different page, or turn it off:
ROAM_GRAPHS='{
"personal": {"token": "...", "graph": "..."},
"work": {"token": "...", "graph": "...", "guidelinesPage": "work/agent rules"},
"private": {"token": "...", "graph": "...", "guidelinesPage": false}
}'
ROAM_GUIDELINES_PAGE='team/agent guidelines' # change the default for every graphResolution order is per-graph guidelinesPage → ROAM_GUIDELINES_PAGE → roam/agent guidelines. Above: personal uses the env override, work uses its own page, and private has guidelines off entirely. Only an explicit false disables it — an unset value never does.
Results are cached for 30 seconds — an edit to the page takes effect without a restart. A starter template lives at .roam/agent-guidelines.template.md.
Note that guidelines are read through the normal page path, so blocks tagged #.rm-hide / #.rm-private are withheld from them too — see below.
Reads of a page containing a soft line break render it as ⏎ so each block
stays on one line — an unescaped newline lands at column 0 and reparents
everything after it on write-back. Such payloads carry a leading
<!-- roam:escaped-newlines --> marker; keep it if you write the markdown
back. Markdown you author is never decoded: backslashes are not special, and
only ⏎ inside a marked payload is interpreted. roam_get_guidelines output
is plain prose — no sentinel, no marker.
Hiding content from the AI
Blocks tagged #.rm-hide or #.rm-private — and everything nested under them — are omitted from the content these tools return. Both the hashtag (#.rm-hide, #[[.rm-hide]]) and link ([[.rm-hide]]) forms work. .rm-private is Roam's existing "hidden from other users" tag; .rm-hide hides from the AI specifically.
This follows the same convention as Roam's official MCP server, so a block tagged for one is hidden from the other.
Applied to: roam_fetch_page_by_title, roam_fetch_block, roam_fetch_page_full_view, roam_get_subpages, roam_search_by_text, roam_search_for_tag, roam_search_by_status, roam_search_block_refs, roam_search_hierarchy, roam_search_by_date.
Hidden blocks are also excluded from the page-rewrite diff, which is what stops them being deleted for being absent from markdown the agent could not have written. roam_update_page_markdown (and roam save --update) replaces a page with what you give it, deleting whatever your markdown omits — so its baseline is pruned by this same filter, on the rule that the baseline a diff deletes from must be the same page the caller was allowed to read. It reports preserved_hidden when it protected anything. Content is preserved; exact ordering relative to visible siblings may shift. This was a real data-loss bug before the fix — see the changelog.
This is a convenience filter, not a security guarantee. roam_datomic_query reads the database directly and deliberately does not apply it, so a capable agent can still surface hidden blocks through raw Datalog. Treat these tags as "keep it out of the AI's way," not "keep it secret."
Tag matching is case-insensitive, and only exact tags match — #.rm-hidden and #.rm-highlight are left alone. The set of hidden UIDs is cached for 30 seconds, so a block tagged just now may remain visible for up to that long.
Configuration
Environment Variables
Single Graph Mode
For a single Roam graph, set these in your environment or a .env file:
ROAM_API_TOKEN=your-api-token
ROAM_GRAPH_NAME=your-graph-nameMulti-Graph Mode (v2.0+)
Connect to multiple Roam graphs from a single server instance:
ROAM_GRAPHS='{
"personal": {"token": "token-1", "graph": "personal-db", "memoriesTag": "#[[Personal Memories]]"},
"work": {"token": "token-2", "graph": "work-db", "protected": true, "memoriesTag": "#[[Work Memories]]"},
"research": {"token": "token-3", "graph": "research-db"}
}'
ROAM_DEFAULT_GRAPH=personal
ROAM_SYSTEM_WRITE_KEY=your-secret-keyGraph Configuration Options:
Property | Required | Description |
| Yes | Roam API token for this graph |
| Yes | Graph name/database identifier |
| No | If |
| No | Tag for |
Two kinds of access control (and how they differ)
The server has two independent locks. They're easy to mix up because both are "keys" — here's the plain version (both are optional and off by default):
Bearer token — | Write key — | |
In a phrase | The key to the front door | The latch on a safe inside |
Controls | Who can reach the server at all | Whether a write to a |
Covers | Everything — reads and writes, all graphs | Only writes, and only to graphs marked |
Protects reading? | Yes | No |
When you need it | Only if the server is reachable beyond your own machine (e.g. | Whenever you want a guard against accidental edits to important graphs |
How it's sent | HTTP header: | A |
Think of a house: the bearer token locks the front door (keeps strangers out entirely), and the write key locks a safe inside (even someone already in the house needs it to change what's in the safe). On your own machine bound to 127.0.0.1, the front door faces a wall, and a server without a token also turns away web pages that try to reach it by DNS rebinding (see HTTP_ALLOWED_HOSTS below), so you don't need the bearer token there. The write key is still handy locally as an "are you sure?" guard, because Roam has no undo.
So: to mark a graph as needing the write key, set protected: true on it and configure ROAM_SYSTEM_WRITE_KEY; callers then pass a matching write_key for any write to that graph.
⚠️
protecteddoes nothing on your default graph. Writes to whichever graphROAM_DEFAULT_GRAPHnames are always allowed, beforeprotectedis ever consulted — the flag guards the graphs you have to ask for by name, on the reasoning that reaching for a non-default graph is the deliberate act worth confirming. If you want a graph write-guarded, it must not be your default.
Optional:
ROAM_MEMORIES_TAG: Default tag forroam_remember/roam_recall(fallback when per-graphmemoriesTagnot set).HTTP_STREAM_PORT: Port for the HTTP Stream transport (defaults to 8088).--servermode only — stdio mode opens no socket, so this is ignored there.HTTP_STREAM_HOST: Host to bind the HTTP transport to (defaults to127.0.0.1, loopback-only).--servermode only. Set to0.0.0.0to expose on the LAN, and setHTTP_AUTH_TOKENwhen you do.HTTP_AUTH_TOKEN: Optional bearer token that locks the whole HTTP endpoint. Unset = no token: the server then answers only requests addressed tolocalhost, an IP address, or a name inHTTP_ALLOWED_HOSTS, and only browser requests from an origin inCORS_ORIGIN. When set, every MCP request must sendAuthorization: Bearer <token>(GET /healthstays open). Use it whenever you bind beyond127.0.0.1. Different fromROAM_SYSTEM_WRITE_KEY— see Two kinds of access control.HTTP_ALLOWED_HOSTS: Hostnames a server withoutHTTP_AUTH_TOKENanswers to, beyondlocalhostand IP addresses (comma-separated; ports are ignored), e.g.roam-mcpfor a Docker service name ormybox.local. A request addressed to any other name gets a 403. That is what stops a web page from reaching the server by pointing its own hostname at127.0.0.1(DNS rebinding). Ignored whenHTTP_AUTH_TOKENis set.CORS_ORIGIN: Browser origins allowed to call the HTTP server (comma-separated; defaults tohttp://localhost:5678,https://roamresearch.com;*allows any). WithoutHTTP_AUTH_TOKEN, a request from any other origin gets a 403.
Running the Server
1. Default Mode (stdio) Best for local integration (e.g., Claude Desktop, IDE extensions). The MCP client launches the process per session and talks to it over stdin/stdout. No port is opened — nothing about MCP over stdio needs one.
Before 3.1.0 this mode also opened an HTTP listener, and bound it to every interface. If you were using that endpoint, run a
--serverdaemon instead; see below.
npx roam-research-mcp2. Shared Server Mode (--server)
Best for a single long-lived, HTTP-only daemon that multiple MCP clients share — instead of each session spawning its own subprocess. This saves memory and gives clients a stable URL.
HTTP_STREAM_PORT=8088 npx roam-research-mcp --serverOr manage it through the roam CLI, which adds start/stop/status/logs:
roam server start # start the shared daemon in the background
roam server start -H 0.0.0.0 # expose on the LAN (no transport auth!)
roam server status # is it up? version, graphs, active sessions
roam server logs -f # follow the log
roam server stop # stop a CLI-started daemonroam server status works no matter how the daemon was launched (it probes /health), so it also reports a daemon started by a LaunchAgent/systemd unit. State (pidfile + log) lives in ~/.roam/ (override with ROAM_HOME).
The two modes are mutually exclusive, and each opens exactly one transport: stdio mode speaks stdio and binds nothing, --server speaks HTTP and reads no stdin. In --server mode the server:
runs HTTP-only (no stdio transport),
binds the exact
HTTP_STREAM_PORTonHTTP_STREAM_HOSTand exits non-zero if the port is taken (no silent drift — a shared daemon must keep a stable URL),exposes
GET /health→{"status":"ok", ...}for liveness checks.
Point MCP clients at it with an HTTP transport config:
{
"mcpServers": {
"roam-research-mcp": {
"type": "http",
"url": "http://127.0.0.1:8088/mcp"
}
}
}Env vars (tokens, graphs) live with the server process, not the client config.
Securing an exposed server (two layers):
If you bind beyond loopback (-H 0.0.0.0), add the perimeter lock:
HTTP_AUTH_TOKEN=$(openssl rand -hex 32) roam server start -H 0.0.0.0Clients then send the token as a header:
{
"mcpServers": {
"roam-research-mcp": {
"type": "http",
"url": "http://<host>:8088/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}Keep both — they do different jobs (see Two kinds of access control above): the bearer token controls who can connect, the write key only guards writes to protected graphs.
⚠️ The write key is not a substitute for the bearer token. On an exposed server without
HTTP_AUTH_TOKEN, anyone on the network can still read every graph (and write non-protected ones). For anything beyond loopback, setHTTP_AUTH_TOKEN.
Keeping it running (macOS LaunchAgent):
Create ~/Library/LaunchAgents/com.example.roam-mcp.plist with RunAtLoad + KeepAlive, your env vars under EnvironmentVariables, and --server as the last ProgramArguments entry. Keep StandardOutPath/StandardErrorPath on a local path (e.g. ~/Library/Logs/), then:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.roam-mcp.plist
curl -s http://127.0.0.1:8088/health # verify3. Docker
docker build -t roam-research-mcp .
docker run --init -p 127.0.0.1:8088:8088 \
-e HTTP_STREAM_HOST=0.0.0.0 -e HTTP_STREAM_PORT=8088 \
--env-file .env roam-research-mcp --serverSet HTTP_AUTH_TOKEN to a random secret in .env before running; MCP requests must include Authorization: Bearer <token>. The published port is loopback-only by default. For remote access, follow Securing an exposed server and intentionally publish on the required host address.
The -e values win over --env-file, so the server listens on the published port whatever .env says. --init lets docker stop end the server straight away instead of killing it after 10 seconds.
Configuring in LLMs
Claude Desktop / Cline:
Add to your MCP settings file (e.g., ~/Library/Application Support/Claude/claude_desktop_config.json):
Pinning the version.
npx -y roam-research-mcpfetches the latest release every time your client starts the server, so a new major version arrives without warning. Pin the major to decide for yourself when to move:
argsYou get
["-y", "roam-research-mcp"]Latest, always — including the next major
["-y", "roam-research-mcp@3"]3.x only; majors need an edit here
["-y", "roam-research-mcp@3.0.0"]Exactly this build
Pinning the major is the sensible default: you still get fixes and new tools, but a breaking change becomes something you opt into. The examples below stay unpinned to match what most people paste in first.
Single Graph:
{
"mcpServers": {
"roam-research": {
"command": "npx",
"args": ["-y", "roam-research-mcp"],
"env": {
"ROAM_API_TOKEN": "your-token",
"ROAM_GRAPH_NAME": "your-graph"
}
}
}
}Multi-Graph:
{
"mcpServers": {
"roam-research": {
"command": "npx",
"args": ["-y", "roam-research-mcp"],
"env": {
"ROAM_GRAPHS": "{\"personal\":{\"token\":\"token-1\",\"graph\":\"personal-db\",\"memoriesTag\":\"#[[Memories]]\"},\"work\":{\"token\":\"token-2\",\"graph\":\"work-db\",\"protected\":true}}",
"ROAM_DEFAULT_GRAPH": "personal",
"ROAM_SYSTEM_WRITE_KEY": "your-secret-key"
}
}
}
}Query Block Parser (v2.11.0+)
A utility for parsing and executing Roam query blocks programmatically. Converts {{[[query]]: ...}} syntax into Datalog queries.
Supported Clauses
Clause | Syntax | Description |
Page ref |
| Blocks referencing a page |
Block ref |
| Blocks referencing a block |
|
| All conditions must match |
|
| Any condition matches |
|
| Exclude matches |
|
| Date range filter |
|
| Full-text search |
|
| Daily notes pages only |
|
| Created or edited by user |
|
| Created by user |
|
| Edited by user |
Relative Dates
The between clause supports relative dates: today, yesterday, last week, last month, this year, 7 days ago, 2 months ago, etc.
Usage
import { QueryExecutor } from 'roam-research-mcp/query';
const executor = new QueryExecutor(graph);
// Execute a query
const results = await executor.execute(
'{{[[query]]: "My Query" {and: [[Project]] {between: [[last month]] [[today]]}}}}'
);
// Parse without executing (for debugging)
const { name, query } = QueryParser.parseWithName(queryBlock);Utility Functions
import { isQueryBlock, extractQueryBlocks } from 'roam-research-mcp/query';
// Detect if text is a query block
isQueryBlock('{{[[query]]: [[tag]]}}'); // true
// Extract all query blocks from a string
extractQueryBlocks(pageContent); // ['{{[[query]]: ...}}', ...]Support
If this project helps you manage your knowledge base or build cool agents, consider buying me a coffee! It helps keep the updates coming.
License
MIT License - Created by Ian Shen.
Available Tools
25 toolsroam_add_todoA
Add a list of todo items as individual blocks to today's daily page in Roam. Each item becomes its own actionable block with todo status. NOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use alias syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: todos (required), graph, write_key. Returns: success
roam_create_outlineA
Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. To create a new page with an outline, use the roam_create_page tool instead. The outline parameter defines new blocks to be created. To nest content under an existing block, provide its UID or exact text in block_text_uid, and ensure the outline array contains only the child blocks with levels relative to that parent. Including the parent block's text in the outline array will create a duplicate block. Best for:
Adding supplementary structured content to existing pages
Creating temporary or working outlines (meeting notes, brainstorms)
Organizing thoughts or research under a specific topic
Breaking down subtopics or components of a larger concept Best for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider
roam_process_batch_actionsinstead. API Usage Note: This tool performs verification queries after creation. For large outlines (10+ items) or when rate limits are a concern, consider usingroam_process_batch_actionsinstead to minimize API calls.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: outline (required), graph, order, write_key, +2 more. Returns: success, page_uid, parent_uid, created_blocks
roam_create_pageA
Create a new standalone page in Roam with optional content, including structured outlines and tables, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:
Creating foundational concept pages that other pages will link to/from
Establishing new topic areas that need their own namespace
Setting up reference materials or documentation
Making permanent collections of information
Creating pages with mixed text and table content in one call. Efficiency Tip: This tool batches page and content creation efficiently. For adding content to existing pages, use
roam_process_batch_actionsinstead.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: title (required), graph, content, write_key. Returns: success, page_uid
roam_create_tableA
Create a table in Roam with specified headers and rows. This tool abstracts the complex nested structure that Roam tables require, making it much easier to create properly formatted tables.
Why use this tool:
Roam tables require precise nested block structures that are error-prone to create manually
Automatically handles the {{[[table]]}} container and nested column structure
Validates row/column consistency before execution
Converts empty cells to spaces (required by Roam)
Example: A table with headers ["", "Column A", "Column B"] and rows [{label: "Row 1", cells: ["A1", "B1"]}] creates a 2x3 table.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: rows (required), headers (required), parent_uid (required), graph, +2 more. Returns: error, success, uid_map, committed, +3 more
roam_datomic_queryARead-onlyIdempotent
Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam's query engine. Note: Roam graph is case-sensitive.
Optimal Use Cases for roam_datomic_query:
Advanced Filtering (including Regex): Use for scenarios requiring complex filtering, including regex matching on results post-query, which Datalog does not natively support for all data types. It can fetch broader results for client-side post-processing.
Highly Complex Boolean Logic: Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.
Arbitrary Sorting Criteria: The go-to for highly customized sorting needs beyond default options.
Proximity Search: For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.
List of some of Roam's data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text). Predicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=). Aggregates (distinct, count, sum, max, min, avg, limit). Tips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: query (required), graph, inputs, write_key, +3 more.
roam_fetch_blockARead-onlyIdempotent
Fetch a block by its UID with optional children (down to a specified depth) and/or ancestors (up to page root). Returns the block's UID, text, order, children array, and optionally an ancestors array with the chain to the page root.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: block_uid (required), depth, graph, write_key, +1 more.
roam_fetch_page_by_titleARead-onlyIdempotent
Fetch page by title. Returns content in the specified format.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: title (required), graph, format, write_key.
roam_fetch_page_full_viewARead-onlyIdempotent
Fetch a complete page view that mirrors what Roam Research shows in its UI: the page's own content, plus all linked references (backlinks) grouped by source page, each with their ancestor breadcrumb context and children expanded to the specified depth. Use this when you need the full picture of a page — both what is written on it and everything else in the graph that references it.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: title (required), graph, write_key, children_depth, +1 more.
roam_find_pages_modified_todayARead-onlyIdempotent
Find pages that have been modified today (since midnight), with pagination and sorting options.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: graph, limit, offset, write_key, +1 more.
roam_get_guidelinesARead-onlyIdempotent
Retrieve this graph's user-defined agent conventions, read from the [[roam/agent guidelines]] page inside the graph (configurable per graph). These are the user's own rules — how they tag, how they name and namespace pages, what to never do, how they want your voice attributed.
Also returns roamSyntax: the rules whose violation destroys content — whole-page rewrites that delete, truncated previews written back as content, retyped block references, and the syntax that differs from standard markdown. These are returned on every call, including when the graph has no guidelines page, and they hold regardless of what the conventions say.
Call this ONCE per graph per session, before other tools, INCLUDING for reads: conventions change how results should be interpreted and presented, not just how content is written. Returns today's daily note title as orientation.
Distinct from roam_markdown_cheatsheet, which is the complete syntax reference — components, queries, embeds, tool selection. Call that when you need to look something up; this one you need before writing at all. Returns exists:false rather than failing when no page has been created.
Inputs: graph, write_key.
roam_get_subpagesARead-onlyIdempotent
Fetch all sub-pages (namespace children) of a given page prefix. Matches by page title prefix — pages titled "Prefix/Something" are sub-pages of "Prefix" and appear in the Hierarchy section of that page. This is namespace/title-prefix matching, distinct from roam_search_hierarchy which traverses block parent/child relationships. Optionally filter to only sub-pages containing a specific tag (e.g. filter active projects with filter_tag="active"), and optionally include each sub-page's full block content.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: prefix (required), graph, write_key, filter_tag, +1 more.
roam_import_markdownA
Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a parent_string is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.
API Usage Note: This tool fetches the full nested structure after import for verification. For large imports or when rate limits are a concern, consider using roam_process_batch_actions with pre-structured actions instead.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: content (required), graph, order, page_uid, +4 more. Returns: success, page_uid, parent_uid, created_blocks
roam_markdown_cheatsheetARead-onlyIdempotent
Provides the comprehensive Roam syntax reference. Covers: formatting, links & references (page refs, block refs, embeds including embed-children and embed-path), tags, dates, tasks, callouts, attributes, queries (native {{query}} with its clause rules and page-ref inheritance, plus :q Datalog tables with built-in rules), tables, kanban, mermaid diagrams (with theme support), advanced components (dropdowns, tooltips, templates, document mode, word-count), CSS tags (#.rm-E, #.rm-hide, etc.), anti-patterns, tool selection guide, and API efficiency tips.
IMPORTANT: Always load this cheatsheet before creating or updating Roam content. It prevents common syntax errors and guides tool selection.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: graph, write_key.
roam_move_blockADestructiveIdempotent
Move a block to a new location (different parent or position). This is a convenience wrapper around roam_process_batch_actions for single block moves.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: block_uid (required), parent_uid (required), graph, order, +1 more. Returns: order, success, block_uid, new_parent_uid
roam_process_batch_actionsADestructive
RATE LIMIT EFFICIENT: This is the most API-efficient tool for multiple block operations. Combine all create/update/delete operations into a single call whenever possible. For intensive page updates or revisions, prefer this tool over multiple sequential calls.
Executes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order.
UID Placeholders for Nested Blocks: Use {{uid:name}} syntax for parent-child references within the same batch. The server generates proper random UIDs and returns a uid_map showing placeholder→UID mappings. Example: { uid: "{{uid:parent1}}", string: "Parent" } then { location: { "parent-uid": "{{uid:parent1}}" }, string: "Child" }. Response includes { success: true, uid_map: { "parent1": "Xk7mN2pQ9" } }.
For actions on existing blocks, a valid block UID is required. Note: Roam-flavored markdown, including block embedding with ((UID)) syntax, is supported within the string property for create-block and update-block actions. For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs. Tools like roam_fetch_page_by_title or other search tools can be used to retrieve these UIDs before executing batch actions. For simpler, sequential outlines, roam_create_outline is often more suitable.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: actions (required), graph, write_key. Returns: error, success, uid_map, committed, +2 more
roam_recallARead-onlyIdempotent
Retrieve all stored memories on page titled ROAM_MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blocks with a specific tag and sort by creation date.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: graph, sort_by, write_key, filter_tag.
roam_rememberA
Add a memory or piece of information to remember, stored on the daily page with ROAM_MEMORIES_TAG tag and optional categories (unless include_memories_tag is false). NOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use alias syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: memory (required), graph, heading, write_key, +3 more. Returns: success, block_uid, parent_uid
roam_rename_pageADestructiveIdempotent
Rename a page by changing its title. Identifies the page by current title or UID.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: new_title (required), uid, graph, old_title, +1 more. Returns: message, success
roam_search_block_refsARead-onlyIdempotent
Search for block references within a page or across the entire graph. Can search for references to a specific block, a page title, or find all block references.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: graph, title, block_uid, write_key, +1 more.
roam_search_by_dateARead-onlyIdempotent
Search for blocks or pages based on creation or modification dates. Not for daily pages with ordinal date titles.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: type (required), scope (required), start_date (required), graph, +3 more.
roam_search_by_statusARead-onlyIdempotent
Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: status (required), graph, exclude, include, +2 more.
roam_search_by_textARead-onlyIdempotent
Search for blocks containing specific text across all pages or within a specific page. Use scope: "page_titles" to search for pages by namespace prefix (e.g., "Convention/" finds all pages starting with that prefix). This tool supports pagination via the limit and offset parameters. Pagination follows hidden-block filtering; block results use UID order and namespace results use title order.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: text (required), graph, limit, scope, +4 more.
roam_search_for_tagARead-onlyIdempotent
Search for blocks containing a specific tag. Use primary_tag for the tag to find, and optionally page_title_uid to limit search to a specific page. Supports pagination via limit and offset, applied after hidden-block filtering in UID order. Use this tool to search for memories tagged with the ROAM_MEMORIES_TAG.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: primary_tag (required), graph, limit, offset, +4 more.
roam_search_hierarchyARead-onlyIdempotent
Search for parent or child blocks in the block hierarchy. Can search up or down the hierarchy from a given block.
IMPORTANT: call roam_get_guidelines for this graph once per session before using this tool, reads included — conventions change how results are read, not just written.
Inputs: graph, child_uid, max_depth, write_key, +2 more.
roam_update_page_markdownADestructiveIdempotent
Update an existing page with new markdown content using smart diff. Preserves block UIDs where possible and generates minimal changes. This is ideal for:
Syncing external markdown files to Roam
AI-assisted content updates that preserve references
Batch content modifications without losing block references
⚠️ This REPLACES the page, it does not append. Any block your markdown does not account for is deleted. Pass the complete intended page, or use roam_process_batch_actions / roam_create_outline to change only part of one. Use dry_run: true to see the actions first.
How it works:
Fetches existing page blocks
Matches new content to existing blocks by text similarity
Generates minimal create/update/move/delete operations
Preserves UIDs for matched blocks (keeping references intact)
#.rm-hide / #.rm-private subtrees are excluded from the diff and left untouched — you cannot see them, so you cannot be asked to account for them. preserved_hidden reports how many, when any.
IMPORTANT: call roam_get_guidelines for this graph once per session, and load the Roam Markdown Cheatsheet, before using this tool.
Inputs: title (required), markdown (required), graph, dry_run, +1 more. Returns: stats, actions, success, summary, +2 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 25 tools
Most tools target distinct resources or actions, and the descriptions explicitly differentiate nearby tools such as roam_create_page vs. roam_create_outline vs. roam_process_batch_actions, and roam_fetch_page_by_title vs. roam_fetch_page_full_view. Some overlap remains, especially between namespace/page-title search and subpage retrieval, or between date search and modified-today search, but the boundaries are generally clear enough for tool selection.
All tools share a consistent roam_ prefix and snake_case formatting, which makes the set predictable. The pattern is mostly roam_verb_noun, though a few names such as roam_markdown_cheatsheet and roam_datomic_query are noun-oriented rather than strict verb-object forms.
With 25 tools, this is a heavy surface for an MCP server, even for a complex graph tool like Roam. The tools are mostly justified by distinct operations, but the set is large enough that an agent may need extra care to choose efficiently, especially across the many search and content-creation variants.
The surface covers most core Roam workflows: page creation, fetching, renaming, markdown updates, block fetching, moving, searching, batch block operations, tables, todos, memories, and references. Minor gaps remain, such as no explicit page deletion tool and block updates/deletes only being exposed through the broader batch-actions tool.
Maintenance
Related MCP Connectors
Zotero MCP server for Claude and ChatGPT: search, citations, safe writes, PDF passages and pages.
Hosted Notion MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Hosted Airtable MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Official Fibery MCP server: query and update your workspace from any AI client via OAuth.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides a standardized interface for AI assistants to interact with Obsidian vaults through a local REST API, enabling reading, writing, searching, and managing notes.69MIT
- AlicenseNot gradedqualityDmaintenanceA server that enables AI assistants like Claude to interact with Roam Research graphs through a standardized interface, providing comprehensive tools for content creation, search, retrieval, and optional memory management.8MIT
- FlicenseAqualityDmaintenanceA Model Context Protocol server that enables Claude Desktop to read from and write to Roam Research graphs, allowing for retrieving page content, finding references, and adding blocks to existing or daily pages.41-
- AlicenseAqualityNot gradedmaintenanceEnables AI assistants to interact with Roam Research graphs through comprehensive API access, supporting page/block operations, markdown import, search, memory storage, and complex batch actions for managing knowledge graphs.18266 npm-