@latellu/atlas-agent-skills 0.3.0 → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.3.0",
3
+ "version": "0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latellu/atlas-agent-skills",
3
- "version": "0.3.0",
3
+ "version": "0.4.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)",
@@ -83,8 +83,7 @@ types, then build the page"), route each sub-task to its own single reference.
83
83
  Choosing between MCP and the management SDK for writes: MCP is for operating content
84
84
  interactively in an agent session (one-off edits, content entry, audits); the SDK is for
85
85
  code that outlives the session (migrations, sync jobs, backends) and for anything MCP
86
- lacks (bulk, scheduling, page unpublish/archive, media delete, **writing translations**,
87
- per-field validation errors).
86
+ lacks (bulk, scheduling, page unpublish/archive, block reorder, media alt-text edits).
88
87
 
89
88
  ## Hard capability boundaries (don't go looking for these)
90
89
 
@@ -71,9 +71,9 @@ Rules that will bite you if unknown:
71
71
  });
72
72
  ```
73
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.
74
+ - **Via MCP (≥ 1.1.0)**: pass the same `translations` map as a tool argument on
75
+ `create_entry`/`update_entry`. Older MCP servers silently drop it if a
76
+ translation write appears to succeed but nothing changes, check the server version.
77
77
 
78
78
  ## Writing pages and blocks
79
79
 
@@ -104,15 +104,21 @@ The real write shape (`POST/PUT /pages`) differs from what the reading API shows
104
104
 
105
105
  - `update` with `blocks` replaces the block list; reordering alone has a dedicated
106
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.
107
+ - **Finding `block_type_id`: read the workspace schema.** Block types are content
108
+ types with `is_block: true`; `GET /api/v1/public/schema` (or the MCP
109
+ `get_workspace_schema` tool) returns each content type's `id`, `is_block`, and field
110
+ list. Use the `id` of an `is_block: true` type as `block_type_id`, and compose the
111
+ block's `data` against its fields exactly like an entry. (Older backends omit
112
+ `id`/`is_block` from the schema — there, block-type UUIDs are dashboard-only.)
113
+ - MCP `create_page`/`update_page` caveats (≥ 1.1.0): `title` is an alias mapped to
114
+ `seo.title` (on update, `seo` is replaced wholesale title alone drops the other seo
115
+ fields), and `seo_translations`/per-block `translations` are regular arguments.
116
+ - **A page without blocks cannot be published** — publish returns an error requiring at
117
+ least one block. Compose at least one block (see above) before `publish_page`.
118
+ - **The write plane has no reads**, and write responses don't echo `seo_translations` —
119
+ you cannot fully verify a translation write without a live key for the same workspace.
120
+ Trick: `PUT /pages/:slug` with body `{}` is a safe no-op that returns the page's
121
+ current base state (status + seo) — the only "read" available on the management plane.
116
122
 
117
123
  ## Practical write checklist
118
124
 
@@ -120,6 +126,7 @@ The real write shape (`POST/PUT /pages`) differs from what the reading API shows
120
126
  2. Uploads first → collect media UUIDs.
121
127
  3. Compose `data` per the table above; localizable required fields go in `translations`.
122
128
  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`.
129
+ 5. On `VALIDATION_ERROR`: the error text carries per-field detail (MCP 1.1.0 and the
130
+ SDK both surface it) fix the named field. The usual culprits are
131
+ slug-instead-of-UUID in relation/image, markdown in richtext, and
132
+ required-localizable fields sitting in `data`.
@@ -1,6 +1,9 @@
1
1
  # Atlas MCP tools
2
2
 
3
3
  The `@latellu/atlas-mcp` server (stdio) exposes Atlas content operations as agent tools.
4
+ This file describes `@latellu/atlas-mcp` **≥ 1.1.0** (the bundled `.mcp.json` uses
5
+ `npx -y`, so it always runs the latest). Older servers lack `translations`,
6
+ `seo_translations`, per-field error detail, and the schema `id`/`is_block` fields.
4
7
 
