← All Claude Builds
Model Context Protocol · Workflowy · Claude

Give ClaudeYour Workflowy.

A complete guide to building a Workflowy MCP server — and the Claude skills that teach Claude how to actually use it. Read, search, and write your entire outline, from any Claude interface.

2 parts · server + skills TypeScript / Node 18+ Local SQLite cache Works in Desktop + Code
00 / WHAT YOU'RE BUILDING

Two halves of one system.

An MCP server is a small program Claude runs in the background. It exposes a set of tools Claude can call. But tools alone aren't enough — Claude also needs to know how your outline is shaped and when to reach for which tool. That knowledge lives in skills. Build both and Claude becomes fluent in your Workflowy.

Half 1 — The Engine

The MCP Server

A Node.js process that talks to the Workflowy API and keeps a local SQLite mirror of your whole outline for instant, token-free search.

  • Connects via Workflowy's official API key
  • Caches every node locally (SQLite + FTS5)
  • Exposes tools: read, search, create, complete, sync
  • Runs over stdio — Claude launches it for you
Half 2 — The Fluency

The Skills

Markdown files that give Claude the map: what your columns mean, where things live, and the exact tool to use for each request.

  • A system skill: your outline's structure & rules
  • A format skill: clean output for copy-paste
  • Auto-trigger on the right phrases
  • Turn raw tools into reliable behavior
01 / BEFORE YOU START

Prerequisites.

You don't need to be a developer, but you do need these installed and one key in hand.

The shortcut
You can hand this entire page to Claude Code and say "build this for me." The guide below is written so a human or Claude can follow it. Everything is copy-paste ready.
PART 1 / THE MCP SERVER

Build the engine.

Six steps: get a key, scaffold the project, understand the one idea that makes it fast, define the tools, and compile. Roughly an afternoon by hand — minutes if Claude drives.

1

Get your Workflowy API key

Log in, visit beta.workflowy.com/api-key, and generate a key. It's a long token — treat it like a password. You'll paste it into the config later, never into your code.

Security
Never commit your key to git or paste it into a shared file. Keep it in config.json (git-ignored) or an environment variable.
2

Scaffold the project

Create a folder and initialize it. This is the entire skeleton — a src/ tree, a build config, and a package manifest.

terminal
# create the project
mkdir workflowy-mcp-server && cd workflowy-mcp-server
npm init -y

# folders for tools, services, and compiled output
mkdir -p src/tools src/services

Your target structure looks like this — each file owns one job:

project layout
workflowy-mcp-server/
├── package.json
├── tsconfig.json
├── config.json.example     ← template users copy & fill
└── src/
    ├── index.ts            ← entry: MCP server + stdio
    ├── config.ts           ← loads API key + bookmarks
    ├── constants.ts        ← API URLs, defaults
    ├── types.ts            ← shared TypeScript interfaces
    ├── services/
    │   ├── api-client.ts   ← Workflowy API wrapper + rate limit
    │   ├── cache.ts        ← SQLite mirror + FTS5 search
    │   └── sync-engine.ts  ← keeps cache fresh
    └── tools/
        ├── read.ts         ← get node, list children
        ├── search.ts       ← full-text search
        ├── write.ts        ← create / edit nodes
        ├── complete.ts     ← mark done / undone
        ├── bookmarks.ts    ← named shortcuts
        └── sync.ts         ← status + force resync
3

Install the dependencies

Deliberately minimal — the MCP SDK, a fast synchronous SQLite driver, a rate limiter, and Zod for validating tool inputs. Node's built-in fetch handles HTTP, so there's no axios.

terminal
npm install @modelcontextprotocol/sdk better-sqlite3 bottleneck zod
npm install -D typescript @types/node @types/better-sqlite3

Add a tsconfig.json and the build/start scripts:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}
package.json — scripts
"type": "module",
"scripts": {
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "tsc --watch"
}
4

The one idea: cache-first

This is the design decision that makes the whole thing usable. The Workflowy API is rate-limited and slow for big outlines. So instead of asking the API every time, the server keeps a local SQLite copy of your entire tree and answers reads & searches from that copy — instantly, and at zero token cost to Claude.

the data flow
Claude  ⇄ stdio ⇄  MCP server
                      │
          reads/search│          ┌───────────────┐
                      ├─────────▶│ SQLite cache  │  ← instant, no API
                      │          │ (FTS5 search) │
                write │          └───────────────┘
                      ▼                  ▲
              ┌──────────────┐   sync    │
              │ Workflowy API│───────────┘
              └──────────────┘

The cache is a single table plus a full-text index. Writes go through the API and update the cache in the same breath (write-through), so new nodes are searchable immediately.

