Now with AI-powered page building via MCP Server

AI features

Every AI action in cmssy is one permission-gated tool definition, shared by the MCP server and the in-app assistant.

cmssy has two ways to let an AI act on your content: the MCP server, which external clients like Claude connect to, and the in-app assistant inside the cmssy dashboard.

They are not two implementations. Both are transports over the same tool core - so a capability added once appears in both, with the same behaviour and the same permission checks.

What a tool is

A tool is a plain object. Here is a real one, in full:

import { z } from "zod";
import type { AiTool, PageSummary } from "../types.js";

const inputSchema = z.object({
  search: z
    .string()
    .optional()
    .describe("Optional text to filter pages by name or slug"),
});

export const listPagesTool: AiTool<Input, Output> = {
  name: "list_pages",
  description:
    "List the workspace's pages (id, name, slug, published), optionally filtered by a search string.",
  inputSchema,
  requiredPermissions: ["pages:view"],
  execute: async ({ search }, ops) => {
    const items = await ops.pages.list(search);
    return { count: items.length, items };
  },
};

Five fields, and each one earns its place:

  • name - what the model calls.
  • description - what the model reads to decide whether to call it. This is prompt surface, not a code comment; a vague description produces a tool nobody uses correctly.
  • inputSchema - a zod schema. It validates the call and doubles as the JSON Schema the client sees, so the two can never drift apart.
  • requiredPermissions - checked before execute runs.
  • execute - the actual work, taking validated input and an ops object.

Why ops is injected

execute never imports a client, opens a connection or reads an environment variable. It receives ops and calls methods on it.

That single choice is what makes the core transport-neutral. The MCP server supplies an ops backed by an API token; the in-app assistant supplies one backed by the signed-in user's session. The tool cannot tell the difference, and neither can be given capabilities the other lacks by accident.

Permissions are enforced here, not at the edge

requiredPermissions lives on the tool, so the check happens in one place regardless of who is calling.

This matters because the two transports authenticate differently. An MCP client presents a token with scopes; the assistant acts as a logged-in member with a role. If each transport carried its own permission logic, they would drift - and the drift would be a privilege escalation, not a rendering bug.

The practical consequence: an AI client can never do more than the credential behind it. Point Claude at a read-only token and it can list and read; it will refuse to publish, because the tool refuses, not because the model was asked nicely.

What the tools cover

The registry is broad - pages and blocks, models and records, media and folders, forms and submissions, members and roles, webhooks, and the full commerce surface of products, carts, orders, discounts and order pipelines.

The shape is consistent: list_* and get_* to read, create_* / update_* / delete_* to write, plus verbs for state transitions like publish_page, unpublish_page, revert_to_published and promote_dev_draft.

Nothing in the set touches your code. There is no tool that writes a file, edits a component or opens a pull request - AI edits content, and block schemas stay in your repository under review.

Dev drafts

One parameter and one tool exist for a specific problem: trying a block type that is not deployed yet.

Write tools accept a target of "devDraft", which edits your personal overlay rather than the shared page draft. Nobody else's preview changes. When the block ships, promote_dev_draft moves your overlay onto the shared draft.

Without this, experimenting with an undeployed block would mean putting content on a shared draft that renders as nothing for every colleague.

Next steps