your notes are
your LLM's second brain.
outl ships an MCP server. Claude Desktop, Claude Code, Cursor, Zed, ChatGPT — anything that speaks the Model Context Protocol — reads and writes your local markdown the same way you do. No upload. No cloud. No copy-paste.
outl --workspace ~/notes mcp serve
Same binary as the CLI. JSON-RPC over stdio. Protocol revision
2024-11-05.
a second brain that stays yours.
Notion AI, Roam AI, Mem — all of them solve the second-brain problem by shipping your notes to their servers and renting the answer back to you. The "AI second brain" market got "second" right and "yours" backwards.
The .md files stay on your disk. outl is a binary that reads them. The LLM host runs locally too. Nothing is uploaded by outl.
MCP is an open protocol. Today: Claude, Cursor, Zed, ChatGPT. Tomorrow: whatever wins. You don't migrate your second brain when you switch models.
Every MCP tool maps 1:1 to a CLI subcommand. Same handlers, same JSON envelope, same exit codes. No parallel logic to drift.
The flow: you write markdown in your editor. outl runs an MCP server in the background. Your LLM client (Claude Desktop, Cursor, …) connects to it. The model can now read any page, search the graph, append to today's journal, move a block — the same set of operations the CLI exposes. The contract is the same one documented at /docs/cli.
wire it into your
LLM host.
The shape is the same everywhere: register a server whose
command is outl and whose args point at the
workspace. Restart the host. Done.
Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json {
"mcpServers": {
"outl": {
"command": "outl",
"args": ["--workspace", "/Users/you/notes", "mcp", "serve"]
}
}
} Restart Claude Desktop. The outl tools show up under the server name. Pin outl://daily/today as a resource and Claude reads today's journal without a tool call.
Claude Code
~/.claude.json (or per-project .mcp.json) {
"mcpServers": {
"outl": {
"command": "outl",
"args": ["--workspace", "/Users/you/notes", "mcp", "serve"]
}
}
} Same JSON shape as Claude Desktop. Inside Claude Code, your notes become tools (outl_page_get, outl_search, etc.) and resources you can @-mention.
Cursor
Settings → MCP → Add Server {
"mcpServers": {
"outl": {
"command": "outl",
"args": ["--workspace", "/Users/you/notes", "mcp", "serve"]
}
}
} Cursor reads MCP servers from the same JSON shape. Once enabled, the composer can grep your notes, append to today's journal, and turn a block into a draft — all without leaving the editor.
Zed
~/.config/zed/settings.json → context_servers {
"context_servers": {
"outl": {
"command": {
"path": "outl",
"args": ["--workspace", "/Users/you/notes", "mcp", "serve"]
},
"settings": {}
}
}
} Zed exposes MCP as context servers. The Assistant panel can pull outl://page/<slug> straight into the conversation.
ChatGPT (desktop)
Settings → Connectors → Add MCP server command: outl
args: ["--workspace", "/Users/you/notes", "mcp", "serve"]
transport: stdio ChatGPT desktop's MCP support is rolling out per region. The transport is stdio, same binary. Once the connector is enabled, ChatGPT can call outl_search and read outl://daily/today.
Any other MCP host
host-specific command: outl
args: ["--workspace", "<absolute path>", "mcp", "serve"]
transport: stdio (JSON-RPC 2.0)
protocol: MCP 2024-11-05 If the host speaks MCP, it speaks outl. Zero special-casing — the server is the same binary, the same handlers, the same workspace lock as the CLI.
verify before you wire it
Pipe two JSON-RPC frames into outl mcp serve. If both come back with valid responses, the server is
healthy.
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| outl --workspace ~/notes mcp serve
tools, resources,
prompts.
MCP surfaces three things to the host. Tools the model can call. Resources it can attach as context without a call. Prompts you can pick from a slash menu. outl exposes all three.
tools
41 of them, each a 1:1 mirror of a CLI subcommand. Destructive
tools (outl_page_delete, outl_block_delete) require confirm: true — the
server fails closed.
Every successful call used to send its payload twice — once as a structured envelope, once as pretty-printed JSON in the text content — and every outline node carried a pre-tokenized inline AST that restates text the model already has. It is invisible on one call and it is most of your context window by the tenth.
A success reply is content-only and compact now, with default-valued fields dropped. Measured against a 2,862-page workspace:
Characters, not tokens — the ratio is what matters. Two things are
deliberately not trimmed: the journal reads keep their JSON, because
the outline is the only place a block id appears and you need one to
write back; and errors keep the structured envelope, because it is
the only machine-readable copy of the error data. If you wrote a
client that read structuredContent on
success, that is the one thing here that breaks.
| tool name | what it does |
|---|---|
| pages | |
| outl_page_get | page meta + outline tree |
| outl_page_create | new page, idempotent on slug · takes a content forest so a page lands with its whole outline in one call |
| outl_page_update | patch title/icon |
| outl_page_rename | rename a slug · does not rewrite [[old]] refs, it reports them in affected_refs |
| outl_page_delete | move the page root to trash · needs confirm: true |
| outl_page_list | list pages, filter by tag:foo or kind:journal|page |
| outl_page_render | projected .md (clean, no sidecar) |
| blocks | |
| outl_block_get | a single block by id |
| outl_block_append | append a child to a page or block |
| outl_block_append_tree | append a whole subtree in one call — prefer it over a chain of appends |
| outl_block_insert | insert a sibling right after another |
| outl_block_update | replace a block's text |
| outl_block_move | reparent · cycle-safe |
| outl_block_delete | move a block to trash · needs confirm: true |
| outl_block_toggle_todo | None → TODO → DOING → DONE → None |
| outl_block_tree | the block plus its descendants |
| journal | |
| outl_daily_today | today's journal (creates on first read) |
| outl_daily_get | a journal by ISO date, today/yesterday/tomorrow, or April 22nd, 2026 |
| outl_daily_append | append to a journal, today by default |
| outl_daily_range | every journal between two inclusive dates |
| finding things | |
| outl_search | full-text across blocks and page titles |
| outl_query | structured filter over pages — tag, property, date range, kind |
| outl_backlinks | pages and blocks that mention [[slug]] |
| outl_block_refs | blocks that cite ((blk-XXXXXX)) |
| outl_block_embed | resolve !((…)) recursively — source block and children |
| outl_tag_list | every tag with usage counts |
| outl_tag_pages | pages mentioning #tag anywhere in their subtree |
| properties | |
| outl_page_prop_set | set a page-level key:: value |
| outl_page_prop_get | read a page property by key |
| outl_page_prop_list | every property on a page |
| templates | |
| outl_template_list | pages carrying a non-empty template:: property |
| outl_template_apply | instantiate a structural template under a target block |
| outl_template_resolve | a callable template's language, source and declared params |
| outl_template_run | run a callable template and write the result subtree under the anchor |
| export & files | |
| outl_export_md | the page as clean markdown |
| outl_export_json | the page's AST and sidecar as JSON |
| outl_export_hugo | the page as a Hugo markdown file |
| outl_asset_add | import a file into assets/ and link it from a new block · takes a path, because MCP is stdio |
| workspace | |
| outl_batch | a sequence of writes in one session · stops at the first error and reports failed_at + applied |
| outl_workspace_info | path, actor, counts, ops |
| outl_workspace_doctor | integrity checks over the op log, sidecars, block refs and lock |
resources
URIs the host can attach as context — no explicit tool call.
Pin outl://daily/today in
Claude Desktop and the model sees the day's context every
turn.
| uri | type | body |
|---|---|---|
| outl://workspace/info | application/json | Workspace path, actor id, counts (pages, blocks, ops). |
| outl://daily/today | text/markdown | Today's journal projection. Lazily creates the file the first time it's read — same semantics as outl daily today in the CLI. |
| outl://page/{slug} | text/markdown | Projected markdown for any page by slug. Use it to attach a specific page as context without a tool call. |
prompts
Slash shortcuts the host renders in the prompt picker. Optional sugar over tools.
| prompt | arguments | does |
|---|---|---|
| outl-summarize-day | date? (ISO) | pulls the daily journal, asks the model for a summary. |
| outl-blog-from-block | block_id | expands a single block into a blog draft. |
what you'll actually do with it.
The point isn't "now the LLM knows about your notes". The point is the everyday glue: stop pasting context manually.
morning summary
you're my second brain. Summarize what's open from yesterday and what's on the journal for today. Pull outl://daily/today.
outcome: 1-paragraph status, links to the blocks Claude touched, ready to paste anywhere.
ask your notes
what did I write about the tree-CRDT trade-offs? call outl_search with q: "tree-crdt trade-offs" and quote the blocks.
outcome: search returns matching blocks with their parent page; the LLM cites them inline with stable block IDs.
block → blog draft
expand blk-9f12 into an 800-word blog post in my voice. tone: direct, terminal-flavored.
outcome: outl_block_get pulls the block + children; the model drafts; you keep the source unchanged.
task triage
list everything with status:: todo across the last 14 daily notes and group by tag.
outcome: outl_query filters by property + date range; the model formats the table; no agent loop needed.
append from chat
add this paragraph to today as a child of blk-3a1d.
outcome: outl_block_append writes one op. The CRDT log records it, the .md updates, your editor sees it on next open.
the honest data flow.
- ✓
Reads
.md+ sidecars on your disk. - ✓ Talks JSON-RPC over stdio to whatever process spawned it.
- ✓ Holds an exclusive workspace lock — no race with the TUI.
- ✓ Records every op in the local CRDT log.
- ✗ Open a network socket. No HTTP, no WebSocket.
- ✗ Upload your notes anywhere. There is no outl cloud.
- ✗ Send telemetry. The binary doesn't phone home.
- ✗ Need an account. You install it and use it.
The LLM host (Claude Desktop, Cursor, ChatGPT, …) is the party that does ship your prompt — including any block content the model loaded — to its model provider. That's how the model gets the context. outl's privacy story doesn't override the host's privacy story. Pick a host whose terms you're comfortable with. If you need air-gap, point an MCP-speaking local model (Ollama via an MCP bridge, etc.) at the same server.
mcp, claude, second brain —
straight answers.
What is MCP and why does outl need one?
MCP (Model Context Protocol) is the open standard from Anthropic for connecting LLMs to local tools and data. outl ships an MCP server (`outl mcp serve`) so Claude, Cursor, Zed, ChatGPT and anything else that speaks MCP can read and write your notes through a stable contract — no copy-paste, no upload, no parsing of human output.
Is outl as MCP server the same as Notion AI or Roam AI?
No. Notion AI and Roam AI send your workspace to a remote model on their servers. outl is local-first: your notes never leave your machine. The LLM client (Claude Desktop, Cursor, etc.) runs locally and talks to `outl mcp serve` on stdio. The model itself runs wherever you chose to run it. There is no outl cloud — and when you sync across devices, that's peer-to-peer over iroh, end-to-end encrypted, with no third-party server holding your notes either.
How do I configure Claude Desktop to use outl as a second brain?
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add an `mcpServers.outl` entry with `command: "outl"` and `args: ["--workspace", "/absolute/path/to/notes", "mcp", "serve"]`. Restart Claude Desktop. The outl tools appear under the server name in the tools panel.
Does this work with ChatGPT or only Claude?
It works with anything that speaks MCP. Claude Desktop, Claude Code, Cursor, Zed all support MCP today. ChatGPT desktop is rolling out MCP connectors. Any new host that adopts the protocol picks outl up for free — the binary doesn't change.
What can the LLM actually do with my notes?
41 tools' worth. Read any page or block, search full-text, run structured queries on properties and tags, list backlinks and block references, append to today's journal or any date, create pages with a whole outline in one call, rename pages, cycle tasks through TODO/DOING/DONE, move blocks, set page properties, run templates, export to markdown, JSON or Hugo, import a file into assets, and apply a batch of writes in one session. Destructive operations (page delete, block delete) require `confirm: true` in the call — without it the server returns a recoverable error and nothing is touched.
How many tokens does an outl MCP call cost?
Far fewer than it used to. Every successful call once sent its payload twice — as a structured envelope and again as pretty-printed JSON — and every outline node carried a pre-tokenized inline AST that restated text the model already had. A success reply is content-only and compact now, with default-valued fields dropped. Measured on a 2,862-page workspace: reading today's journal fell from about 12.2k to 5.9k characters, reading a journal page from about 22k to 5.9k, and listing every page from about 726k to 303k. Two things are deliberately not trimmed: journal reads keep their JSON, because the outline is the only place a block id appears and you need one to write back, and errors keep the structured envelope because it is the only machine-readable copy of the error data.
Can an agent make several edits without a round-trip per block?
Yes, and it should. `outl_block_append_tree` appends a block and its whole recursive subtree in one call, `outl_page_create` accepts a content forest so a new page lands fully populated, and `outl_batch` applies a sequence of writes in a single workspace session. A batch stops at the first error and reports `failed_at` plus what it `applied`, so the caller can resume the suffix that never ran instead of guessing what landed.
Does outl upload my notes to Anthropic or OpenAI?
outl does not upload anything. outl is a local binary that reads and writes .md files on your disk. What the LLM host does with the data you feed it is governed by that host's privacy policy (e.g. Claude Desktop's terms with Anthropic). Pick a host you trust.
Can I run outl as an MCP server without the TUI?
Yes. `outl mcp serve` is its own subcommand and doesn't need the TUI to be open. It holds an exclusive lock on the workspace — you can't have `outl serve` and `outl mcp serve` writing to the same workspace at the same time. Two readers are fine.
What's the difference between the CLI and the MCP server?
Same logic, two surfaces. Every CLI subcommand (`outl page get`, `outl search`, `outl block append`, …) maps 1:1 to an MCP tool (`outl_page_get`, `outl_search`, `outl_block_append`). The MCP server holds the workspace in-memory between calls and caches the index, so it's faster than re-invoking the CLI. Both speak the same JSON envelope.
Is the MCP server free and open source?
Yes. The MCP server is a subcommand of the same MIT-licensed Rust binary as the rest of outl. No separate license, no paid tier, no telemetry. Source: github.com/outlmd/outl.
Where do I find the full tools and resources reference?
On this page (tables below) and in the project docs at /docs/mcp. The CLI counterpart contract — same handlers, same error codes — lives at /docs/cli.
give your LLM
a second brain.
One binary. One config block. Same notes you already have.