LightCMS
LightCMS MCP server lets an AI agent fully manage a LightCMS site through tools.
Content: create, read, update, delete/restore, publish/unpublish, bulk update, publish multiple, preview, version history/revert, export, backlinks.
Templates & snippets: CRUD templates and reusable Go-template snippets for rendering and
lc:queryindex pages.Assets: list, get, upload (file/base64/URL), delete, and browse asset folders.
Organization: manage folders, collections, redirects, and site configuration.
Search: full-text/semantic/hybrid search, site-wide or scoped search-and-replace preview/execute, reindex embeddings.
Theme: read/update theme, inspect versions, pin/unpin, revert.
Forks: create/list/get fork workspaces, fork pages, remove pages, merge, archive, delete for staged review before live.
Bulk ops: bulk field operation, bulk content update, dry-run validation, auto-republish options.
Safety: RBAC/admin restrictions, preview/dry-run, mandatory confirmation for destructive site-wide changes.
LightCMS
LightCMS is a Go-powered content management system built for the AI era. It's simultaneously AI-native (semantic search, built-in Claude-powered chat widget, MCP server for agent control), agentically controllable (Claude Code and any MCP client can read, write, publish, and bulk-import content via 130 MCP tools), and agentically updatable (the codebase is clean, well-structured Go — coding agents can safely extend it). For teams that want a CMS that works with AI rather than around it.
What's New in v7.3 — SEO & AI
Feature | Summary |
AI Traffic | See which AI crawlers read your site (training vs. AI search vs. user-initiated fetches), what they read most, and how many visits ChatGPT, Perplexity, Claude, Gemini and Copilot send you. |
AI Crawler Policy | Allow or block AI training, AI search and user fetches separately, with per-crawler overrides and an optional Content-Signal line, rendered into robots.txt. |
Markdown Copies | Every page is also available as clean Markdown at |
Richer Structured Data | Authors, publisher with logo, breadcrumbs, FAQ markup, and modified dates that only move when the page content actually changes. |
RSS & Atom Feeds |
|
Hide from Search & AI | Per-page noindex that also removes the page from the sitemap, llms.txt, feeds and IndexNow. |
Related MCP server: sitemd
What's New in v7.2
Feature | Summary |
IndexNow | Published, edited, moved and deleted pages are pushed to Bing, Yandex and other IndexNow engines right away. Zero-config: each install generates its own key, verifies it owns its |
CMS Agent | Configurable email digests (via Resend) covering site health, traffic, review queue, agent activity and broken links, with optional AI commentary. |
What's New in v7.1
Feature | Summary |
Ambient Copilot | The copilot is a slide-in drawer on every admin page — floating 🤖 button, fullscreen toggle, ChatGPT-style searchable chat history, live typing indicator, and rendered tables. It edits, creates, publishes, and reads analytics and maintenance reports in plain language. |
Analytics Ranges | 60/90-day presets and custom date ranges across all analytics views. |
Fast AI-Crawler Endpoints | llms.txt serves in ~1 second on large sites (projected queries), and homepages emit schema.org WebSite JSON-LD with SearchAction. |
Version Visibility | The admin sidebar shows the running version. |
What's New in v7.0
Feature | Summary |
Agent Sandbox ("PRs for content") | Agents work in an isolated fork with copy-on-write; a person reviews a complete per-field diff and merges exactly what was reviewed. The server enforces it: a Sandbox key or connection can't change the live site at all. |
Agent Governance | Every key and OAuth connection acts as a person and is Sandbox or Full; merging its own forks is a separate permission only an admin grants. Per-session change ledger with one-call rollback (merges included), and per-version provenance (human vs agent, which session, which key or connection). |
Admin Copilot |
|
llms.txt + JSON-LD | Auto-generated |
MCP for Readers | Public read-only MCP endpoint at |
Local Embeddings |
|
Self-Maintaining Sites | Daily maintenance scans surface stale pages, missing meta, and broken links as an agent-ready work queue. |
What's New in v6.0
Feature | Summary |
Content Approvals | Contributors submit content for approval; editors/admins approve or reject from |
Contributor Role | New RBAC role between Viewer and Editor. Can create content + upload assets (pending), post comments, and submit for approval — but cannot publish directly or manage system settings. |
Approval Workflows | Configurable trigger-based workflows (contributor, folder path, template ID, or tag). Sequential or concurrent mode with configurable approver lists. |
Content Discussion | Inline comment thread at the bottom of every edit page. @mention autocomplete, live badge count, admin delete. |
Tabbed Bottom Panel | Discussion, Version History, and Forks organized into tabs on the content edit page. |
Approvals Dashboard | My Queue + Other Pending + Workflow Config at |
Dashboard Sections | Requiring Approvals and Recent Comments appear on the admin dashboard when relevant. |
New Webhook Events |
|
14 new MCP tools | Full comment and approval lifecycle — list/create/delete comments, full workflow CRUD, list/get/submit/approve/reject/cancel approval requests. |
What's New in v5.0
Feature | Summary |
Import Pipeline | Three new import types — RSS/Atom feeds, Markdown/ZIP upload, CSV bulk import — with a unified job dashboard at |
RSS/Atom Import | Configure recurring feed sources with hourly/daily/weekly schedules, template mapping, folder targeting, and auto-publish. |
Markdown Import | Upload |
CSV Import | Upload CSV files and map columns to content fields. Specify the title column; all other columns become fields automatically. |
Real-time Job Status | SSE-powered live log stream at |
10 new MCP import tools |
|
Agentic bulk content creation |
|
Deduplication | Imports match by |
What's New in v4.5
Feature | Summary |
Webhooks | HMAC-SHA256 signed events for |
Scheduled Publishing | Set a future |
Content Locking | Advisory lock when editing (30-min expiry). Warning banner if another user is already editing. Admins can force-unlock. |
Incremental Static Regeneration (ISR) | Template layout changes regenerate affected pages in 20-page batches, preventing server overload on large sites. |
Edge Caching Headers | ETag, Cache-Control, Last-Modified, and Vary headers on all public pages. 304 Not Modified support. |
Cloudflare Integration | Configure Zone ID + API Token to auto-purge Cloudflare cache on publish/unpublish. |
Structured JSON Logging | All server logs emit structured JSON with |
Rate Limit Dashboard | New tab on the audit log page showing locked IPs, attempt counts, and a one-click clear button. |
MCP Prompt Resources | Three new MCP resources: |
Why LightCMS?
Lightweight: A clean, focused codebase that's easy to understand, modify, and extend. No bloated frameworks or complex abstractions.
AI-Native: Built from the ground up for the AI era:
MCP Integration: Full Model Context Protocol server with 130 tools and 3 prompt resources for website management. Supports both local stdio and HTTP streamable transports — connect from Claude Code, Claude Desktop, or any MCP-compatible client.
OAuth 2.1 for Remote Agents: Sandboxed desktop apps like Claude's Cowork can securely connect over HTTP using OAuth 2.1 with PKCE. No embedded passwords: sign in once, choose what the agent may do, and it acts as you within that.
Fork-Friendly: Designed to be forked and customized by Claude Code. Ask Claude to add new content types, modify templates, or build custom features — the codebase is structured for AI-assisted development.
Natural Language Website Management: Skip the admin UI entirely. Create pages, manage assets, customize themes, and publish content through conversation.
Features
Content Management
Template System: Define reusable content structures with custom fields (text, richtext, image, date, select, markdown)
Static Page Generation: Fast page loads from pre-rendered HTML — no runtime templating overhead
Content Versioning: Full version history with diff comparison and one-click revert
Soft Delete: Recover deleted content with undelete functionality
Content Tagging: Tag any content item with one or more freeform labels, then query by tag across your site
Snippets: Named HTML template fragments used as reusable rendering units in dynamic queries
lc:queryDirectives: Embed live content queries directly in template layouts — at publish time they expand into rendered lists of matching pagesContent Collections: Auto-generated paginated listing pages filtered by category
Folders & URL Organization: Hierarchical content organization with clean URL paths
Rich Text Editor: Quill 2 for visual content editing (vendored in
static/admin/quill/, served from the site itself)Regex Search & Replace: Site-wide or scoped search-and-replace with RE2 regex support, capture groups, and mandatory preview step
Bulk Operations: Update or apply field operations across up to 100 pages in a single API call; export/transform/re-import pipelines
Scheduled Publishing (v4.5+): Set a future
publish_attimestamp; a background scheduler auto-publishes at the right timeContent Locking (v4.5+): Advisory lock while editing (30-min expiry); warning banner if another user holds the lock; admins can force-unlock
Incremental Static Regeneration (v4.5+): Template layout changes regenerate pages in 20-page batches to prevent server overload on large sites
Import Pipeline (v5.0+)
RSS/Atom Feed Sources: Configure recurring import sources with configurable schedule (hourly/daily/weekly), template mapping, folder targeting, and auto-publish. Manage sources and run history from
/cm/imports. Feed URLs must be publicly reachable (private and loopback addresses are refused, v7.4.3).Markdown + ZIP Import: Upload
.mdfiles or.ziparchives of Markdown. YAML frontmatter in each file controls title, slug, folder, template, tags, and scheduled publish time. Supports Notion exports, Obsidian vaults, Hugo/Jekyll site migrations, and AI-generated content.CSV Bulk Import: Upload a CSV and specify which column is the title; all other columns are stored as content fields automatically.
Real-time SSE Job Status: Live log stream at
/cm/imports/{jobID}— watch imports happen line-by-line or review full history after the fact.MCP-first Design:
import_markdownis specifically designed for AI agents to generate and import large content batches in a single call, replacing dozens ofcreate_contentcalls with oneimport_markdown+ oneget_import_job. See MCP.md for workflow examples.
Content Forks (v4.0+)
Fork Workspaces: Create named staging workspaces where sets of page edits can be authored, previewed, and reviewed before going live
Sparse Model: Only edited pages live in a fork — unmodified pages fall through to live content automatically
Fork Preview Mode: Activate via a floating bar injected into the live site; a cookie routes all page requests through the fork so you see exactly how the site will look after merge
Merge with Conflict Detection: Admins merge forks into live content; if a live page was changed after the fork was created, the conflict is recorded (fork wins). New pages created in the fork are inserted into live on merge, keeping the publish state they had in the fork (normally draft) unless the merge is run with
publish_newMerges Clean Up (v7.4+): A merge deletes the fork's page copies and keeps the fork record, with created/updated counts, as history. Forks merged before v7.4 can be cleaned with
purge_fork_copiesFork Copies Stay Out of the Way (v7.4+): Fork copies are left out of content listings and search unless
include_forksis set, and can never be published directly or in bulk — only mergedHold Flag (v7.4+): Mark any draft
holdto block every way of publishing it (publish, bulk publish, scheduled publish, approval, fork merge) until the flag is clearedFull MCP Toolset: 9 dedicated fork tools —
list_forks,create_fork,get_fork,fork_page,remove_fork_page,merge_fork,archive_fork,delete_fork,purge_fork_copies
Batch & Parallel Operations
Designed for agents that prefer parallelized, high-throughput workflows over sequential single-item calls:
bulk_update_content: Update up to 100 pages in a single API call. Each item uses merge semantics — only the fields you specify are touched. Supportsdry_runvalidation before committing, andauto_republishto re-publish all previously-published pages in the same call, eliminating a separate publish stepbulk_field_operation: Apply a single operation (set,clear,prepend,append,wrap) to a field across every matching page in one call. Scope by template, folder, category, or explicit ID list. Ideal for adding disclaimers, updating metadata, or clearing stale fields across a content typepublish_multiple: Publish a list of IDs — or all drafts at once withpublish_all_drafts: true— in a single request instead of looping overpublish_contentexport_content: Dump full field data for a scoped set of pages as a structured JSON array. Designed for export → transform → re-import pipelines; pair withbulk_update_contentfor large-scale content migrationsScoped Search & Replace: Both
scoped_search_replace_previewandscoped_search_replace_executeaccept scope filters (folder, template, category, IDs) so agents can target precise subsets rather than running site-wide operationsParallel-Safe Read API:
list_contentwithinclude_data: truereturns full field values in one fetch; agents can fan out reads across multiplelist_content/get_contentcalls concurrently and then batch-write withbulk_update_content
Recommended agent pattern for large updates: list_content → transform in parallel → bulk_update_content (up to 50/call) → publish_multiple.
AI Chat Widget (v4.2+)
Embeddable Widget: Add a floating AI chat bubble to any page with a single
<script>tag — self-contained, no build step requiredTwo-Phase Pipeline: Each visitor query runs hybrid semantic+fulltext search to retrieve relevant content excerpts, then streams those excerpts through Claude Haiku for a conversational synthesized answer
SSE Streaming: Haiku's token-by-token output is forwarded live to the browser via Server-Sent Events; falls back to plain JSON for non-SSE clients
AI-Optional: Without an Anthropic API key, the widget works as a search-in-chat experience returning ranked excerpts — no API cost
Fully Configurable: Admin "Chat Widget" page controls title, welcome message, placeholder text, primary color, position (bottom-left/right), max results, and editable system/user prompt templates
Dedicated Rate Limiting: Separate per-IP (5/min) and global (30/min) limiters independent from the search and API limiters
Source Attribution: Every response includes the content pages whose excerpts were used, with titles and paths
Multi-User Access Control & Approvals (v2.0+, v6.0+)
Role-Based Access Control (RBAC): Four roles — admin, editor, contributor, viewer — with granular permission enforcement on all admin UI pages and REST API endpoints
Contributor Role (v6.0+): Create content, upload assets, post comments, and submit for editorial approval — without publish rights
Approval Workflows (v6.0+): Configurable trigger-based workflows (by role, folder, template, or tag). Sequential or concurrent mode. Default (no workflow): any editor or admin can approve
Content Approvals Dashboard (v6.0+): My Queue, Other Pending, and Workflow Config at
/cm/approvals. Sidebar badge shows pending countContent Discussion (v6.0+): Inline threaded comments on every content edit page. @mention autocomplete, admin delete, live count badge
User Management: Admin panel for creating users, assigning roles, disabling accounts, and resetting passwords
Audit Log: Persistent, searchable log of all mutations (who did what, when) with 365-day retention
Rate Limit Dashboard (v4.5+): New tab on the audit log page showing locked IPs, attempt counts, and a clear button
User-Scoped API Keys: API keys inherit the permissions of their owning user
Force Password Change: Temporary passwords trigger a mandatory change on first login
Smart End-User Search
Hybrid Search: Combines full-text exact matching with semantic vector search (Voyage AI embeddings), merged via reciprocal rank fusion
Configurable Ranking: All ranking weights editable in the admin panel — nav boost, title boost, boosted templates, demoted path prefixes, and penalty scores
Intelligent Defaults: Nav-linked pages surface first, concept-template pages rank above generic content, video transcripts are deprioritised
Title Boost: Pages where the query appears in the title always rank above body-only matches
Typeahead Suggestions: Fast prefix-matching suggestions — pages for direct navigation, keywords for full search — with the same structural ranking
Works Without Embeddings: Falls back to full-text search if no Voyage API key is configured
Rate Limiting: Per-IP and global rate limiting for DDoS protection
Developer & Integration
REST API: Full
/api/v1/JSON API with API key authentication, RBAC-enforcedMCP Server: 130 tools + 3 prompt resources for agentic website management (stdio + HTTP streamable)
OAuth 2.1: Authorization code flow with PKCE for remote MCP clients — no embedded passwords
CLI Tool: Command-line interface for all content management operations
URL Redirects: 301/302 redirect rules managed from the admin panel
Webhooks (v4.5+): HMAC-SHA256 signed event delivery for 6 event types (
content.create,content.update,content.publish,content.unpublish,content.delete, and more); per-webhook secrets; delivery history with retry visibility; admin UI at/cm/webhooks. Webhook URLs must be publicly reachable: a URL that resolves to a loopback, private or link-local address is refused and logged as "blocked" (v7.4.3)Cloudflare Integration (v4.5+): Auto-purge Cloudflare cache on publish/unpublish via Zone ID + API Token
Edge Caching Headers (v4.5+): ETag, Cache-Control, Last-Modified, Vary, and 304 Not Modified on all public pages
Structured JSON Logging (v4.5+): All server logs emit structured JSON with timestamp, level, message, and context fields
Site Customization
Theme Customization: Colors, fonts, border radius, custom CSS — all editable in the admin panel with version history
Header/Footer HTML: Full HTML control over site chrome injected around all pages
Asset Management: Upload and manage images, documents, and other files with path-based serving
Prerequisites
Go 1.24 or later
MongoDB Atlas account (free tier works great)
Quick Start
Clone the repository
Copy
config.dev.json.exampletoconfig.dev.jsonEdit
config.dev.jsonwith your MongoDB connection stringRun
go run cmd/server/main.goVisit http://localhost:8082/cm and log in with your email and password
On first run, an admin account is created — set
LIGHTCMS_ADMIN_EMAILto use your email, or it defaults toadmin@localhost
MongoDB Atlas Setup
Step 1: Create an Atlas Account
Go to MongoDB Atlas
Sign up for a free account (no credit card required)
Step 2: Create a Cluster
Click "Build a Database"
Select "M0 FREE" (Shared) tier
Choose your preferred cloud provider and region (closest to you)
Click "Create Deployment"
Step 3: Set Up Database Access
Create a database user:
Username:
lightcms(or your choice)Password: Generate a secure password (save this!)
Click "Create User"
Add your IP address:
Click "Add My Current IP Address"
Or add
0.0.0.0/0to allow access from anywhere (less secure, but convenient for development)Click "Finish and Close"
Step 4: Get Your Connection String
Click "Connect" on your cluster
Select "Drivers"
Copy the connection string, it looks like:
mongodb+srv://lightcms:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majorityReplace
<password>with your actual password
Step 5: Create Your Config File
For development, copy the example and fill in your values:
cp config.dev.json.example config.dev.jsonEdit config.dev.json:
{
"port": "8082",
"mongo_uri": "mongodb+srv://lightcms:YOUR_PASSWORD@cluster0.xxxxx.mongodb.net/lightcms",
"env": "development",
"session_secret": "any-random-string-for-dev"
}For production, use config.prod.json:
cp config.prod.json.example config.prod.jsonEdit with production values (use openssl rand -hex 32 for session_secret).
Installation
# Clone or navigate to the project
cd lightcms
# Install dependencies
go mod tidy
# Run the server
go run cmd/server/main.goOr use the run script:
./run.shConfiguration
LightCMS uses JSON config files. Create either:
config.dev.json- for developmentconfig.prod.json- for production (takes precedence if both exist)
Field | Description |
| Server port (e.g., "8082" for dev, "80" for prod) |
| MongoDB Atlas connection string |
| MongoDB database name. Optional, default |
| Environment: "development" or "production" |
| Random string for session encryption |
| Public URL of the site |
|
|
When MONGO_URI is set, configuration comes from environment variables instead (MONGO_URI, SESSION_SECRET, BASE_URL, PORT, ENV, SECURE_COOKIES).
Database name. The server and the cmd/ tools use the database lightcms unless told otherwise. Set the DATABASE_NAME environment variable (it wins over database_name in the config file) to point an instance at another database on the same cluster, for example a staging or test copy. The server logs the database it connected to at startup.
Plain HTTP in development. With secure_cookies: false the admin works over http://localhost: cookies are not marked Secure and the CSRF origin check compares against http://. With secure_cookies: true (the production default) the admin must be reached over HTTPS; the CSRF check rejects plain-HTTP origins.
Note: Config files contain secrets and are excluded from git via .gitignore.
Usage
Accessing the Site
Public site: http://localhost:8082
Admin panel: http://localhost:8082/cm
First Login
Log in at /cm/login with your email and password. On first startup with an empty database, LightCMS creates an admin account from the LIGHTCMS_ADMIN_EMAIL environment variable (defaults to admin@localhost with password admin123). Change your password immediately after logging in.
To reset a password from the command line:
go run cmd/resetpw/main.go user@example.comCreating Content
Log in to the admin panel at
/cmGo to Content → New Content
Select a template (Blog Post, Press Release, Explanatory Page, etc.)
Fill in the fields
Check "Published" and save
Managing Users (Admin Only)
Go to Users in the left sidebar (visible to admins only)
Create users with email, display name, and role (admin / editor / viewer)
Users receive a temporary password and are prompted to change it on first login
Disable accounts or reset passwords from the edit page
View a full audit trail of all user actions at Audit Log
Creating Custom Templates
Go to Templates → New Template
Define your fields (text, textarea, richtext, date, image, select)
Create an HTML layout using
{{.field_name}}placeholdersSave the template
Available placeholders:
{{.title}}- Content title{{.slug}}- URL slug{{.published_at}}- Publication date{{.your_field_name}}- Any custom field you define
Dynamic Index Pages: Tags, Snippets, and lc:query
LightCMS includes a system for building dynamic index pages that automatically update as you publish content. Three features work together: tags label individual pages, snippets define how each result is rendered, and lc:query directives embed live queries directly inside template layouts.
Tags
Tags are freeform string labels you attach to any content item. A page can have zero or many tags. They're the primary way to group content for querying.
Setting tags in the admin UI:
Open any content item in the editor
Find the Tags field (below the main fields)
Type a tag name and press Enter — repeat for multiple tags
Save the content item
Setting tags via the API:
curl -X PUT http://localhost:8082/api/v1/content/{id} \
-H "Authorization: Bearer lc_your_key" \
-H "Content-Type: application/json" \
-d '{"tags": ["AI & Machine Intelligence", "Featured"]}'Tags are exact-match strings. Capitalization and spaces are preserved — "AI & Machine Intelligence" and "ai & machine intelligence" are treated as different tags.
Snippets
A snippet is a named HTML template fragment stored in the CMS. When lc:query runs, it renders each matching content item through a snippet and concatenates the results.
Creating a snippet:
Go to Settings → Snippets in the admin panel
Click New Snippet, give it a name (e.g.
glossary-pill)Write HTML using Go template variables:
<a href="{{.FullPath}}">{{.Title}}</a>Available variables inside a snippet:
Variable | Description |
| The content item's title |
| The public URL path (e.g. |
| URL slug only (e.g. |
| Meta description field |
| Publication timestamp |
Example snippets:
A pill-style link (for glossary / tag cloud layouts):
<a href="{{.FullPath}}" class="pill">{{.Title}}</a>A card with description:
<div class="card">
<h3><a href="{{.FullPath}}">{{.Title}}</a></h3>
<p>{{.MetaDescription}}</p>
</div>A simple list item:
<li><a href="{{.FullPath}}">{{.Title}}</a></li>lc:query Directives
An lc:query directive is an HTML comment you embed in a template layout. At page generation time — before the page is rendered — the CMS finds all matching content items, renders each one through the named snippet, and replaces the comment with the combined HTML.
Syntax:
<!-- lc:query filter="tag:TAGNAME" sort="title:asc" snippet="snippet-name" -->Attributes:
Attribute | Required | Description |
| Yes | Filter expression. Currently supports |
| No | Sort field and direction: |
| Yes | Name of the snippet to render each result through. |
Example in a template layout:
<h2>AI & Machine Intelligence</h2>
<div class="links">
<!-- lc:query filter="tag:AI & Machine Intelligence" sort="title:asc" snippet="glossary-pill" -->
</div>After the page is published, the directive is replaced with the rendered output of every published page tagged AI & Machine Intelligence, each passed through the glossary-pill snippet:
<h2>AI & Machine Intelligence</h2>
<div class="links">
<a href="/artificial-intelligence" class="pill">Artificial Intelligence</a>
<a href="/machine-learning" class="pill">Machine Learning</a>
<a href="/neural-networks" class="pill">Neural Networks</a>
</div>Important: lc:query directives must be placed in the template's HTML layout field, not inside content data fields. The CMS processes them during page generation before Go's template engine runs (which would otherwise strip HTML comments).
Automatic Regeneration
Index pages that use lc:query are automatically regenerated whenever:
A tagged content item is published or updated
The template layout is changed
The snippet is updated
Regenerate All is triggered manually from the admin panel
This means you never need to manually rebuild your index pages — publish a new concept page tagged "Games & Interactive Experiences" and it appears in every index that queries for that tag within seconds.
Complete Walkthrough: Building a Tagging-Powered Index
Here's how to build a concepts glossary that automatically stays up to date.
Step 1: Tag your concept pages
For each concept page, add the appropriate tag in the content editor. You can use as many tags as you like, and the same content item can appear in multiple index sections.
Step 2: Create a snippet
In Settings → Snippets, create a snippet named glossary-pill:
<a href="{{.FullPath}}">{{.Title}}</a>Step 3: Create a template with lc:query sections
Create a new template (e.g. "Concepts Index") with this HTML layout:
<article class="index-page">
<h1>{{.title}}</h1>
<div class="page-content">
<h2>AI & Machine Intelligence</h2>
<div class="concept-links">
<!-- lc:query filter="tag:AI & Machine Intelligence" sort="title:asc" snippet="glossary-pill" -->
</div>
<h2>Games & Interactive Experiences</h2>
<div class="concept-links">
<!-- lc:query filter="tag:Games & Interactive Experiences" sort="title:asc" snippet="glossary-pill" -->
</div>
<h2>3D Graphics & Rendering</h2>
<div class="concept-links">
<!-- lc:query filter="tag:3D Graphics & Rendering" sort="title:asc" snippet="glossary-pill" -->
</div>
</div>
</article>Note that {{.title}} is the Go template variable for the content item's title. Template variables use Go's {{.field}} syntax and are resolved after lc:query directives are expanded.
Step 4: Create and publish an index page
Create a new content item using your "Concepts Index" template. Give it a title and slug (e.g. /glossary). Publish it — the static page is generated with all the current tagged content already in place.
Step 5: Keep publishing
From now on, every time you create and publish a new concept page with a matching tag, all index pages that query for that tag are automatically regenerated and updated.
Template Variables
Beyond content data fields, templates have access to a few built-in variables:
Variable | Description |
| The content item's title |
| URL slug |
| Publication timestamp |
| Any custom field defined in the template (richtext fields render as HTML) |
Custom fields defined on your template are available directly by key. If you define a field with key intro, it's available as {{.intro}} in the layout. Richtext fields are automatically marked safe — their HTML is rendered as-is without escaping.
Content Authoring: Pre-Processing & Markup Features
LightCMS processes content field values before rendering them into your template. Authors can use the following markup features in any text or richtext data field.
Wikilinks
Link between pages using double-bracket syntax. Wikilinks are resolved at publish time and automatically kept up to date when a page's title or path changes.
Syntax | Result |
| Link to a page matched by title (case-insensitive) |
| Same, with custom link text |
| Link to a page by its exact URL path |
| Path link with custom link text |
Broken links (no matching page found) render as <span class="broken-link">Page Title</span> so they are easy to identify and fix.
Snippet Includes
Embed a named snippet inline inside any content field:
[[include:snippet-name]]The snippet-name must exactly match the Name field of a snippet in Settings → Snippets. Snippet includes are useful for reusable content blocks such as callouts, disclaimers, and calls to action that appear on many pages.
Table of Contents
Place {{.lc_toc}} anywhere in your template's HTML layout to inject an auto-generated table of contents at that position:
<nav class="sidebar">
{{.lc_toc}}
</nav>
<article>
{{.body}}
</article>At page generation time, LightCMS scans all headings in the final rendered HTML and outputs a <nav class="lc-toc"> block with anchor links to each one. Headings automatically receive id= attributes derived from their text content (see Heading IDs below), so the TOC links work without any extra setup.
Heading IDs
All headings (<h1> through <h6>) in rendered page output automatically receive id= attributes derived from their text. This enables deep-linking to specific sections.
Example:
<!-- In your content field -->
<h2>Getting Started</h2>
<!-- Rendered output -->
<h2 id="getting-started">Getting Started</h2>The id is generated by lowercasing the text and replacing spaces and punctuation with hyphens. If two headings produce the same id, a numeric suffix is appended (getting-started-2, etc.).
Markdown Field Type
Template fields can be given the type markdown instead of text or richtext. Markdown fields support GitHub Flavored Markdown (GFM) including tables, strikethrough, task lists, and autolinks. The field value is converted to HTML at page generation time.
Markdown fields are a good choice for structured content that benefits from simple markup without a WYSIWYG editor — documentation pages, changelogs, FAQs, and similar content.
To create a Markdown field, set the field type to markdown when defining the template:
{ "name": "body", "label": "Body", "type": "markdown", "required": true }Inline Tag Detection
Mention #tagname anywhere in a content field to automatically tag the page with that label. The tag is added to the page's tag list and participates in lc:query index pages just like manually applied tags.
Tag rules:
Must start with a letter
May contain letters, numbers, underscores, or hyphens
Example:
This article covers #machine-learning and #aiadds both tags
This is a convenient alternative to editing the Tags field separately — useful when writing content in Markdown fields or richtext where you want to tag inline.
Creating Collections
Collections display grouped content (like a blog listing page).
Go to Collections → New Collection
Set the category filter to match your content's category
Define item and page templates
The collection will be available at
/collection-slug
Customizing the Theme
Go to Theme in the admin panel
Adjust colors, fonts, and border radius
Add custom CSS if needed
Save to apply changes site-wide
End-User Search
LightCMS exposes a public search API at /api/search that your site's frontend can call.
Search API
GET /api/search?q=QUERY&mode=hybrid&limit=10Parameter | Description |
| Search query (required) |
|
|
| Max results 1–50 (default 10) |
{
"query": "game design",
"mode": "hybrid",
"total": 3,
"results": [
{ "id": "...", "title": "Game Design", "full_path": "/concepts/game-design",
"snippet": "...matching context...", "score": 0.97, "match_type": "both" }
]
}match_type is exact, semantic, or both.
Typeahead Suggest API
GET /api/search/suggest?q=PREFIX&limit=8Returns two lists for building a live typeahead dropdown:
{
"keywords": ["game design", "game mechanics"],
"pages": [{"title": "About Jon Radoff", "path": "/about"}]
}keywords — extracted from published content; clicking one triggers a full search
pages — direct-navigation results, ranked by: nav-linked → boosted-template → title-starts-with → title-contains → demoted paths
JavaScript Example
<input type="text" id="q" placeholder="Search..." autocomplete="off">
<ul id="suggest"></ul>
<div id="results"></div>
<script>
const input = document.getElementById('q');
const suggest = document.getElementById('suggest');
const results = document.getElementById('results');
let timer;
// Typeahead while typing
input.addEventListener('input', () => {
clearTimeout(timer);
const q = input.value.trim();
if (q.length < 2) { suggest.innerHTML = ''; return; }
timer = setTimeout(async () => {
const r = await fetch('/api/search/suggest?q=' + encodeURIComponent(q) + '&limit=8');
const d = await r.json();
suggest.innerHTML = [
...(d.pages || []).map(p => `<li><a href="${p.path}">📄 ${p.title}</a></li>`),
...(d.keywords || []).map(k => `<li><a onclick="doSearch('${k}')">🔍 ${k}</a></li>`),
].join('');
}, 200);
});
// Full search on Enter
input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(input.value); });
async function doSearch(q) {
suggest.innerHTML = '';
const r = await fetch('/api/search?q=' + encodeURIComponent(q) + '&mode=hybrid&limit=10');
const d = await r.json();
results.innerHTML = (d.results || [])
.map(r => `<div><a href="${r.full_path}"><strong>${r.title}</strong></a><p>${r.snippet}</p></div>`)
.join('') || '<p>No results.</p>';
}
</script>Ranking Configuration
Ranking weights are configurable in the admin panel under Tools → End User Search → Search Ranking. Defaults: title-match boost 0.20, nav-page boost 0.15, concept-template boost 0.05, video-path penalty −0.05. Configure your Voyage AI key under Configuration to enable semantic search.
Project Structure
lightcms/
├── cmd/
│ ├── server/main.go # HTTP server entry point
│ ├── mcp/main.go # MCP server entry point
│ ├── cli/main.go # CLI tool entry point
│ └── resetpw/main.go # Password reset utility
├── config/
│ └── config.go # Configuration loading
├── internal/
│ ├── apiclient/ # Reusable HTTP client for REST API
│ ├── auth/ # Authentication, RBAC permissions, session management
│ ├── cli/ # CLI subcommands and output formatting
│ ├── database/ # MongoDB connection & operations
│ ├── handlers/ # HTTP handlers (admin UI + REST API)
│ ├── mcp/ # MCP server and tool definitions
│ ├── middleware/ # API auth middleware (API keys + OAuth)
│ ├── models/ # Data models & default templates
│ ├── oauth/ # OAuth 2.1 authorization server
│ └── services/ # Business logic (content, search, users, audit, etc.)
├── static/ # CSS, JS, and uploaded files
├── content/ # Custom pages and generated HTML
└── .goreleaser.yaml # Release configurationDefault Templates
Blog Post
Fields: title, excerpt, featured_image, content, author, tags
Press Release
Fields: headline, subheadline, dateline, release_date, body, boilerplate, contact_info
Explanatory Page
Fields: title, subtitle, hero_image, intro, main_content, sidebar, cta_text, cta_link
Concept Page
Fields: title, definition, topic_links — ideal for wiki-style knowledge base entries
Standard Page, Blank Page, Homepage
General-purpose layouts for flexible content.
Multi-User Access Control
LightCMS v2.0+ supports multiple users with role-based permissions.
Roles
Role | Capabilities |
admin | Full access: manage users, templates, theme, settings, audit log, all API keys |
editor | Create/edit/delete/publish content; upload and delete assets; manage own API keys |
viewer | Read-only access to content, templates, assets, and settings |
Audit Log
Every mutation (content create/update/delete/publish, user management, settings changes, logins) is logged with the acting user's email, timestamp, and relevant details. Logs are retained for 365 days and accessible at /cm/audit.
API Key Permissions
API keys created by a user inherit that user's role. A key created by an editor cannot perform admin-only operations even if its token is shared. Admins can manage all keys; non-admins can only manage their own.
First-Time Migration
On first startup with an empty users collection, LightCMS automatically creates an admin user from the existing password hash in the database. Set the LIGHTCMS_ADMIN_EMAIL environment variable to specify which email address to use (defaults to admin@localhost).
API Keys
API keys are required for the REST API, MCP server, and CLI tool. Create them from the admin panel.
Log in at
/cmGo to Settings → API Keys
Click Create New Key, give it a name and description
Copy the key immediately — it's only shown once
Keys use the format lc_ followed by 32 hex characters. They're stored as SHA-256 hashes and inherit the permissions of the creating user.
OAuth 2.1 Authorization
LightCMS implements OAuth 2.1 so that remote MCP clients (like Claude's Cowork) can securely connect without embedding passwords or API keys. This follows the standard authorization code flow with PKCE.
Endpoints
Endpoint | Purpose |
| Dynamic client registration (RFC 7591) |
| Authorization page (sign-in + consent) |
| Token exchange and refresh |
| Token revocation (RFC 7009) |
| JWKS endpoint (opaque tokens, returns empty) |
Security
A connection acts as the person who approved it (v7.5): every request loads that person's current role, so a role change, a disabled account or a disconnected app takes effect at once. The person also chooses the connection's access on the consent page: Sandbox (the default; it can change only forks it creates) or Full (anything their role can do). An admin can also let it merge its own forks.
Tokens are for this server's MCP endpoint only (RFC 8707):
/mcpaccepts them,/api/v1does not, and an MCP session stays bound to the connection that opened it.PKCE (S256) required for all authorization requests
Single use: authorization codes and refresh tokens are consumed atomically; a refresh token that comes back after it was used ends the connection
Short-lived access tokens: 1-hour TTL
Refresh tokens: 30-day TTL
Revocation:
POST /oauth/revoke(client credentials required) ends a token, or with a refresh token the whole connection; people disconnect apps at Connected Apps (/cm/connected-apps)Registration: rate limited per address; redirect URIs must be
https, orhttpto this device (loopback)Rate limiting: failed login attempts trigger progressive lockouts (1 min → 5 min → 15 min)
All tokens stored as SHA-256 hashes in the database
How Clients Connect
Client fetches
/.well-known/oauth-authorization-serverto discover endpointsClient calls
POST /oauth/registerwith its name and redirect URIClient sends you to
/oauth/authorizewith a PKCE challengeYou sign in, see who the app will act as and where it sends you back, choose its access, and approve
Client exchanges the authorization code for access + refresh tokens
Client uses the access token as a Bearer token on
/mcp
This is all handled automatically by MCP-compatible clients — you just provide your LightCMS URL and approve the connection.
REST API
LightCMS provides a full REST API at /api/v1/ authenticated with API keys. All endpoints enforce RBAC — the permissions of the key's owner, limited by the key's access, determine what's allowed. (OAuth access tokens are issued for the MCP endpoint and are not accepted here.)
Authentication
Include an API key in the Authorization header:
curl -H "Authorization: Bearer lc_your_key_here" http://localhost:8082/api/v1/contentA key set to Sandbox access (the default for keys created in the admin UI) can read the site and change only forks it creates: every other write is refused with 403. GET/POST /api/v1/sandbox and POST /api/v1/sandbox/end open and close a sandbox fork for the key.
Endpoints
Resource | Endpoints |
Content |
|
Templates |
|
Snippets |
|
Assets |
|
Theme |
|
Config |
|
Redirects |
|
Folders |
|
Collections |
|
Search |
|
API Keys |
|
Utility |
|
All endpoints return JSON. PUT endpoints support partial updates (only include fields you want to change).
CLI Tool
The lightcms CLI provides command-line access to all content management operations.
Installation
# Build from source
go build -o bin/lightcms ./cmd/cli
# Or download a release binary from GitHubConfiguration
export LIGHTCMS_URL=http://localhost:8082
export LIGHTCMS_API_KEY=lc_your_key_hereOr use flags: --url and --api-key.
Commands
lightcms content list # List all content
lightcms content get <id> # Get content by ID
lightcms content create --template <id> --title "My Post" --slug my-post --data '{"body":"Hello"}'
lightcms content publish <id> # Publish content
lightcms content versions <id> # Show version history
lightcms template list # List templates
lightcms asset upload --file logo.png --path /images/logo.png
lightcms theme update --primary-color "#1a1a2e"
lightcms search "search terms" # Search content
lightcms api-key create --name "CI/CD" # Create API key
lightcms --json content list # JSON outputRun lightcms --help for full usage.
MCP Server (AI-Powered Content Management)
LightCMS includes a full MCP (Model Context Protocol) server with 130 tools and 3 prompt resources for managing your entire website through AI agents. It supports two transport modes:
Stdio — for local tools like Claude Code
HTTP Streamable — for remote/sandboxed clients like Claude's Cowork, Claude Desktop, or any MCP-compatible app
Option A: Local Setup (Claude Code via Stdio)
Best for developers using Claude Code directly on the same machine.
Create an API key in the admin panel at
/cm→ Settings → API KeysRun the setup script:
export LIGHTCMS_API_KEY=lc_your_key_here
./setup-mcp.shOr register manually:
go build -o bin/lightcms-mcp ./cmd/mcp
claude mcp add --transport stdio lightcms-mcp \
-e LIGHTCMS_URL="http://localhost:8082" \
-e LIGHTCMS_API_KEY="lc_your_key_here" \
-- /path/to/lightcms/bin/lightcms-mcpRestart Claude Code and run /mcp to verify.
Option B: Remote Setup (Cowork / Claude Desktop via HTTP + OAuth)
Best for sandboxed desktop apps that can't run local binaries. The HTTP MCP endpoint at /mcp supports OAuth 2.1 authorization — no API keys or passwords need to be embedded in the client.
How it works:
The client discovers your LightCMS instance via well-known endpoints
It registers as an OAuth client (one-time, automatic)
You sign in in the browser and choose its access: Sandbox (default) or Full
The client receives short-lived access tokens and refreshes them automatically
The connection acts as you, with your role, limited by the access you chose. See and change it, or disconnect it, at /cm/connected-apps.
To connect from a remote MCP client, just provide your LightCMS URL (e.g., https://yoursite.example.com). The client handles the rest using standard OAuth 2.1 discovery.
Discovery endpoints:
Endpoint | Purpose |
| OAuth server metadata (RFC 8414) |
| Protected resource metadata (RFC 9728) |
| MCP server card with tool schemas |
Authentication
The MCP HTTP endpoint accepts both authentication methods:
API keys (
lc_prefix) — long-lived, created in admin panelOAuth 2.1 tokens — short-lived, obtained through the authorization flow
Both act as a person (the key's owner, or whoever approved the connection) and enforce that person's role, limited by the key's or connection's access. Each MCP session is bound to the credential that opened it, and its tools call the REST API in-process as that caller.
Agent access: Sandbox, Full, and merging (v7.5)
Setting | What it allows |
Sandbox | Read the site; create forks and change only the forks this key or connection created; upload new images, PDFs, fonts and text files (nothing executable, nothing replaced). Everything else is refused by the server, whichever tool sends it. |
Full | Anything the owner's role allows. |
Can merge its own forks | Off by default; only an admin can turn it on, for an account that can merge. Lets |
Every merge names the review it was approved from: get_fork_diff returns a digest, and merge_fork refuses if the fork or a live page it changes was edited since. Merges write a version for each page they change, so rollback_agent_session undoes them.
Available Tools (130 total) + 3 Prompt Resources
Content (19): list, read, create, update, update by path, delete, restore, publish, unpublish, publish multiple, preview, versions (list + get), revert, bulk create, bulk update, bulk field operation, export, backlinks
Templates (5): create, read, update, delete, list
Snippets (5): create, read, update, delete, list
Assets (6): upload, upload from URL, read, delete, list files and folders
Search (7): full-text search, end-user search, search-and-replace (global + scoped, preview + execute), reindex embeddings
Settings (23): theme CRUD + versioning + pinning, site config, redirects, folders, collections, regenerate all content
Forks (9): list, create, get, fork page, remove page, merge, archive, delete, purge copies
Import (10, v5.0+): list/create/update/delete/trigger import sources, import markdown, import CSV, list/get/cancel import jobs
Webhooks (6, v4.5+): list, create, update, delete webhooks; regenerate secret; list deliveries
Content Locking (4, v4.5+): get lock, acquire lock, release lock, force-unlock
Scheduled Publishing (3, v4.5+): schedule publish, list scheduled, cancel scheduled
Audit & Link Check (3, v4.5+): list audit logs, start link check, get link check results
Comments (3, v6.0+): list comments, post comment, delete comment
Approvals (11, v6.0+): list/get/create/update/delete approval workflows; list/get/submit/approve/reject/cancel approval requests
Agent Sandbox & Governance (10, v7.0+): start/get/end agent sandbox, fork diff, agent session changes, session rollback, maintenance report, run maintenance scan, backfill published dates, repair fork damage
IndexNow (3, v7.2.3+): get status, enable/disable, submit
SEO & AI (3, v7.3+): get/update SEO settings, AI traffic
Prompt Resources (3, v4.5+):
lightcms://site/structure,lightcms://content/recent,lightcms://theme/config
For detailed API documentation, see MCP.md.
Environment Variables (Stdio Mode)
LIGHTCMS_URL— Server URL (default:http://localhost:8082)LIGHTCMS_API_KEY— API key (required for stdio mode)
MCP Examples
These examples show how the MCP tools work together to manage a website through natural language. Each example lists the user prompt and the exact MCP tool calls that execute behind the scenes.
Example 1: Create and Publish a Blog Post
Prompt: "Create a blog post about AI agents and publish it"
Tool calls:
list_templates— finds the Blog Post template and its IDcreate_content— creates the post with template ID, title, slug, and field data:{ "template_id": "6971098ad0761968133b8e43", "title": "The Rise of AI Agents", "slug": "rise-of-ai-agents", "data": { "excerpt": "How autonomous AI agents are reshaping software development.", "content": "<p>AI agents represent a fundamental shift...</p>", "author": "Editorial Team" } }publish_content— makes it live; a static HTML page is generated at/rise-of-ai-agents
Example 2: Update the Site Theme
Prompt: "Change the site colors to a dark theme with blue accents"
Tool calls:
get_theme— reads current theme settings (colors, fonts, header/footer HTML)update_theme— applies the new palette:{ "primary_color": "#1a1a2e", "secondary_color": "#16213e", "accent_color": "#0f3460", "background_color": "#0a0a0a", "text_color": "#e0e0e0" }All published pages are automatically regenerated with the new theme.
Example 3: Search and Replace Across the Entire Site
Prompt: "Replace 'Acme Corp' with 'Acme Industries' everywhere on the site"
Tool calls:
search_replace_preview— shows affected pages without making changes:{ "search": "Acme Corp", "replace": "Acme Industries" }Returns a list of content items, matched fields, and match counts.
search_replace_execute— applies the replacement after user confirmation. Each affected content item gets a new version for rollback capability.
Example 4: Create a Custom Template
Prompt: "Create a template for team member profiles with name, role, bio, and photo"
Tool calls:
create_template— defines the structure and HTML layout:{ "name": "Team Member", "slug": "team-member", "fields": [ { "name": "role", "label": "Role", "type": "text", "required": true }, { "name": "photo", "label": "Photo", "type": "image", "required": false }, { "name": "bio", "label": "Biography", "type": "richtext", "required": true } ], "html_layout": "<div class=\"team-member\"><img src=\"{{.photo}}\" alt=\"{{.title}}\"><h2>{{.title}}</h2><h3>{{.role}}</h3><div>{{.bio}}</div></div>" }The template is immediately available for creating content.
Example 5: Upload an Image and Use It in Content
Prompt: "Upload this logo and add it to the about page"
Tool calls:
upload_asset— uploads the file as base64 with a serve path:{ "filename": "logo.png", "serve_path": "/images/logo.png", "data_base64": "iVBORw0KGgoAAAANSUhEUg..." }The image is now accessible at
/images/logo.png.get_content— retrieves the about page by path to get its current dataupdate_content— adds the image reference to the page's content field
Example 6: Set Up URL Redirects
Prompt: "The old blog moved from /news to /blog — set up redirects for the old URLs"
Tool calls:
create_redirect— creates a permanent redirect:{ "from_path": "/news", "to_path": "/blog", "status_code": 301, "description": "Blog section moved from /news to /blog" }Visitors to
/newsare automatically redirected to/blog. Multiple redirects can be created in sequence for different paths.
Example 7: Content Versioning and Rollback
Prompt: "Show me the version history for the homepage, then revert to version 2"
Tool calls:
get_content— retrieves the homepage by path (/) to get its IDget_content_versions— returns the full version history with timestamps and comments:Version 5 — "Updated hero section" — 2026-02-25 Version 4 — "Added testimonials" — 2026-02-20 Version 3 — "Redesigned layout" — 2026-02-15 Version 2 — "Initial content" — 2026-02-10 Version 1 — "Created page" — 2026-02-08revert_to_version— rolls back to version 2, creating a new version 6 with the old data. The published page is regenerated immediately.
Example 8: Organize Content with Folders
Prompt: "Create a /docs section and move the API reference page into it"
Tool calls:
create_folder— creates the URL path segment:{ "name": "Documentation", "slug": "docs" }get_content— retrieves the API reference page to get its IDupdate_content— moves it into the new folder:{ "id": "...", "folder_path": "/docs" }The page is now accessible at
/docs/api-referenceinstead of/api-reference.
Example 9: Build a Content Collection (Blog Index)
Prompt: "Create a blog listing page that shows all blog posts sorted by newest first"
Tool calls:
create_collection— defines the collection with category filter, sorting, and display templates:{ "name": "Blog", "slug": "blog", "category": "blog", "sort_field": "created_at", "sort_order": "desc", "items_per_page": 10, "item_template": "<article><h2><a href=\"{{.Path}}\">{{.Title}}</a></h2><p>{{.excerpt}}</p><time>{{.PublishedAt}}</time></article>", "page_template": "<div class=\"blog-index\"><h1>Blog</h1>{{.Items}}{{.Pagination}}</div>" }A paginated blog listing is now live at
/blog, automatically including any content with category "blog".
Example 10: Build a Dynamic Tagged Index Page
Prompt: "Create a glossary index that automatically lists all my concept pages grouped by category, and keep it updated as I add new pages"
Tool calls:
create_snippet— creates a reusable rendering template for each result:{ "name": "glossary-pill", "html": "<a href=\"{{.FullPath}}\">{{.Title}}</a>" }create_template— creates the index page template withlc:querydirectives embedded:{ "name": "Concepts Index", "slug": "concepts-index", "fields": [ { "name": "intro", "label": "Introduction", "type": "textarea" } ], "html_layout": "<article class=\"index-page\">\n<h1>{{.title}}</h1>\n{{if .intro}}<p>{{.intro}}</p>{{end}}\n\n<h2>AI & Machine Intelligence</h2>\n<div class=\"links\">\n<!-- lc:query filter=\"tag:AI & Machine Intelligence\" sort=\"title:asc\" snippet=\"glossary-pill\" -->\n</div>\n\n<h2>Games & Interactive Experiences</h2>\n<div class=\"links\">\n<!-- lc:query filter=\"tag:Games & Interactive Experiences\" sort=\"title:asc\" snippet=\"glossary-pill\" -->\n</div>\n</article>" }create_content— creates the index page using the new template:{ "template_id": "<concepts-index-template-id>", "title": "Concepts Glossary", "slug": "glossary", "data": { "intro": "An index of all concepts, grouped by category." } }update_content— tags several existing concept pages (each call):{ "tags": ["AI & Machine Intelligence"] }publish_content— publishes the index page; thelc:querydirectives are resolved at this moment and the page is generated with all currently-tagged content already populated.
From now on, every time a new concept page is published with a matching tag, the index page at /glossary is automatically regenerated — no further action needed.
Example 11: Full-Text Search and Content Audit
Prompt: "Find all pages that mention 'pricing' and show me which ones are still in draft"
Tool calls:
search_content— performs a full-text search across all content fields:{ "query": "pricing", "search_type": "fulltext" }Returns matching content items with their publish status, paths, and which fields matched:
Found 4 results for 'pricing': - "Pricing Plans" at /pricing — published — matched in: content - "Enterprise FAQ" at /enterprise-faq — published — matched in: content, sidebar - "New Pricing Draft" at /new-pricing — draft — matched in: title, content - "Q1 Press Release" at /press/q1-update — draft — matched in: bodyThe two draft items can then be reviewed, edited, and published as needed.
Example 12: Bulk Content Migration
Prompt: "Add a 'last_reviewed' field to every page in our /docs section and publish them all"
Tool calls:
list_content— fetches all content under/docswith full field data in one call:{ "folder_path": "/docs", "include_data": true }Returns IDs, titles, current field values, and publish status for all 34 pages.
(parallel) Agent fans out into batches of 50 and calls
bulk_update_contentconcurrently:{ "updates": [ { "id": "abc123", "data": { "last_reviewed": "2026-03-24" } }, { "id": "def456", "data": { "last_reviewed": "2026-03-24" } } ], "version_comment": "Added last_reviewed field — Q1 2026 audit", "auto_republish": true }auto_republish: truere-publishes every previously-published page immediately — no separate publish step needed.
All 34 pages are updated and live in two parallel calls instead of 34 sequential ones.
Example 13: Fork-Based Staged Redesign
Prompt: "Redesign the homepage and /about page in a staging area so I can preview before publishing"
Tool calls:
create_fork— creates a named staging workspace:{ "name": "Q2 Redesign", "description": "Homepage and About refresh" }Returns a fork ID. The live site is completely unaffected.
fork_page— copies the homepage into the fork and applies edits:{ "fork_id": "fork_abc123", "content_id": "homepage_id", "data": { "hero_headline": "Build the web with AI", "hero_subtext": "..." } }fork_page— does the same for/about:{ "fork_id": "fork_abc123", "content_id": "about_id", "data": { "body": "<p>Updated company story...</p>" } }A floating preview bar is injected into the live site — visiting it with the fork cookie active shows both pages exactly as they'll look after merge.
merge_fork— after approval, merges both fork pages into live content and regenerates their static HTML:{ "fork_id": "fork_abc123" }Returns a summary of merged pages (counts plus
created_idsandupdated_ids) and any conflicts detected. Pages the fork adds are created as drafts unless you pass"publish_new": true; pages on hold always stay drafts. The fork's page copies are deleted once the merge succeeds.
Example 14: Site-Wide Notice with Auto-Republish
Prompt: "Prepend a deprecation notice to all pages in our /v1 docs section and republish them"
Tool calls:
bulk_field_operation— appends the notice to a specific field across the entire folder in one call:{ "operation": "prepend", "field": "body", "value": "<div class=\"deprecation-notice\"><strong>⚠️ This page covers v1 (deprecated).</strong> See <a href=\"/docs\">current docs</a>.</div>\n\n", "folder_path": "/v1", "auto_republish": true, "version_comment": "Added v1 deprecation notice" }Returns counts of updated and republished pages. The live site reflects the change immediately.
If the notice ever needs to be removed, a single scoped_search_replace_execute with the exact notice HTML reverts all pages in one call.
Working with Snippets
# List all snippets
list_snippets
# Create a callout snippet
create_snippet {
"name": "callout-warning",
"description": "Warning callout box",
"html": "<div class=\"callout callout-warning\"><strong>⚠️ {{.Title}}</strong><p>{{.Body}}</p></div>"
}
# Use a snippet inline in content
update_content {
"id": "...",
"data": {
"body": "Here is important information:\n\n[[include:callout-warning]]\n\nContinued text..."
}
}Content Tagging & Index Pages
# Tag a page at creation time
create_content {
"template_id": "...",
"title": "Introduction to AI",
"slug": "intro-to-ai",
"tags": ["AI & Machine Intelligence", "Getting Started"],
"data": { "body": "..." }
}
# Inline tagging via content body
update_content {
"id": "...",
"data": {
"body": "This page covers #machine-learning and #neural-networks."
}
}
# Query directive in a template layout (for index pages)
<!-- lc:query filter="tag:AI & Machine Intelligence" sort="title:asc" snippet="concept-card" -->Bulk Operations
# Step 1: Export all Concept Pages with specific fields
export_content {
"template_name": "Concept Page",
"fields": ["definition", "layer_badge"]
}
# Step 2: Transform the data externally, then bulk update
bulk_update_content {
"updates": [
{ "id": "abc123", "data": { "layer_badge": "<new html>" } },
{ "id": "def456", "data": { "layer_badge": "<new html>" } }
],
"version_comment": "Updated layer badges"
}
# Clear a field across all pages of a template
bulk_field_operation {
"operation": "clear",
"field": "old_badge",
"template_name": "Concept Page",
"version_comment": "Cleared deprecated field"
}
# Prepend a disclaimer to all blog posts
bulk_field_operation {
"operation": "prepend",
"field": "body",
"value": "<div class=\"disclaimer\">Views are my own.</div>",
"template_name": "Blog Post",
"version_comment": "Added disclaimer to all posts"
}Regex Search & Replace
# Preview: find all pages with old badge HTML pattern
scoped_search_replace_preview {
"search": "<div class=\"badge-v1\".*?</div>",
"replace": "",
"regex": true,
"template_name": "Concept Page"
}
# Execute after confirming preview
scoped_search_replace_execute {
"search": "<div class=\"badge-v1\".*?</div>",
"replace": "",
"regex": true,
"template_name": "Concept Page",
"version_comment": "Removed old v1 badge HTML"
}Wikilinks
# Link to another page by title
update_content {
"id": "...",
"data": {
"body": "See also: [[Machine Learning]] and [[AI Ethics|ethics considerations]]."
}
}
# Find what pages link to a given path
get_backlinks { "path": "/concepts/machine-learning" }Content Versioning
# See version history for a page
get_content_versions { "content_id": "abc123" }
# Restore a previous version
revert_to_version {
"content_id": "abc123",
"version": 3,
"version_comment": "Reverted to pre-redesign version"
}Development
# Run with hot reload (using air)
go install github.com/cosmtrek/air@latest
air
# Build all binaries
go build -o bin/lightcms-server ./cmd/server
go build -o bin/lightcms-mcp ./cmd/mcp
go build -o bin/lightcms ./cmd/cli
# Run the server
./bin/lightcms-serverSecurity Notes
For production:
Use a strong
session_secret— minimum 32 characters (generate withopenssl rand -hex 32). The server hard-fails on startup if this requirement isn't met in production.Set
LIGHTCMS_ADMIN_EMAILso the initial admin account uses your real emailChange the default admin password immediately after first login
Use HTTPS (put behind a reverse proxy like nginx or caddy)
Restrict MongoDB Atlas IP whitelist to your server IPs
API keys inherit the permissions of their owning user — keep admin keys secure
Review the audit log regularly at
/cm/auditRegularly backup your MongoDB database
Configure
max_upload_bytesin site settings to cap file upload size for your use case
Security features built in:
CSRF protection on all
/cmroutesAdmin Content-Security-Policy: scripts and stylesheets load from the site itself only (the editor's Quill files are vendored, not fetched from a CDN)
RBAC permission checks on all admin handlers and REST API endpoints
Session cookies: SameSite=Strict, 24-hour expiry, Secure in production
File uploads: extension whitelist + MIME validation + configurable size cap
API request body cap: 10 MiB enforced on all
/api/v1/endpointsLogin rate limiting: escalating lockouts (1 min → 5 min → 15 min)
Per-endpoint rate limiters: regenerate (2/min), search-replace execute (10/min), bulk-update (5/min), export (5/min), reindex (1/min)
Passwords: bcrypt with cost=12
Audit logging on all mutations with 365-day retention
Fly.io deployments:
Fly-Client-IPheader used for real client IP (unspoofable, unlikeX-Forwarded-For)Chat widget prompt injection defense: user input wrapped in XML delimiters,
</sequences escaped
Privacy Policy
LightCMS is self-hosted software — you control your database, your hosting, and your data. The MCP server and CLI tool connect to your LightCMS instance via the REST API — no data is transmitted to Metavert LLC or any third party.
For the full privacy policy, see: https://www.metavert.io/lightcms-privacy-policy
License
MIT
Available Tools
129 toolsacquire_content_lockAcquire Content LockAIdempotent
Acquire an advisory lock on a content item for the current API user. Returns conflict if another user holds the lock.
Inputs: content_id (required).
approve_requestApprove RequestA
Approve an approval request. When all required approvals are collected the content is automatically published.
Inputs: id (required), comment.
archive_forkArchive ForkAIdempotent
Archive a fork without merging it. The fork and its pages are preserved but the fork becomes read-only. Requires admin role.
Inputs: fork_id (required).
backfill_published_datesBackfill Published DatesAIdempotent
Give published pages that have no published date (created before v7.3.2) a published date equal to their creation date, for structured data and feeds. Metadata only: pages don't re-render. Use dry_run first to see the count.
Inputs: dry_run.
bulk_create_contentBulk Create ContentA
Create up to 100 content items in a single call using efficient batch insert.
Items are inserted via MongoDB InsertMany for maximum throughput. If one item fails (e.g., duplicate path), the rest continue. Published items get their static HTML generated in parallel (up to 10 concurrent).
Set upsert: true to update existing pages at the same path instead of failing on duplicates.
Returns: total attempted, succeeded, failed counts, and per-item {id, full_path, success, error} details.
Inputs: items (required), upsert, version_comment.
bulk_field_operationBulk Field OperationADestructive
Apply a single field operation to all matching content pages in one call.
Operations:
clear: set field to empty string
set: replace field value with a fixed string
prepend: add text before existing field value
append: add text after existing field value
wrap: surround existing value with before/after strings
Use scope filters to limit which pages are affected (content_ids, folder_path, template_name, category). Set dry_run: true to preview which pages would be changed without saving. Only live pages are changed: fork copies named in content_ids come back in "skipped" as {id, reason}.
Example: {"operation": "prepend", "field": "disclaimer", "value": "Note: ", "template_name": "Blog Post"}
Inputs: field (required), operation (required), after, value, +7 more.
bulk_update_contentBulk Update ContentADestructive
Update up to 100 content items in a single call. Use instead of calling update_content in a loop.
Each update in the array specifies the content ID and only the fields you want to change (merge semantics on data). Use clear_fields to explicitly clear field values to empty string. Set dry_run: true to validate all IDs exist without committing changes.
Returns: total attempted, succeeded, failed counts, and per-item success/error details.
Tip: call list_content with include_data: true first to get IDs + current field values, transform as needed, then submit here. Recommended batch size: up to 50 per call for optimal performance.
Inputs: updates (required), dry_run, version_comment.
cancel_approval_requestCancel Approval RequestADestructive
Cancel a pending approval request.
Inputs: id (required).
cancel_import_jobCancel Import JobBDestructive
Cancel a running import job
Inputs: id (required).
cancel_scheduled_publishCancel Scheduled PublishBIdempotent
Clear the scheduled publish time for a content item.
Inputs: content_id (required).
create_approval_workflowCreate Approval WorkflowC
Create a new approval workflow. Trigger types: all_contributor, folder_path, template_id, tag. Mode: sequential or concurrent.
Inputs: mode (required), name (required), trigger (required), approvers, +2 more.
create_collectionCreate CollectionB
Create a new content collection. Collections display grouped content with custom templates.
Inputs: name (required), slug (required), category, sort_field, +5 more.
create_commentPost CommentA
Post a discussion comment on a content item. Requires discussion.post permission.
Inputs: text (required), content_id (required), mentions.
create_contentCreate ContentA
Create a new content item. Requires a template_id, title, slug, and the data fields defined by the template.
Workflow:
Call list_templates to find the right template and its field names.
Create the content with data matching those fields.
Call publish_content to make it live (or set published=true here to do both in one step).
Set use_header=true, use_footer=true, use_theme=true for pages that should use the site layout. Always include version_comment to make history readable.
Content data fields support rich markup features:
[[Wikilinks]] and [[Page Title|display text]] — link to other pages by title or path; auto-update when paths change
[[include:snippet-name]] — embed a named snippet inline (reusable callouts, CTAs, disclaimers)
#hashtags — mention #tagname anywhere to automatically tag the page
Markdown fields (type "markdown") — GitHub Flavored Markdown converted to HTML at publish time Templates can use {{.lc_toc}} in their HTML layout to inject an auto-generated table of contents.
Inputs: data (required), slug (required), title (required), template_id (required), +16 more.
create_folderCreate FolderB
Create a new content folder. Folders create URL path segments for organizing content.
Inputs: name (required), slug (required), parent_id.
create_forkCreate ForkA
Create a new named fork workspace for staging content changes.
A fork is an isolated workspace where you can edit copies of live pages without affecting the public site. When ready, an admin can merge the fork to push all changes live at once.
Typical workflow:
create_fork — create the workspace
fork_page — copy pages you want to edit into the fork (returns a fork page ID)
update_content (with the fork page ID) — make your edits
merge_fork — merge all changes to the live site (admin only)
Returns the fork ID needed for subsequent fork operations.
Inputs: name (required), description.
create_import_sourceCreate Import SourceB
Create a new RSS/Atom import source that automatically pulls content from a feed
Inputs: url (required), name (required), active, schedule, +3 more.
create_redirectCreate RedirectA
Create a new URL redirect. Use 301 for permanent redirects, 302 for temporary.
Inputs: to_path (required), from_path (required), description, status_code.
create_snippetA
Create a new snippet. Snippets are Go templates that render one content item in an lc:query index page. Reference by name:
Inputs: html (required), name (required).
create_templateCreate TemplateB
Create a new template. Define fields with name, label, type (text, textarea, richtext, date, image, select), and options.
Inputs: name (required), slug (required), fields (required), html_layout (required), +2 more.
create_webhookCreate WebhookA
Create a new webhook. Returns the generated HMAC-SHA256 secret ONCE — save it immediately.
Valid event types: content.create, content.update, content.publish, content.unpublish, content.delete, comment.created, content.pending_approval, asset.pending_review
Inputs: url (required), name (required), events (required), active.
delete_approval_workflowDelete Approval WorkflowCDestructive
Delete an approval workflow.
Inputs: id (required).
delete_assetDelete AssetADestructiveIdempotent
Delete an asset from the library. Removes both the file and database record.
Inputs: id (required).
delete_collectionDelete CollectionADestructiveIdempotent
Delete a collection. This does not delete the content in the collection.
Inputs: id (required).
delete_commentDelete CommentBDestructive
Delete a discussion comment. Requires comment.delete permission (admin only).
Inputs: comment_id (required), content_id (required).
delete_contentDelete ContentADestructiveIdempotent
Soft-delete a content item. The content can be restored later. Removes the static HTML page.
Inputs: id (required).
delete_folderDelete FolderADestructiveIdempotent
Delete an empty folder. Cannot delete folders that contain content or subfolders.
Inputs: id (required).
delete_forkDelete ForkADestructive
Permanently delete a fork and all its page copies. Deleting a merged fork removes only its history record (and any leftover copies) — the merged live pages are not affected. Requires admin role.
Inputs: fork_id (required).
delete_import_sourceDelete Import SourceCDestructiveIdempotent
Delete an RSS import source
Inputs: id (required).
delete_redirectDelete RedirectCDestructiveIdempotent
Delete a redirect.
Inputs: id (required).
delete_snippetCDestructive
Delete a snippet by ID.
Inputs: id (required).
delete_templateDelete TemplateADestructiveIdempotent
Delete a template. Cannot delete system templates or templates that have content using them.
Inputs: id (required).
delete_webhookDelete WebhookBDestructiveIdempotent
Permanently delete a webhook.
Inputs: id (required).
end_agent_sandboxEnd Agent SandboxADestructive
End the sandbox session. action='submit' keeps the fork for human review and merge (at /cm/forks/{id}); action='discard' permanently deletes the fork and all its changes. Agents cannot merge their own sandbox — merging is a human decision.
Inputs: action (required).
end_user_searchEnd User SearchBRead-only
Search published content using full-text exact match, semantic (AI) similarity, or hybrid mode. Returns page titles, paths, and snippets.
Inputs: query (required), mode, limit.
export_contentExport ContentARead-only
Export content items with their full field data as a structured JSON array.
Use for batch transformations: export → transform externally → re-import via bulk_update_content.
Scope filters (all optional):
template_name: most useful for bulk workflows, e.g. "Concept Page" or "Blog Post"
category, folder_path, content_ids: narrower scoping options
Use fields: ["field1", "field2"] to include only specific data fields instead of all fields.
Only live pages are exported: fork copies named in content_ids come back in "skipped" as {id, reason}.
Returns: total count and array of items with id, title, slug, full_path, template_name, published, and data.
Inputs: fields, category, content_ids, folder_path, +1 more.
force_unlock_contentForce Unlock ContentADestructiveIdempotent
Admin only: force-release any lock on a content item regardless of who holds it.
Inputs: content_id (required).
fork_pageFork PageAIdempotent
Copy a live page into a fork workspace so you can edit it without affecting the public site.
Provide either:
path: the URL path of the page (e.g. "/about", "/blog/my-post") — preferred when you know the URL
content_id: the MongoDB ObjectID of the content item
Returns the fork page ID. Use this ID with update_content to make edits:
fork_page → get fork_page_id
update_content with id=fork_page_id to edit
get_content with id=fork_page_id to verify
merge_fork when all edits are ready (admin only)
If the page is already in this fork, returns the existing fork copy.
Inputs: fork_id (required), path, content_id.
get_agent_sandboxGet Agent Sandbox StatusARead-only
Show whether an agent sandbox is active, and if so its fork ID, name, and the pages changed so far (with per-field diffs against live).
get_agent_session_changesGet Agent Session ChangesARead-only
The audit ledger for an agent session: every content item it changed, with actions and timestamps. Defaults to the current MCP session. Requires audit permissions.
Inputs: session_id.
get_ai_trafficGet AI TrafficBRead-only
AI visibility report: requests from AI crawlers by purpose (training, AI search, user-initiated fetch) and by crawler, the pages AI reads most, visits referred by AI assistants (ChatGPT, Perplexity, Claude, Gemini…), and where they land.
Inputs: days.
get_approval_requestGet Approval RequestBRead-only
Get details of a single approval request including decisions so far.
Inputs: id (required).
get_approval_workflowGet Approval WorkflowBRead-only
Get a single approval workflow by ID.
Inputs: id (required).
get_assetGet AssetARead-only
Get asset metadata by ID or path. Does not return file content (use the serve path to access the file).
Inputs: id, path.
get_backlinksGet BacklinksARead-only
Find all published pages that link to the given URL path. Links are tracked automatically whenever a page is published — both [[Wikilinks]] and ordinary links in content fields are indexed.
Use this to discover which pages reference a given page (wiki-style backlink graph), assess the impact of deleting or renaming a page, or find orphaned pages with no inbound links.
Example: {"path": "/about"} returns every published page whose content contains a link to /about.
Inputs: path (required).
get_collectionGet CollectionBRead-only
Get a collection by ID.
Inputs: id (required).
get_contentGet ContentARead-only
Get a single content item by ID or path. Returns full content including all field data (title, slug, full_path, data fields, published state).
Prefer path when you know the URL: {"path": "/about"} Use id when you have the MongoDB ObjectID: {"id": "abc123"}
Set include_rendered=true to also receive the fully rendered HTML output (template + theme header/footer applied). Useful for verifying what visitors see without publishing.
Tip: to preview unsaved edits before publishing, use preview_content instead.
Inputs: id, path, include_rendered.
get_content_lockGet Content LockARead-only
Get the current advisory lock status for a content item. Returns lock holder and expiry, or {locked: false} if unlocked.
Inputs: content_id (required).
get_content_versionGet Content VersionBRead-only
Get a specific version of a content item with full field data.
Inputs: version (required), content_id (required).
get_content_versionsGet Content VersionsBRead-only
Get the version history for a content item. Returns list of versions with timestamps.
Inputs: content_id (required).
get_folderGet FolderCRead-only
Get a folder by ID.
Inputs: id (required).
get_forkGet ForkARead-only
Get details of a fork workspace including its status and list of pages.
Returns: fork metadata + array of pages (id, title, full_path, updated_at). Use the page id with get_content or update_content to read/edit fork pages.
Inputs: id (required).
get_fork_diffGet Fork DiffARead-only
Per-field differences between each page in a fork and its live counterpart — the review surface for merging a fork. Status 'added' means the page exists only in the fork.
Inputs: id (required).
get_import_jobGet Import JobBRead-only
Get the status, results, and log of a specific import job
Inputs: id (required), include_logs.
get_indexnow_statusGet IndexNow StatusARead-only
IndexNow notifies Bing, Yandex and other engines when pages change. Returns whether it is active (and why not if inactive), the key file URL, whether the key file verified, total URLs submitted, and recent submissions with HTTP results.
get_link_check_resultsGet Link Check ResultsBRead-only
Get the status and results of a link check job. Status is "running", "done", or "failed".
Inputs: job_id (required).
get_maintenance_reportGet Maintenance ReportARead-only
The latest site-health scan: stale pages (no update in ~6 months), published pages missing meta descriptions, and lingering drafts. Use it as a work queue — fix issues via the sandbox workflow.
get_seo_settingsGet SEO & AI SettingsBRead-only
The site's search & AI settings: AI crawler policy (training / AI search / user fetch, per-crawler overrides) with the resulting robots.txt, Markdown copies, default author/publisher for structured data, and feed settings.
get_site_configGet Site ConfigBRead-only
Get site configuration including title templates.
get_snippetBRead-only
Get a snippet by ID.
Inputs: id (required).
get_templateGet TemplateARead-only
Get a single template by ID or slug. Returns full template including fields and HTML layout.
Inputs: id, slug.
get_themeGet ThemeARead-only
Get current theme settings including colors, fonts, and custom HTML for header/footer.
get_theme_versionGet Theme VersionBRead-only
Get a specific version of theme settings with full data.
Inputs: version (required).
get_theme_versionsGet Theme VersionsARead-only
Get the version history for theme settings. Returns list of versions with timestamps.
import_csvImport CSVB
Import content from CSV data. Each row becomes a content page. Specify which column is the title; all columns become content fields.
Inputs: csv_data (required), title_column (required), folder_path, slug_column, +2 more.
import_markdownImport MarkdownA
Import one or more Markdown pages into LightCMS. Each page can include YAML frontmatter (title, slug, folder, template, published, publish_at). Ideal for bulk content creation by AI agents — generate markdown with frontmatter and import directly.
Example frontmatter:
title: My Page slug: my-page folder: /blog template: Blog Post published: false
Page body content here.
Inputs: pages (required), auto_publish, default_folder, default_template.
list_approval_requestsList Approval RequestsARead-only
List pending approval requests. Use filter=mine to see only requests in your queue.
Inputs: filter.
list_approval_workflowsList Approval WorkflowsARead-only
List all configured approval workflows. Requires approval.manage_workflows permission.
list_asset_foldersList Asset FoldersARead-only
List all unique folder paths in the asset library.
list_assetsList AssetsBRead-only
List all assets in the asset library. Assets are files like images, documents, CSS, JS, etc.
Inputs: folder.
list_audit_logsList Audit LogsARead-only
List recent audit log entries. Admin only. Supports filtering by action and resource type.
Inputs: limit, action, resource.
list_collectionsList CollectionsARead-only
List all content collections. Collections group and display content by category.
list_commentsList CommentsBRead-only
List all discussion comments on a content item.
Inputs: content_id (required).
list_contentList ContentARead-only
List all content items with optional filters. Returns content metadata including title, path, publish status, and timestamps.
Add include_data: true to get full field data for all items in one call, or include_fields: ["field1", "field2"] to fetch only specific fields — both avoid per-item get_content calls for bulk workflows.
Up to 20 concurrent update_content calls are safe. For larger batches, prefer bulk_update_content (up to 100 items per call).
Inputs: limit, offset, category, folder_id, +4 more.
list_foldersList FoldersBRead-only
List all content folders. Folders organize content into URL path segments.
list_forksList ForksARead-only
List all content fork workspaces. Forks let you stage changes to multiple pages as a batch before merging them live.
Each fork shows its status (active/merged/archived), page count, and who created it. Use get_fork to see the specific pages in a fork.
list_import_jobsList Import JobsBRead-only
List recent import jobs with their status and results
Inputs: limit.
list_import_sourcesList Import SourcesBRead-only
List all configured RSS/Atom import sources
list_redirectsList RedirectsARead-only
List all URL redirects configured for the site.
list_scheduled_contentList Scheduled ContentARead-only
List all unpublished content items that have a scheduled publish time set.
Inputs: folder.
list_snippetsARead-only
List all snippets. Snippets are reusable Go-template HTML fragments used in lc:query index page directives.
list_templatesList TemplatesBRead-only
List all available templates. Templates define content structure with fields and HTML layout.
list_webhook_deliveriesList Webhook DeliveriesBRead-only
List recent delivery attempts for a webhook.
Inputs: id (required), limit.
list_webhooksList WebhooksARead-only
List all registered webhooks. Secrets are never returned.
merge_forkMerge ForkADestructive
Merge all pages in a fork workspace into live content. Requires admin role.
For each page in the fork:
If a matching live page exists (same URL): updates it with the fork content (fork wins). If the live page was edited after the fork was created, records a conflict but still merges.
If no live page exists at that URL: creates a new live page. It keeps the publish state it had in the fork (normally draft) unless publish_new is true, which publishes it. Pages on hold always stay drafts.
After merging, the fork status changes to "merged", its page copies are deleted, and the created/updated counts are kept on the fork record. Published live pages are regenerated immediately.
Returns: updated and created counts, created_ids and updated_ids (live page IDs), not_published (new pages publish_new left as drafts, with the reason), and any conflicts detected.
ALWAYS confirm with the user before merging, as this pushes changes to the live site.
Inputs: fork_id (required), publish_new.
pin_theme_versionPin Theme VersionAIdempotent
Lock a theme version so it is protected from automatic pruning or accidental overwrite. Pinned versions are marked with locked=true in get_theme_versions.
Use this to preserve milestone theme states (e.g., after a major redesign) before making further changes.
Example: {"version": 5}
Inputs: version (required).
preview_contentPreview ContentARead-only
Render a content item's HTML without saving or publishing. Use this to verify what a page will look like before publishing.
Also accepts optional title/data overrides to preview unsaved edits: {"id": "abc123", "data": {"body": "New text"}}
Returns rendered_html and any warnings (missing required fields, unclosed tags, unresolved placeholders).
Inputs: id (required), data, title.
publish_contentPublish ContentADestructiveIdempotent
Publish a content item, making it visible on the public site. Generates the static HTML page.
Inputs: id (required).
publish_multiplePublish MultipleADestructiveIdempotent
Publish multiple content items in a single call. Use this instead of calling publish_content in a loop.
Examples:
Publish specific pages: {"ids": ["abc123", "def456"]}
Publish all drafts at once: {"publish_all_drafts": true}
Held pages and fork copies are never published by this tool: they come back in "skipped" as {id, reason}. publish_all_drafts ignores fork copies entirely.
Returns published IDs, skipped items, and any failures.
Inputs: ids, publish_all_drafts.
purge_fork_copiesPurge Fork CopiesADestructiveIdempotent
Delete the page copies still attached to a merged or archived fork. Requires admin role.
Merges clean up after themselves since v7.4; forks merged before that kept their copies. Live pages are never touched, and the fork record is kept. Active forks are refused.
Use dry_run: true first to see the count and the list of {id, full_path}. Returns the number deleted and writes an audit log entry.
Inputs: fork_id (required), dry_run.
regenerate_all_contentRegenerate All ContentAIdempotent
Regenerate all published static HTML pages. Use after major theme or template changes.
regenerate_webhook_secretRegenerate Webhook SecretADestructive
Generate a new HMAC-SHA256 signing secret for a webhook. The old secret stops working immediately. The new secret is returned ONCE — save it.
Inputs: id (required).
reindex_embeddingsReindex EmbeddingsAIdempotent
Regenerate vector embeddings for all published content. Required after initial setup or if embeddings become stale.
reject_requestReject RequestADestructive
Reject an approval request. A comment explaining the rejection is required and will be posted to the content's discussion thread.
Inputs: id (required), comment (required).
release_content_lockRelease Content LockAIdempotent
Release the advisory lock on a content item held by the current API user.
Inputs: content_id (required).
remove_fork_pageRemove Fork PageADestructiveIdempotent
Remove a page from a fork workspace (discards the fork copy, does not affect the live page).
Inputs: fork_id (required), page_id (required).
restore_contentRestore ContentAIdempotent
Restore a soft-deleted content item. Regenerates static page if content was published.
Inputs: id (required).
revert_theme_to_versionRevert Theme to VersionADestructive
Revert theme to a previous version. Creates a new version with the old data.
Inputs: version (required), version_comment.
revert_to_versionRevert to VersionADestructive
Revert content to a previous version. Creates a new version with the old data.
Inputs: version (required), content_id (required), version_comment.
rollback_agent_sessionRollback Agent SessionADestructiveIdempotent
Undo every content change made by an agent session as a unit: content the session created is soft-deleted, deleted content is restored, and updated content reverts to its latest pre-session version. Defaults to the current MCP session ("undo everything I did"). Non-content changes (templates, settings) are listed for manual review but not auto-reverted.
Inputs: session_id.
run_maintenance_scanRun Maintenance ScanAIdempotent
Run a site-health scan now and return the fresh report. Scans also run automatically once a day.
Inputs: link_check.
schedule_content_publishSchedule Content PublishAIdempotent
Set a future publish date/time for a content item. The scheduler automatically publishes it when the time arrives.
Inputs: content_id (required), publish_at (required).
scoped_search_replace_executeScoped Search Replace ExecuteADestructive
Execute a search-and-replace limited to a subset of pages. ALWAYS run scoped_search_replace_preview first and show results to the user before executing.
Scope options (all optional):
content_ids, folder_path, template_name, category
Set auto_republish: true to immediately re-publish all previously-published pages after updating them, collapsing the execute + publish_multiple flow into one call.
Only live pages are changed: fork copies named in content_ids come back in "skipped" as {id, reason}.
Example: {"search": "old text", "replace": "new text", "folder_path": "/blog", "auto_republish": true, "version_comment": "Updated old references"}
Inputs: search (required), replace (required), regex, category, +5 more.
scoped_search_replace_previewScoped Search Replace PreviewARead-only
Preview a search-and-replace limited to a subset of pages. Safer than site-wide replacement.
Scope options (all optional — leave blank to match all pages):
content_ids: specific page IDs
folder_path: pages under /blog, /docs, etc.
template_name: pages using "Concept Page", "Blog Post", etc.
category: pages with a matching category
Only live pages are searched: fork copies named in content_ids come back in "skipped" as {id, reason}.
Example: {"search": "old text", "replace": "new text", "folder_path": "/blog"}
Always run preview before execute.
Inputs: search (required), replace (required), regex, category, +5 more.
search_contentSearch ContentBRead-only
Search across all content items by title or full text. Returns matching content with paths and match context.
Inputs: query (required), search_type, include_forks, include_deleted.
search_replace_executeSearch Replace ExecuteADestructive
Execute a site-wide search-and-replace across all content. Modifies every matching page permanently.
MANDATORY workflow:
Run search_replace_preview and show the user which pages will be affected.
Get explicit user confirmation before executing.
Run search_replace_execute with a clear version_comment.
Set auto_republish: true to immediately re-publish all previously-published pages after updating them, collapsing the execute + publish_multiple flow into one call.
For targeted replacements, use scoped_search_replace_execute instead.
Inputs: pairs, regex, search, replace, +2 more.
search_replace_previewSearch Replace PreviewARead-only
Preview a site-wide search-and-replace without making any changes. ALWAYS run this before search_replace_execute.
Returns: affected page count, total match count, and per-page field breakdown. Only live pages are searched; fork copies are never matched or rewritten. For targeted replacements (a folder, template, or category), use scoped_search_replace_preview instead.
Inputs: pairs, regex, search, replace.
set_indexnow_enabledEnable/Disable IndexNowAIdempotent
Turn automatic IndexNow submission on or off for this site (on by default).
Inputs: enabled (required).
start_agent_sandboxStart Agent SandboxA
Start a sandboxed editing session backed by a content fork ("pull request for content").
While the sandbox is active:
update_content / update_content_by_path transparently copy pages into the fork and edit the copies (copy-on-write). Live content is never modified.
create_content creates new pages inside the fork.
Publishing, deleting, bulk operations, and search/replace-execute are blocked.
The site is unaffected until a human reviews the fork at /cm/forks and merges it.
Recommended for any multi-page or risky editing task. End with end_agent_sandbox.
Inputs: name, description.
start_link_checkStart Link CheckA
Start an asynchronous broken-link check across all published content.
Returns a job_id immediately. Poll get_link_check_results(job_id) until status is "done".
Example workflow: start_link_check() → {job_id: "abc123"} get_link_check_results("abc123") → if status=="running", wait and retry; if "done", review broken_links
submit_for_approvalSubmit Content for ApprovalB
Explicitly submit a content item for editorial approval. Contributors must submit content for approval before it can be published.
Inputs: content_id (required).
submit_indexnowSubmit URLs to IndexNowA
Submit pages to IndexNow now. Changes are already submitted automatically on publish/update/delete; use this to resubmit specific paths, or all=true to submit every published page (once per hour max).
Inputs: all, paths.
trigger_import_sourceTrigger Import SourceA
Manually trigger an RSS import source to run immediately
Inputs: id (required).
unpin_theme_versionUnpin Theme VersionAIdempotent
Remove the lock from a previously pinned theme version.
Example: {"version": 5}
Inputs: version (required).
unpublish_contentUnpublish ContentADestructiveIdempotent
Unpublish a content item, removing it from the public site. Removes the static HTML page.
Inputs: id (required).
update_approval_workflowUpdate Approval WorkflowC
Update an existing approval workflow.
Inputs: id (required), mode (required), name (required), trigger (required), +3 more.
update_collectionUpdate CollectionCDestructiveIdempotent
Update a collection's settings.
Inputs: id (required), name, slug, category, +6 more.
update_contentUpdate ContentADestructiveIdempotent
Update an existing content item by ID. Creates a new version automatically. Only send fields you want to change.
For partial data updates, only the keys you include in "data" are changed — existing keys are preserved (merge semantics). Use clear_fields: ["field1", "field2"] to explicitly set fields to empty string. Set dry_run: true to validate the update without saving. To update by URL path instead of ID, use update_content_by_path. Always include version_comment so the version history is useful.
Up to 20 concurrent update_content calls are safe. For larger batches (>20 items), prefer bulk_update_content instead.
Example: {"id": "abc123", "data": {"body": "Updated text"}, "version_comment": "Revised intro paragraph"}
Content data fields support rich markup features:
[[Wikilinks]] and [[Page Title|display text]] — link to other pages by title or path; auto-update when paths change
[[include:snippet-name]] — embed a named snippet inline (reusable callouts, CTAs, disclaimers)
#hashtags — mention #tagname anywhere to automatically tag the page
Markdown fields (type "markdown") — GitHub Flavored Markdown converted to HTML at publish time Templates can use {{.lc_toc}} in their HTML layout to inject an auto-generated table of contents.
Inputs: id (required), data, hold, slug, +21 more.
update_content_by_pathUpdate Content by PathADestructiveIdempotent
Update content identified by its URL path instead of its ID. Useful when you know the page URL but not the MongoDB ID.
Example: {"path": "/about", "title": "About Us", "data": {"body": "Updated content"}}
The path always resolves to the live page, never to a fork copy (inside an agent sandbox the write goes to the sandbox copy as usual).
Only the fields you provide are changed. Always include a version_comment describing what changed.
Inputs: path (required), data, hold, tags, +9 more.
update_import_sourceUpdate Import SourceCDestructiveIdempotent
Update an RSS import source configuration
Inputs: id (required), url, name, active, +4 more.
update_redirectUpdate RedirectCDestructiveIdempotent
Update an existing redirect.
Inputs: id (required), to_path, from_path, description, +1 more.
update_seo_settingsUpdate SEO & AI SettingsAIdempotent
Change search & AI settings. Only the fields you pass change. Blocking AI training crawlers does not affect Google/Bing search.
Inputs: author_url, feed_limit, author_name, author_type, +13 more.
update_site_configUpdate Site ConfigCDestructiveIdempotent
Update site configuration. Title templates support {{title}} and {{site_name}} placeholders.
Inputs: title_template, title_template_no_title.
update_snippetA
Update an existing snippet's name and/or HTML template.
Inputs: id (required), html (required), name (required).
update_templateUpdate TemplateADestructiveIdempotent
Update an existing template. Changing the HTML layout will regenerate all content using this template.
Inputs: id (required), name, slug, fields, +3 more.
update_themeUpdate ThemeADestructiveIdempotent
Update theme settings. Only the fields you provide are changed — all other settings are preserved (partial update, safe to call without get_theme first).
Changing header_html or footer_html triggers background regeneration of all published pages. Changing colors, fonts, or custom_css does NOT require content regeneration.
Use pin_theme_version to protect important milestones before making major changes.
Inputs: logo_url, head_html, site_name, custom_css, +11 more.
update_webhookUpdate WebhookBDestructiveIdempotent
Update an existing webhook (name, URL, events, active). Partial update — only provided fields change.
Inputs: id (required), url, name, active, +1 more.
upload_assetUpload AssetA
Upload or replace an asset in the asset library. Re-uploading to the same serve_path replaces the existing file in place — no need to delete first.
Provide file content via one of:
file_path: Absolute local path to the file (preferred for files >100KB — avoids MCP transport size limits)
data_base64: Base64-encoded file content (fine for small files)
Validates file type and MIME type for security.
Inputs: filename (required), serve_path (required), file_path, data_base64, +1 more.
upload_asset_from_urlUpload Asset from URLA
Fetch a public URL and store it as a LightCMS asset. Useful for importing images or files from the web without downloading them locally first.
Example: {"url": "https://example.com/logo.png", "serve_path": "/assets/logo.png", "description": "Site logo"}
If serve_path is omitted, the filename is derived from the URL. Returns id, serve_path, mime_type, and size.
Inputs: url (required), serve_path, description.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 129 tools
Most tools target clearly distinct resources, but several clusters create real ambiguity: the fork tools (create_fork/fork_page/merge_fork) vs the sandbox tools (start_agent_sandbox/end_agent_sandbox) describe the same underlying mechanism, search_replace_execute vs scoped_search_replace_execute vs preview variants overlap, and update_content vs update_content_by_path vs bulk_update_content compete for the same job. Detailed descriptions help, but an agent still has to reason carefully about which of several near-duplicates to pick.
Names overwhelmingly follow a predictable verb_noun snake_case pattern (list_content, get_content, create_content, update_content, delete_content), with parallel families for templates, folders, collections, snippets, redirects, webhooks, and approvals. Minor deviations exist — get_theme_version (singular) alongside get_theme_versions, force_unlock_content vs acquire/release_content_lock, and import_csv/import_markdown — but they remain readable and semantically consistent.
129 tools is excessive even for a broad CMS domain, far past the 50+ 'extreme mismatch' threshold. Many operations could be consolidated (e.g., separate lock get/acquire/release/force, singular/plural version getters, three publish paths), so each tool does not clearly earn its place.
The surface is remarkably complete: full content CRUD plus versions, soft-delete/restore, drafts/publish/scheduling, locks, comments, templates/fields, folders, collections, snippets, themes with versioning, assets, webhooks, redirects, imports, approval workflows, forks/sandbox staging, SEO/IndexNow, maintenance scans, and audit logs. Almost no lifecycle gap is left uncovered.
Maintenance
Related MCP Connectors
AI-operated knowledge-graph CMS for business websites: entities, pages, blocks, media, domains.
AI-native Git-based CMS for Astro. Create and publish content in the browser, without learning git.
Build, edit and run real hosted websites from your AI - content, SEO, menus, store, rollback.
Headless CMS for sites built with AI: model content, import it, wire the site, verify.
Related MCP Servers
- AlicenseAqualityCmaintenanceSimple and free publishing of content on the web for AI Agents28,811 npmMIT
- AlicenseNot gradedqualityFmaintenanceStatic site generator / website building toolkit for AI coding agents like Claude, Codex, Cursor, Gemini, OpenClaw, etc. No subscription, no lock-in — host your site anywhere.13 npm9Elastic 2.0

NextBlock CMSofficial
AlicenseNot gradedqualityBmaintenanceOpen-source Next.js + Supabase CMS with a per-site MCP server for building and editing sites from any agent20AGPL 3.0- AlicenseNot gradedqualityBmaintenanceEnables AI clients to manage a self-hosted content hub: create, edit, publish, and update articles across multiple platforms, plus AI writing and account management.2MIT