src/services/cache.ts — schema
CREATE TABLE nodes (
  id          TEXT PRIMARY KEY,   -- Workflowy node id
  parent_id   TEXT,               -- for breadcrumbs
  name        TEXT,               -- the node text
  note        TEXT,
  priority    INTEGER,            -- sibling sort order
  layout_mode TEXT,               -- bullets / todo / h1..h3
  completed_at INTEGER,           -- null = active
  modified_at  INTEGER,
  cached_at    INTEGER NOT NULL
);

-- full-text search: this is what makes search instant
CREATE VIRTUAL TABLE nodes_fts USING fts5(
  id UNINDEXED, name, note,
  content='nodes', content_rowid='rowid'
);
Why it matters
SQLite does the intelligence work; Claude only receives the handful of nodes that matched. A 200,000-node outline searches in milliseconds and spends none of Claude's context window scanning the tree.
5

Define the tools

Tools are the verbs Claude can perform. Keep the set small and unambiguous — one obvious tool per intent, so Claude never has to guess. A lean, opinionated toolset like this works better than a sprawling one:

ToolTypeWhat it does
wf_find_nodeSearchThe primary way in. Cascading search: exact match → date → full-text. Returns nodes with breadcrumb paths.
wf_smart_createWriteCreates a whole nested subtree in one call, then refreshes the cache so it's instantly findable.
wf_get_nodeReadFetch one node's details by id.
wf_list_childrenReadDirect children, sorted by priority, completed hidden by default.
wf_list_bookmarksBookmarkList your named shortcuts to key areas of the outline.
wf_get_bookmarkBookmarkResolve a bookmark → return that node's children.
wf_complete_itemCompleteMark a node done (API + cache).
wf_uncomplete_itemCompleteUn-mark a completed node.
wf_sync_statusSyncReport cache size + last sync time.
wf_force_syncSyncFull resync from the API on demand.

Each tool registers with the MCP SDK, declares its inputs with a Zod schema, and returns text. Here's the shape — wf_find_node as the model:

src/tools/search.ts
import { z } from "zod";

export function registerSearch(server, cache) {
  server.tool(
    "wf_find_node",
    "Search your Workflowy. Use this to LOCATE any node.",
    { query: z.string(), limit: z.number().max(100).default(20) },
    async ({ query, limit }) => {
      // all local SQLite — zero API calls, zero rate limit
      const hits = cache.search(query, limit);
      return { content: [{ type: "text",
        text: formatWithBreadcrumbs(hits) }] };
    }
  );
}
Two rules that save you pain
1. Log to stderr, never stdout — stdout is reserved for the MCP protocol. 2. Always return breadcrumb paths with search results (Reference › Work › Client › Task) so Claude knows where a node lives.
6

Add your config, then build

Ship a config.json.example so users know what to fill in. The key can also come from an environment variable, which takes precedence.

config.json.example
{
  "workflowy_api_key": "YOUR_API_KEY_HERE",
  "bookmarks": {
    "tasks":     { "node_id": "", "label": "Tasks" },
    "capture":   { "node_id": "", "label": "Inbox / Capture" },
    "reference": { "node_id": "", "label": "Reference" }
  },
  "cache_path": "./workflowy-cache.db",
  "sync_interval_minutes": 60
}

To find a bookmark's node_id: open that bullet in Workflowy and copy the last segment of its URL. Then compile:

terminal
cp config.json.example config.json   # then paste your key + ids
npm run build                        # tsc → dist/
node dist/index.js                   # should hang, waiting on stdio = correct
That "hang" is success
An MCP server over stdio has no UI. When node dist/index.js sits there silently, it's alive and waiting for Claude to speak to it. Press Ctrl+C and move on to Part 2.
PART 2 / CONNECT TO CLAUDE

Wire it in.

You register the server once per interface by pointing Claude at your compiled dist/index.js and handing it your API key. Use the absolute path.

A

Claude Desktop

