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

agent-cms

v0.3.0

Published

Agent-first headless CMS for Cloudflare Workers. Schema, content, and assets via MCP. GraphQL delivery.

Readme

agent-cms

Agent-first headless CMS. Runs as a Cloudflare Worker backed by D1 and R2 in your own account. No hosted service, no admin UI. Agents define schemas, manage content, and publish — via MCP.

What you get

  • Structured text with typed blocks — a document tree where rich components (code blocks, media, custom types) are embedded inline. One GraphQL query returns the full tree with discriminated block unions. Map directly to React/Svelte/Vue components in a single server hop. Render with react-datocms, vue-datocms, or datocms-svelte — the structured text format is DAST, an open standard.
  • Hybrid search — FTS5 for keyword matching, Cloudflare Vectorize for semantic similarity, combined with reciprocal rank fusion. All on D1.
  • Draft/publish with preview — records start as drafts. Publishing captures a version snapshot. Models can declare a canonicalPathTemplate so agents and editors get preview URLs for drafts. Short-lived preview tokens grant draft access to GraphQL without editor credentials. Schedule publish/unpublish at future datetimes. Full version history — restore to any snapshot, and the restore itself is reversible.
  • Geospatial filteringlat_lon field type with near(latitude, longitude, radius) queries in GraphQL.
  • Automatic reverse references — link model A to model B, and B gets a query field for all records in A that reference it, with full filtering, ordering, and pagination.
  • Two MCP servers — admin MCP (/mcp) for schema and content, editor MCP (/mcp/editor) scoped to content operations only. Create editor tokens with optional expiry.
  • Multi-locale with fallback chains — per-field opt-in. Locale A falls back to B falls back to C. The GraphQL resolver walks the chain.
  • 24 field types — string, text, boolean, integer, float, date, date_time, slug (auto-generated), media (with focal point + blurhash), media_gallery, link, links, structured_text, seo (title + description + image + twitter card), json, color (RGBA), lat_lon, video. All validated with Effect schemas.
  • Tree hierarchies and sortable collections — parent-child nesting and explicit position ordering as first-class model properties.
  • Dynamic SQL builder — the query engine builds SQL at runtime from the content schema. No ORM, no generated client. The content schema is decoupled from your application schema — run this on the same D1 database as your site.
  • Responsive images — Cloudflare Image Resizing with focal points, blurhash for progressive loading, color palette extraction. R2 storage, no external service.
  • Bulk operations — create up to 1000 records in a single call.
  • Schema portability — export the full schema as JSON (no IDs, just api_keys), import it on a fresh instance.
  • Three interfaces — REST API, GraphQL, and MCP, all auto-generated from the content schema.
  • Effect-TS throughout — typed errors, dependency injection via services and layers, no try/catch. The whole CMS is a single Worker.

Quick start

Copy the prompt from PROMPT.md into Claude Code. It assesses your project, asks how you want to integrate (standalone Worker, service binding, or mounted in an existing Worker), and wires everything up — including D1 database, wrangler config, and MCP server connection.

Interfaces

/mcp — Admin agent interface

MCP server with tools for schema management, content operations (CRUD, bulk insert, publish/unpublish, reorder), asset management, search, and schema import/export. Requires writeKey.

/mcp/editor — Editorial agent interface

Reduced MCP server for content-authoring agents. Accepts either an editor token or writeKey. Exposes schema introspection, record CRUD, drafts, publish/unpublish, version restore, assets, site settings, and search. Does not expose schema mutation, token management, or admin operations.

/graphql — Content delivery

Read-only GraphQL API. Supports filtering, ordering, pagination, locale fallback, and draft previews via X-Include-Drafts.

{
  all_posts(
    filter: { _status: { eq: "published" } }
    order_by: [_created_at_DESC]
    first: 10
  ) {
    id
    title
    slug
    cover_image {
      url
      width
      height
      alt
    }
    body {
      value
      blocks {
        ... on CodeBlockRecord {
          id
          code
          language
        }
      }
    }
  }
}

Naming conventions

Model api_key values (snake_case) map to GraphQL names:

| api_key | GraphQL type | Single query | List query | Meta query | |---------|-------------|-------------|------------|------------| | blog_post | BlogPost | blog_post | all_blog_posts | _all_blog_posts_meta | | category | Category | category | all_categories | _all_categories_meta |

Block types get a Record suffix: code_blockCodeBlockRecord.

Field api_key values stay snake_case in queries: cover_image, published_at.

Performance model

GraphQL nesting is not compiled into one giant SQL join. The server fetches root records, batches linked records and StructuredText work into set-oriented SQL, then assembles the nested shape in memory. See PERFORMANCE.md.

MCP resources and prompts

Agents connecting via MCP get two resources:

  • agent-cms://guide — workflow order, naming conventions, field value formats
  • agent-cms://schema — current schema as JSON

