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 |
|---|---|
| treeA | 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 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_pages; shared blocks and globals are not in the tree (list_blocks, list_globals). |
| get_contentA | 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_contentA | Create a new content item (page/block/global) as a draft. Pass the field content in |
| update_contentA | Save the working draft of a content item. |
| set_fieldA | 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 |
| publishA | 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_variantA | 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. |
| unpublishA | 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_draftA | 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_contentA | 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_contentA | 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_contentA | 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_contentA | 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_trashA | List the trashed content you can see in this site, with documentId, type, kind, name and deletedAt. Use the documentId with restore_content. |
| list_versionsA | 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_versionA | 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_blocksA | List this site's SHARED blocks — reusable block documents (banners, forms, CTAs…) that pages place by reference in a content area, so one edit updates every page using them. Each has documentId, type, name, per-locale status and folderId. Edit one with get_content/update_content on its documentId. Inline blocks live inside a page's data and are not listed here. |
| list_globalsA | List this site's global singletons (header, footer, site settings…). Globals are NOT in the page tree and are not blocks, so this is the only way to enumerate them; edit one with get_content/update_content/set_field on its documentId, like any other document. |
| list_pagesA | List EVERY page you can see in this site as one flat array (documentId, name, type, parentId, per-locale status) — no tree walking needed. Use it to find a page by name or to pick a parent/reference target; use tree to see one level in display order. |
| list_content_typesA | List every content type in the instance (shared by all sites): its name, kind (page | block | global) and fields, each field annotated with valueFormat + valueExample — the JSON shape update_content expects. Large; when you already know the type, get_content_type returns just that one. |
| get_content_typeA | Get one content type's definition: name, kind, displayName, schemaType and its fields (name, type, required, options, allowed blocks of a content area…). Each field includes valueFormat + valueExample — the exact JSON shape create_content/update_content expect. Call this before writing content of a type. Returns null for an unknown name. |
| create_content_typeA | 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_typeA | Replace a content type's definition (add/remove/rename fields, change options). name and kind are immutable. Send the FULL definition — get_content_type, 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_templatesA | List all content-type templates — named ContentTypeDef recipes. Includes the BUILT-IN library (starter pages/blocks that ship with Paperboy, read-only) alongside stored templates. Each template's name is the content type name it materialises by default. |
| get_type_templateA | Get one type template's full ContentTypeDef by name (stored or built-in), each field annotated with valueFormat + valueExample. Use it to inspect a template before instantiate_type_template, or as a starting definition for create_content_type. |
| create_type_templateA | Save a content type definition as a reusable template (a starter/backup recipe). The template's name is the content type name instantiate materialises by default — use a name no existing template takes (built-in template names are reserved). |
| 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_submissionsA | List form submissions in this site, newest first, one page at a time. Answers come with a snapshot of 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. |
| get_form_submissionA | Get one form submission in full: its answers with the field-label snapshot, the form it belongs to, when it was received and when retention deletes it. Requires submission.read. |
| export_form_submissionsA | Export form submissions as CSV text (one row per submission, one column per field) and return it with the row count. This is a copy of visitor personal data: the export is audit-logged and requires submission.read. |
| 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_by_idA | Read one content item by documentId exactly as the website receives it. Returns the item as the public Delivery API serves it — published only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. Use get_content to read a draft for editing. |
| delivery_get_by_slugA | Read one content item by its slug (the last URL segment). Slugs are unique only among siblings, so prefer delivery_get_by_path for a page's full URL. Returns the item as the public Delivery API serves it — published only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. Use get_content to read a draft for editing. |
| delivery_get_by_pathA | Read the page a visitor gets at a URL path — the way to check what a live URL shows. Returns the item as the public Delivery API serves it — published only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. Use get_content to read a draft for editing. |
| 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_searchA | 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_pages or tree are usually faster. |
| delivery_globalA | Read a global singleton (header, footer, site settings…) by its type, as the website receives it. Find the types with list_globals. Returns the item as the public Delivery API serves it — published only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. Use get_content to read a draft for editing. |
| delivery_startA | Read this site's start page — the page served at / (set with set_start_page) — as the website receives it. Null when no start page is set. Returns the item as the public Delivery API serves it — published only (unless preview), private fields removed, plus computed urlPath, fieldTypes and SEO metadata on pages — or null when nothing matches. Use get_content to read a draft for editing. |
| list_sitesA | List the sites in this instance and say which one this server acts in. This server is pinned to ONE site (PAPERBOY_SITE, a slug or id); content in another site is invisible to every tool here, so an empty tree or search may mean the wrong site, not missing content. |
| get_site_configB | Get this site's configuration: startPageId (the page served at /, or null) and previewBaseUrl (the frontend origin the admin's preview frames). Read-only. |
| 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_redirectsA | Permanently delete redirect rules by id. 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_foundA | Paths that answered 404 on the frontend, most hits first (path only — no query string). coveredBy is the id of a rule that now answers the path. Create a redirect for the ones with real traffic. |
| 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_userA | Create a CMS user account that signs in to the admin with this email and password (instance-wide, all sites). Admin only; needs a token not limited to one site. Returns {id}. Share the password with the person out of band; they can change it and enable 2FA themselves. |
| 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_webhookA | Permanently delete a webhook (Admin only); no further events are sent to its URL. 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.