# The MCP

![An AI assistant plugged into a stack of notes](/docs/concepts/mcp.webp)

The Potion MCP lets an AI assistant work directly in your workspace: find and write notes, tag them, set properties, build views, set up the graph, read attached files, and design html blocks. It uses the [Model Context Protocol](https://modelcontextprotocol.io), so **any client that supports MCP servers can connect** — Claude, Cursor, VS Code, Zed, and others. Potion is not tied to one assistant.

This page covers connecting, how the tools behave, and every tool by area. For what the things themselves are, start with [Concepts](/docs/concepts).

This page is also available as plain Markdown at [/docs/mcp.md](/docs/mcp.md).

::toc

## Connecting

- **Server URL:** `https://mcp.potion.run` (Streamable HTTP)
- **Sign-in:** OAuth with the Google account you use for Potion. Your assistant opens the sign-in page the first time.
- **Plan:** Pro. On another plan every tool answers with an upgrade message, except `get_user_info`.

How you add it depends on the client. The server URL is the only part that matters; everything else is that client's own shape.

**Claude Code**: `claude mcp add --transport http potion https://mcp.potion.run`, or install the Potion plugin, which bundles this server with skills describing how Potion works.

**Claude desktop, claude.ai, and other hosted assistants**: add a custom connector with the server URL above.

**Cursor, VS Code, Zed, and most editors**: add it to the MCP server list in settings, as an HTTP server pointing at that URL.

**An agent reading the docs**: point it at [/llms.txt](/llms.txt), which lists every section as Markdown, or [/llms-full.txt](/llms-full.txt) for all of them in one file.

**Anything else**: a config like this one.

```json
{
  "mcpServers": {
    "potion": { "type": "http", "url": "https://mcp.potion.run" }
  }
}
```

### Choosing the workspace

Every tool works in **one workspace at a time**. Without anything extra that is your default workspace, which `set_default_workspace` changes.

To pin a connection to a specific workspace, for example one per project folder, send a `workspace` header with the workspace's **name**:

```json
{
  "mcpServers": {
    "potion": {
      "type": "http",
      "url": "https://mcp.potion.run",
      "headers": { "workspace": "Research" }
    }
  }
}
```

The header matches by name, so renaming that workspace breaks the connection until the header is updated.

## How the tools behave

**Everything is scoped to you and one workspace.** The server knows who you are from sign-in; no tool takes a user id, and the database itself refuses any row that is not yours.

**Names in, ids out.** Tools accept the names people use (tag names, property keys, preset and lens names, note aliases) and return ids for precise follow-up calls. A name that does not exist is refused with a list of what does, never guessed at.

**Changing is not rewriting.** Notes, view filters and lens scopes each have a precise tool for a small change beside the one that replaces everything:

| To | Precise | Replaces all |
| --- | --- | --- |
| change a note | `append_to_note` (add), `edit_note` (one passage) | `update_note` |
| change a view's filter | `edit_view_filter` (one condition, by id) | `update_view` with `filter` |
| change a lens's scope | `edit_lens_scope` (one condition, by id) | `update_lens` with `scope` |

The precise tools exist because an AI resending a whole note or filter can quietly change the parts it was not asked about.

**Destructive actions are guarded.** Deleting a note moves it to the trash for 30 days. `delete_property_def` previews what would be lost and only deletes when called again with `confirm`. A property still used by a view, preset or lens is refused, with the names of what uses it. Workspaces cannot be renamed or deleted through the MCP at all, and neither can a structure be deleted (it takes every row with it); both are done in the web app.

**Errors explain themselves.** A refused call returns `success: false`, an `error` saying why, and often the next step to take.

## Tools

### Workspaces and account

| Tool | Does |
| --- | --- |
| `get_user_info` | who is signed in |
| `list_workspaces` | your workspaces, and which is the default |
| `get_default_workspace` | the workspace tools use when no header is sent |
| `set_default_workspace` | change it |
| `create_workspace` | a new, empty workspace |
| `init_workspace` | create a first workspace if there is none |
| `get_context` | the headers the server received, to check a `workspace` header |

### Notes

| Tool | Does |
| --- | --- |
| `find_note` | find a note you named, checking aliases first, then titles |
| `list_aliases` | every note that has an alias |
| `read_note` | a note with its tags, properties, files, and links in both directions |
| `list_notes` | the 30 newest notes |
| `create_note` | a new note, with tags, alias, cover and pin |
| `append_to_note` | add text to the end, optionally under a heading |
| `edit_note` | replace one exact passage |
| `update_note` | replace the title, body or tags (guard with `if_unchanged`) |
| `delete_note` | move a note to the trash |
| `list_revisions` | earlier versions of a note, kept 7 days |
| `restore_revision` | put an earlier version back (itself undoable) |
| `upload_image` | store an image, from a public URL, an upload (`upload_id`) or as data, and get its permanent address `/api/images/<id>` for a body, a cover, or a property a block inlines |

### Search

| Tool | Does |
| --- | --- |
| `search_notes` | ranked full-text search: titles, then tags, then text; `"phrases"` and `-exclusions` |
| `search_notes_by_tags` | notes with all, or any, of some tags |
| `search_notes_by_property` | compare, sort and find missing values: `rating gt 7`, `status eq unread`, `rating empty` |

### Links

| Tool | Does |
| --- | --- |
| `list_note_links` | what a note links to, and its backlinks |
| `lint_links` | links to notes not written yet, and notes nothing links to |
| `get_agenda` | what is unfinished in the workspace, ranked, each with a suggested next call |

### Tags

| Tool | Does |
| --- | --- |
| `list_tags` | every tag with its note count and linked tags |
| `link_tags` | adding one tag also adds others, including on notes already tagged |
| `unlink_tags` | remove those rules (tags already applied stay) |
| `list_tag_links` | the rules, and why a tag appeared on a note |

Tags are created by using them on a note; there is no separate create step.

### Properties

| Tool | Does |
| --- | --- |
| `list_property_defs` | the workspace vocabulary, with how many notes and views use each |
| `create_property_def` | define a typed property: text, number, date, bool, select, json |
| `update_property_def` | rename, relabel, reorder, or widen a select's options |
| `delete_property_def` | preview, then with `confirm` delete a property and every value of it |
| `set_property` | set or clear one property on one note |

### Views

| Tool | Does |
| --- | --- |
| `list_views` | every view with its filter, sort and display |
| `read_view` | one view and the notes it matches, in its order |
| `create_view` | a feed view (filter, sort, display) or a note view |
| `update_view` | change the name, filter, sort, display, cover, colour, icon or pin |
| `edit_view_filter` | add, replace, remove, regroup or move one condition |
| `delete_view` | delete the view (not its notes) |

The filter these take is described in [Filters and views](/docs/filters).

### Graph

| Tool | Does |
| --- | --- |
| `list_lenses` | every lens, its scope summary, settings, and a link to open it |
| `read_lens` | one lens with node ids and how many notes its scope holds |
| `create_lens` | name, scope, and only the settings that differ from the defaults |
| `update_lens` | rename, replace the scope, change some settings, make default, reorder |
| `edit_lens_scope` | change one condition of the scope, like `edit_view_filter` |
| `delete_lens` | delete the setup; the next lens becomes default if needed |

A lens is a saved way of looking at the graph. See [Graph](/docs/graph).

### Records

Structured rows that are not notes — a weight on a date, a rating, a logged idea. They appear in no note surface, and exist to be queried and charted.

| Tool | Does |
| --- | --- |
| `list_structures` | the shapes that exist, what each is for, and its fields. Call it first |
| `create_structure` | declare a shape before any row of it exists; a `file` field takes a file's id, and a `record` field takes a row's id of the structure named in `structure`; a field can be `required` and `unique` (one row per value), a number can carry a `unit` and `min` / `max`, any field a display `label`; `unique_together` makes one row per combination; `max_rows: 1` makes a settings object whose write replaces its one row, `max_rows: N` caps a list; `immutable` makes rows written once, never edited |
| `update_structure` | change the description or label, add a field or a choice, turn `unique` on or off, set `max_rows`, or turn `immutable` on or off. Tightening a rule (required, unique, removing a choice, a lower `max_rows`) is refused while rows disagree; renames, retypes, removals and a changed unit are refused |
| `write_record` | add one row, or many at once. Refuses an unknown structure; names every missing required field at once; refuses a unique value already taken, naming the row, unless `on_conflict: "update"` updates that row instead (a batch is all or nothing: one bad row writes none, and is named); after a write, lists the optional fields left empty so the assistant can offer to fill them |
| `list_records` | the rows of one structure, filtered, sorted and paged; every row carries its permanent `id`, and a filter on `id` pulls one |
| `summarize_records` | count, sum, average, min and max of a number field, computed by the database: whole, per day/week/month/year, or per value of a field |
| `update_record` | correct one row; a partial change is merged and re-validated |
| `delete_record` | remove one row (soft) |

A record cannot exist before its structure, and field changes are additive only. See [Record](/docs/concepts#record).

| Tool | Does |
| --- | --- |
| `list_webhooks` | the subscriptions on the workspace's structures: URL, events, narrowers, on, off or paused |
| `create_webhook` | subscribe a URL the user gave to a structure; narrow it with `filter` (rows that match) and `fields` (updates to those fields). Answers with the signing secret, once |
| `update_webhook` | change the URL, events, filter or fields; turn it off or on; `resume` after a pause |
| `delete_webhook` | unsubscribe |
| `list_webhook_deliveries` | what was sent and how the receiver answered; `send_test` queues a test event first |

How events are sent and signed: [Webhooks](/docs/webhooks).

### Presets

| Tool | Does |
| --- | --- |
| `list_presets` | every preset with what it sets and asks |
| `read_preset` | one preset in full, by id or name |
| `create_preset` | tags, fixed values, and values to `ask` for |
| `update_preset` | change it (lists passed replace the whole list) |
| `delete_preset` | delete it; notes it was applied to keep their tags and values |
| `apply_preset` | apply to a note, with `answers` for its questions |

### Tasks

A task is one AI job you start on an open note from its **Tasks** menu, run on your own AI account: a name, a kind, and the instructions the model is given every time. An assistant can write tasks for you here; it cannot run one, because running needs your AI key, which the MCP never holds. See [Tasks](/docs/ai#tasks).

| Tool | Does |
| --- | --- |
| `list_tasks` | the tasks you wrote, the built-in ones (listed, not editable here), and the kinds with what each may do |
| `create_task` | a `name`, a `kind` and `instructions`, with an optional `description` and `model` |
| `update_task` | change the name, description, instructions or model, by id or name; the kind never changes |
| `delete_task` | delete a task you wrote, by id or name; no note is touched |

| Kind | May |
| --- | --- |
| `write` | read the note and write text, appended to the end of the note when it is done |
| `speak` | read the note and say its answer aloud; nothing is kept |
| `watch` | watch the YouTube video the note holds and write text, appended to the note when it is done |

A kind that writes only appends to the note it was started on; no kind edits what is there, reads another note or reaches the network, and instructions cannot widen that. A workspace holds 30 tasks; a name is 60 characters and unique, a description 140, instructions 2,000.

### Layouts

| Tool | Does |
| --- | --- |
| `list_layouts` | every layout, or the ones that fit a note or preset |
| `read_layout` | its html and the notes using it |
| `create_layout` | from html, or lifted from a note's block |
| `update_layout` | change it, which changes every note using it |
| `apply_layout` | make a note render through a layout |
| `detach_layout` | give a note its own copy instead of the reference |
| `load_live_layout`, `read_live_layout`, `write_live_layout`, `publish_live_layout` | work on a draft in the web app's live editor, then publish it |

### Html blocks

| Tool | Does |
| --- | --- |
| `list_block_libraries` | the libraries a block may load, and when to use each |
| `read_block_data_api` | the full [Block Data API](/docs/block-data-api) reference |

### Files

| Tool | Does |
| --- | --- |
| `upload_file` | add a file you send (PDF, EPUB, DOCX, markdown, text, HTML, CSV, JSON), as an upload (`upload_id`) or as data: attached to a note, or to the workspace alone when `note_id` is left out |
| `start_upload` | get a one-time link to send a file from your machine, so its bytes never pass through the conversation (see below) |
| `fetch_file` | attach a file straight from a public URL |
| `list_files` | files, attached or kept in the workspace alone: their status and storage used |
| `describe_file` | a file's outline and opening, to decide what to read |
| `read_file` | its text by pages, section or chunks |
| `search_file` | full-text search inside attachments |
| `rename_file` | rename it; `[[file:…]]` references follow |
| `process_file` | run text extraction again |
| `delete_file` | delete a file and its text |

#### Sending a file without typing it

Every argument of a tool call is written out by the model, so a long page or a binary file cannot sensibly travel inside one. `start_upload` moves the bytes over plain HTTP instead:

1. `start_upload({ kind })` returns an `upload_id`, a link and a ready `curl` command. `kind` is `text` (up to 2 MB), `image` (10 MB) or `file` (50 MB).
2. Send the file to the link with one `PUT`, headers unchanged. The link works once, for ten minutes.
3. Use the id: write `{{upload:<upload_id>}}` in the `content` of `create_note`, `update_note` or `append_to_note` (inside an html fence for a page), or pass `upload_id` to `upload_image` or `upload_file`.

- An upload is used once and then removed; one that is never used is deleted after an hour. A person may have ten waiting at a time.
- Pass the file's `sha256` to `start_upload` and anything else sent to the link is refused: the link is the only credential, and it appears in the conversation.
- A note built from uploads may hold 2,097,152 characters at most. The tool then answers with the note's length and not its content; `read_note` returns it.
- Uploaded text is the note's content like any other: an html block still runs in the sandbox, with no network.

### Sidebar

| Tool | Does |
| --- | --- |
| `list_quick_access` | the notes pinned to the sidebar, in order |
| `add_to_quick_access` | pin a note, with an icon and colour |
| `update_quick_access` | change a pin's icon or colour |
| `remove_from_quick_access` | unpin it |

## Good first requests

- "What's in my Potion?" — the assistant lists workspaces, views and tags.
- "Save this article to Potion with the Article preset."
- "Make a view of films I rated 8 or more, best first."
- "Make a lens of my Rust notes with their tags, and give me the link."
- "Write me a task that lists the action items of a note as a checklist."
- "Which of my notes link to *Machine Learning*?"
- "Read the PDF on my *Research* note and summarise chapter 3."
