Developers
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.
MCP server
For AI assistants. List, read, search and edit blocks, with the same tools Nolë uses. Changes show up live on the canvas.
POST /mcpREST API
For your own code. Plain HTTP and JSON, versioned under /api/v1. Read-only for now: it returns the content of the blocks you ask for.
GET /api/v1/node-datas01/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.
In the app, open Settings → MCP & API tokens and click New token.
- 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.
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,
POSTonly - Auth header
Authorization: Bearer nlnr_..., orx-api-key: nlnr_...
Claude Code
One linePaste this in a terminal. The token dialog in Settings prints the same command with your token already in it.
claude mcp add --transport http nolenor \
https://amicable-kudu-722.convex.site/mcp \
--header "Authorization: Bearer nlnr_..." \
--scope userClaude 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.
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:
{
"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.
canvasId:k17abc...|nodeIds:n1a2b3,n4c5d603/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 tokenlist_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 tokencreate_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_...(orx-api-key)- Permission
- A read token is enough
/api/v1/node-datas
| Parameter | Description |
|---|---|
canvasId | Required. The canvas id. |
nodeIds | Required. 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.
curl "https://amicable-kudu-722.convex.site/api/v1/node-datas?canvasId=k17abc...&nodeIds=n1a2b3,n4c5d6" \
-H "Authorization: Bearer $NOLENOR_TOKEN"/api/v1/node-datas
The same read, with the parameters in a JSON body, for lists too long for a URL. Nothing is written.
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"] }'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.
{
"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.
{ "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.