Edit the config file (create it if it doesn't exist), then fully restart Claude Desktop.

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
claude_desktop_config.json
{
  "mcpServers": {
    "workflowy": {
      "command": "node",
      "args": ["/absolute/path/to/workflowy-mcp-server/dist/index.js"],
      "env": { "WORKFLOWY_API_KEY": "your-key-here" }
    }
  }
}
B

Claude Code (CLI)

Register it with one command — Claude Code writes the config for you:

terminal
claude mcp add workflowy node /absolute/path/to/dist/index.js \
  --env WORKFLOWY_API_KEY=your-key-here

Then type /mcp inside Claude Code to connect and confirm the workflowy server shows as connected.

Claude Code only
If you ever restart or rebuild the server, Claude Code won't reconnect on its own — type /mcp again. Claude Desktop reconnects automatically.

First run: let it sync

The very first time it connects, ask Claude to run wf_force_sync, then wf_sync_status. The initial sync pulls your whole outline into the cache — a big board can take a bit, but every read after that is instant. Now ask: "Find my tasks node in Workflowy." If it comes back with a breadcrumb, the engine works.

PART 3 / THE SKILLS

Teach Claude to use it.

The server gives Claude ability. Skills give it judgment. A skill is a small Markdown file with a trigger. When your words match the trigger, Claude loads the skill's knowledge before acting — so it picks the right tool, respects your structure, and formats output the way you want.

What a skill actually is
A folder with a SKILL.md file. The top (frontmatter) says when to load it. The body is plain instructions Claude reads. No code. Drop it in your skills directory and it's live.
1

The system skill — the map

This is the important one. Your MCP server knows the mechanics of Workflowy but nothing about your outline. The system skill supplies the structural knowledge the tools need to operate correctly: what your top-level columns mean, where different kinds of things live, your naming conventions, and which tool to reach for.

skills/workflowy-system/SKILL.md
---
name: workflowy-system
description: >-
  My Workflowy operating system. Trigger whenever the user asks
  to do anything IN Workflowy — search, create, move, read — or
  mentions a column (Tasks, Capture, Reference) or area of their
  outline. Companion to the Workflowy MCP tools: it gives the
  structural map those tools need to operate correctly.
---

# My Workflowy — Structure

## Top-level columns
- **TASKS** — active work. Search here for anything "to do".
- **CAPTURE** — inbox / journal. New raw notes land here.
- **REFERENCE** — the deep archive (most nodes live here).

## Rules the tools must follow
- To LOCATE a node, always use `wf_find_node`. Never dump a tree.
- "Search IN/UNDER <section>" → pass that section as the bookmark
  (WHERE), and the search terms as the query (WHAT).
- Create at the top by default. Never add notes to nodes —
  use child bullets instead.
- Return ALL matching results. Don't cap arbitrarily.

That's the whole trick: describe your outline in plain English, name the tools, and state your rules. Claude now behaves like it has worked inside your board for years.

2

The format skill — clean output

Workflowy imports plain-text outlines by indentation. When you ask Claude to draft something you'll paste into Workflowy, you want perfectly nested bullets — not prose. A format skill guarantees it.

skills/workflowy-format/SKILL.md
---
name: workflowy-format
description: >-
  Output content as a nested bullet outline ready to paste into
  Workflowy. Trigger on "Workflowy format", "format for Workflowy",
  "WF format", or any list/plan/outline meant for pasting in.
---

# Workflowy Format Rules

- Output ONLY the outline — no preamble, no code fence.
- One item per line. Nest with 2 spaces per level.
- Parent bullets first, children indented beneath.
- No blank lines between bullets (Workflowy reads them as breaks).
- Keep each bullet short; push detail to child bullets, not notes.
3

Where they go & how they fire

Drop each skill folder into your skills directory and it's available immediately — no restart, no build.

  • Claude Code: ~/.claude/skills/<skill-name>/SKILL.md
  • Claude Desktop: add via the app's skills/capabilities settings.

The description line is the entire firing mechanism. Claude scans descriptions on every message; when your request matches, it loads that skill's body before touching the tools. So write the description in terms of the phrases you actually say — "sort my inbox", "add to my calendar", "what's in my tasks". The more concrete the trigger, the more reliably it fires.

The payoff
Tools + skills together mean you can say "drop these three ideas into Capture" or "what's in my Tasks about the client?" in plain English — and Claude reaches for the exact right tool, scoped to the exact right part of your outline, every time.
PART 4 / VERIFY & DEBUG

When something's off.

SymptomFix
Server won't appear in ClaudeUse an absolute path to dist/index.js. Confirm npm run build produced dist/. In Claude Code, re-run /mcp.
"No API key configured"Key missing or misplaced. Set WORKFLOWY_API_KEY in the interface config's env, or fill config.json.
Search returns nothingCache is empty or still building. Run wf_force_sync, then wf_sync_status to watch node count climb.
Garbled / protocol errorsYou logged to stdout. Switch every console.log to console.error — stdout is protocol-only.
Claude picks the wrong toolTighten the skill description with the exact phrases you use, and keep your toolset small — one obvious tool per intent.
Rate-limit (429) errorsThrottle API calls (a limiter like bottleneck) and back off on 429. Reads come from cache, so this should be rare.
Test writes safely
Before letting Claude write to live nodes, make a dedicated sandbox bullet and point all create/edit tests at it. One bad delete on a real column is a bad day — a throwaway node costs nothing.