Skip to content
webuildsocietyPublic

About

Agent-first headless content API to create and retrieve markdown programatically without human account setup (human text editor at pullnote.com if needed)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Pullnote

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.

Claude Plugin

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

  1. Ask your project admin to invite you — you'll receive an email with a magic link
  2. Open the link to get your invite token
  3. Tell Claude: "Join the Pullnote project with token YOUR_TOKEN"
  4. 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.


Getting started

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);

For AI Agents

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 projects

Alternative: 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

Add a note

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'
});

Retrieve a note

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 returned

Or just the components

var title = await pn.getTitle('/all-about-being-blue');
var htmlContent = await pn.getHtml('/all-about-being-blue');

List surrounding notes

Get surrounding parents, parent, children and sibling notes - useful for building menus and breadcrumb trails.

var notes = await pn.getSurrounding("/blog");

Find notes

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('/');

Remove a note

await pn.remove("all-about-being-blue");

Add a user

Users can edit content by logging in at https://www.pullnote.com

await pn.addUser("support@pullnote.com");

Auto-generate a Sitemap

var xml = await pn.getSitemap("https://myurl.com");

API documentation

PullnoteClient API

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

Note structure

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
}

constructor(apiKey: string)

Create a new PullnoteClient instance.

Parameters:

  • apiKey (string): Your Pullnote API key. api.pullnote.com').

Example:

const pn = new PullnoteClient('YOUR_API_KEY');

ping(path: string): Promise

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');
}

exists(path: string): Promise

Check if a note exists at the given path.

Parameters:

  • path (string): The path to check.

Returns:

  • Promise<boolean>: true if the note exists, otherwise false.

Example:

const found = await pn.exists('my-note-path');

get(path: string, format?: string): Promise

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(path: string, note: Note): Promise

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(path: string, changes: Partial): Promise

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' });

remove(path: string): Promise

Delete a note at the given path.

Parameters:

  • path (string): The note path.

Returns:

  • Promise<void>

Example:

await pn.remove('blog/cats');

delete(path: string): Promise

Alias for remove.


getMd(path: string): Promise

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');

getHtml(path: string): Promise

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');

getTitle(path: string): Promise

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');

getData(path: string): Promise<Record<string, any>>

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'}

setData(path: string, data: Record<string, any>): Promise

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' });

getImage(path: string): Promise

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');

getHead(path: string): Promise<{ title: string, description: string, imgUrl: string }>

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(path: string, prompt: string, imgPrompt?: string): Promise

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.index

getAll(): Promise<Note[]>

Retrieve all notes in the database.

Returns:

  • Promise<Note[]>: Array of all notes.

Example:

const allNotes = await pn.getAll();

getParent(path: string): Promise<Note | null>

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');

getBreadcrumbs(path: string): Promise<Note[]>

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');

getChildren(path: string): Promise<Note[]>

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');

getSiblings(path: string): Promise<Note[]>

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');

getIndex(path: string): Promise

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');

setIndex(path: string, index: number): Promise

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);

getSitemap(siteUrl: string, staticPages?: (string | { path: string })[]): Promise

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']);

addUser(email: string, nomdeplume?: string): Promise

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");

removeUser(email: string): Promise

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 LLM

Pullnote also offers the use of our own LLM to generate placeholder content and images - just hit the endpoints with prompt instead of content

MCP server for LLM Integration

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.

Endpoint

POST https://api.pullnote.com/mcp

Discovery

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

Adding Pullnote MCP Server to Cursor

  • 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]"
    }
  }
}

Authentication

Request Format

Send a JSON body with the following fields:

  • action: One of create, retrieve, update, delete, list
  • tool: Must be note (future tools may be supported)
  • params: Object with parameters for the action (see below)
  • pullnote_key: Your project API key

Example: Create a Note

{
  "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"
}

Response Format

All successful responses return:

{
  "ok": true,
  "result": { ... }
}
  • The result field contains the note object, a list of notes, or { ok: true } for deletions.

Error Handling

  • Missing or invalid pullnote_key returns 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.

Notes

  • 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

Code Contributors

See packages/client/README.md for instructions on contributing to the maintenance of this package.

License

MIT — see LICENSE

About

Agent-first headless content API to create and retrieve markdown programatically without human account setup (human text editor at pullnote.com if needed)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages