Your canvases, from any tool.

Plug Claude, Cursor or any MCP client into your canvases so it reads and edits them like Nolë does. Or call the REST API from your own scripts. One token, your rights, nothing more.


01/Get a token

Start in Settings.

The MCP server and the REST API take the same API token. It acts as you: it opens the canvases you can open, and no others.

  1. 1.

    In the app, open Settings → MCP & API tokens and click New token.

  2. 2.

    Name it, then pick a permission. Read lists, reads and searches, and is all the REST API needs. Write also creates and edits blocks, on the canvases where you are editor or owner.

  3. 3.

    Copy the token (it starts with nlnr_). It is shown once: we only keep a digest of it. The same dialog gives you the exact endpoint URL and a ready-to-paste Claude Code command.

Treat a token like a password. If one leaks, revoke it from the same page: every tool using it loses access at once. The list shows when each token was last used.

02/Connect over MCP

A second Nolë, in the tool you already use.

Nolënor speaks the Model Context Protocol over streamable HTTP. Give your client the endpoint and the token, and it can work on your canvases.

Endpoint
https://amicable-kudu-722.convex.site/mcp
Transport
Streamable HTTP, stateless, POST only
Auth header
Authorization: Bearer nlnr_..., or x-api-key: nlnr_...

Claude Code

One line

Paste this in a terminal. The token dialog in Settings prints the same command with your token already in it.

terminal
claude mcp add --transport http nolenor \
  https://amicable-kudu-722.convex.site/mcp \
  --header "Authorization: Bearer nlnr_..." \
  --scope user

Claude Desktop

Add a custom connector with the endpoint URL. Claude Desktop only offers preset header names, with no Authorization: pick x-api-key and paste the raw token as its value.

Custom connector
URL      https://amicable-kudu-722.convex.site/mcp
Header   x-api-key: nlnr_...

Cursor and other clients

Any client that reads an mcpServers JSON file (Cursor's mcp.json, a project .mcp.json…) takes this:

mcp.json
{
  "mcpServers": {
    "nolenor": {
      "type": "http",
      "url": "https://amicable-kudu-722.convex.site/mcp",
      "headers": { "Authorization": "Bearer nlnr_..." }
    }
  }
}

Point it at a block

Right-click a block on the canvas, then Copy ID → Canvas & node ID. Paste the reference into your conversation, and the assistant knows exactly which canvas and block you mean.

clipboard
canvasId:k17abc...|nodeIds:n1a2b3,n4c5d6

03/MCP tools

The same hands as Nolë.

A read token sees the first two tools, a write token sees them all. Every tool but list_canvases takes a canvasId, and every edit goes through your rights on that canvas.

Read

Read or write token
list_canvases
Every canvas you can open, with its id, name and description. The id is the canvasId every other tool takes.
bash
A sandboxed, read-only shell over one canvas. The nole command lists (nole ls), reads (nole read), searches (nole search) and dumps tables (nole table); grep, jq, awk and friends do the rest.

Write

Write token
create_node
Add blocks: documents, tables, links, values, titles, apps…
set_node_data
Set a block's values (tables have their own tools).
create_connection
Link two blocks.
group_nodes
Gather blocks in a frame.
move_and_style_nodes
Move blocks, or change their colour or display variant.
delete_elements
Send blocks and connections to the trash, where they stay 30 days.
insert_blocks
Insert paragraphs, headings, lists… into a document.
replace_block
Rewrite one block of a document.
patch_block_text
Edit a passage of text in place.
update_block_props
Change a document block's properties.
delete_blocks
Remove blocks from a document.
table_update_schema
Add, rename or remove a table's columns.
table_insert_rows
Append rows to a table.
table_update_rows
Edit rows of a table.
table_delete_rows
Remove rows from a table.
patch_app_node_code
Edit the code of an App block.

What it can't do: browse the web, generate images, transcribe audio, use Nolë's memory or skills, or touch your settings and sharing. Its edits appear as “Agent” in a block's Versions, so you can always take one back.

04/REST API

Read blocks from your own code.

One endpoint for now: the content of up to 100 blocks of a canvas, in one call. Send the token with every request.

Base URL
https://amicable-kudu-722.convex.site/api/v1
Auth header
Authorization: Bearer nlnr_... (or x-api-key)
Permission
A read token is enough
GET

/api/v1/node-datas

ParameterDescription
canvasIdRequired. The canvas id.
nodeIdsRequired. 1 to 100 node ids, comma-separated (nodeIds=a,b) or repeated (nodeIds=a&nodeIds=b).

Both ids come from Copy ID → Canvas & node ID in the app. Node ids are the short ids of that reference.

terminal
curl "https://amicable-kudu-722.convex.site/api/v1/node-datas?canvasId=k17abc...&nodeIds=n1a2b3,n4c5d6" \
  -H "Authorization: Bearer $NOLENOR_TOKEN"
POST

/api/v1/node-datas

The same read, with the parameters in a JSON body, for lists too long for a URL. Nothing is written.

terminal
curl -X POST "https://amicable-kudu-722.convex.site/api/v1/node-datas" \
  -H "Authorization: Bearer $NOLENOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "canvasId": "k17abc...", "nodeIds": ["n1a2b3", "n4c5d6"] }'
read-blocks.js
const res = await fetch("https://amicable-kudu-722.convex.site/api/v1/node-datas", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NOLENOR_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ canvasId: "k17abc...", nodeIds: ["n1a2b3"] }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const { nodeDatas, notFound } = await res.json();

Response

200 OK, with the blocks found and the ids that weren't: unknown, on another canvas, or in the trash. Those don't fail the request. An id asked twice comes back once.

200 OK
{
  "nodeDatas": [
    {
      "nodeId": "n1a2b3",
      "nodeDataId": "j57xyz...",
      "type": "blocknote",
      "values": { "...": "..." },
      "updatedAt": 1760781234567
    }
  ],
  "notFound": ["n4c5d6"]
}
nodeId
The node id you asked for.
nodeDataId
Id of the content behind the block. Several blocks can share one, when the same content is shown on several canvases.
type
link, image, blocknote, value, title, pdf, table, app, audio, video, frame or custom.
templateId
Custom blocks only: the template that defines the shape of values.
values
The block's content. Its fields depend on type.
updatedAt
Last change, in milliseconds since the epoch.

05/Errors

One shape for every error.

A status code, a stable code to branch on, and a message meant for a human.

400 Bad Request
{ "error": { "code": "bad_request", "message": "Missing canvasId." } }
400
bad_request
canvasId missing, nodeIds missing or empty, more than 100 nodeIds, or a POST body that isn't a JSON object.
401
unauthorized
No token, or an invalid or revoked one.
404
canvas_not_found
The canvas doesn't exist, or your token can't open it. Both answer the same, so the API never reveals that a private canvas exists.

The MCP endpoint answers 401 to a missing or revoked token too, and 405 to anything but POST.

Building something?

Missing an endpoint, or a tool your agent needs? Nolënor is open source: open an issue, or read how it works.