Turn your Logseq graph into memory and workspace for AI agents. A
Model Context Protocol server for
Logseq with safety-scoped writes, an audit trail in your
daily journal, and verified queries exposed as tools. Built on FastMCP (the
high-level API of the official mcp package).
Targets the file/Markdown ("OG") version of Logseq — and plain-text files are part of why a graph makes good agent memory: git-syncable, greppable, durable, no lock-in. The newer DB (SQLite) version changed the underlying schema; some methods may behave differently there.
Agents need durable memory, and you already maintain one — your graph. The missing piece is access you can trust: an agent should read broadly and write usefully, but never touch what it shouldn't — and never do anything you can't see. Three design choices make that possible:
- Namespace-scoped writes. Agents write only under their own prefix
(
byAgent/by default), plus one deliberately narrow cross-namespace channel that can change nothing but a task'sTODO/DOING/DONEmarker. Blacklisted pages are hidden and redacted from every read. - An audit trail in your daily journal. Every successful write appends a
line like
22:30 [[byAgent]] wrote [[byAgent/readingList/...]]to today's journal — reviewing your agents' work becomes part of a morning routine you already have. - Verified queries as tools. Ship known-good Datalog from config as named
tools (
query_week_plan, …), so agents don't compose datascript by hand and cheaper models stay reliable.
I run a small fleet of Claude Code agents with this server on an always-on Mac mini, against my live personal graph:
- Nightly research. A link dropped into the reading list from the phone; at
night an agent claims it (
status:: researching), reads the article — or shallow-clones and reads the repo — writes a structured summary onto the page and flips it toread. - Morning brief. At 08:30 a small model assembles a one-page dashboard — what was read overnight, week-plan progress, current NOW/DOING tasks — and sends a single push notification.
- One journal for everyone. The human's tasks and the agents' audit lines interleave in the same daily note:
The pages the researcher writes — properties, summary, relevance — link straight into the rest of the graph:
flowchart LR
A[AI agents] -- MCP tools --> S[logseq-mcp]
S -- HTTP API --> L[Logseq graph]
S -. audit line per write .-> J[daily journal]
Y((you)) --> L
Y -- morning review --> J
- A running Logseq with the local HTTP API server enabled (Settings → Features → HTTP APIs server, then start it from the 🔌 menu).
- An authorization token created in the HTTP API server settings.
Local (stdio), token from the environment:
claude mcp add logseq --scope user --env LOGSEQ_API_TOKEN=<YOUR_TOKEN> -- uvx mcp-server-logseqOr point it at a remote instance over Streamable HTTP (how phone and remote sessions reach a headless host — see Transports):
claude mcp add logseq --scope user --transport http http://<host>:8000/mcp \
--header "Authorization: Bearer <LOGSEQ_MCP_HTTP_TOKEN>"{
"mcpServers": {
"logseq": {
"command": "uvx",
"args": ["mcp-server-logseq"],
"env": {
"LOGSEQ_API_TOKEN": "<YOUR_TOKEN>",
"LOGSEQ_API_URL": "http://127.0.0.1:12315"
}
}
}
}| Source | Token | URL |
|---|---|---|
| Environment | LOGSEQ_API_TOKEN |
LOGSEQ_API_URL (default http://localhost:12315) |
| CLI flag | --api-key |
--url |
The token is read from the environment or --api-key; it is never stored in
code. A .env file is supported (see .env.example).
Behaviour beyond the defaults is set in a TOML file — path from
LOGSEQ_MCP_CONFIG (default ~/.config/logseq-mcp/config.toml). Custom queries
live in EDN files next to it. The server runs fine with no config file (safe
read-mostly defaults); see examples/config.toml for a
full annotated example.
| Section | Key options |
|---|---|
[read] |
resolve_depth — how deep to expand ((block refs)) |
[write] |
agent_write_prefix (default byAgent), allow_agents_write_any |
[search] |
files_path — graph folder; set it to use the ripgrep backend |
[blacklist] |
pages — pages (and subpages) to hide and redact everywhere |
[tasks] |
allow_status_change — gate for set_task_status |
[audit_log] |
enabled — log writes to today's journal |
[worklog] |
enabled, namespace, root_block, exclude, agent_namespace, agent_exclude — the worklog channel |
[queries.<name>] |
a named query: file/inline query, register_as_tool, … |
Secrets and the API URL stay in the environment, never in this file.
By default the server runs over stdio (for Claude Desktop and other local clients). A Streamable HTTP transport is also available for remote/networked use (e.g. a phone client):
LOGSEQ_MCP_HTTP_TOKEN=<client-secret> \
mcp-server-logseq --transport streamable-http --host 0.0.0.0 --port 8000
# MCP endpoint: http://<host>:8000/mcpEnv vars: LOGSEQ_MCP_TRANSPORT, LOGSEQ_MCP_HOST, LOGSEQ_MCP_PORT,
LOGSEQ_MCP_HTTP_TOKEN (or --http-token).
The Streamable HTTP transport requires a bearer token: every request must
send Authorization: Bearer <LOGSEQ_MCP_HTTP_TOKEN>, or it gets 401. The
server refuses to start in this mode without a token set. Note this is a
distinct secret from LOGSEQ_API_TOKEN:
| Secret | Direction |
|---|---|
LOGSEQ_API_TOKEN |
this server → Logseq |
LOGSEQ_MCP_HTTP_TOKEN |
client (phone) → this server |
⚠️ A bearer token over plain HTTP is only safe on an already-encrypted channel. Don't expose the raw port to the open internet. The easy path for a home/headless host is Tailscale: install it on the host and the client, and reachhttp://<host>.<tailnet>.ts.net:8000/mcpover the encrypted tunnel — no domains, nginx, or certificates. (tailscale servecan add TLS if you wanthttps://.)
Build once:
docker build -t logseq-mcp .Quick try (ephemeral — --rm removes the container on stop):
docker run --rm -p 8000:8000 \
-e LOGSEQ_API_TOKEN=<logseq-token> \
-e LOGSEQ_MCP_HTTP_TOKEN=<client-secret> \
-e TZ=Europe/Moscow \
logseq-mcpPersistent deploy (e.g. a headless Mac mini) — run once; --restart brings it
back after reboots:
docker run -d --name logseq-mcp --restart unless-stopped -p 8000:8000 \
-e LOGSEQ_API_TOKEN=<logseq-token> \
-e LOGSEQ_MCP_HTTP_TOKEN=<client-secret> \
-e TZ=Europe/Moscow \
-e LOGSEQ_MCP_CONFIG=/cfg/config.toml \
-v /path/to/config-dir:/cfg:ro \
-v "/path/to/your/graph:/graph:ro" \
logseq-mcp-v .../config-dir:/cfg— folder holding yourconfig.toml(+queries/,rules/); setfiles_path = "/graph"in it to enable file search. Omit both the mount andLOGSEQ_MCP_CONFIGto run on defaults.-v .../graph:/graph— your Logseq graph folder (read-only), for file search.-e TZ=<zone>— local time for audit-log timestamps (image bundlestzdata; the clock is UTC otherwise).
The container serves Streamable HTTP on port 8000 and talks to a Logseq running
on the host. On Docker Desktop (macOS/Windows) the default
LOGSEQ_API_URL=http://host.docker.internal:12315 already points at the host;
on Linux add --add-host=host.docker.internal:host-gateway (or set
LOGSEQ_API_URL to the host IP). Make sure Logseq's HTTP API server is running
and listening.
All read output is normalized to a flat JSON shape and passed through the
blacklist. Reads resolve ((block refs)) non-lossily (the resolved block's
uuid/status is kept so you can act on it). Results go out as compact JSON, and a
block carries only the fields it has: empty ones are left out, and raw_content
appears only when it differs from text — on a busy journal that is a third of the
indented, fully-keyed form.
- search — full-text search over block content (
query,regex?,limit?,case_sensitive?,exclude_journals?). Uses ripgrep overfiles_pathwhen set, else a datascript content match. - find_tasks — task blocks by
markers?,tag?,under_tag?(descendant),page?,priority?,limit?. - list_pages — page names under a namespace
prefix?(depth?limits levels). Discovers a namespace's child pages, which are separate pages a parent'sread_pagewon't show. Structure only, not block content. - custom_query — run a named query from the config (
name,inputs?). - list_custom_queries — list the configured queries.
- datascript_query — run a raw Datalog query (
query,inputs?,rules?).
- get_logseq_guide — returns the authoritative guide for querying/writing this graph (verified Datalog gotchas: lowercase names, prefix descendants, marker and journal-day types, tags vs refs, read/write scoping). A single source of truth co-located with the server, so agents don't re-derive (and mis-derive) behaviour.
- read_page — a page as a normalized block tree (
page,depth?). A dateYYYY-MM-DDreads that day's journal, whatever the graph's title format; a page that does not exist is an error rather than an empty tree. - read_block — a block and its children (
uuid,depth?).
- write_note — create/append/replace a page under
agent_write_prefix(subpath,content?,mode?,properties?). - set_page_properties — set/remove page properties (
subpath,properties; anullvalue removes one). - edit_block — replace one block's content (
uuid,old_content,new_content). Read-before-write is enforced: the edit is rejected unlessold_contentmatches the block's exact current content. Agent namespace only.
- create_task — create a task block in the agent namespace (
title,agent,project?,marker?,priority?,tags?,plan_page?,blocks_on?,on_page?). The only way to create tasks —write_noterejects content that starts with a task marker. - set_task_status — change only a task's marker (
uuid,status); gated by[tasks].allow_status_change.
An agent that does work should be able to say so where the rest of the work is recorded — the journal — in the same shape a human keeps by hand:
#_worklog
#_work/dynamo
((6a9eb940-758e-4966-a31d-f91e79c35f42))
17:50 #_agents/claude/work-scout told how to update
- list_work_projects — the closed set of projects a note may be filed under
(the direct children of
[worklog].namespace, minusexclude). - list_agents — the closed set of names a note may be signed with, as
<runtime>/<name>: the pages exactly two levels below the agent registry[worklog].agent_namespace(default_agents), from any runtime — a new one (_agents/codex/...) needs no config. - add_journal_note — append
HH:MM #<agent> <text>to today's journal under a project (text,work,agent,task?), nested under a((ref))when a task is given. - get_worklog_setup_template — the worklog section for an agent's instructions
file (CLAUDE.md, AGENTS.md): which task to log under, when to move its status,
what an entry looks like. Called only when the user asks to set that up — "add
the worklog to CLAUDE.md" in any session is enough. The template is
worklog_setup.md.
The signature sits inline on the note, not as a grouping level: several agents
logging against one project would otherwise split it into parallel subtrees, each
with its own chronology. Note that the server cannot authenticate the caller — every
session presents the same bearer token — so agent is an honesty convention
validated against a list, not an identity. It catches a typo, not a masquerade.
What the list CAN promise is that agents cannot add themselves to it. Two rules
hold that line. The registry lives outside the agents' write prefix, so
write_note cannot create a page there — the server refuses to start if it is
configured inside. And only pages that have a file count: writing
[[_agents/claude/x]] in any note makes Logseq create a reference-only page, which
would otherwise be enough to register a name. Registering an agent therefore means
creating its page, by hand, in Logseq.
Every header is reused only while it is still the last block at its level: the
root_block among the journal's top-level blocks, a project group inside the root,
a task anchor inside its group. A journal is chronological, and a header reused
wherever it stands keeps collecting later notes: a root opened in the morning would
render an evening note above the afternoon lines that preceded it, and with two
agents on two projects a note would join its project's earlier group, above the
other project's later line. Once anything else lands after a header, the next note
opens a fresh one — so the day reads in the order things happened, at the price of
repeated headers when work interleaves.
This is the only channel that writes outside agent_write_prefix, so it trades
path confinement for a narrow contract: append-only, today's journal only,
everything it creates lives under a root_block, work must come from the enum,
task must be an existing task block, and the server stamps the time (a caller
cannot pass one, so a resumed agent can't log a remembered clock). Text starting
with a task marker is rejected — use create_task.
- query_<name> — each config query with
register_as_tool = trueis exposed as its own tool.
git clone https://github.com/dailydaniel/logseq-mcp.git
cd logseq-mcp
cp .env.example .env # fill in LOGSEQ_API_TOKEN
uv sync
uv run mcp-server-logseqInspect with the MCP Inspector:
npx @modelcontextprotocol/inspector uv --directory . run mcp-server-logseqMIT