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_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, the whole level in one call. Returns an array of nodes, each with documentId, type, name, per-locale status and hasChildren — call list_tree again with a node's documentId to go deeper. Trashed pages are left out, and a page without children (or a parentId that matches nothing) gives [] rather than an error, so check hasChildren first. 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). Requires the content.read permission (Admin, Editor, Author, Viewer — an Author only within their own sections). |
| 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 or global) as an unpublished draft, filled in ONE call: pass its fields in |
| update_contentA | Save fields into a content item's working draft; nothing goes live until publish_content. Each value's shape follows its field type (list_content_types {name} gives valueFormat): text/markdown → string; richtext → TipTap doc {type:'doc',content:[…]}; contentArea → array of block instances; reference → {documentId}; image → asset documentId. |
| set_fieldA | Write ONE field of an item's draft as a plain string; the other fields are kept. It is the safest way to send long text or Markdown, because a flat string survives client serialization that can mangle long strings nested in update_content's |
| publish_contentA | Make a content item's working draft live in one locale (a LANGUAGE BRANCH: Norwegian text published into 'en' lands on the English site, so a clear mismatch is refused). A future publishAt SCHEDULES it instead: it stays invisible until then; call again with a new publishAt to reschedule, or without one to go live now. expireAt unpublishes it automatically later. Never fake a schedule with a date field — that publishes now. A changed slug or position gets a 301 from the old URL. Fires content.published. Refused when there is nothing new to publish, when required fields are empty (the error names them), or, if the site requires human review of agent drafts, until an editor approves it. Returns the item in the get_content shape with its new status. To take it down use unpublish_content; to drop unpublished edits, discard_draft. |
| 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 — re-typing it field by field loses fields. It REPLACES the target locale's draft (its unpublished edits are lost; its published version stays live until you publish_content the copy). The text is copied as-is: to translate it afterwards, run assist_writing {task:'translate'} and save with set_field. Returns the target locale's new draft in the get_content shape. To copy into a NEW item instead, use duplicate_content. Requires the content.update permission (Admin, Editor, Author — an Author only within their own sections). |
| unpublish_contentA | 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_content brings it back. Name the item by documentId and the branch by locale (omit it for the item's own language); other locales are unaffected. Returns the item in the get_content shape, now unpublished in that locale. To remove the item entirely use trash_content. Requires the content.publish permission (Admin, Editor). |
| discard_draftA | Throw away the unpublished draft changes of one item (documentId) in one locale (omitted: the item's own language), 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. To roll back to an older version rather than the live one, use restore_version. Returns {ok:true}. Requires the content.update permission (Admin, Editor, Author — an Author only within their own sections). |
| move_contentA | Move a page in the page tree: parentId re-parents it (its URL path follows its new position, and the old URL gets a 301 automatically), beforeId/afterId place it among its siblings. Its whole subtree moves with it. Refused when the new parent is the page itself, is not a page, or lies outside your sections. Sibling order only sticks under a parent with a manual child sort; a computed sort (name, date…) re-orders it. Returns {ok:true}. To change only the URL's last segment, set a new slug with update_content instead; blocks and globals are not in the tree. Requires the content.update permission (Admin, Editor, Author — an Author only within their own sections). |
| 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. locale only picks which language of the copy comes back. Returns the new item in the get_content shape; edit it, then publish_content. To copy one language into another locale of the SAME item use copy_variant; to start from nothing use create_content. Requires the content.create permission (Admin, Editor, Author — an Author only within their own sections). |
| trash_contentA | Move a content item (by documentId) AND its whole page subtree to the trash: every locale is unpublished at once and disappears from delivery, so there is no locale to pass. Recoverable — restore_content brings it back (as drafts; publish again explicitly). An unknown documentId is refused as not found. Returns {trashed: }. To only take it off the live site, use unpublish_content instead. Requires the content.delete permission (Admin, Editor). |
| 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_content for each locale that should go live. Refused if its parent is still in the trash (restore that first) or a live sibling took its URL meanwhile. There is no locale to pass: every language comes back. Returns {restored: }. Find trashed ids with list_content {kind:'trash'}. To roll back an item that is not trashed, use restore_version instead. Requires the content.delete permission (Admin, Editor). |
| list_versionsA | List a content item's version history in one locale, newest first and complete (no paging); locale picks the language branch (omitted: the item's own). Each version's id, versionNumber, status, whether it is the live one, name, and who created it when. An unknown documentId is refused as not found. Use it to find what to roll back to — pass a version's |
| restore_versionA | Roll back an item to an older version: copy that version's name, slug and fields into the working draft of one locale. versionId is the version's |
| list_contentA | 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. kind 'trash': items removed with trash_content (any kind), newest first, with deletedAt; nothing is purged automatically, and restore_content brings one back. Everything comes back in one call (no paging). Edit any of them with get_content/update_content on its documentId. Requires the content.read permission (Admin, Editor, Author, Viewer — an Author only within their own sections). |
| list_content_typesA | 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. Each field carries valueFormat + valueExample: the exact JSON shape create_content/update_content expect. These are LIVE types editors fill in; reusable type recipes (templates) live in the schema toolset. Omit name to get every full definition, which is large, so prefer name. Requires the content.read permission (Admin, Editor, Author, Viewer — an Author only within their own sections). |
| list_assetsA | List this site's whole media library (images and PDFs), newest first, in one call (no paging, so a big library is a big response): each asset's documentId, filename, mime, size, public url, alt text, attribution for stock imports, and folder. Media is per site: pass |
| update_asset_altA | Set (overwrite) an image asset's alt text — the description screen readers announce and search engines index. Alt text belongs to the ASSET, not to a page: every page showing the image picks up the change, and set_field/update_content cannot set it. documentId is the asset's (from list_assets or an upload), not a page's; an unknown one is refused as not found. Describe what the image shows in one sentence; use "" only for a purely decorative image. Returns the updated asset with its new alt. assist_writing cannot write alt text (it needs the image itself), so look at the image and write it. Requires the content.update permission (Admin, Editor, Author — an Author only within their own sections). |
| delete_assetA | Permanently delete a media asset — by the asset's documentId from list_assets, not a page's — removing 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. To stop using an image on one page, just clear that field with set_field and keep the asset. Returns {ok:true}. Requires the content.delete permission (Admin, Editor). |
| search_stock_imagesA | Search the configured stock photo provider (Settings → Stock images; Unsplash) for photos to use on a page. Returns up to 24 candidates, best match first, each with id, description and attribution; nothing is stored yet. To USE one: upload_asset {stockImageId: } imports it into the media library, then set_field the returned asset documentId on an image field. Refused with a hint when no provider key is configured. To reuse a photo you already have, list_assets. Requires the content.read permission (Admin, Editor, Author, Viewer — an Author only within their own sections). |
| create_upload_linkA | Get a 15-minute 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 (pass |
| upload_assetA | Add a file (PNG, JPEG, GIF, WEBP or PDF, max 5 MB) to the media library from exactly one source: a public URL, base64 bytes, or a stock photo id from search_stock_images (stored with its attribution). Returns the new asset — set its documentId on an image field with set_field. Each call stores a NEW asset, even for a file already in the library (reuse one via list_assets). A URL must be publicly reachable: private-network addresses are refused at every redirect hop, and a download over 5 MB is cut off with an error. For files on YOUR machine prefer create_upload_link: base64 through a tool call costs roughly 1 token per 2-3 bytes. Requires the content.create permission (Admin, Editor, Author — an Author only within their own sections). |
| list_sitesA | Report where this server acts and in which languages. Returns: sites (every site you can reach: id, slug, name), activeSiteId, tokenScopedTo (the one site your token is limited to, or null), activeSite (startPageId — the page served at /, or null — and previewBaseUrl, where the admin previews a page as {previewBaseUrl}/{locale}{urlPath}), and locales (enabled language branches in order: code, displayName, isDefault, fallback). It lists no content (use list_tree or list_content for that). Use it to pick the |
| assist_writingA | Run the CMS's writing assistant on a piece of text: SEO titles and descriptions, summaries, polishing, rewrites, variants, translation, drafting from a brief, schema.org field suggestions. Returns {result, provider}: result is the text (for variants, a JSON array of 3 strings). It saves nothing — write the result with set_field or update_content. It uses the model set in Settings → AI; without one, only meta_title, meta_description and summarize work (by truncation) and the rest are refused with a hint. It cannot write image alt text, since it never sees the image: write that yourself and save it with update_asset_alt. Requires the content.update permission (Admin, Editor, Author — an Author only within their own sections). |
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 25 tools
Most tools target distinct resources and actions (content vs assets vs versions), but a few pairs overlap: list_content vs list_tree both list pages, update_content vs set_field both edit drafts, and upload_asset vs create_upload_link both add assets. Descriptions explicitly differentiate when to use each, so misselection is unlikely but possible.
All names use snake_case with a leading verb (list_, get_, create_, update_, etc.), and the noun patterns are predictable across content, asset, and version tools. Minor forms like assist_writing (verb + gerund) remain readable and do not break the overall convention.
25 tools is on the heavy side for a single server, but the domain is broad—content lifecycle, assets, versions, sites, and AI writing—and each tool covers a distinct operation without obvious redundancy. Slightly over the typical 3–15 range, but justified for a full CMS API.
Core content lifecycle is thoroughly covered: create/read/update/publish/unpublish/trash/restore, versioning, locale variants, and asset upload/delete/alt text. Minor gaps exist—no direct asset file replacement, no text search for content, and no schema or site write operations—but these are either out of scope or workable via list operations.