@latellu/atlas-agent-skills 0.3.1 → 0.5.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.1",
3
+ "version": "0.5.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.1",
3
+ "version": "0.5.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)",
@@ -82,9 +82,7 @@ types, then build the page"), route each sub-task to its own single reference.
82
82
 
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
- 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).
85
+ code that outlives the session (migrations, sync jobs, backends) and for structured per-field error objects.
88
86
 
89
87
  ## Hard capability boundaries (don't go looking for these)
90
88
 
@@ -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,19 +104,17 @@ 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
116
  - **A page without blocks cannot be published** — publish returns an error requiring at
117
- least one block. Combined with `block_type_id` being undiscoverable, an API-only
118
- workflow can create and edit pages but **never publish a new one**; publishing needs
119
- someone to add a block in the dashboard first (or block-type UUIDs supplied to you).
117
+ least one block. Compose at least one block (see above) before `publish_page`.
120
118
  - **The write plane has no reads**, and write responses don't echo `seo_translations` —
121
119
  you cannot fully verify a translation write without a live key for the same workspace.
122
120
  Trick: `PUT /pages/:slug` with body `{}` is a safe no-op that returns the page's
@@ -128,6 +126,7 @@ The real write shape (`POST/PUT /pages`) differs from what the reading API shows
128
126
  2. Uploads first → collect media UUIDs.
129
127
  3. Compose `data` per the table above; localizable required fields go in `translations`.
130
128
  4. Create (draft) → read back → verify → publish.
131
- 5. On `VALIDATION_ERROR` via MCP (no field detail): re-check step 1-3 against this file
132
- before retrying — the usual culprits are slug-instead-of-UUID in relation/image,
133
- 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.2.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, the schema `id`/`is_block` fields, page/entry lifecycle tools (unpublish/archive/duplicate/schedule), bulk operations, reorder tools, media alt-text editing, list meta+cursor pagination, automatic 429 retry, and pre-parsed block data in get_page.
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
 
@@ -26,83 +29,79 @@ errors).
26
29
 
27
30
  | Tool | Params | Notes |
28
31
  |---|---|---|
29
- | `list_entries` | `content_type`, `page?`=1, `limit?`=20, `status?` | `status` is free text and **cannot** reveal drafts to a live key |
30
- | `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 |
32
+ | `list_entries` | `content_type`, `page?`=1, `limit?`=20, `status?`, `cursor?` | `status` is enum (draft \| published \| archived \| scheduled); live key **cannot** reveal drafts. Returns `{ data, meta }` where `meta` has `total_data`, `total_pages` (offset mode) or `next_cursor` (cursor mode). Use `cursor: meta.next_cursor` to page past the 1000-page limit. |
33
+ | `get_entry` | `content_type`, `slug` | resolves by `slug` alone — `content_type` is accepted but errors clearly if the slug belongs to a different content type |
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
- | `delete_entry` | `content_type`, `slug` | returns plain text, not JSON |
38
+ | `schedule_entry` | `content_type`, `slug`, `publish_at` | schedules publish for a future time (ISO 8601 UTC); needs `content:publish` scope |
39
+ | `bulk_entries` | `content_type`, `operations` | batch publish/archive/schedule/delete by entry UUIDs; each op in array: `{ op, id, publish_at? }` |
40
+ | `bulk_delete_entries` | `content_type`, `ids` | batch delete by entry UUIDs |
41
+ | `reorder_entries` | `content_type`, `ids` | reorder entries; pass the complete ordered list of entry UUIDs |
42
+ | `delete_entry` | `content_type`, `slug` | returns JSON `{ deleted: true, ... }` |
36
43
 
37
44
  ### Pages — read: live key · write: mgmt key
38
45
 
39
46
  | Tool | Params | Notes |
40
47
  |---|---|---|
41
- | `list_pages` | `page?`=1, `limit?`=20 | summaries, no blocks |
42
- | `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
- | `delete_page` | `slug` | plain-text result |
48
+ | `list_pages` | `page?`=1, `limit?`=20, `cursor?` | summaries, no blocks. Returns `{ data, meta }` with cursor pagination option (same as `list_entries`). |
49
+ | `get_page` | `slug` | block tree — each block's `data` is already parsed to an object (no JSON.parse needed); nested children and translations included |
50
+ | `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?}]` |
51
+ | `update_page` | `slug`, `title?`, `blocks?`, `seo?`, `seo_translations?` | passing `blocks` replaces the whole list; `seo` replaces wholesale (title alone drops other seo fields) |
52
+ | `publish_page` | `slug` | publish a page; fails on blockless pages |
53
+ | `unpublish_page` | `slug` | unpublish a page; needs `content:publish` scope |
54
+ | `archive_page` | `slug` | archive a page; needs `content:publish` scope |
55
+ | `duplicate_page` | `slug` | copy to a new draft |
56
+ | `schedule_page` | `slug`, `publish_at` | schedule publish for a future time (ISO 8601 UTC); needs `content:publish` scope |
57
+ | `reorder_page_blocks` | `slug`, `block_ids` | reorder blocks; pass the complete ordered list of block IDs |
58
+ | `delete_page` | `slug` | returns JSON `{ deleted: true, ... }` |
47
59
 
