npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@purposeinplay/payload-mcp

v0.2.0

Published

Payload CMS 3 plugin: draft documents via Claude Code (MCP). Wraps @payloadcms/plugin-mcp with a hardened, least-privilege, draft-only authoring tool surface.

Readme

@purposeinplay/payload-mcp

Draft Payload CMS documents through Claude Code (Model Context Protocol). This plugin is a thin wrapper around the official @payloadcms/plugin-mcp: it reuses that plugin's maintained transport and API-key auth, and layers a hardened, draft-only authoring tool surface on top that accepts full Lexical content (including rich blocks like Banner and FAQ).

How it wraps @payloadcms/plugin-mcp

payloadMcpPlugin() composes the official plugin (never forks it) and then hardens the host config. The official plugin owns the plumbing; we own the draft-authoring tools and the security controls:

flowchart TB
    CC["Claude Code<br/>(MCP client)"]

    subgraph AIW["payloadMcpPlugin() — this package"]
      direction TB
      OFF["@payloadcms/plugin-mcp — composed inside (delegated)<br/>• Streamable-HTTP transport at /api/mcp<br/>• bearer / API-key auth<br/>• payload-mcp-api-keys collection"]
      OURS["what we add / customize on top<br/>• create_draft_post · update_draft_post (Lexical + blocks)<br/>• read tools: list_authors · list_categories · list_block_types · list_posts · get_post<br/>• block allowlist · href + media-ref validation · no file uploads<br/>• draft-only beforeChange guard (survives prompt injection)<br/>• deny-bot access on every collection + global<br/>• overrideAuth attaches the key's user for attribution"]
      OFF --> OURS
    end

    CC -->|"Bearer &lt;MCP key&gt;"| AIW
    AIW -->|"Local API · overrideAccess · draft:true"| DB[("Payload<br/>draft posts only")]

| Delegated to @payloadcms/plugin-mcp | Added / customized by this plugin | |---|---| | MCP Streamable-HTTP transport (POST /api/mcp, SSE off) | Custom create_draft_post / update_draft_post tools accepting Lexical + blocks | | Bearer / API-key authentication | Read helper tools (authors, categories, block types, posts, get) | | The payload-mcp-api-keys collection + per-key tool toggles | Content validation: block allowlist, href sanitize, media-ref checks, no uploads | | JSON-RPC plumbing & request lifecycle | Draft-only beforeChange guard + createdBy attribution | | — | Deny-bot access wrap (a leaked key can only draft) | | — | Generic collection CRUD tools left disabled — writes go through our tools only |

The composition, in one place (src/plugin.ts):

// 1) compose the official plugin: transport + auth + our custom tools
config = await mcpPlugin({
  userCollection: authCollection,
  overrideAuth: async (req, getDefault) => {          // resolve the key's user...
    const s = await getDefault()
    if (s?.user) req.user = s.user                     // ...and attach it for attribution
    return s
  },
  mcp: {
    handlerOptions: { disableSse: true },
    tools: buildPayloadMcpTools({ /* draft + read tools, validator, media policy */ }),
    // no `collections` passed → the official plugin exposes no generic CRUD tools
  },
})(config)

// 2) harden the (now-extended) config: deny-bot everywhere, draft-guard + createdBy on the target
config.collections = config.collections.map(wrapWithDenyBotAndDraftGuard)

Least-privilege and draft-only by design: the AI can only ever create/edit drafts — a human reviews and publishes in the CMS. A leaked key grants nothing over raw REST. Every draft is attributed to the acting user. The media/upload vector (SVG laundering) stays closed.

New here? Start with INTEGRATION.md. Internals: ARCHITECTURE.md and the system map. Recipes + troubleshooting: USAGE.md.


Installation

pnpm add @purposeinplay/payload-mcp @payloadcms/plugin-mcp

Peers (a Payload 3.86 app already has the first three): payload, @payloadcms/richtext-lexical, zod, and @payloadcms/plugin-mcp (the official MCP plugin this wraps).

Quick start

// payload.config.ts
import { buildConfig } from 'payload'
import { payloadMcpPlugin } from '@purposeinplay/payload-mcp'

export default buildConfig({
  // ...
  plugins: [
    // register LAST so deny-bot also covers collections other plugins inject
    payloadMcpPlugin({
      collection: 'posts',            // the collection to draft into (needs versions.drafts)
      basePath: '/blog',              // your Next basePath, if any (for the returned admin URL)
      // Rich-text blocks the AI may emit.
      allowedBlocks: ['banner', 'code', 'faq', 'youtubeBlock', 'mediaBlock'],
      // Let the AI reference EXISTING library images by id (it can never upload a file). Every ref is
      // validated server-side: must exist, be image/*, and not be an SVG.
      media: { refFields: { mediaBlock: ['media'] } },
    }),
  ],
})

