Paperboy CMS
OfficialServer Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| MCP_EMAIL | No | Login email (no token) | |
| MCP_TOKEN | No | Token from Settings, MCP | |
| DATABASE_URL | Yes | Paperboy Postgres URL | |
| MCP_PASSWORD | No | Password for MCP_EMAIL | |
| PAPERBOY_SITE | No | Default site slug or id |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| list_tree | List ONE level of the page tree: the direct children of parentId, or the top-level pages when it is omitted, in the order the container sorts them. Each node has documentId, type, name, per-locale status and hasChildren — call list_tree again with a node's documentId to go deeper. Use it to find the parentId for create_content/move_content. For every page at once use list_content {kind:'page'}; shared blocks and globals are not in the tree (list_content with kind block or global). |
| get_content | Read one content item for editing: its working version (the draft if one exists, else the published version) in one locale, with name, slug, status, revision and the full |
| create_content | Create a new content item (page/block/global) as a draft. Pass the field content in |
| update_content | Save the working draft of a content item. |
| set_field | Set ONE field of a content item's draft to a plain string value (merged — other fields are kept). THE MOST ROBUST WAY TO WRITE LONG TEXT/MARKDOWN CONTENT: a flat string parameter survives tool-call serialization that can mangle long strings nested inside |
| publish | Publish the working draft of a content item for a locale. The locale is a LANGUAGE BRANCH — publishing Norwegian text into 'en' puts it on the English site; a strong language/branch mismatch is refused unless allowLanguageMismatch is true. A future publishAt SCHEDULES the go-live instead: the draft stays unpublished (and invisible to readers) until then, and shows on the dashboard under Scheduled publishing. Call publish again with a new publishAt to reschedule, or without one to go live now. NEVER fake scheduling by putting the date in a data field — that publishes immediately. |
| copy_variant | Copy a document's working version (name, slug and EVERY field) from one locale to another, server-side and atomically. USE THIS to move content that landed in the wrong language branch — never re-type the data field by field (a real agent moved 4 of 9 fields by hand and published an article without its body). The target locale gets a draft; publish it separately. |
| unpublish | Take a content item off the live site in one locale: delivery stops serving it there (its URL starts answering 404) and content areas that reference it drop it. Fires the content.unpublished webhook. The content and its version history are kept, so publish brings it back. Other locales are unaffected. To remove the item entirely use trash_content. |
| discard_draft | Throw away the unpublished draft changes of one locale, so the working version falls back to the published one. The live site is unchanged; the discarded draft cannot be recovered. Refused for content that was never published (there is nothing to fall back to) — use trash_content to remove that instead. |
| move_content | Move a page in the tree: give parentId to re-parent it (its URL path changes with its new position, and the old URL gets a 301 automatically), and/or beforeId/afterId to place it among its siblings. The page's whole subtree moves with it. Sibling order only sticks under a parent whose child sort is manual; a computed sort (name, date…) re-orders it. Returns {ok:true}. |
| duplicate_content | Copy a content item into a NEW unpublished document beside the original (same parent), named " (copy)", with every field of each locale's working version. A copied page gets a fresh slug, so it never collides with the original's URL. Children are not copied. Returns the new item; edit it, then publish. |
| trash_content | Move a content item AND its whole page subtree to the trash: every locale is unpublished at once and disappears from delivery. Recoverable — restore_content brings it back (as drafts; publish again explicitly). Returns {trashed: }. To only take it off the live site, use unpublish instead. |
| restore_content | Bring a trashed item back to its old place in the tree, together with the descendants that were trashed with it. Restored content is NOT republished — call publish for each locale that should go live. Refused if its parent is still in the trash or a live sibling took its URL meanwhile. Returns {restored: }. Find trashed ids with list_trash. |
| list_trash | List the content in this site's trash that you can see — items removed with trash_content, which are unpublished in every locale and invisible to the live site but still recoverable. Each entry has documentId, type, kind, name and deletedAt (ISO time). Nothing is purged automatically; pass a documentId to restore_content to bring it (and the subtree trashed with it) back. Read-only. |
| list_versions | List a content item's version history in one locale, newest first: each version's id, versionNumber, status, whether it is the live one, name, and who created it when. Pass a version's |
| restore_version | Roll back: copy an older version's content into the working draft of that locale. Nothing goes live until you publish; the version history keeps every earlier version. Returns the updated item. |
| list_content | List EVERY item of one kind in this site that you can see, as one flat array (documentId, name, type, per-locale status, plus parentId for pages and folderId for blocks) — no tree walking needed. kind 'page': pages, to find one by name or pick a parent/reference target (list_tree shows one level in display order). kind 'block': SHARED blocks — reusable block documents (banners, forms, CTAs…) that pages place by reference, so one edit updates every page using them; inline blocks live inside a page's data and are not listed. kind 'global': singletons (header, footer, site settings…), which are neither in the page tree nor blocks, so this is the only way to enumerate them. Edit any of them with get_content/update_content on its documentId. |
| list_content_types | Read the content model (instance-wide, shared by all sites). Pass name to get ONE type's definition — name, kind, displayName, schemaType and fields (name, type, required, options, allowed blocks of a content area…); an unknown name is refused with the list of real names. Call this before writing content of a type. Omit name to list every type (large). Each field carries valueFormat + valueExample: the exact JSON shape create_content/update_content expect. These are LIVE types editors fill in; for reusable recipes see list_type_templates. |
| create_content_type | Create a new content type (the schema editors fill in) from a full ContentTypeDef. Content types are instance-wide, shared by every site, so this needs a token not limited to one site. Faster for common shapes: list_type_templates + instantiate_type_template. The definition is validated and a bad one is refused with the offending path. |
| update_content_type | Replace a content type's definition (add/remove/rename fields, change options). name and kind are immutable. Send the FULL definition — read it with list_content_types {name}, edit, send back; a field you leave out is removed from the schema. Existing content is not migrated: its stored values for removed fields are simply no longer shown or delivered. Instance-wide (all sites). |
| list_type_templates | Read the type-template library — named ContentTypeDef RECIPES, not live types: the BUILT-IN starters that ship with Paperboy (read-only) plus stored templates. Pass name to get one template's full definition (to inspect before instantiate_type_template, or as a starting point for create_content_type); omit it to list them all. Each field carries valueFormat + valueExample. A template's name is the content type name it materialises by default. For the types editors fill in today, see list_content_types. |
| create_type_template | Save a ContentTypeDef as a reusable TEMPLATE in the library — a recipe, not a live type: nothing becomes editable until instantiate_type_template turns it into a content type. Use it to keep a starter/backup of a type or to move it to another instance via export_type_templates; to create a type editors can use right now, call create_content_type instead. The template's name is the type name it materialises by default and must not clash with an existing or built-in template. Instance-wide; returns the stored template. |
| update_type_templateA | Replace a stored type template's definition (name and kind are immutable; built-in templates are read-only — copy one under a new name with create_type_template instead). Content types already instantiated from it are NOT changed; re-apply with instantiate_type_template {updateExisting:true}. |
| delete_type_templateA | Permanently delete a stored type template from the library (built-ins can't be deleted). Content types instantiated from it, and their content, are NOT affected. Returns {ok:true}. |
| instantiate_type_templateA | Materialise a type template into a real content type. Default: creates the type under the template's own name. If that type already exists the call is REFUSED (409) — pass updateExisting: true to re-apply the template onto the existing type (kind must match; existing content is not migrated), or asName: "OtherName" to create a type with a DIFFERENT name (must match ^[A-Z][a-zA-Z0-9]*$, e.g. "CaseStudy"). Pass withBlocks: true to ALSO create the block types the template's content areas reference (recursively; existing types are left untouched) — recommended for built-ins like FaqPage so the set works out of the box. |
| export_type_templatesA | Export type templates as a portable JSON document ({format, version, exportedAt, templates}) that import_type_templates on another Paperboy instance accepts. Omit names to export everything (built-ins included). |
| import_type_templatesA | Import type templates from an export document. Per-template outcome: new names are created; existing stored templates are skipped unless overwrite: true; built-in names are always skipped (they ship with every instance). Returns {created, updated, skipped:[{name, reason}]}. |
| list_formsA | List the forms (Form blocks) in this site with formId, name, submission count and time of the last submission. Start here before list_form_submissions or export_form_submissions. Submissions are visitor personal data: requires the submission.read permission (Admin/Editor). |
| list_form_submissions | Read form submissions in this site. Pass submissionId to get ONE in full (answers, field-label snapshot, form, received and retention-expiry times); otherwise get a page of them, newest first. Answers keep the field labels as they were when submitted, so a later edit to the form does not rewrite history. Visitor personal data: requires submission.read. For a full copy as a file use export_form_submissions. |
| export_form_submissions | Export ALL matching form submissions in one go (no paging) as CSV text, returned as {csv, rows}. Columns: Submitted at, Submission ID, Locale, then one column per field labelled as when answered (fields from older versions of the form included). Cells that a spreadsheet would run as a formula are escaped. This is a copy of visitor personal data: the export is audit-logged and requires submission.read. To read submissions as JSON, page through list_form_submissions instead. |
| delete_form_submissionA | Permanently delete ONE form submission (irreversible; audit-logged; requires submission.manage). Returns {ok:false} when no such submission exists. To erase everything one person sent, use erase_form_submissions. |
| erase_form_submissionsA | Data-subject erasure: delete EVERY submission in the active site whose answers contain this email address, in any field. Irreversible; requires submission.manage. Returns {deleted: }; the audit log records that an erasure happened, never the address. Copies in database backups survive until the backups rotate. |
| list_assetsA | List this site's media library (images and PDFs): each asset's documentId, filename, mime, size, public url, alt text, attribution for stock imports, and folder. An image field stores the asset's documentId — set it with set_field. To add files use upload_asset, create_upload_link or import_stock_image. |
| update_asset_altA | Set (overwrite) an image asset's alt text — the description screen readers announce and search engines index, wherever the image is used. Describe what the image shows in one sentence; use "" only for a purely decorative image. Returns the updated asset. |
| delete_assetA | Permanently delete a media asset: the database row and the stored file. Irreversible. Content that still points at it is not blocked: delivery resolves those fields to null, so the image silently disappears from those pages — clear or replace them first. Returns {ok:true}. |
| search_stock_imagesA | Search the configured stock photo provider (Settings → Stock images; Unsplash). Returns photo candidates with id, description and attribution. To USE a photo: call import_stock_image with its id, then set_field the returned asset documentId on an image field. |
| import_stock_imageA | Import a stock photo (by the id from search_stock_images) into the media library. Downloads the image, stores it as a regular asset with alt text + attribution, and returns the asset — set its documentId on an image field with set_field. |
| create_upload_linkA | Get a short-lived link for uploading LOCAL files (images, PDFs) into the media library — the way to upload files on your own machine, which this server can't read. POST them as multipart/form-data with the returned header, e.g. with curl (several -F file=@… per request is fine). The response lists each stored asset's documentId: set it on an image field with set_field, and give it alt text with update_asset_alt. The link uploads as you, into this site, and expires after 15 minutes. |
| upload_assetA | Add a file (PNG, JPEG, GIF, WEBP or PDF, max 5 MB) to the media library from a public URL or from base64 bytes, and return the asset — set its documentId on an image field with set_field. For files on YOUR machine prefer create_upload_link: base64 through a tool call costs roughly 1 token per 2-3 bytes. |
| delivery_get | Read ONE item exactly as the website receives it from the public Delivery API — the way to check what visitors see. Name it by exactly one of: path (a page's URL, e.g. '/blog/my-post' — the usual choice), documentId, slug, global (a singleton's type, e.g. 'SiteHeader'), or start:true (the page served at /). Returns published content only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. To read a draft for editing use get_content; for many items use delivery_list or delivery_search. |
| delivery_listA | List the published content of one type as the website receives it (e.g. every BlogPost for a listing). Supports pagination (limit/offset), sorting and equality filters on data fields; without sort it follows the parent's declared child order. Private fields are removed. Returns { items, total }. |
| delivery_search | Full-text search over the website's content (item names + public field text) in one locale, as visitors would search it: published only (unless preview), private fields never matched or returned. Returns { items, total }. To find content for editing, list_content or list_tree are usually faster. |
| list_sites | Say where this server acts: every site you can reach (id, slug, name), which one is active, whether your token is limited to one site, and the active site's setup — startPageId (the page served at /, or null; change it with set_start_page) and previewBaseUrl (the frontend the admin's preview frames a page at, as {previewBaseUrl}/{locale}{urlPath}). Content in another site is invisible to every tool unless you pass its slug as |
| set_start_pageA | Choose which page this site serves at / (the home page), or clear it with null. Takes effect immediately on the live site, in every locale the page is published in. Returns {ok:true}. |
| list_redirectsA | List the site's redirect rules with each target's live URL, hit counts and problems (target-missing, target-unpublished, shadowed, chain, loop). Rules apply on the frontend ONLY where no published page answers the path. Pass targetDocumentId to see which old URLs point at one page. |
| create_redirectA | Create a redirect rule. source: a path ("/old-page"), a prefix ("/blog/*" — the rest of the path is carried over) or a full URL on the old site (reduced to its path). Target: EITHER targetDocumentId (+ optional targetLocale) to follow a page wherever it moves, OR target = a path or https:// URL — a path that names a published page is stored as that page. status: 301 (default, permanent — passes link equity), 302/307 temporary, 308 permanent, 410 gone (no target). A target that is itself redirected is collapsed to the final destination; loops are refused. Publishing a new slug or moving a page creates 301s automatically — check list_redirects before adding one by hand. |
| update_redirectA | Update a redirect rule by id (from list_redirects). Same fields and rules as create_redirect; omitted fields are unchanged. |
| delete_redirect | Permanently delete one redirect rule, or several at once, by id. Irreversible. The old URLs then answer 404 (or reach a page now published there) — check list_not_found afterwards. Returns {deleted: }. |
| test_redirectA | Follow a URL the way a visitor would: a published page answers first, then the rules, hop by hop. Returns each step (page, redirect, gone, external, not-found, loop) and the hop count. Use it to verify a migration: every old URL should end on a page in one hop. |
| list_not_found | List URL paths visitors hit that answered 404 on the website, as reported by the frontend: path (no query string), hits, firstSeen, lastSeen, and coveredBy — the id of a redirect rule that now answers it, or null. Most hits first, up to 2000. Use it after a migration or slug change: create_redirect for uncovered paths with real traffic, then test_redirect to verify. Read-only; no visitor data is stored. |
| list_usersA | List every CMS user account (instance-wide): id, email, name, roles, section scopes, locked flag and createdAt. Requires the user.manage permission (Admin). |
| create_user | Create a CMS user account (an editor of the admin, not a website visitor) that signs in with this email and password. Users and roles are instance-wide, so they apply in every site; sections narrow what they can edit. Admin only, with a token not limited to one site. Returns {id}; fails if the email is taken. Give the password to the person out of band — they can change it and enable 2FA themselves. To change an existing account use update_user. |
| update_userA | Change a user's name, roles and/or section scopes (Admin only). Omitted fields are kept; roles and sections REPLACE the current lists. Changes apply from the user's next request. Returns {ok:true}. |
| delete_userA | Permanently delete a user account (Admin only): they can no longer sign in and their browser sessions end. Content they wrote stays. You cannot delete yourself or the last Admin. Irreversible. Returns {ok:true}. |
| list_delivery_keysA | List this site's Delivery API keys — the credentials a website uses to read content: id, name, type (public | preview) and timestamps. Secrets are never shown again after creation. Admin only. |
| create_delivery_keyA | Mint a Delivery API key for this site (Admin only). A public key reads published content only — safe for a website build; a preview key also reads drafts, so keep it server-side. The secret is returned ONCE in this response; store it now. |
| rename_delivery_keyA | Change a Delivery API key's label (Admin only). The secret and its access are unchanged. Returns {ok:true}. |
| revoke_delivery_keyA | Revoke a Delivery API key (Admin only): every request using it is refused from now on, so a website still using it stops getting content. Irreversible — mint a new key with create_delivery_key. Returns {ok:true}. |
| list_webhooksA | List this site's outbound webhooks: id, name, target url, subscribed events and status. Secrets are not shown. Admin only. |
| create_webhookA | Subscribe a URL to this site's events (Admin only): Paperboy POSTs a JSON payload signed with HMAC-SHA256 on each event, e.g. to trigger a static-site rebuild. Private-network URLs are refused. The signing secret is returned ONCE; store it to verify deliveries. |
| delete_webhook | Permanently delete one of this site's webhooks (Admin only). Events are sent at the moment they happen and never queued, so nothing is pending: from now on its URL simply receives no more events, and its signing secret is gone. Irreversible. There is no update_webhook — to change a URL or event list, delete it and create_webhook again (the new one gets a new secret). Returns {ok:true}. |
| list_auditA | Read the append-only audit log, newest first: who did what (create, publish, delete, login, export…), when, to which document, from which surface (ip 'mcp' = agent). Use it to find out who changed something. Requires audit.read and a token not limited to one site. |
| list_localesA | List the enabled locales (language branches), in order, with which one is the default. Each content item has one variant per locale; pass these codes as |
| ai_assistA | Run the CMS's writing assistant on a piece of text and return the result — it does NOT save anything; write the output yourself with set_field/update_content. Uses the model configured in Settings → AI (Anthropic or OpenAI-compatible). Without a configured model only meta_title, meta_description and summarize work (a basic truncation); the other tasks return an error saying so. Image alt text is not available here (it needs the image itself): write it and save with update_asset_alt. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 74 tools
Despite 74 tools, descriptions carefully differentiate overlapping-sounding operations: get_content vs delivery_get_by_id (draft vs published), tree vs list_pages (one level vs flat), list_blocks vs list_globals vs list_pages, and the many content-type/template tools are clearly scoped. The delivery_get_by_id/slug/path trio is explicitly ranked (prefer path), leaving almost no ambiguity.
Overwhelmingly a consistent verb_noun snake_case pattern (list_/get_/create_/update_/delete_ reused per resource), which aids scanning. A few bare single-word exceptions like tree, publish, unpublish, and ai_assist break the pattern but remain readable.
74 tools is heavy and well above a well-scoped range, even for a full CMS platform. The domain is genuinely broad (content, versions, blocks, forms, assets, delivery API, redirects, users, keys, webhooks, audit), but this many discrete tools invites consolidation.
Coverage is exceptional: full content lifecycle (create/update/publish/unpublish/discard/trash/restore/versions), content types and templates, form submissions with export and erasure, media assets, redirects, users, delivery keys, webhooks, audit and locales. No obvious dead ends for the stated CMS purpose.