Roam Research MCP Server

by 2b3pro

98 stars
500 downloads
Not rated
GitHub

About

Integrates with Roam Research to enable searching, creating, and manipulating graph content for automated note-taking and intelligent information retrieval.

Details

Author
2b3pro
GitHub stars
98
Downloads
500
Categories
Productivity, Other, Database, Knowledge Base, AI, Design, Developer Tools, Search, Infrastructure, API
Tags
#notes, #visualization

- Multi‑graph support with optional write protection per graph.
- 20+ MCP tools for fetching, searching, creating, and updating content.
- Standalone CLI with nine commands: get, search, save, refs, update, batch, rename, status, server.
- Stdin piping for all content creation and retrieval commands.
- HTTP Stream transport with a health endpoint and optional bearer authentication.
- Shared server mode (HTTP‑only daemon) for multiple MCP clients.
- Docker deployment support.
- Datalog query support for advanced filtering.

Setting up with Highlight

This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:

  1. Download and install Highlight from highlightai.com/download
  2. Navigate to the plugins tab and select "Add Custom Plugin"
  3. Configure the plugin with the settings below
    Plugin Name Roam Research MCP Server
    Command (node, npx, python, etc.)

    Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.

  4. Enable "Start Automatically" if you want the plugin to start when Highlight launches

From the repository

Install globally via npm install -g roam-research-mcp. Configure environment variables (ROAM_API_TOKEN, ROAM_GRAPH_NAME for a single graph, or ROAM_GRAPHS for multiple graphs). Run the server with npx roam-research-mcp or use the roam CLI for terminal commands. For AI assistants, add the server to MCP settings with the appropriate command and environment variables.

Claude Desktop / Cursor

Paste into your MCP client config file to install this server.

{
    "mcpServers": {
        "roam research mcp server": {
            "roam-research-mcp": {
                "command": "npx",
                "args": [
                    "roam-research-mcp"
                ]
            }
        }
    }
}

McpServers

{
    "roam-research-mcp": {
        "command": "npx",
        "args": [
            "roam-research-mcp"
        ]
    }
}

I created this project to solve a personal problem: I wanted to manage my Roam Research graph directly fromClaude Code(and other LLMs). As I built theModel 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-featuredStandalone 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.

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.

Reach for the official server whenyou 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 whenRoam 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 offerdelete_page; this project takes the more conservative line.

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. SeeAgent guidelines.
- #.rm-hide/#.rm-private— both withhold tagged blocks from AI-facing content. Tag once, hidden from both. See
Hiding content from the AI.

TheroamCLI lets you interact with your graph directly from the terminal. It supportsstandard input (stdin) pipingfor all content creation and retrieval commands, making it perfect for automation workflows.

# 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. Runroam <command> --helpfor details on any command.

npm install -g roam-research-mcp # The 'roam' command is now available globally

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 optionalgraphandwrite_keyparameters. Usegraphto target a specific graph from yourROAM_GRAPHSconfig, andwrite_keyfor write operations on protected graphs.

Structured results from write tools (v3.0.0+)

The ten write tools declare anoutputSchemaand returnstructuredContent— a validated object — alongside the usual text. A client can readpage_uid,uid_maporsuccessdirectly 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 }

- Nothing was taken away.The text channel is unchanged, so a client that ignoresstructuredContentbehaves 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 from 2.x:three write-result fields were renamed —uidpage_uid(roam_create_page),created_uidscreated_blocks(roam_create_outline,roam_import_markdown) andpreservedUidspreserved_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 thechangelogfor why.

roam_get_guidelinesreads a pageinside 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 fromCUSTOM_INSTRUCTIONS_PATH, and the two compose:

Just create the page.With no configuration at all,roam_get_guidelinesreads[[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 returnsexists: falserather than failing, so it is always safe to call.

It also returns the rules that aren't yours to set

Alongside your conventions, everyroam_get_guidelinesresponse carries aroamSyntaxfield: the short list of things thatdestroycontent —roam_update_page_markdowndeleting every block your markdown omits, truncatedstructurepreviews written back as if they were content, block references retyped as plain text — plus a caution that reads silently exclude#.rm-hidesubtrees, and the handful of places Roam's markdown inverts standard markdown.

Two reasons it rides here rather than in the cheatsheet. It reacheseveryclient, including one that never callsroam_markdown_cheatsheet; and it is returned even when a graph hasnoguidelines page, which is exactly the case where an agent has least context. The layering is deliberate:your conventions win on style,roamSyntaxwins on data safety.No convention can make a truncated preview complete.

The full syntax reference — components, queries, embeds, tool selection — stays inroam_markdown_cheatsheet.roamSyntaxis ~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 graph

Resolution order isper-graphguidelinesPageROAM_GUIDELINES_PAGEroam/agent guidelines. Above:personaluses the env override,workuses its own page, andprivatehas guidelines off entirely.Only an explicitfalsedisables 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-privateare withheld from them too — see below.

Blocks tagged#.rm-hideor#.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-privateis Roam's existing "hidden from other users" tag;.rm-hidehides 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 beingdeletedfor being absent from markdown the agent could not have written.roam_update_page_markdown(androam 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 thatthe baseline a diff deletes from must be the same page the caller was allowed to read.It reportspreserved_hiddenwhen 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 thechangelog.

This is a convenience filter, not a security guarantee.roam_datomic_queryreads the database directly and deliberately doesnotapply 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-hiddenand#.rm-highlightare 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.

For a single Roam graph, set these in your environment or a.envfile:

ROAM_API_TOKEN=your-api-token ROAM_GRAPH_NAME=your-graph-name

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-key

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 areoptional and off by default):

Think of a house: thebearer token locks the front door(keeps strangers out entirely), and thewrite 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 to127.0.0.1, the front door faces a wall — you don't need the bearer token there. The write key is still handy locally as an "are you sure?" guard, becauseRoam has no undo.

So: to mark a graph as needing the write key, setprotected: trueon it and configureROAM_SYSTEM_WRITE_KEY; callers then pass a matchingwrite_keyfor 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 toaskfor 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.

- 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 thewholeHTTP endpoint. Unset = open (fine for loopback). 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— seeTwo kinds of access control.

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 modealso*opened an HTTP listener, and bound it to every interface. If you were using that endpoint, run a--serverdaemon instead; see below.

2. Shared Server Mode (--server)Best for a single long-lived, HTTP-only daemon thatmultiple 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 --server

Or manage it through theroamCLI, 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 daemon

roam server statusworks 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 withROAM_HOME).

The two modes are mutually exclusive, and each opens exactly one transport: stdio mode speaks stdio and binds nothing,--serverspeaks HTTP and reads no stdin. In--servermode the server:

- runsHTTP-only(no stdio transport),
- binds theexactHTTP_STREAM_PORTonHTTP_STREAM_HOSTandexits non-zero if the port is taken(no silent drift — a shared daemon must keep a stable URL),
- exposesGET /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 theserverprocess, 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.0

Clients then send the token as a header:

{ "mcpServers": { "roam-research-mcp": { "type": "http", "url": "http://<host>:8088/mcp", "headers": { "Authorization": "Bearer <token>" } } } }

Keepboth— they do different jobs (seeTwo kinds of access controlabove): the bearer token controlswho can connect, the write key only guardswrites to protected graphs.

⚠️ The write key isnota substitute for the bearer token. On an exposed server withoutHTTP_AUTH_TOKEN, anyone on the network can stillread 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.plistwithRunAtLoad+KeepAlive, your env vars underEnvironmentVariables, and--serveras the lastProgramArgumentsentry. KeepStandardOutPath/StandardErrorPathon alocalpath (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 # verify
docker run -p 8088:8088 --env-file .env roam-research-mcp --server

Add to your MCP settings file (e.g.,~/Library/Application Support/Claude/claude_desktop_config.json):

Pinning the version.npx -y roam-research-mcpfetches thelatestrelease 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:

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.

{ "mcpServers": { "roam-research": { "command": "npx", "args": ["-y", "roam-research-mcp"], "env": { "ROAM_API_TOKEN": "your-token", "ROAM_GRAPH_NAME": "your-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" } } } }

A utility for parsing and executing Roam query blocks programmatically. Converts{{[[query]]: ...}}syntax into Datalog queries.

Thebetweenclause supports relative dates:today,yesterday,last week,last month,this year,7 days ago,2 months ago, etc.

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);
No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.