Simple cloud-based headless CMS — store and retrieve content via NPM, REST, or MCP, with a visual editor at pullnote.com and a Claude plugin for conversational editing.
The Pullnote Claude plugin lets Claude read and edit your website or app content conversationally — no code, no API keys. Once installed, you can say things like "Update the About page intro" or "Write a new blog post about X and publish it" and Claude handles the rest.
Install (Claude Code terminal or Cowork Plugins panel)
/plugin marketplace add https://www.pullnote.com/.claude-plugin/marketplace.json
/plugin install pullnote@pullnote
Get started with an invite
- Ask your project admin to invite you — you'll receive an email with a magic link
- Open the link to get your invite token
- Tell Claude: "Join the Pullnote project with token YOUR_TOKEN"
- Claude generates a local MLAuth cryptographic identity, registers it, and joins the project automatically
What Claude can do once connected
- Read and update any page or content path
- Edit specific named blocks within a page without touching the rest
- Create new notes, list content, manage structure
- All writes are signed with Claude's local keypair — no shared secrets
For the full agent instruction set, see skills/pullnote/SKILL.md. This file also serves as a quick-start for agents in other tools (Cursor, Windsurf, etc.) that can read a URL.
Sign up for a free API key from https://www.pullnote.com
npm install @pullnote/client
import { PullnoteClient } from '@pullnote/client';
const pn = new PullnoteClient(PULLNOTE_KEY);AI agents can authenticate using MLAuth cryptographic identities instead of API keys. This provides decentralized authentication without needing to manage secrets.
First, register your agent identity with MLAuth and obtain your dumbname and private key.
import { PullnoteClient } from '@pullnote/client';
const pn = new PullnoteClient({
dumbname: 'your-agent-name',
privateKeyPath: '~/.mlauth/private.pem'
});
// First request auto-registers and creates a project
const note = await pn.get('/');
// Get agent info and project details
const info = await pn.getAgentInfo();
console.log(info.projects); // Array of your projectsAlternative: Provide private key directly
const pn = new PullnoteClient({
dumbname: 'your-agent-name',
privateKey: '-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----'
});Alternative: Custom signing function (for browser or other environments)
const pn = new PullnoteClient({
dumbname: 'your-agent-name',
signer: async (message) => {
// Your custom signing logic here
// Must return base64-encoded ECDSA signature
return yourCustomSignFunction(message);
}
});For complete agent documentation, see https://www.pullnote.com/skill.md
await pn.add('/all-about-being-blue', {
title: 'A Kinda Blue',
content: 'This is my content page about the colour blue.'
});Or use a prompt
await pn.add('/all-about-being-blue', {
title: 'A Kinda Blue',
prompt: 'Write a short piece about the importance of the color blue'
});var note = await pn.get('/all-about-being-blue');If your note contains embedded blocks (<!-- block_id --> markers), note.content contains only the main content, and the blocks are returned in note.blocks.
To retrieve just a single block:
var block = await pn.get('/all-about-being-blue#contact_us');
// block.content contains only that block's markdown
// block.block_id identifies which block was returnedOr just the components
var title = await pn.getTitle('/all-about-being-blue');
var htmlContent = await pn.getHtml('/all-about-being-blue');Get surrounding parents, parent, children and sibling notes - useful for building menus and breadcrumb trails.
var notes = await pn.getSurrounding("/blog");Search for notes with filters, sorting, and field selection.
// Find all notes with title containing "Hello"
var notes = await pn.find('/', { title: "Hello" });
// Find notes with data of London under the path "apple", with custom sorting
var appleLondonNotes = await pn.find('/apple', { "data.city": "London" }, "title", 1);
// Find notes with specific fields only
var summaries = await pn.find('/', {}, "modified", -1, "path,title,data");
// Return all notes
var allNotes = await pn.find('/');await pn.remove("all-about-being-blue");Users can edit content by logging in at https://www.pullnote.com
await pn.addUser("support@pullnote.com");var xml = await pn.getSitemap("https://myurl.com");The PullnoteClient class provides a set of methods to interact with the Pullnote API. Below is a summary of all available methods:
| Method | Description |
|---|---|
| get | Retrieve a note by path (optionally in a specific format) |
| find | Search for notes with filters, sorting, and field selection |
| add | Add a new note at a given path |
| update | Update an existing note at a given path |
| remove/delete | Delete a note at a given path |
| COMPONENTS | |
| getMd | Retrieve a note's content as Markdown |
| getHtml | Retrieve a note's content as HTML |
| getTitle | Retrieve a note's title |
| getImage | Retrieve a note's image URL |
| getHeadHtml | Retrieve a note's SEO friendly head tags (title, description, imgUrl) |
| FOLDERS | |
| getSurrounding | List related surrounding notes (parent, siblings, children - useful for menus) |
| list | Synonym for getSurrounding |
| getAll | Retrieve summary of all notes in the database - same as find('/') |
| getParent | Retrieve a note's parent folder note (if it exists) |
| getParents | Retrieve all parents for a note |
| getChildren | Retrieve the children of a note |
| getSiblings | Retrieve the siblings of a note |
| getBreadcrumbs | Synonym for getParents |
| UTILITIES | |
| exists | Check if a note exists at a given path |
| getIndex / setIndex | Set a bespoke ordering |
| getData / setData | Save JSON data against a note e.g. a related product key |
| getSitemap | Returns a dynamic XML sitemap |
| addUser | Add a human editor to your project |
| removeUser | Remove a human editor from your project |
| clear | Clear the internal cache |
interface Note {
_id: string;
project_id: string;
path: string; // e.g. blog/cats
title: string;
description?: string;
imgUrl?: string; // either your own or pullnote hosted if added via backend
prompt?: string; // LLM prompt for AI title, description, content
imgPrompt?: string; // LLM prompt for AI images
content?: string; // Main content (markdown); embedded blocks are available via `blocks`
blocks?: Array<{ id: string; content: string }>;
block_id?: string; // Only present for `GET /path#block_id`
created: string; // date time
modified: string; // date time
author?: string; // nom-de-plume (any string)
data?: Record<string, any>; // JSON metadata
index?: number;
}Example JSON
{
"_id": "abc123",
"project_id": "pr123",
"path": "blog/all-about-being-blue",
"title": "A Kinda Blue",
"description": "A note about the color blue.",
"imgUrl": "https://example.com/image.png",
"prompt": "Write about blue.",
"imgPrompt": "Generate a blue image.",
"content": "# Blue",
"blocks": [],
"created": "2024-06-01T12:00:00Z",
"modified": "2024-06-01T12:00:00Z",
"author": "james",
"data": { "locale": "en-GB" },
"index": 0
}Create a new PullnoteClient instance.
Parameters:
apiKey(string): Your Pullnote API key. api.pullnote.com').
Example:
const pn = new PullnoteClient('YOUR_API_KEY');Check a note at the given path
Parameters:
path(string): The path to check.
Returns:
Promise<PingResponse>: A diagnostic payload from the Pullnote API. Successful lookups return{ msg: "Note found", title, path, found: 1, status: 200, project }. Missing notes return{ msg: "Note not found", path, found: 0, status: 404, project }. When a note has been moved you’ll get{ msg: "Note moved", path, redirect, found: 0, status: 301, project }.
Example:
const ping = await pn.ping('my/note/path');
if (ping.found) {
console.log(`Note found: ${ping.title}`);
} else if (ping.redirect) {
console.log(`Note moved to ${"/" + ping.redirect}`);
} else {
console.log('Note not found');
}Check if a note exists at the given path.
Parameters:
path(string): The path to check.
Returns:
Promise<boolean>:trueif the note exists, otherwisefalse.
Example:
const found = await pn.exists('my-note-path');Retrieve a note by path. Optionally specify a format ('md' or 'html').
Parameters:
path(string): The note path.format(string, optional): Format ('md', 'html', or '').
Returns:
Promise<Note>: The note object.
Example:
const note = await pn.get('blog/cats');find(path: string, find: Record<string, any>, sort?: string, sortDirection?: number, fields?: string): Promise<Note[]>
Find an array of notes starting at the given path with optional filtering, sorting, and field selection.
Parameters:
path(string): The base path to search from (e.g., '/' for all notes, '/blog' for blog notes).find(Record<string, any>): Filters to apply to the search. Can include nested data fields using dot notation (e.g.,{"data.city": "London"}).sort(string, optional): Field to sort by (e.g., 'title', 'modified', 'index').sortDirection(number, optional): 1 for ascending, -1 for descending, 0 for default sorting.fields(string, optional): Comma-separated list of fields to return (e.g., 'path,title,data,content_md').
Returns:
Promise<Note[]>: Array of notes matching the criteria.
Examples:
// Find all notes with title containing "Hello"
const notes = await pn.find('/', { title: "Hello" });
// Find Apple notes under London with custom sorting
const londonNotes = await pn.find('/london', { "data.brand": "Apple" }, "title", 1);
// Find notes with specific fields only
const summaries = await pn.find('/', {}, "modified", -1, "path,title,data");
// Find notes in a specific folder
const blogPosts = await pn.find('/blog', {}, "modified", -1);Add a new note at the given path.
Parameters:
path(string): The note path.note(Note): The note object to add.
Returns:
Promise<Note>: The created note object.
Example:
await pn.add('blog/cats', { title: 'Cats', content: '# Meow' });Update an existing note at the given path. Note: documents can be moved by passing a new path in changes.
Parameters:
path(string): The note path.changes(Partial): The changes to apply.
Returns:
Promise<Note>: The updated note object.
Example:
await pn.update('blog/cats', { title: 'Interesting Cats' });Delete a note at the given path.
Parameters:
path(string): The note path.
Returns:
Promise<void>
Example:
await pn.remove('blog/cats');Alias for remove.
Retrieve a note's content as Markdown.
Parameters:
path(string): The note path.
Returns:
Promise<string>: The Markdown content.
Example:
const md = await pn.getMd('blog/cats');Retrieve a note's content as HTML.
Parameters:
path(string): The note path.
Returns:
Promise<string>: The HTML content.
Example:
const html = await pn.getHtml('blog/cats');Retrieve a note's title.
Parameters:
path(string): The note path.
Returns:
Promise<string>: The note title.
Example:
const title = await pn.getTitle('blog/cats');Retrieve a note's data object. Data can be any JSON record set always to be returned with the note. (this is useful for formatting pages that are about something specific)
Parameters:
path(string): The note path.
Returns:
Promise<Record<string, any>>: The data object.
Example:
await pn.add('my-site-in-france', {title: 'France', content: '...', data: { locale: 'en-FR', currency: 'EUR'}});
const data = await pn.getData('my-site-in-france'); // Returns { locale: 'en-FR', currency: 'EUR'}Set a note's data object.
Parameters:
path(string): The note path.data(Record<string, any>): The data object to set.
Returns:
Promise<void>
Example:
await pn.setData('blog/cats', { product_id: 'KITTY123' });Retrieve a note's image URL.
Parameters:
path(string): The note path.
Returns:
Promise<string>: The image URL.
Example:
const imgUrl = await pn.getImage('blog/cats');Retrieve a note's head metadata.
Parameters:
path(string): The note path.
Returns:
Promise<{ title: string, description: string, imgUrl: string }>
Example:
const head = await pn.getHead('my-note-path');Generate content for a note using an AI prompt.
Parameters:
path(string): The note path.prompt(string): The AI prompt for content generation.imgPrompt(string, optional): The AI prompt for image generation.
Returns:
Promise<any>: The generated content.
Example:
await pn.generate('my-note-path', 'Write about blue.');getSurrounding() / list(path: string, sort?: string, sortDirection?: number): Promise<{ self: Note, parent: Note | null, parents: Note[], children: Note[], siblings: Note[], index: number }>
List notes and related sub-lists for a given path. Returns an object containing the current note (self), its parent, an array of parent notes (breadcrumbs), children, siblings, and the index. Useful for building menus and navigation structures.
Parameters:
path(string): The base path ("" for all notes, "/" for root level).sort(string, optional): Field to sort by (default: 'auto').sortDirection(number, optional): 1 for ascending, -1 for descending (default: 0 = auto).
Returns:
Promise<{ self: Note, parent: Note | null, parents: Note[], children: Note[], siblings: Note[], index: number }>: An object containing the current note and related sub-lists.
Example:
const list = await pn.list('/blog');
// list.self, list.parent, list.parents, list.children, list.siblings, list.indexRetrieve all notes in the database.
Returns:
Promise<Note[]>: Array of all notes.
Example:
const allNotes = await pn.getAll();Retrieve the parent note object.
Parameters:
path(string): The note path.
Returns:
Promise<Note | null>: The parent note object, or null if it's a root note.
Example:
const parent = await pn.getParent('my-note-path');Retrieve the breadcrumb trail for a note.
Parameters:
path(string): The note path.
Returns:
Promise<Note[]>: Array of parent notes leading up to the current note.
Example:
const breadcrumbs = await pn.getBreadcrumbs('my-note-path');Retrieve the children of a note.
Parameters:
path(string): The note path.
Returns:
Promise<Note[]>: Array of child notes.
Example:
const children = await pn.getChildren('my-note-path');Retrieve the siblings of a note.
Parameters:
path(string): The note path.
Returns:
Promise<Note[]>: Array of sibling notes.
Example:
const siblings = await pn.getSiblings('my-note-path');Retrieve the index of a note.
Parameters:
path(string): The note path.
Returns:
Promise<number>: The index of the note.
Example:
const index = await pn.getIndex('my-note-path');Set the index of a note.
Parameters:
path(string): The note path.index(number): The new index.
Returns:
Promise<void>
Example:
await pn.setIndex('my-note-path', 0);Generate an XML sitemap for your site.
Parameters:
siteUrl(string): The base URL of your site (e.g., 'https://mysite.com').staticPages(array, optional): List of static page paths or objects with path and modified date.
Returns:
Promise<string>: The XML sitemap as a string.
Example:
const sitemap = await pn.getSitemap('https://mysite.com', ['/about', '/contact']);Add a user to your project (for editor access). This allows the specified email to log in and edit content for your project at pullnote.com.
Parameters:
email(string): The user's email address.nomdeplume(string, optional): The user's display name (optional).
Returns:
Promise<any>: The API response.
Example:
await pn.addUser("support@pullnote.com");Remove a user from your project (revokes their editor access).
Parameters:
email(string): The user's email address to remove.
Returns:
Promise<any>: The API response.
Example:
await pn.removeUser("support@pullnote.com");Pullnote also offers the use of our own LLM to generate placeholder content and images - just hit the endpoints with prompt instead of content
Pullnote exposes an MCP-compatible API endpoint for LLM tools such as Cursor, Claude Code etc to allow AI to create, retrieve, update, delete, and list notes programmatically.
POST https://api.pullnote.com/mcp
OpenAI specification for LLMs to discover which tools / actions are available (note: your LLM should automatically attempt to pick this up) https://api.pullnote.com/openapi.json
- Navigate to Settings... > Cursor Settings > Tools & Integrations > New MCP Server
- Paste the following (or add to your remoteServers list if already there)
{
"mcpServers": { },
"remoteServers": {
"pullnote-api": {
"url": "https://api.pullnote.com/mcp?key=[YOUR_PULLNOTE_API_KEY]"
}
}
}- All requests require a valid
pullnote_keyin the request body. - You can get a free one from https://www.pullnote.com
Send a JSON body with the following fields:
action: One ofcreate,retrieve,update,delete,listtool: Must benote(future tools may be supported)params: Object with parameters for the action (see below)pullnote_key: Your project API key
{
"action": "create",
"tool": "note",
"params": {
"title": "Interesting ducks",
"content": "Pre-written content e.g. An article about interesting ducks.",
"imgUrl": "https://your-domain.com/pre-existing-article-image.png",
"prompt": "Optional AI prompt to auto-generate content e.g. Write an interesting article about ducks",
"imgPrompt": "Optional AI prompt to auto-generate an article image"
},
"pullnote_key": "YOUR_KEY_HERE"
}All successful responses return:
{
"ok": true,
"result": { ... }
}- The
resultfield contains the note object, a list of notes, or{ ok: true }for deletions.
- Missing or invalid
pullnote_keyreturns 401/403. - Unknown actions or tools return 400.
- Not found errors return 404.
- All errors are returned as JSON with an appropriate HTTP status code.
- The MCP endpoint is designed for programmatic access by LLM tools and external services.
- Accounts are subject to usage limits due to storage and inference costs - see https://www.pullnote.com/pricing
See packages/client/README.md for instructions on contributing to the maintenance of this package.
MIT — see LICENSE