5
8
  **Environment** — read tools need `ATLAS_LIVE_API_KEY`, write tools need
6
9
  `ATLAS_MGMT_API_KEY`; with only one set, the other half of the tools fails client-side
@@ -18,7 +21,7 @@ errors).
18
21
 
19
22
  | Tool | Params | Returns |
20
23
  |---|---|---|
21
- | `get_workspace_schema` | — | `{ workspace: { slug, name, locales, default_locale }, content_types: [...] }` |
24
+ | `get_workspace_schema` | — | `{ workspace: { slug, name, locales, default_locale }, content_types: [...] }` — each content type carries `id` and `is_block`; types with `is_block: true` are the valid `block_type_id` values for page blocks |
22
25
  | `list_content_types` | — | `content_types` array (client-side view of the schema) |
23
26
  | `get_content_type` | `content_type` | one content type with its full field list; error text if the slug doesn't exist |
24
27
 
@@ -28,8 +31,8 @@ errors).
28
31
  |---|---|---|
29
32
  | `list_entries` | `content_type`, `page?`=1, `limit?`=20, `status?` | `status` is free text and **cannot** reveal drafts to a live key |
30
33
  | `get_entry` | `content_type`, `slug` | resolves by `slug` alone — `content_type` is accepted but not sent |
31
- | `create_entry` | `content_type`, `data`, `slug?` | creates a **draft**; slug auto-generated when omitted |
32
- | `update_entry` | `content_type`, `slug`, `data` | **full replace** of `data`, not a patch |
34
+ | `create_entry` | `content_type`, `data`, `slug?`, `translations?` | creates a **draft**; slug auto-generated when omitted; `translations` = `{locale: {data: {...}}}` (required+localizable fields go HERE) |
35
+ | `update_entry` | `content_type`, `slug`, `data`, `translations?` | **full replace** of `data` (and per-locale `translations`), not a patch |
33
36
  | `publish_entry` / `unpublish_entry` / `archive_entry` | `content_type`, `slug` | need the `content:publish` scope |
34
37
  | `duplicate_entry` | `content_type`, `slug` | copies to a new draft |
35
38
  | `delete_entry` | `content_type`, `slug` | returns plain text, not JSON |
@@ -40,9 +43,9 @@ errors).
40
43
  |---|---|---|
41
44
  | `list_pages` | `page?`=1, `limit?`=20 | summaries, no blocks |
42
45
  | `get_page` | `slug` | block tree — each block's `data` is a **JSON string**, parse it |
43
- | `create_page` | `title`, `slug?`, `blocks?`, `seo?` | draft; `seo` accepts only `title`/`description` here |
44
- | `update_page` | `slug`, `title?`, `blocks?`, `seo?` | |
45
- | `publish_page` | `slug` | the ONLY page lifecycle tool — see gaps below |
46
+ | `create_page` | `slug`, `title?`, `blocks?`, `seo?`, `seo_translations?` | draft; `title` is an alias for `seo.title`; `seo` accepts `title`/`description`/`keywords`/`og_image`/`canonical`; blocks = `[{block_type_id, parent_id?, position, data, translations?}]` |
47
+ | `update_page` | `slug`, `title?`, `blocks?`, `seo?`, `seo_translations?` | passing `blocks` replaces the whole list; `seo` replaces wholesale (title alone drops other seo fields) |
48
+ | `publish_page` | `slug` | the ONLY page lifecycle tool — see gaps below; fails on blockless pages |
46
49
  | `delete_page` | `slug` | plain-text result |
47
50
 
48
51
  ### Media
@@ -51,14 +54,16 @@ errors).
51
54
  |---|---|---|
52
55
  | `get_media` | `id` | live key |
53
56
  | `upload_media` | `file_path` (absolute), `folder?`="/", `alt_text?` | mgmt key; reads the local file, multipart upload |
57
+ | `delete_media` | `id` | mgmt key; plain-text result |
54
58
 
