A Rust MCP server that gives Claude (Code, Desktop, or web) read/write access to your Workflowy graph, plus a generic template for turning that raw access into a working second brain.
There are two ways to use it:
- Bare MCP server. Wire the binary into your MCP host and call the 41 tools directly. No templates, no opinions.
- Second brain. Hand
BOOTSTRAP.mdto Claude. The assistant follows the script: builds the binary, wires the host, sets up your secondBrain directory at whatever path you choose (exposed to the server via$SECONDBRAIN_DIR), and installs the wflow skill that drives every subsequent session.
The methodology in option 2 is opinionated; the server itself is not. The
repo only ships generic templates — your node IDs, drafts, and session logs
live wherever $SECONDBRAIN_DIR points, never in this repo.
You need: Rust 1.75+ (rustup install stable), a Workflowy account with an
API key (Settings → API in Workflowy), and an MCP host (Claude Code,
Claude Desktop, or claude.ai web).
git clone https://github.com/dromologue/workflowyMCP.git ~/code/workflowy-mcp-server
cd ~/code/workflowy-mcp-server
cargo build --release
echo "WORKFLOWY_API_KEY=<your-token>" > .envThe binary reads .env from the directory it's launched from. The
clone path above (~/code/workflowy-mcp-server) is the working
directory for the host's MCP launch — the host runs the binary from
there, so .env resolves correctly. Recommended alternative: put
the same key in your MCP host's env block (see the next section), so
the binary works regardless of where it's launched from.
Wire the resulting target/release/workflowy-mcp-server into your MCP host:
- Claude Code:
claude mcp add workflowy -- $(pwd)/target/release/workflowy-mcp-server - Claude Desktop: edit
~/Library/Application Support/Claude/claude_desktop_config.json(macOS) or%APPDATA%\Claude\claude_desktop_config.json(Windows). See BOOTSTRAP.md for the JSON shape.
Verify by calling the workflowy_status tool — the host should return
status: "ok", api_reachable: true, authenticated: true. If
authenticated: false, your API key is wrong or not reaching the
binary; check the env-var section below.
That's it for plain MCP usage.
The server reads three env vars at runtime. The repository ships no
machine-specific defaults: a path you don't set is a feature you don't
use. Set them in the env block of your MCP host config (Claude Code:
~/.claude.json; Claude Desktop: claude_desktop_config.json) and,
when you also use the wflow-do CLI from a shell, in your shell
profile (~/.zshrc or ~/.bashrc).
| Variable | Required? | What it controls |
|---|---|---|
WORKFLOWY_API_KEY |
Yes | Bearer token for the Workflowy API. |
SECONDBRAIN_DIR |
Optional | Absolute path to your operational secondBrain directory (drafts, session logs, briefs, memory). When set, the review tool's bucket-d session-log scan and the wflow-do index default output path read from $SECONDBRAIN_DIR/session-logs/. Unset or empty disables those features (graceful skip). |
WORKFLOWY_INDEX_PATH |
Optional | Absolute path to the persistent name-index JSON. Conventionally $SECONDBRAIN_DIR/memory/name_index.json. Unset or empty disables persistence — the index then lives only in memory for the lifetime of each process. |
Example MCP host env block (Claude Code or Desktop):
"env": {
"WORKFLOWY_API_KEY": "<your token>",
"SECONDBRAIN_DIR": "/absolute/path/to/secondBrain",
"WORKFLOWY_INDEX_PATH": "/absolute/path/to/secondBrain/memory/name_index.json"
}Example shell profile (so the CLI agrees with the MCP server):
export SECONDBRAIN_DIR="/absolute/path/to/secondBrain"
export WORKFLOWY_INDEX_PATH="$SECONDBRAIN_DIR/memory/name_index.json"Neither path needs to be inside the user's home directory — a Dropbox /
iCloud / Google Drive folder works as long as the host process can
read and write it. Paths with spaces are fine in the MCP env block
(JSON quoting handles it) and in the shell profile (the export line
quotes the value).
Hand BOOTSTRAP.md to Claude. The assistant runs through
six steps: build, wire the host, bootstrap your secondBrain directory at
the path you set in $SECONDBRAIN_DIR, populate your structural node IDs,
install the wflow skill, and (optionally) pre-warm the persistent name
index. After bootstrap, the assistant follows
templates/skills/wflow/SKILL.md as the
operating manual.
The detailed long-form walkthrough — multi-surface deployment, large-tree
convergence, troubleshooting — lives in docs/SETUP.md.
The repo is generic. Everything specific to your Workflowy tree, your intellectual frameworks, and your in-progress work lives outside it. These are the files you populate (or accept the bundled defaults from the wflow skill, then customise):
| File | Lives at | What it holds | Why it's external |
|---|---|---|---|
workflowy_node_links.md |
$SECONDBRAIN_DIR/memory/ (canonical) and ~/.claude/skills/wflow/ (bundled fallback for surfaces that can't read $SECONDBRAIN_DIR) |
Cached UUIDs for your structural Workflowy nodes (Inbox, Tasks, Reading List, Distillations, Themes, etc.) plus a Triage Sources table that defines which nodes Workflow 6 sweeps. Editing this list is how you add a new triage target — capture sub-node, Slack-saved-items mirror — without changing the skill. | The skill is portable; your tree isn't. The skill references this file so different users can have entirely different Workflowy structures. |
distillation_taxonomy.md |
$SECONDBRAIN_DIR/memory/ (canonical) and ~/.claude/skills/wflow/ (bundled fallback) |
Your distillation pillars (the top-level conceptual buckets you organise atomic notes into), themes (cross-cutting tags), inbound routing rules (which topic goes to which pillar), and the named frameworks you work with. | The skill describes the synthesis workflow generically and reads your actual taxonomy from this file. Pillars and frameworks are deeply personal; embedding one user's into the skill would force everyone else to inherit them. |
name_index.json |
$WORKFLOWY_INDEX_PATH (typically $SECONDBRAIN_DIR/memory/name_index.json) |
Auto-managed by the MCP server. Persistent name index that turns Workflowy URL fragments and short-hashes into full UUIDs in O(1). Survives restarts; checkpoints every 30 s; refreshes via background walks every 30 minutes. | You don't write this file directly — the server maintains it. But it lives in your secondBrain so it's portable across your machines (e.g. via Google Drive / Dropbox / iCloud). |
drafts/, session-logs/, briefs/ |
$SECONDBRAIN_DIR/ |
Your in-flight distillation work (drafts/), per-session audit trails (session-logs/), and external-facing handoff documents (briefs/). |
The skill's end-of-session discipline writes to these locations. They're per-user by definition. |
tasks/todo.local.md |
inside this repo (gitignored) | Cross-session engineering follow-ups for the workflowy-mcp-server itself — issues you noticed, ideas to revisit, items the previous habit would have filed as Workflowy "system tasks." | Engineering todos belong with the code, not in your Workflowy outline; gitignored so each contributor's local list stays out of the tracked tree. |
Precedence rule. When the skill needs data from the two memory files,
it prefers the canonical at $SECONDBRAIN_DIR/memory/<file>.md. If that
path isn't readable (e.g. claude.ai web with no Filesystem allowlist), it
falls back to the bundled copy at ~/.claude/skills/wflow/<file>.md. The
bundled copy is overwritten on each skill ZIP rebuild, so canonical edits
need to be re-bundled before they reach surfaces that depend on the
fallback.
workflowyMCP/
├── BOOTSTRAP.md ← LLM-facing install script (hand to Claude)
├── README.md ← this file
├── docs/SETUP.md ← long-form bootstrap notes
├── specs/specification.md ← authoritative behavioural spec
├── templates/
│ ├── secondbrain/ ← skeleton copied to $SECONDBRAIN_DIR
│ │ ├── README.md
│ │ ├── memory/workflowy_node_links.md (template; user fills in)
│ │ ├── memory/distillation_taxonomy.md (template; user fills in)
│ │ ├── drafts/ session-logs/ briefs/
│ └── skills/wflow/SKILL.md ← the operating manual the assistant follows
└── src/ ← Rust MCP server source
User-specific data (node IDs, drafts, session logs, briefs) belongs at
whatever path you set via $SECONDBRAIN_DIR. The repo content stays
generic so the next person who clones it gets a clean starting point.
The server exposes 41 tools. node_id accepts any of: full UUID (with or
without hyphens), 12-char URL-suffix short hash, or 8-char prefix.
parent_id (and any other parent-scoped argument) accepts null or
omission as "workspace root" across the whole tool surface — create_node,
batch_create_nodes, insert_content, list_children, and the rest
behave the same way.
| Category | Tools |
|---|---|
| Search & navigate | node_at_path, resolve_link, search_nodes, find_node, get_node, list_children, tag_search, get_subtree, find_backlinks, path_of, find_by_tag_and_path, read_batch |
| Create & edit | create_node, batch_create_nodes, insert_content, smart_insert, convert_markdown, edit_node, move_node, reorder_nodes, delete_node, complete_node, duplicate_node, create_from_template, bulk_update, bulk_tag, transaction, export_subtree |
| Mirror discipline | create_mirror (convention-based: duplicates the canonical's name into a new parent and writes mirror_of: to the new node's note), audit_mirrors |
| Todos & scheduling | list_todos, list_upcoming, list_overdue, daily_review, since |
| Project management | get_project_summary, get_recent_changes |
| Diagnostics & ops | workflowy_status, health_check, cancel_all, build_name_index, review, get_recent_tool_calls |
Native task completion. complete_node(node_id) toggles the
Workflowy completed boolean — the legacy #done tag-as-completion
workaround is deprecated for tasks. bulk_update(operation: "complete"|"uncomplete", filter: …)
toggles a filtered set in one call; transaction accepts the same ops
with rollback. Wire payload is POST /nodes/{id} with
{"completed": true|false}.
Reordering siblings (reorder_nodes). Workflowy's move_node
priority is position-relative-to-siblings and renormalises after every
call, so a naive forward priority=0,1,2,… loop fights itself when you
try to batch-reorder a set. reorder_nodes(parent_id, node_ids[])
takes the desired head-first order and walks it in reverse, issuing
move_node with priority=0 per id — every move plants its node at
position 0, the previously-planted nodes shift one step right, and after
N moves the head of the parent's children is the requested sequence.
Side effect: ids not currently under parent_id are reparented as part
of the reorder (the primitive is built on move_node, not a sibling-
only assertion). Capped at 200 ids per call. Returns Complete or
Partial { reason: cancelled | timeout } with per-id ok / error / skipped entries; partial outcomes are safe to re-issue with the full
list because each reverse-priority-0 move is idempotent. The
orchestration lives once in crate::workflows::reorder_nodes_via_priority
and is shared with wflow-do reorder --parent <id> --node <id> --node <id>.
Mirror creation (convention). Workflowy's REST API does not expose
native mirror creation, so create_mirror(canonical_node_id, target_parent_id)
implements the documented mirror_of: / canonical_of: note convention
that audit_mirrors already understands: a new node is created under
the target parent with the same name as the canonical, and its
description carries mirror_of: <canonical_uuid>. Edits to the
canonical do not propagate to the mirror — the link is structural
and human-curated, not live. Pass an optional pillar to write a
canonical_of: <pillar> marker to the canonical when it lacks one;
existing markers are never overwritten. Pass dry_run=true to preview
the resolved canonical, target_parent, and mirror name (verbatim copy of
the canonical's name) without writing — useful when batching multiple
mirror passes across a synthesis. The mirror's create + the optional
canonical edit (and the dry-run preview) run through the shared
crate::workflows module, the same code path the
wflow-do create-mirror [--dry-run] CLI calls.
scope_resolved diagnostic field. Every tool that accepts an
Option<NodeId> for parent_id (create_node, batch_create_nodes,
insert_content, create_mirror, list_children, find_node,
search_nodes) returns a scope_resolved field naming what the server
actually targeted: workspace_root when the resolved scope was None
(caller passed null or omitted), or scoped:<full-uuid> otherwise.
Read this field after every call where parent_id was null/omitted to
verify the server resolved to where you intended — pre-2026-05-09
callers had no way to audit this without inspecting the wire payload.
insert_content payload cap. The hard cap is 80 lines per call,
lowered from 200 on 2026-05-04 after the failure-report 2026-05-03
session observed ≥80-line payloads failing at the MCP transport layer
with no diagnostic. Above the cap, the call returns a typed error with
a chunking instruction; chunk to ≤80 lines and pass the previous
batch's last_inserted_id as the next call's parent_id to keep the
hierarchy stitched together.
Truncation envelope. Every walk-shaped tool that emits JSON includes the same four fields when its 20 s walk budget fires:
{
"truncated": true,
"truncation_limit": 10000,
"truncation_reason": "timeout",
"truncation_recovery_hint": "Call build_name_index(parent_id=...) … then re-issue with use_index=true …"
}Read truncation_reason and truncation_recovery_hint on every walk
response. For name-based queries (search_nodes, find_node),
use_index=true answers in O(1) from the persistent name index without
burning the walk budget — populate it first with
build_name_index(parent_id=<scope>). Index path is name-only;
description-content matching still needs a live walk.
For large workspaces, prefer node_at_path (path of names → UUID, ~1 second
on any tree size) and resolve_link (Workflowy URL + optional parent path
→ full node info) over search_nodes. They cost O(depth) API calls instead
of O(tree).
Conventions parsed from node text:
- Tags:
#inbox,#review,#urgent - Assignees:
@alice,@bob - Due dates:
due:2026-03-15,#due-2026-03-15, or bare2026-03-15(priority order)
Every API-touching handler runs inside a uniform run_handler wrapper
that observes the server-wide cancel registry and applies a
kind-appropriate wall-clock deadline:
| Tool kind | Budget | Examples |
|---|---|---|
| Read | 30 s | get_node, list_children |
| Write | 15 s | create_node, delete_node, edit_node |
| Bulk | 180 s | insert_content, transaction, bulk_update, path_of, node_at_path |
| Walk | 20 s (internal) | search_nodes, get_subtree, find_node |
cancel_all interrupts any in-flight tool within ~50 ms. On budget
expiry, bulk operations return a structured partial-success payload
(status: "partial", created_count, last_inserted_id, etc.) so the
caller can resume — no "no result received" without diagnostic. The
180 s bulk budget leaves 60 s of margin under the MCP transport's
4-min hard timeout (Claude Desktop, claude.ai web) so the
partial-success envelope is reachable on every surface — lowered from
210 s on 2026-05-09 after a sub-cap insert_content payload was
observed hanging the full 4 minutes with no diagnostic on claude.ai
web.
For the full list (transport-timeout retry, authenticated/api_reachable
decoupling, null parameter handling and the scope_resolved audit
field, the 2026-05-04 move_node unification that collapsed the
previous wrapper-vs-bare divergence, etc.) see
specs/specification.md. 331 lib + 12 CLI
tests pin the 43 contracts, including 21 wiremock-driven failure-mode
tests that run in under 2 seconds, plus build-time invariant tests:
parameter_bearing_tools_publish_non_empty_input_schema_propertiesfails the build if a tool's published schema has emptyproperties(the rmcpParameters<T>wrapper rename trap).every_walk_tool_emits_full_truncation_envelope_in_jsonfails the build if a walk-shaped tool emitstruncation_limitwithout the reason + recovery_hint companions.cli_covers_every_non_diagnostic_mcp_toolfails the build if a new MCP tool ships without its matchingwflow-dosubcommand.cancel_all_preempts_inflight_create_node_via_run_handlerand thepath_ofcompanion pin the cancel-registry safety net.move_node_embeds_propagation_retry_looppins the 2026-05-04 unification: the move retry now lives insideclient.move_nodeitself, so every caller (handler, transaction, CLI) gets identical resilience without having to remember which wrapper to call.every_scoped_tool_emits_scope_resolved_in_responseandbulk_budget_leaves_mcp_transport_margin(2026-05-09) pin the diagnostic surface: every parent-scoped tool surfacesscope_resolved, and the bulk budget is held below the MCP transport cap so partial-success envelopes are always reachable.no_duplicated_renderer_or_scope_label_definitions_outside_canonical_modulespins the duplication-audit contract: the subtree renderers (render_subtree_markdown,render_subtree_opml) and thescope_resolved_labelrenderer live once each, with both surfaces re-exporting fromutils::subtree::*/workflows::*.
A second binary exposes the same operations as a plain shell command. Full surface parity with the MCP server — every non-diagnostic tool has a matching subcommand, enforced at build time. Useful as a fallback when the MCP transport drops or when you want a Bash-driven workflow.
The CLI and the MCP server share more than the API client: workflow
orchestration that used to be duplicated (create_mirror,
insert_content, transaction, bulk_update, smart_insert, plus
the renderers used by export_subtree, plus the dry-run preview and
scope_resolved label added on 2026-05-09) lives in
src/workflows.rs and
src/utils/subtree.rs. Both surfaces call the
same shared function and wrap the typed result in their own envelope
(structured tool_error for the MCP handler, stdout/JSON for the
CLI). A build-time test grep-audits the source so any future
contributor reintroducing a parallel implementation in either binary
fails the build.
target/release/wflow-do status # liveness
target/release/wflow-do search --query "concept maps" # substring filter
target/release/wflow-do find "Tasks" --use-index # O(1) index lookup
target/release/wflow-do complete <uuid> # mark task done
target/release/wflow-do bulk-update complete --tag urgent # bulk-toggle by filter
target/release/wflow-do --dry-run delete <uuid> # preview
target/release/wflow-do reindex --root <UUID> --root <UUID> # pre-warm indexForty-one subcommands grouped: read & navigate (status, health-check,
get, children, subtree, find, search, tag-search,
backlinks, find-by-tag-and-path, node-at-path, path-of,
resolve-link, since); todos & scheduling (todos, overdue,
upcoming, daily-review, recent-changes, project-summary);
single-node writes (create, move, delete, edit, complete);
bulk writes (insert, smart-insert, duplicate, template,
bulk-update, bulk-tag, batch-create, transaction, export);
graph hygiene (audit-mirrors, create-mirror, review, index,
reindex, build-name-index); diagnostics (cancel-all,
recent-tools).
Use --json for raw output, --dry-run (write verbs only) to preview
without calling the API.
cargo build # debug
cargo build --release # optimised
cargo test --lib # 331 unit tests
cargo test # full suite (lib + portability + traceability + eval coverage)
cargo check # type-check onlyArchitectural overview: CLAUDE.md. Behavioural spec: specs/specification.md.
MIT