Two prompts for common workflows:

  • setup-content-model — design and create content models from a description
  • generate-graphql-queries — generate typed GraphQL queries for a model

/api — REST

JSON REST API for programmatic access. Models, fields, records, assets, locales, publish/unpublish, scheduling, bulk operations, schema import/export.

/api/search — Search

FTS5 keyword search with BM25 ranking and snippets, scoped to all models or a single model. When AI + VECTORIZE bindings are configured:

  • keyword — FTS5. Phrases ("exact match"), prefix (word*), boolean (AND/OR).
  • semantic — Vectorize cosine similarity.
  • hybrid (default) — Reciprocal rank fusion of keyword + semantic results.

Draft preview

Records on draft-enabled models start as drafts. The CMS stores both the draft state (real columns) and the published snapshot. GraphQL serves published content by default. Draft preview lets agents and editors see unpublished content on the real site before publishing.

How it works

  1. Set a URL template on your modelcanonicalPathTemplate: "/posts/{slug}". The CMS resolves {slug} from the record and includes _previewPath in tool/REST responses.

  2. Configure the site URL — pass siteUrl to createCMSHandler. The CMS uses this to generate fully assembled preview links.

  3. Agent or editor requests a preview — the get_preview_url MCP tool returns a ready-to-share link with a short-lived preview token baked in. One tool call, no assembly required.

  4. User clicks the preview link — the site's enable route validates the token against the CMS, sets a cookie, and redirects to the content page. The site then fetches draft content from GraphQL.

  5. Disable draft mode — clear the cookie via /api/draft-mode/disable?redirect=/.

MCP workflow

The agent never assembles preview URLs manually. One tool call does everything:

Agent: create_record({ modelApiKey: "post", data: { title: "Draft Post", ... } })
CMS:   { id: "abc", _status: "draft", _previewPath: "/posts/draft-post" }

Agent: get_preview_url({ recordId: "abc", modelApiKey: "post" })
CMS:   { url: "https://mysite.com/api/draft-mode/enable?token=pvt_...&redirect=/posts/draft-post",
         previewPath: "/posts/draft-post", expiresAt: "2026-03-24T17:00:00Z" }

Agent: "Here's your draft preview: https://mysite.com/api/draft-mode/enable?token=pvt_...&redirect=/posts/draft-post"

If the agent has browser access, it can follow the link to visually verify content and iterate before publishing.

Site integration

The package exports createPreviewHandler for the common case (Astro, SvelteKit, generic Workers). For Next.js, use draftMode().enable() in your route handler alongside the CMS cookie — see the framework-specific notes below.

import { createPreviewHandler } from "agent-cms";

const preview = createPreviewHandler({
  cmsBaseUrl: "https://my-cms.workers.dev",
});

// Mount at /api/draft-mode/* in your site's router

The handler validates the token against the CMS, sets an HttpOnly cookie (__agentcms_preview) with the token, and redirects. The cookie's Max-Age matches the token's remaining TTL.

In your GraphQL client, forward the cookie as a header when present:

const previewToken = getCookie("__agentcms_preview");
const headers: Record<string, string> = {};
if (previewToken) {
  headers["X-Preview-Token"] = previewToken;
  headers["Cache-Control"] = "no-store"; // bypass CDN for draft content
}

GraphQL responses in preview mode include Cache-Control: private, no-store and a _preview field:

{ _preview { enabled expiresAt } }

Use this to render a preview banner — or include the <agent-cms-preview-bar> web component that shows "Draft Mode" with a disable link.

Service bindings

When your site and CMS are in the same Cloudflare account, skip cookies entirely. Pass X-Preview-Token directly on the service binding call:

const res = await env.CMS.fetch("https://internal/graphql", {
  headers: {
    "X-Preview-Token": previewToken, // from your site's own session/cookie
  },
  body: JSON.stringify({ query }),
});

No CORS, no cross-origin cookies, zero latency. This is the recommended path for Cloudflare deployments.

Framework notes

Astro / SvelteKit / generic Workers — use createPreviewHandler directly. Read the cookie in middleware and forward as X-Preview-Token.

Next.js — your enable route must also call draftMode().enable() to integrate with Next.js ISR/caching. Set SameSite=None; Secure on the cookie for iframe compatibility. See the DatoCMS Next.js pattern — the same approach applies.

URL templates

Templates use {field_name} placeholders resolved from the record:

| Template | Record | Result | |----------|--------|--------| | /posts/{slug} | { slug: "hello-world" } | /posts/hello-world | | /docs/{category}/{slug} | { category: "guides", slug: "setup" } | /docs/guides/setup | | /{slug} | { slug: "about" } | /about |

Templates are paths only — no origin, no locale prefix. Locale URL strategies (prefix, subdomain, root folding) are handled by your site's routing, not the CMS. The enable route accepts an optional locale param for your middleware to use.

