@latellu/atlas-agent-skills 0.2.0 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/atlas-cms/SKILL.md +13 -2
- package/skills/atlas-cms/references/authoring.md +125 -0
- package/skills/atlas-cms/references/mcp.md +12 -0
- package/skills/atlas-cms/references/sdk-delivery.md +15 -6
- package/skills/atlas-cms/references/sdk-management.md +14 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atlas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Work with Atlas CMS from Claude Code: bundles the Atlas MCP server (read/write content via tools) and the atlas-cms skill that teaches agents when and how to use the MCP tools, the @latellu/atlas-sdk delivery/management clients, and the @latellu/atlas-cli type generator.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Latellu",
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Then point your agent at the skill (via `AGENTS.md`, Cursor rules, etc.) and reg
|
|
|
33
33
|
| Path | Purpose |
|
|
34
34
|
| --- | --- |
|
|
35
35
|
| `skills/atlas-cms/SKILL.md` | Router: shared Atlas concepts + decision table (MCP vs SDK vs CLI) |
|
|
36
|
-
| `skills/atlas-cms/references/` | Per-surface guides: MCP tools, SDK delivery, SDK management, CLI typegen |
|
|
36
|
+
| `skills/atlas-cms/references/` | Per-surface guides: MCP tools, SDK delivery, SDK management, CLI typegen, content authoring (field formats, translations, blocks) |
|
|
37
37
|
| `.mcp.json` | Atlas MCP server config (used by the Claude Code plugin) |
|
|
38
38
|
| `.claude-plugin/` | Claude Code plugin + marketplace manifests |
|
|
39
39
|
| `adapters/other-agents.md` | Setup for non-Claude agents |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latellu/atlas-agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent skill + MCP configuration for Atlas CMS. Works as a Claude Code plugin and as plain markdown guidance for any AI coding agent (Cursor, Codex, opencode, Gemini CLI, ...).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Latellu (https://github.com/tenriajeng)",
|
|
@@ -75,6 +75,7 @@ is `-1` in cursor mode).
|
|
|
75
75
|
| Wanting typed content interfaces for the SDK | CLI type generator | [references/cli-typegen.md](references/cli-typegen.md) |
|
|
76
76
|
| Writing content from your own code (scripts, migrations, backends) | SDK management client | [references/sdk-management.md](references/sdk-management.md) |
|
|
77
77
|
| Operating content via MCP tools in this session (`list_entries`, `create_entry`, ...) | Atlas MCP server | [references/mcp.md](references/mcp.md) |
|
|
78
|
+
| Composing any `data` payload — field value formats, translations, page blocks | Authoring guide | [references/authoring.md](references/authoring.md) |
|
|
78
79
|
|
|
79
80
|
Do **not** read references you don't need. If a task spans surfaces (e.g. "generate
|
|
80
81
|
types, then build the page"), route each sub-task to its own single reference.
|
|
@@ -82,8 +83,18 @@ types, then build the page"), route each sub-task to its own single reference.
|
|
|
82
83
|
Choosing between MCP and the management SDK for writes: MCP is for operating content
|
|
83
84
|
interactively in an agent session (one-off edits, content entry, audits); the SDK is for
|
|
84
85
|
code that outlives the session (migrations, sync jobs, backends) and for anything MCP
|
|
85
|
-
lacks (bulk, scheduling, page unpublish/archive, media delete,
|
|
86
|
-
errors).
|
|
86
|
+
lacks (bulk, scheduling, page unpublish/archive, media delete, **writing translations**,
|
|
87
|
+
per-field validation errors).
|
|
88
|
+
|
|
89
|
+
## Hard capability boundaries (don't go looking for these)
|
|
90
|
+
|
|
91
|
+
- **Schema is dashboard-only.** No API, SDK, CLI, or MCP surface creates content types,
|
|
92
|
+
fields, or block types. "Create a content type" = ask the user to do it in the
|
|
93
|
+
dashboard, then continue.
|
|
94
|
+
- **No field-value queries.** The public list endpoints filter by `type`/`locale` only —
|
|
95
|
+
"all articles where category = X" means paging through and filtering client-side.
|
|
96
|
+
- **The management plane is write-only** (no GET endpoints). Reading always needs a
|
|
97
|
+
live key; a write-then-verify flow therefore needs both keys.
|
|
87
98
|
|
|
88
99
|
Deep API detail lives at https://docs.atlas.latellu.com — link there rather than
|
|
89
100
|
duplicating endpoint documentation into this skill.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Authoring valid content (field formats, translations, page blocks)
|
|
2
|
+
|
|
3
|
+
What actually gets validated on write, what each field type's value must look like, and
|
|
4
|
+
how to write localized content. Applies to both the management SDK and the MCP write
|
|
5
|
+
tools — read this before composing any `data` payload.
|
|
6
|
+
|
|
7
|
+
## What the API validates (and what it silently accepts)
|
|
8
|
+
|
|
9
|
+
On entry create/update the backend enforces exactly four rules:
|
|
10
|
+
|
|
11
|
+
1. `required` — the field must be present and non-empty. For **richtext**, the Tiptap
|
|
12
|
+
empty states `""`, `"<p></p>"`, `"<p><br></p>"` count as empty.
|
|
13
|
+
2. `unique` — value must not collide with another entry of the workspace.
|
|
14
|
+
3. `relation` fields — value must be a valid **entry UUID** (or array of UUIDs) that
|
|
15
|
+
exists in the workspace. Slugs are rejected.
|
|
16
|
+
4. `image` fields — value must be a valid **media UUID** (or array) that exists in the
|
|
17
|
+
workspace. Upload first (`media.upload` / `upload_media`), then reference the
|
|
18
|
+
returned `id`.
|
|
19
|
+
|
|
20
|
+
**Everything else is stored as-is.** A number in a text field, a garbage date, a select
|
|
21
|
+
value outside the options — none of it is rejected. Validation failures you don't get
|
|
22
|
+
from the API become rendering bugs in the dashboard/frontend, so discipline comes from
|
|
23
|
+
you: fetch the schema first and match each field's type exactly.
|
|
24
|
+
|
|
25
|
+
## Value format per field type
|
|
26
|
+
|
|
27
|
+
| Field type | Write this | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `text`, `textarea` | plain string | |
|
|
30
|
+
| `richtext` | **Tiptap HTML string**, e.g. `"<p>Hello <strong>world</strong></p>"` | not markdown, not JSON; the dashboard editor is Tiptap |
|
|
31
|
+
| `number` / `boolean` | JSON number / boolean | not validated — don't send strings |
|
|
32
|
+
| `date` | ISO 8601 string, `"2026-08-01"` (or full timestamp) | not format-validated; ISO keeps the dashboard picker working |
|
|
33
|
+
| `select` | one of the schema's `options`, exact string | not validated — off-list values break the generated literal-union types |
|
|
34
|
+
| `image` | media UUID string, or array of them | validated against workspace media |
|
|
35
|
+
| `relation`, `content_type_reference` | entry UUID string, or array | validated against workspace entries; **UUID, not slug** |
|
|
36
|
+
|
|
37
|
+
Reading back: the value is whatever was stored. Content created through this API holds
|
|
38
|
+
UUIDs for image/relation; older or imported datasets may hold URLs/slugs — when
|
|
39
|
+
rendering, branch on the shape (e.g. `value.startsWith("http")` → use directly,
|
|
40
|
+
otherwise resolve via `media.get(id)`).
|
|
41
|
+
|
|
42
|
+
## Writing translations (localized entries)
|
|
43
|
+
|
|
44
|
+
Entry create/update accept a `translations` map alongside `data`:
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
{
|
|
48
|
+
"slug": "hello",
|
|
49
|
+
"data": { "title": "Hello", "views": 10 }, // base locale
|
|
50
|
+
"translations": {
|
|
51
|
+
"de": { "data": { "title": "Hallo" } }, // only localizable fields
|
|
52
|
+
"ja": { "data": { "title": "こんにちは" } }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Rules that will bite you if unknown:
|
|
58
|
+
|
|
59
|
+
- **A `required` + `localizable` field is validated against `translations`, not
|
|
60
|
+
`data`** — it must have a non-empty value in at least one translation locale, or the
|
|
61
|
+
write fails with "required localizable field ... is missing".
|
|
62
|
+
- Translation `data` should contain **only localizable fields**; non-localizable values
|
|
63
|
+
live in base `data` and fall back automatically on read.
|
|
64
|
+
- **Via the management SDK**: pass `translations` as an extra key on
|
|
65
|
+
`CreateEntryInput`/`UpdateEntryInput` (the input types accept extra keys):
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await client.entries("article").update("hello", {
|
|
69
|
+
data: fullBaseData, // full replace — include everything
|
|
70
|
+
translations: { de: { data: { title: "Hallo" } } }, // also a full replace per locale
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Via MCP: not possible.** The `create_entry`/`update_entry` tools forward only
|
|
75
|
+
`slug` and `data` — a `translations` key is dropped. Translating content needs the
|
|
76
|
+
SDK or the dashboard; say so instead of trying.
|
|
77
|
+
|
|
78
|
+
## Writing pages and blocks
|
|
79
|
+
|
|
80
|
+
The real write shape (`POST/PUT /pages`) differs from what the reading API shows:
|
|
81
|
+
|
|
82
|
+
```jsonc
|
|
83
|
+
{
|
|
84
|
+
"slug": "landing", // required on create; there is NO "title" field —
|
|
85
|
+
"seo": { // the page's title lives in seo.title
|
|
86
|
+
"title": "Landing",
|
|
87
|
+
"description": "...",
|
|
88
|
+
"keywords": ["a", "b"], // array of strings
|
|
89
|
+
"og_image": "https://...",
|
|
90
|
+
"canonical": "https://..."
|
|
91
|
+
},
|
|
92
|
+
"seo_translations": { "de": { "title": "Landung", ... } },
|
|
93
|
+
"blocks": [
|
|
94
|
+
{
|
|
95
|
+
"block_type_id": "<UUID>", // required per block — see warning below
|
|
96
|
+
"parent_id": null, // block UUID for nesting, or null
|
|
97
|
+
"position": 0,
|
|
98
|
+
"data": { "...": "block fields" },
|
|
99
|
+
"translations": { "de": { "data": { "...": "..." } } }
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `update` with `blocks` replaces the block list; reordering alone has a dedicated
|
|
106
|
+
endpoint (`blocksReorder(slug, ids)` in the SDK).
|
|
107
|
+
- **`block_type_id` is effectively dashboard-only knowledge.** The public read API
|
|
108
|
+
returns each block's type *name* and its instance id — not its `block_type_id` — and
|
|
109
|
+
the management plane has no read endpoints at all. To compose new blocks
|
|
110
|
+
programmatically you must be given the block-type UUIDs (from the dashboard). If you
|
|
111
|
+
don't have them, structure the content as **entries** instead (fully writable blind)
|
|
112
|
+
and keep pages for dashboard-managed layout.
|
|
113
|
+
- MCP `create_page`/`update_page` extra caveats: the `title` argument is ignored by the
|
|
114
|
+
backend (set `seo.title`), `seo` there accepts only `title`/`description`, and block
|
|
115
|
+
`translations`/`seo_translations` aren't expressible.
|
|
116
|
+
|
|
117
|
+
## Practical write checklist
|
|
118
|
+
|
|
119
|
+
1. `get_content_type` / schema → exact field names, types, required, localizable, select options.
|
|
120
|
+
2. Uploads first → collect media UUIDs.
|
|
121
|
+
3. Compose `data` per the table above; localizable required fields go in `translations`.
|
|
122
|
+
4. Create (draft) → read back → verify → publish.
|
|
123
|
+
5. On `VALIDATION_ERROR` via MCP (no field detail): re-check step 1-3 against this file
|
|
124
|
+
before retrying — the usual culprits are slug-instead-of-UUID in relation/image,
|
|
125
|
+
markdown in richtext, and required-localizable fields sitting in `data`.
|
|
@@ -74,6 +74,10 @@ errors).
|
|
|
74
74
|
These exist in the backend/SDK but have **no MCP tool**; say so and point to the
|
|
75
75
|
dashboard or the management SDK (`sdk-management.md`) rather than improvising:
|
|
76
76
|
|
|
77
|
+
- **Translations cannot be written.** `create_entry`/`update_entry` forward only
|
|
78
|
+
`slug`/`data` — a `translations` key is dropped. "Translate this entry" is an SDK or
|
|
79
|
+
dashboard job (see `authoring.md`), and note that a required+localizable field can
|
|
80
|
+
make MCP creates fail outright (its value must be in `translations`).
|
|
77
81
|
- Page lifecycle beyond publish: **no `unpublish_page`, `archive_page`,
|
|
78
82
|
`duplicate_page`**.
|
|
79
83
|
- **No** bulk operations, entry/page **scheduling**, or block **reorder** tools.
|
|
@@ -81,6 +85,14 @@ dashboard or the management SDK (`sdk-management.md`) rather than improvising:
|
|
|
81
85
|
only through MCP.
|
|
82
86
|
- Pagination is offset-only (`page`/`limit`); there is no cursor option, so listings
|
|
83
87
|
past page 1000 (`PAGE_TOO_DEEP`) are unreachable via MCP.
|
|
88
|
+
- Composing page **blocks** needs `block_type_id` UUIDs that no read surface exposes
|
|
89
|
+
(dashboard-only knowledge) — prefer entries for agent-authored content. Also:
|
|
90
|
+
`create_page`'s `title` argument is ignored by the backend (the title is `seo.title`),
|
|
91
|
+
and its `seo` accepts only `title`/`description`.
|
|
92
|
+
|
|
93
|
+
Before composing any `data` payload, read `authoring.md` — it defines the exact value
|
|
94
|
+
format per field type (richtext = Tiptap HTML, relation/image = UUIDs, etc.), which
|
|
95
|
+
matters double here because MCP swallows per-field validation errors.
|
|
84
96
|
|
|
85
97
|
## Gotchas
|
|
86
98
|
|
|
@@ -155,17 +155,26 @@ export function BlockRenderer({ blocks }: { blocks: AtlasBlock[] }) {
|
|
|
155
155
|
|
|
156
156
|
## Resolving reference fields
|
|
157
157
|
|
|
158
|
-
`image` / `relation` / `content_type_reference`
|
|
159
|
-
|
|
158
|
+
`image` / `relation` / `content_type_reference` values are raw strings, never resolved
|
|
159
|
+
objects. Content written through the API holds **UUIDs** (that's what the backend
|
|
160
|
+
validates); imported/seeded datasets may hold URLs instead — branch on the shape:
|
|
160
161
|
|
|
161
162
|
```ts
|
|
162
163
|
const article = await atlas.entries("article").get(slug);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
|
|
165
|
+
const cover = article.data.cover_image.startsWith("http")
|
|
166
|
+
? article.data.cover_image // legacy/imported: direct URL
|
|
167
|
+
: await atlas.media.get(article.data.cover_image); // API-written: media UUID
|
|
168
|
+
|
|
169
|
+
// relation fields hold entry UUIDs; the public API fetches entries by SLUG only,
|
|
170
|
+
// so resolve relations by listing the target type and matching on id:
|
|
171
|
+
const authors = await atlas.entries("author").list({ limit: 100 });
|
|
172
|
+
const author = authors.items.find((a) => a.id === article.data.author);
|
|
167
173
|
```
|
|
168
174
|
|
|
175
|
+
Also note: list endpoints have **no field-value filters** (only type/locale/page/sort) —
|
|
176
|
+
"entries where category = X" means paging + filtering client-side.
|
|
177
|
+
|
|
169
178
|
## Limits to remember
|
|
170
179
|
|
|
171
180
|
- Delivery keys see **published content only**; draft preview is not yet supported by
|
|
@@ -32,10 +32,19 @@ const w = client.entries("news-article");
|
|
|
32
32
|
await w.create({ slug: "hello", data: { title: "Hello", body: "..." } });
|
|
33
33
|
// CreateEntryInput = { slug, data, ... } → creates a DRAFT
|
|
34
34
|
// POST /content-types/:type/entries
|
|
35
|
+
// Field value formats (richtext = Tiptap HTML, relation/image = UUIDs, ...):
|
|
36
|
+
// see authoring.md before composing `data`.
|
|
35
37
|
|
|
36
38
|
await w.update("hello", { data: { title: "Hello v2", body: "..." } });
|
|
37
39
|
// UpdateEntryInput = { slug?, data?, ... } — send the FULL data object you want stored
|
|
38
40
|
|
|
41
|
+
// Localized content: pass a `translations` map as an extra key (accepted by the API
|
|
42
|
+
// on both create and update; required+localizable fields MUST live here, not in data):
|
|
43
|
+
await w.update("hello", {
|
|
44
|
+
data: fullBaseData,
|
|
45
|
+
translations: { de: { data: { title: "Hallo" } }, ja: { data: { title: "こんにちは" } } },
|
|
46
|
+
});
|
|
47
|
+
|
|
39
48
|
await w.publish("hello"); // PATCH .../publish
|
|
40
49
|
await w.unpublish("hello"); // PATCH .../unpublish
|
|
41
50
|
await w.archive("hello"); // PATCH .../archive
|
|
@@ -53,8 +62,11 @@ All idOrSlug parameters accept either the entry ID or its slug.
|
|
|
53
62
|
## Pages
|
|
54
63
|
|
|
55
64
|
```ts
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
// NOTE: pages have NO top-level title — the title lives in seo.title.
|
|
66
|
+
// Block objects need a block_type_id UUID; see authoring.md for the full write shape
|
|
67
|
+
// (seo_translations, block translations, nesting) and the block_type_id caveat.
|
|
68
|
+
await client.pages.create({ slug: "landing", seo: { title: "Landing" }, blocks: [...] });
|
|
69
|
+
await client.pages.update("landing", { seo: { title: "Landing v2" } });
|
|
58
70
|
await client.pages.publish("landing");
|
|
59
71
|
await client.pages.unpublish("landing"); // pages have the FULL lifecycle here
|
|
60
72
|
await client.pages.archive("landing"); // (unlike the MCP tools)
|