55
59
  ## Recommended write workflow
56
60
 
57
61
  1. **Schema first.** Call `get_content_type` (or `get_workspace_schema`) before any
58
62
  `create_entry`/`update_entry`. The `data` argument is an unvalidated map; the backend
59
- rejects wrong shapes with 400, and **per-field validation detail is lost through
60
- MCP** you only see `Atlas API error: 400 - validation failed`. Knowing the exact
61
- field names/types up front is the difference between one call and five.
63
+ rejects wrong shapes with 400. Error results include per-field detail
64
+ (`title: title is required`) and a `traceId` read them instead of guessing.
65
+ Knowing the exact field names/types up front is still the difference between one
66
+ call and five.
62
67
  2. **Read-modify-write for updates.** `update_entry` replaces `data` wholesale:
63
68
  `get_entry` → merge your change into the full object → `update_entry` with everything.
64
69
  3. **Create → verify → publish.** `create_entry` makes a draft; `publish_entry` needs
@@ -74,25 +79,15 @@ errors).
74
79
  These exist in the backend/SDK but have **no MCP tool**; say so and point to the
75
80
  dashboard or the management SDK (`sdk-management.md`) rather than improvising:
76
81
 
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`).
81
82
  - Page lifecycle beyond publish: **no `unpublish_page`, `archive_page`,
82
83
  `duplicate_page`**.
83
84
  - **No** bulk operations, entry/page **scheduling**, or block **reorder** tools.
84
- - **No `delete_media` / `update_media`** (alt-text edits) — media is upload-and-read
85
- only through MCP.
85
+ - **No `update_media`** (alt-text edits) — media is upload/read/delete through MCP.
86
86
  - Pagination is offset-only (`page`/`limit`); there is no cursor option, so listings
87
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
88
 
93
89
  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.
90
+ format per field type (richtext = Tiptap HTML, relation/image = UUIDs, etc.).
96
91
 
97
92
  ## Gotchas
98
93
 
@@ -108,3 +103,7 @@ matters double here because MCP swallows per-field validation errors.
108
103
  actually landed (e.g. `get_entry`) before retrying creates.
109
104
  - One MCP server = one workspace (the key's). Multi-workspace work needs one server
110
105
  registration per workspace.
106
+ - **The server never checks that the live and mgmt keys belong to the same workspace.**
107
+ A mismatched pair is maximally confusing: schema and reads come from workspace A while
108
+ every write 404s ("content type not found") against workspace B. If reads succeed but
109
+ writes 404 on types you just listed, suspect mismatched keys before anything else.
@@ -67,7 +67,7 @@ All idOrSlug parameters accept either the entry ID or its slug.
67
67
  // (seo_translations, block translations, nesting) and the block_type_id caveat.
68
68
  await client.pages.create({ slug: "landing", seo: { title: "Landing" }, blocks: [...] });
69
69
  await client.pages.update("landing", { seo: { title: "Landing v2" } });
70
- await client.pages.publish("landing");
70
+ await client.pages.publish("landing"); // fails unless the page has ≥1 block — see authoring.md
71
71
  await client.pages.unpublish("landing"); // pages have the FULL lifecycle here
72
72
  await client.pages.archive("landing"); // (unlike the MCP tools)
73
73
  await client.pages.schedule("landing", "2026-08-01T09:00:00Z");
@@ -113,8 +113,8 @@ try {
113
113
  if (e instanceof AtlasError) {
114
114
  e.status; // 400
115
115
  e.code; // "VALIDATION_ERROR"
116
- e.errors; // [{ field: "title", message: "must be a string" }, ...] ← per-field detail,
117
- } // management client only (MCP loses this) — surface it to users
116
+ e.errors; // [{ field: "title", message: "must be a string" }, ...] ← per-field detail
117
+ } // (structured here; MCP 1.1.0 appends it to the error text) — surface it to users
118
118
  }
119
119
  ```
120
120