Preview tokens

  • Created internally by get_preview_url or via POST /api/preview-tokens
  • Default expiry: 24 hours (editorial workflows are async)
  • Stored as SHA-256 hashes in D1 (a database breach doesn't leak usable tokens)
  • Grant read-only access to all draft content via GraphQL
  • X-Preview-Token header takes precedence over __agentcms_preview cookie if both are present

Editor tokens

Editor tokens are the credential for non-admin editing flows. Create them via POST /api/tokens or the editor_tokens MCP tool (with action: "create"). The raw token is shown once; the server stores a hash.

Editor tokens can access /mcp/editor, REST content/asset operations, and draft GraphQL previews. They cannot mutate schema, manage tokens, or run admin operations.

For editor onboarding with OAuth, the package exports createCmsAdminClient and createEditorMcpProxy to stand up an app-land MCP gateway. See examples/editor-mcp/.

Scheduling

Schedule publish/unpublish at future datetimes via REST or MCP. To execute schedules automatically, add a cron trigger:

import { createCMSHandler } from "agent-cms";

let cachedHandler: ReturnType<typeof createCMSHandler> | null = null;

function getHandler(env: Env) {
  if (!cachedHandler) {
    cachedHandler = createCMSHandler({
      bindings: { db: env.DB, assets: env.ASSETS, writeKey: env.CMS_WRITE_KEY },
    });
  }
  return cachedHandler;
}

export default {
  fetch(request: Request, env: Env) {
    return getHandler(env).fetch(request);
  },
  scheduled(_controller: ScheduledController, env: Env) {
    return getHandler(env).runScheduledTransitions();
  },
};
{ "triggers": { "crons": ["* * * * *"] } }

Without a cron trigger, schedules are stored and queryable but do not execute.

Lifecycle hooks

React to content events with hooks passed to createCMSHandler:

createCMSHandler({
  bindings: { db: env.DB, assets: env.ASSETS, writeKey: env.CMS_WRITE_KEY },
  hooks: {
    onPublish: ({ modelApiKey, recordId }) => fetch(env.DEPLOY_HOOK_URL, { method: "POST" }),
    onRecordCreate: ({ modelApiKey, recordId }) => { /* notify, sync, etc. */ },
  },
});

Available: onRecordCreate, onRecordUpdate, onRecordDelete, onPublish, onUnpublish. All receive { modelApiKey, recordId }. Fire-and-forget.

Bindings

Only DB is required. Everything else is optional and degrades gracefully.

| Binding | Type | What it enables | |---------|------|-----------------| | DB | D1 | Required. Content storage, schema, FTS5 search. | | ASSETS | R2 | Asset file storage and serving via /assets/. | | AI | Workers AI | Embedding generation for semantic search. | | VECTORIZE | Vectorize | Semantic vector search. Requires AI. | | CMS_WRITE_KEY | Secret | Auth for writes, MCP, and publish. Without it, writes are open. | | ASSET_BASE_URL | Variable | Public URL prefix for assets and Image Resizing. Must be a custom domain for transforms. | | SITE_URL | Variable | Your site's public URL (e.g. https://mysite.com). Required for get_preview_url to generate fully assembled preview links. |

{
  "d1_databases": [{ "binding": "DB", "database_name": "my-cms-db", "database_id": "..." }],
  "r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-cms-assets" }],
  "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-cms-content" }],
  "ai": { "binding": "AI" },
  "vars": { "ASSET_BASE_URL": "https://cms.example.com" }
}

To create the Vectorize index: npx wrangler vectorize create my-cms-content --dimensions=384 --metric=cosine

Assets

Asset binaries live in R2. Metadata in D1. Served from /assets/:id/:filename.

  • MCP/editor: import_asset_from_url — download, store, register in one step
  • Browser: PUT /api/assets/:id/file then register metadata
  • Server: upload to R2, then POST /api/assets

Focal points, blurhash, and color palette are stored per-asset. Cloudflare Image Resizing generates responsive variants at the edge.

Stack

  • Runtime: Cloudflare Workers
  • Database: D1 (managed SQLite)
  • Assets: R2 + Cloudflare Image Resizing
  • Search: SQLite FTS5 + Cloudflare Vectorize
  • Application: Effect
  • GraphQL: graphql-yoga with generated SDL
  • Testing: Vitest (pnpm test)

Examples

  • examples/blog/ — CMS Worker + Astro SSR site with typed GraphQL (gql.tada), structured text rendering, responsive images, service bindings, draft preview mode
  • examples/nextjs/ — Next.js App Router with draftMode() integration, multi-root GraphQL queries, preview bar component
  • examples/editor-mcp/ — editor onboarding: app-land OAuth gateway, scoped editor tokens, separate MCP URLs for developers and editors

License

MIT