Then run a migration (the plugin adds fields + the payload-mcp-api-keys collection), create the bot user, mint an MCP API key, and connect Claude Code. Full steps in INTEGRATION.md.

Options

| Option | Type | Default | Notes | |---|---|---|---| | collection | string | — (required) | The collection the AI drafts into. Must have versions.drafts enabled. | | allowedBlocks | string[] | [] | blockType slugs the AI may emit in rich text. Security allowlist — omit upload/media-bearing blocks. | | allowUploadNodes | boolean | false | Permit raw Lexical upload nodes. Leave off — use a media block + media instead. | | media | { collection?, refFields?, allowedMimePrefixes?, denySvg? } | — | Enable image blocks by letting the AI reference existing media by id (never upload). refFields maps blockType → its upload field names (e.g. { mediaBlock: ['media'] }). Each ref is validated: exists · mime in allowedMimePrefixes (default ['image/']) · not SVG (denySvg, default true). Omit to keep image blocks off. | | basePath | string | '' | The host app's Next basePath (e.g. /blog). Prepended to the returned admin URL. | | authCollection | string | admin.user | Collection the MCP key's user (bot) belongs to. | | authorsCollection | string | authors | Collection authors are resolved from (by slug). | | categoriesCollection | string | categories | Collection categories are resolved from (by slug). | | botEmail | string | PAYLOAD_MCP_BOT_EMAIL | Email of the bot user drafts are attributed to. | | dailyDraftCap | number | 50 (PAYLOAD_MCP_DAILY_DRAFT_CAP) | Per-user drafts per rolling 24h. | | serverUrl | string | config.serverURL | Public origin for the returned adminUrl. | | verboseLogs | boolean | false | Emit the official plugin's MCP logs. | | enabled | boolean | true | Set false to register nothing. |

Authentication

Auth is handled by the official plugin's payload-mcp-api-keys collection, made per-editor and scoped: each editor creates/sees/manages only their own key, admins see all, and the bot / leaked-key principal + anonymous get nothing. Every draft is attributed to the key's user (createdBy), so you get real per-editor attribution and per-editor revocation. Claude Code connects with the key as a bearer — no static env token, no OAuth server. Who counts as "admin" is the isAdmin option (default: user.roles includes 'admin').

Each API-key page shows a Connect Claude Code panel — the ready-to-paste claude mcp add … command for that key (endpoint URL + bearer) with a copy button — so editors can connect without touching config. (Admin UI component; run pnpm generate:importmap in the consumer after install.)

Tool surface (MCP)

| Tool | Does | |---|---| | list_authors | slug + name of author bylines | | list_categories | slug + title of categories | | list_block_types | the block slugs allowed in content (+ field shapes, best-effort) | | list_posts | recent posts (id, title, slug, status) | | get_post | one post, content rendered back to markdown | | create_draft_post | new draft from Lexical content (or contentMarkdown) → id + adminUrl | | update_draft_post | edit a draft the actor created (stays a draft) |

Content is authored as Lexical ({ root: { children: [...] } }), with blocks as { type: 'block', fields: { blockType, ... } }; contentMarkdown is a prose-only convenience. There is deliberately no publish tool — publishing is a human action in the CMS.

HTTP endpoint

  • POST ${app}/api/mcp — the MCP Streamable-HTTP endpoint (owned by @payloadcms/plugin-mcp; SSE disabled, so GET returns a JSON-RPC "method not allowed" and never holds a stream open).

Security model (at a glance)

  • Bot identity is a plugin-injected payloadMcpBot marker on the auth collection (not a role). It — and any payload-mcp-api-keys principal — is denied all normal CRUD on every collection
    • global, so raw REST with the key returns 401/403. The only write path is the MCP tools.
  • The tools write via Local API with overrideAccess, constrained by: a block allowlist (unknown blocks rejected), rejection of raw upload nodes, href sanitization (including inside nested block rich text), slug resolution (unknown authors/categories rejected, never auto-created), and a per-user daily cap. Block subfield shapes are validated by payload.create against the collection's own config.
  • Images: the AI can never upload a file (no binary path; the bot can't create media). With media set it may reference existing library media by id, and every ref is validated server-side — must exist, be image/*, and not be an SVG — so a malicious/wrong asset can't be embedded.
  • Draft-only is enforced by a beforeChange invariant keyed on the MCP request (and the bot marker) — it holds even under overrideAccess and survives prompt-injection ("publish this").

See ARCHITECTURE.md for the full picture.