48
60
  ### Media
49
61
 
50
62
  | Tool | Params | Notes |
51
63
  |---|---|---|
52
64
  | `get_media` | `id` | live key |
53
- | `upload_media` | `file_path` (absolute), `folder?`="/", `alt_text?` | mgmt key; reads the local file, multipart upload |
65
+ | `upload_media` | `file_path` (absolute), `folder?`="/", `alt_text?` | mgmt key; reads the local file, multipart upload. Pre-validates client-side: ≤10 MB, extension allowlist (.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf). |
66
+ | `update_media` | `id`, `alt_text?`, `folder?` | mgmt key; edit metadata (alt text and/or folder). |
67
+ | `delete_media` | `id` | mgmt key; returns JSON `{ deleted: true, ... }` |
54
68
 
55
69
  ## Recommended write workflow
56
70
 
57
71
  1. **Schema first.** Call `get_content_type` (or `get_workspace_schema`) before any
58
72
  `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.
73
+ rejects wrong shapes with 400. Error results include per-field detail
74
+ (`title: title is required`) and a `traceId` read them instead of guessing.
75
+ Knowing the exact field names/types up front is still the difference between one
76
+ call and five.
62
77
  2. **Read-modify-write for updates.** `update_entry` replaces `data` wholesale:
63
78
  `get_entry` → merge your change into the full object → `update_entry` with everything.
64
79
  3. **Create → verify → publish.** `create_entry` makes a draft; `publish_entry` needs
65
80
  the `content:publish` scope (403 without it, with a clear message).
66
- 4. **Before `upload_media`**: check the file is ≤ 10 MB and has a known extension.
67
- Validation is entirely server-side; MIME is inferred from the extension and unknown
68
- extensions upload as `application/octet-stream`, which the backend always rejects
69
- (so `.svg`, `.heic` fail). Allowed: `image/*`, `video/*`, `application/pdf` (i.e.
70
- `.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf`).
81
+ 4. **Before `upload_media`**: the tool pre-validates client-side (≤ 10 MB, extension
82
+ allowlist), so failures are caught before upload. Allowed extensions:
83
+ `.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf`.
71
84
 
72
85
  ## Capability gaps — route elsewhere instead of retrying
73
86
 
74
- These exist in the backend/SDK but have **no MCP tool**; say so and point to the
75
- dashboard or the management SDK (`sdk-management.md`) rather than improvising:
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`).
81
- - Page lifecycle beyond publish: **no `unpublish_page`, `archive_page`,
82
- `duplicate_page`**.
83
- - **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.
86
- - Pagination is offset-only (`page`/`limit`); there is no cursor option, so listings
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`.
87
+ The MCP manage plane is now fully covered. These gaps remain:
88
+
89
+ - **No automatic merge on `update_entry`** — the tool replaces `data` wholesale, so you
90
+ must read-modify-write yourself: `get_entry` merge your changes → `update_entry`.
91
+ The management SDK offers an `update` overload that accepts a patch, but MCP does not.
92
+ - **Schema is dashboard-only.** Creating content types, fields, or block types is not
93
+ exposed via MCP, SDK, or API. "Create a content type" = ask the user to do it in the
94
+ dashboard, then continue.
92
95
 
93
96
  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.
97
+ format per field type (richtext = Tiptap HTML, relation/image = UUIDs, etc.).
96
98
 
97
99
  ## Gotchas
98
100
 
99
101
  - **Drafts are invisible to live keys** — `list_entries(status="draft")` returning
100
102
  empty means key scoping, not an empty workspace.
101
- - **`get_page` block `data` needs `JSON.parse`** per block, or blocks look empty.
102
- - **`get_entry` ignores `content_type`** slugs shared across types resolve to
103
- whatever the backend finds by slug.
104
- - **No client-side 429 retry** — on `Atlas API error: 429`, back off exponentially and
105
- retry the tool call yourself.
103
+ - **`get_entry` errors if the slug belongs to a different content type** — content types
104
+ used to share slug namespaces, now you get a clear error instead of silent mismatch.
106
105
  - Every write sends a fresh auto-generated `Idempotency-Key` per call, so re-issuing a
107
106
  failed create as a *new tool call* is a new operation — check whether the first call
108
107
  actually landed (e.g. `get_entry`) before retrying creates.
@@ -69,7 +69,7 @@ await client.pages.create({ slug: "landing", seo: { title: "Landing" }, blocks:
69
69
  await client.pages.update("landing", { seo: { title: "Landing v2" } });
70
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
- await client.pages.archive("landing"); // (unlike the MCP tools)
72
+ await client.pages.archive("landing"); // (MCP also has parity since v1.2.0)
73
73
  await client.pages.schedule("landing", "2026-08-01T09:00:00Z");
74
74
  await client.pages.delete("landing");
75
75
  await client.pages.blocksReorder("landing", ["block-id-2", "block-id-1", "block-id-3"]);
@@ -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