@bettercms-ai/mcp 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @bettercms-ai/mcp
2
+
3
+ BetterCMS [Model Context Protocol](https://modelcontextprotocol.io) server. Lets an
4
+ AI assistant author your content schema and entries through the BetterCMS Management
5
+ API, authenticated with a short-lived, per-project `content:manage` key minted via
6
+ the OAuth 2.0 Device Authorization Grant (RFC 8628) — no pasted long-lived key.
7
+
8
+ ## Tools
9
+
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `create_model` | Create a content model (schema) with optional initial fields. |
13
+ | `add_field` | Append a field to an existing model (read-modify-write; never removes fields). |
14
+ | `create_entry` | Create a content entry; applies `data`/`status` via a follow-up update. |
15
+ | `create_form` / `update_form` | Author a form (fields + settings) to embed with `<BcmsForm>`. |
16
+ | `create_component` / `update_component` | Author a reusable component (blockJson tree + props) to render with `<BcmsBlocks>`. |
17
+ | `list_forms` / `get_form` / `list_components` / `get_component` | Discover forms & components to wire into site code. |
18
+
19
+ See [SKILL.md](./SKILL.md) for how an AI agent should author pages, forms, and components.
20
+
21
+ All operations are **additive** — there are no destructive tools. Deleting models,
22
+ fields, or entries is dashboard-only by design.
23
+
24
+ ## Usage
25
+
26
+ Add to your MCP client config (e.g. Claude Desktop / Claude Code):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "bettercms": {
32
+ "command": "npx",
33
+ "args": ["-y", "@bettercms-ai/mcp"],
34
+ "env": { "BETTERCMS_API_URL": "https://api.bettercms.ai" }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Registry auth (current)
41
+
42
+ This package is published to **GitHub Packages**, which requires authentication
43
+ **even for public scoped packages**. For `npx @bettercms-ai/mcp` to resolve, the
44
+ host machine needs an `~/.npmrc` with:
45
+
46
+ ```
47
+ @bettercms-ai:registry=https://npm.pkg.github.com/
48
+ //npm.pkg.github.com/:_authToken=<a GitHub PAT with read:packages>
49
+ ```
50
+
51
+ > **TODO (future):** publish `@bettercms-ai/mcp` (and `types`/`sdk`) to the public
52
+ > **npmjs.org** registry so `npx @bettercms-ai/mcp` works with zero `.npmrc` setup
53
+ > for any host/user. Until then the GitHub Packages auth above is required.
54
+
55
+ On the first tool call the server prints an authorization prompt to **stderr**
56
+ (visible in your MCP client's logs):
57
+
58
+ ```
59
+ ┌─ BetterCMS authorization required ─────────────────────────
60
+ │ Visit: https://demo.bettercms.ai/activate
61
+ │ Enter code: WDJB-MJHT
62
+ │ Or open: https://demo.bettercms.ai/activate?code=WDJB-MJHT
63
+ └────────────────────────────────────────────────────────────
64
+ ```
65
+
66
+ Approve it in the dashboard (pick a workspace/project) and the server caches the
67
+ minted token. It auto-refreshes on expiry; if the refresh token is revoked it
68
+ re-runs the device flow.
69
+
70
+ ## Environment
71
+
72
+ | Var | Default | Purpose |
73
+ |-----|---------|---------|
74
+ | `BETTERCMS_API_URL` | `https://api.bettercms.ai` | API origin (device-auth + Management API). |
75
+ | `BETTERCMS_MCP_CREDENTIALS` | `~/.bettercms/mcp-credentials.json` | Token cache path (0600). |
76
+ | `BETTERCMS_MCP_CLIENT_NAME` | `BetterCMS MCP` | Label shown on the approval screen. |
77
+
78
+ ## Develop
79
+
80
+ ```bash
81
+ bun install
82
+ bun run --filter @bettercms-ai/mcp typecheck
83
+ bun run --filter @bettercms-ai/mcp test
84
+ bun run --filter @bettercms-ai/mcp build
85
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,65 @@
1
+ # BetterCMS MCP — authoring skill
2
+
3
+ How an AI agent should author content, schema, **forms**, and **components** in
4
+ BetterCMS through this MCP. The MCP is bound to ONE project (the API key's project);
5
+ everything you create lands there.
6
+
7
+ The guided prompts ship with the server — prefer them: `studio` (router),
8
+ `propose_schema`, `new_page`, `new_form`, `new_component`. This file is the reference
9
+ behind them.
10
+
11
+ ## Golden rules
12
+ - **Confirm before creating.** Propose the shape (fields / block tree) and get the
13
+ user's approval (AskUserQuestion) before any create/update. Never silently guess.
14
+ - **Read before you edit.** Use the `list_*` / `get_*` tools first so you keep existing
15
+ keys/blocks and don't duplicate.
16
+ - **Additive where possible.** `add_field`/`add_page_field` append; passing `fields`
17
+ to `update_form` or `blockJson` to `update_component` REPLACES — include everything
18
+ you want to keep.
19
+
20
+ ## Pages & schema
21
+ Design the project's schema from its code — the connected repo, or, for an uploaded
22
+ project, its source in your working directory. Destructure each page into a tree:
23
+ plain fields, **group** (one nested object), **repeater** (a repeatable array). Then
24
+ `create_page` per page. (See the `propose_schema` prompt.) Pages created here show up
25
+ in the dashboard Content tab.
26
+
27
+ ## Forms
28
+ Tools: `list_forms`, `get_form`, `create_form`, `update_form`.
29
+
30
+ 1. Collect the fields with the user. Field: `{ key, label, type, required?, placeholder?,
31
+ options?, defaultValue?, showIf? }`. Types: `text, email, textarea, select` (needs
32
+ `options`), `checkbox, number, phone, date, url, consent, hidden`. `showIf:
33
+ { field, equals }` shows a field conditionally.
34
+ 2. Settings: `name` (the handle for `getForm('Name')`), `submitLabel?`, `successMessage?`,
35
+ `redirectUrl?`.
36
+ 3. Confirm → `create_form { name, fields, ... }` (returns the id) or
37
+ `update_form { formId, ... }`.
38
+ 4. **Wire it into the site**: in a `@bettercms-ai/next` site,
39
+ `import { getForm } from "@bettercms-ai/next"` then render
40
+ `<BcmsForm form={await getForm("Name")} />` where the user wants it.
41
+
42
+ ## Components (reusable Webflow-style symbols)
43
+ Tools: `list_components`, `get_component`, `create_component`, `update_component`.
44
+
45
+ 1. Design the `blockJson` tree — an array of blocks `{ type, id, props }`. Types:
46
+ `heading {text, level}`, `text {text}`, `image {src, alt}`, `button {text, href}`,
47
+ `spacer {height}`, `video {url}`, and `columns {columns: block[][], gap}` which nests
48
+ child blocks. Every block needs a stable unique `id`.
49
+ 2. Optional `props` — overridable fields `{ key, label, target: { blockId, path }, type,
50
+ defaultValue? }` (type: `text|richtext|image|url|boolean`) so each instance can be
51
+ customized.
52
+ 3. Confirm the structure → `create_component { name, slug, category?, blockJson, props? }`
53
+ (returns the id) or `update_component { componentId, ... }`. **Updating a component
54
+ re-bakes every page that embeds it.**
55
+ 4. **Render it**: `<BcmsBlocks blocks={component.blockJson} components={...} />` from
56
+ `@bettercms-ai/next`.
57
+
58
+ Authoring `blockJson` blind is error-prone — keep trees small, confirm with the user,
59
+ and read back with `get_component` to verify.
60
+
61
+ ## Errors
62
+ - `401/403` → the MCP key needs (re)authorizing (re-run "Connect your AI").
63
+ - `409 slug_taken` → offer a different slug.
64
+ - `create_form`/`create_component` missing → the host has a stale cached MCP; tell the
65
+ user to `rm -rf ~/.npm/_npx` and restart, or that their connector predates this skill.
@@ -0,0 +1,132 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ /**
4
+ * Resolved configuration for the BetterCMS MCP server.
5
+ *
6
+ * A single `BETTERCMS_API_URL` (origin, no path) drives both the device-auth
7
+ * endpoints and the Management API base the SDK targets:
8
+ * device: {apiUrl}/api/v1/auth/device/*
9
+ * management: {apiUrl}/api/v1 (SDK appends /management/content/*)
10
+ */
11
+ interface McpConfig {
12
+ apiUrl: string;
13
+ deviceBaseUrl: string;
14
+ managementBaseUrl: string;
15
+ credentialsPath: string;
16
+ clientName: string;
17
+ }
18
+ declare function loadConfig(env?: NodeJS.ProcessEnv): McpConfig;
19
+
20
+ /** Credentials cached between runs so the device flow runs only once per env. */
21
+ interface StoredCredentials {
22
+ accessToken: string;
23
+ refreshToken: string;
24
+ /** Epoch ms when the access token expires. */
25
+ accessTokenExpiresAt: number;
26
+ workspaceId: string | null;
27
+ projectId: string | null;
28
+ }
29
+ /**
30
+ * An authorization the user has been sent off to approve but hasn't yet.
31
+ * Persisted so a later tool call can *resume* polling that same code instead of
32
+ * minting a fresh one — this is what lets the flow survive across the
33
+ * "return the link → user approves → retry" round-trip in clients (VS Code)
34
+ * that never surface the server's stderr prompt.
35
+ */
36
+ interface PendingDevice {
37
+ deviceCode: string;
38
+ userCode: string;
39
+ verificationUri: string;
40
+ /** verification_uri with `?code=` prefilled — the link we hand the user. */
41
+ verificationUriComplete: string;
42
+ intervalSeconds: number;
43
+ /** Epoch ms when the device code expires. */
44
+ expiresAt: number;
45
+ }
46
+ /** Persistence boundary for credentials (file-backed in prod, in-memory in tests). */
47
+ interface TokenStore {
48
+ read(): Promise<StoredCredentials | null>;
49
+ write(creds: StoredCredentials): Promise<void>;
50
+ clear(): Promise<void>;
51
+ /** In-progress device authorization awaiting approval, if any. */
52
+ readPending(): Promise<PendingDevice | null>;
53
+ writePending(pending: PendingDevice): Promise<void>;
54
+ clearPending(): Promise<void>;
55
+ }
56
+
57
+ /** Injectable seams so tests can run without real timers / network / stderr. */
58
+ interface DeviceAuthDeps {
59
+ fetch?: typeof fetch;
60
+ sleep?: (ms: number) => Promise<void>;
61
+ log?: (message: string) => void;
62
+ now?: () => number;
63
+ }
64
+ /**
65
+ * Drives the OAuth 2.0 Device Authorization Grant (RFC 8628) against the
66
+ * BetterCMS backend and hands the SDK a valid `content:manage` access token.
67
+ *
68
+ * - `getAccessToken()` returns a usable token: cached if fresh, refreshed if
69
+ * expired, or freshly minted via the full device flow if there's nothing valid.
70
+ * - All human-facing output goes to stderr — stdout is the MCP JSON-RPC channel.
71
+ */
72
+ declare class DeviceAuthClient {
73
+ private readonly config;
74
+ private readonly store;
75
+ private readonly fetchImpl;
76
+ private readonly sleep;
77
+ private readonly log;
78
+ private readonly now;
79
+ private inFlight;
80
+ private refreshInFlight;
81
+ constructor(config: McpConfig, store: TokenStore, deps?: DeviceAuthDeps);
82
+ /** Return a valid access token, doing the least work necessary. Single-flighted. */
83
+ getAccessToken(): Promise<string>;
84
+ private resolveToken;
85
+ /**
86
+ * Resume a still-live authorization if one is persisted, otherwise start a
87
+ * fresh one; then grace-poll. Throws {@link DeviceAuthPendingError} (carrying
88
+ * the activation link) if the user hasn't approved within the grace window.
89
+ */
90
+ private runDeviceFlow;
91
+ /** Request a fresh device code, persist it as pending, and log a breadcrumb. */
92
+ private startDeviceFlow;
93
+ /**
94
+ * Poll the token endpoint until `deadline`. Returns the access token on
95
+ * approval, or null if the deadline passes while still pending. Throws
96
+ * {@link DeviceAuthError} on a terminal outcome (denied / expired).
97
+ */
98
+ private pollForApproval;
99
+ /**
100
+ * Exchange the stored refresh token for a new access token. Single-flighted:
101
+ * the device `/refresh` endpoint is single-use (it rotates the refresh token
102
+ * and revokes the prior access key), so a burst of concurrent 401s must NOT
103
+ * each fire their own refresh — the first would rotate, and the rest would
104
+ * send the now-stale token, get `invalid_grant`, and wipe the freshly-minted
105
+ * credentials. Collapsing them into one in-flight rotation keeps the session
106
+ * alive without a needless re-auth.
107
+ */
108
+ refresh(): Promise<string | null>;
109
+ /**
110
+ * Forget the cached credentials and start a fresh device flow. Called when the
111
+ * bound project was deleted server-side (a key bound to a dead project can never
112
+ * succeed again) — clearing lets the user re-authorize against a LIVE project.
113
+ * Returns a new token if approval is fast, else throws {@link DeviceAuthPendingError}
114
+ * carrying the activation link (the next tool call resumes into the new project).
115
+ */
116
+ resetAndReauthorize(): Promise<string>;
117
+ private doRefresh;
118
+ private persist;
119
+ }
120
+
121
+ interface BuildServerDeps {
122
+ auth: DeviceAuthClient;
123
+ managementBaseUrl: string;
124
+ }
125
+ /**
126
+ * Build the BetterCMS MCP server with its tools registered. Auth is lazy — the
127
+ * device flow runs on the first tool call, not at connect time, so the MCP
128
+ * handshake/tool-listing never blocks on user approval.
129
+ */
130
+ declare function buildServer(deps: BuildServerDeps): McpServer;
131
+
132
+ export { DeviceAuthClient, buildServer, loadConfig };