Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP (Model Context Protocol)

Crap CMS includes a built-in MCP server that lets AI assistants (Claude Desktop, Cursor, VS Code extensions, custom agents) interact with your CMS content and schema.

The MCP server auto-generates tool definitions from your Lua-defined collections and globals. Any CMS instance automatically gets a full MCP API matching its schema.

Configuration

Add an [mcp] section to crap.toml:

[mcp]
enabled = true              # Enable MCP server (default: false)
http = false                # Enable HTTP transport on /mcp (default: false)
config_tools = false        # Enable config generation tools (default: false)
api_key = ""                # API key for HTTP auth (required when http = true)
include_collections = []    # Whitelist (empty = all)
exclude_collections = []    # Blacklist (takes precedence over include)

Transports

stdio (default)

Run the MCP server as a subprocess that reads JSON-RPC from stdin and writes to stdout:

crap-cms mcp

Or from outside the config directory:

crap-cms mcp -C /path/to/config

For Claude Desktop, add to your claude_desktop_config.json:

{
  "mcpServers": {
    "my-cms": {
      "command": "crap-cms",
      "args": ["mcp", "-C", "/path/to/config"]
    }
  }
}

HTTP

When mcp.http = true, the admin server exposes a POST /mcp endpoint. Send JSON-RPC 2.0 requests as the request body.

An api_key is required when HTTP transport is enabled. Requests must include an Authorization: Bearer <key> header. The server will refuse to start if mcp.http = true and api_key is empty.

Auto-Generated Tools

Content CRUD (per collection)

For each collection (e.g., posts), five tools are generated:

ToolDescription
find_postsQuery documents with filters, ordering, pagination
find_by_id_postsGet a single document by ID
create_postsCreate a new document
update_postsUpdate an existing document
delete_postsDelete a document

Input schemas are generated from your field definitions. Required fields, select options, and relationship types are all reflected in the JSON Schema.

Global CRUD (per global)

For each global (e.g., settings):

ToolDescription
global_read_settingsRead the global document
global_update_settingsUpdate the global document

Schema Introspection

Always available:

ToolDescription
list_collectionsList all collections with their labels and capabilities
describe_collectionGet full field schema for a collection or global
list_field_typesList all field types with descriptions and capabilities
cli_referenceGet CLI command reference (all or specific command)

Config Generation Tools (opt-in)

When config_tools = true:

ToolDescription
read_config_fileRead a file from the config directory
write_config_fileWrite a Lua file to the config directory
list_config_filesList files in the config directory

These are opt-in because they allow writing to the filesystem.

MCP Descriptions

Add optional mcp tables to your Lua definitions to provide context for AI assistants:

Collection level

return {
  slug = "posts",
  mcp = {
    description = "Blog posts with title, content, and author relationship",
  },
  fields = { ... }
}

Field level

crap.fields.select({
  name = "status",
  mcp = {
    description = "Publication status - controls visibility on the frontend",
  },
  options = { ... },
})

If no mcp.description is set, the tool falls back to admin.description (for fields) or a generated description based on the collection label.

Collection Filtering

Use include_collections and exclude_collections to control which collections are exposed via MCP:

[mcp]
enabled = true
exclude_collections = ["users"]  # Hide sensitive collections

exclude_collections takes precedence when a collection appears in both lists.

Security & Access Model

MCP operates with full access — collection-level and field-level access control functions are not applied. This is by design: MCP is a machine-to-machine API surface (equivalent to Lua’s overrideAccess = true), gated by transport-level authentication:

  • stdio: Access is controlled by who can run the process.
  • HTTP: Access is controlled by the api_key setting. An API key is required when http = true — the server will refuse to start without one. As a defense-in-depth measure, the HTTP endpoint also rejects all requests if the API key is somehow empty at runtime.

To restrict which collections are accessible, use include_collections / exclude_collections. These filters are enforced both in tool listing (tools/list) and at execution time, so knowing a collection slug is not enough to bypass the filter.

All MCP write operations (create, update, delete) are logged at info level for audit purposes. Hooks still fire on all MCP writes (same lifecycle as admin/gRPC).

Resources

The MCP server also exposes read-only resources:

URIDescription
crap://schema/collectionsFull schema of all collections as JSON
crap://schema/globalsFull schema of all globals as JSON
crap://configCurrent configuration (secrets sanitized: auth.secret, email.smtp_pass, mcp.api_key)

Query Parameters

The find_* tools accept these parameters:

ParameterTypeDescription
whereobjectFilter conditions (same syntax as gRPC/Lua API)
order_bystringSort field (prefix with - for descending, e.g., "-created_at")
limitintegerMax results per page
pageintegerPage number, 1-indexed (page mode only)
after_cursorstringForward cursor (cursor mode only, mutually exclusive with page and before_cursor)
before_cursorstringBackward cursor (cursor mode only, mutually exclusive with page and after_cursor)
depthintegerRelationship population depth
searchstringFull-text search query

Response Format

find_* tools return a JSON object with docs and pagination:

{
  "docs": [
    { "id": "abc123", "title": "Hello World", "created_at": "2026-01-15T09:00:00Z" }
  ],
  "pagination": {
    "totalDocs": 25,
    "limit": 10,
    "hasNextPage": true,
    "hasPrevPage": false,
    "totalPages": 3,
    "page": 1,
    "pageStart": 1,
    "nextPage": 2
  }
}

In cursor mode, page/totalPages/pageStart/nextPage/prevPage are replaced by startCursor/endCursor.

Where clause example

{
  "name": "find_posts",
  "arguments": {
    "where": {
      "status": { "equals": "published" },
      "created_at": { "greater_than": "2024-01-01" }
    },
    "order_by": "-created_at",
    "limit": 10
  }
}

Supported operators: equals, not_equals, greater_than, greater_than_equal, less_than, less_than_equal, like, contains, in (array), not_in (array), exists, not_exists.

Note: MCP uses shortened operator names (greater_than_equal, less_than_equal) compared to the gRPC/Lua API which uses greater_than_or_equal and less_than_or_equal.